mirror of
				https://github.com/PabloMK7/citra.git
				synced 2025-10-31 05:40:04 +00:00 
			
		
		
		
	qt: Add option to uninstall a game. (#7064)
* qt: Add option to uninstall a game. * Address review comments.
This commit is contained in:
		
							parent
							
								
									3d55270de6
								
							
						
					
					
						commit
						07839fb3ce
					
				
					 9 changed files with 223 additions and 46 deletions
				
			
		|  | @ -16,6 +16,7 @@ | |||
| #include <QLabel> | ||||
| #include <QLineEdit> | ||||
| #include <QMenu> | ||||
| #include <QMessageBox> | ||||
| #include <QModelIndex> | ||||
| #include <QStandardItem> | ||||
| #include <QStandardItemModel> | ||||
|  | @ -459,8 +460,11 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { | |||
|     switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) { | ||||
|     case GameListItemType::Game: | ||||
|         AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(), | ||||
|                      selected.data(GameListItemPath::TitleRole).toString(), | ||||
|                      selected.data(GameListItemPath::ProgramIdRole).toULongLong(), | ||||
|                      selected.data(GameListItemPath::ExtdataIdRole).toULongLong()); | ||||
|                      selected.data(GameListItemPath::ExtdataIdRole).toULongLong(), | ||||
|                      static_cast<Service::FS::MediaType>( | ||||
|                          selected.data(GameListItemPath::MediaTypeRole).toUInt())); | ||||
|         break; | ||||
|     case GameListItemType::CustomDir: | ||||
|         AddPermDirPopup(context_menu, selected); | ||||
|  | @ -522,28 +526,36 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, | ||||
|                             u64 extdata_id) { | ||||
| void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, | ||||
|                             u64 program_id, u64 extdata_id, Service::FS::MediaType media_type) { | ||||
|     QAction* open_save_location = context_menu.addAction(tr("Open Save Data Location")); | ||||
|     QAction* open_extdata_location = context_menu.addAction(tr("Open Extra Data Location")); | ||||
|     QAction* open_application_location = context_menu.addAction(tr("Open Application Location")); | ||||
|     QAction* open_update_location = context_menu.addAction(tr("Open Update Data Location")); | ||||
|     QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location")); | ||||
|     QAction* open_texture_dump_location = context_menu.addAction(tr("Open Texture Dump Location")); | ||||
|     QAction* open_texture_load_location = | ||||
|         context_menu.addAction(tr("Open Custom Texture Location")); | ||||
|     QAction* open_mods_location = context_menu.addAction(tr("Open Mods Location")); | ||||
|     QAction* open_dlc_location = context_menu.addAction(tr("Open DLC Data Location")); | ||||
|     QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache")); | ||||
|     QAction* dump_romfs = context_menu.addAction(tr("Dump RomFS")); | ||||
|     QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); | ||||
|     context_menu.addSeparator(); | ||||
|     QAction* properties = context_menu.addAction(tr("Properties")); | ||||
| 
 | ||||
|     QMenu* shader_menu = context_menu.addMenu(tr("Disk Shader Cache")); | ||||
|     QAction* open_shader_cache_location = shader_menu->addAction(tr("Open Shader Cache Location")); | ||||
|     shader_menu->addSeparator(); | ||||
|     QAction* delete_opengl_disk_shader_cache = | ||||
|         shader_menu->addAction(tr("Delete OpenGL Shader Cache")); | ||||
| 
 | ||||
|     QMenu* uninstall_menu = context_menu.addMenu(tr("Uninstall")); | ||||
|     QAction* uninstall_all = uninstall_menu->addAction(tr("Everything")); | ||||
|     uninstall_menu->addSeparator(); | ||||
|     QAction* uninstall_game = uninstall_menu->addAction(tr("Game")); | ||||
|     QAction* uninstall_update = uninstall_menu->addAction(tr("Update")); | ||||
|     QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); | ||||
| 
 | ||||
|     QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); | ||||
|     context_menu.addSeparator(); | ||||
|     QAction* properties = context_menu.addAction(tr("Properties")); | ||||
| 
 | ||||
|     const u32 program_id_high = (program_id >> 32) & 0xFFFFFFFF; | ||||
|     const bool is_application = program_id_high == 0x00040000 || program_id_high == 0x00040010; | ||||
| 
 | ||||
|  | @ -564,22 +576,36 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra | |||
|         open_extdata_location->setVisible(false); | ||||
|     } | ||||
| 
 | ||||
|     auto media_type = Service::AM::GetTitleMediaType(program_id); | ||||
|     open_application_location->setEnabled(path.toStdString() == | ||||
|                                           Service::AM::GetTitleContentPath(media_type, program_id)); | ||||
|     open_update_location->setEnabled( | ||||
|         is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, | ||||
|                                                                      program_id + 0xe00000000) + | ||||
|                                            "content/")); | ||||
|     auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||||
|     const auto update_program_id = program_id | 0xE00000000; | ||||
|     const auto dlc_program_id = program_id | 0x8C00000000; | ||||
| 
 | ||||
|     const auto is_installed = | ||||
|         media_type == Service::FS::MediaType::NAND || media_type == Service::FS::MediaType::SDMC; | ||||
|     const auto has_update = | ||||
|         is_application && FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, | ||||
|                                                                      update_program_id) + | ||||
|                                            "content/"); | ||||
|     const auto has_dlc = | ||||
|         is_application && | ||||
|         FileUtil::Exists(Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, dlc_program_id) + | ||||
|                          "content/"); | ||||
| 
 | ||||
|     open_application_location->setEnabled(is_installed); | ||||
|     open_update_location->setEnabled(has_update); | ||||
|     open_dlc_location->setEnabled(has_dlc); | ||||
|     open_texture_dump_location->setEnabled(is_application); | ||||
|     open_texture_load_location->setEnabled(is_application); | ||||
|     open_mods_location->setEnabled(is_application); | ||||
|     open_dlc_location->setEnabled(is_application); | ||||
|     dump_romfs->setEnabled(is_application); | ||||
| 
 | ||||
|     delete_opengl_disk_shader_cache->setEnabled(opengl_cache_exists); | ||||
| 
 | ||||
|     uninstall_all->setEnabled(is_installed || has_update || has_dlc); | ||||
|     uninstall_game->setEnabled(is_installed); | ||||
|     uninstall_update->setEnabled(has_update); | ||||
|     uninstall_dlc->setEnabled(has_dlc); | ||||
| 
 | ||||
|     auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | ||||
|     navigate_to_gamedb_entry->setVisible(it != compatibility_list.end()); | ||||
| 
 | ||||
|     connect(open_save_location, &QAction::triggered, this, [this, program_id] { | ||||
|  | @ -641,7 +667,63 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, u64 progra | |||
|     connect(delete_opengl_disk_shader_cache, &QAction::triggered, this, [program_id] { | ||||
|         ForEachOpenGLCacheFile(program_id, [](QFile& file) { file.remove(); }); | ||||
|     }); | ||||
| }; | ||||
|     connect(uninstall_all, &QAction::triggered, this, [=, this] { | ||||
|         QMessageBox::StandardButton answer = QMessageBox::question( | ||||
|             this, tr("Citra"), | ||||
|             tr("Are you sure you want to completely uninstall '%1'?\n\nThis will " | ||||
|                "delete the game if installed, as well as any installed updates or DLC.") | ||||
|                 .arg(name), | ||||
|             QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||
|         if (answer == QMessageBox::Yes) { | ||||
|             std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles; | ||||
|             if (is_installed) { | ||||
|                 titles.emplace_back(media_type, program_id, name); | ||||
|             } | ||||
|             if (has_update) { | ||||
|                 titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id, | ||||
|                                     tr("%1 (Update)").arg(name)); | ||||
|             } | ||||
|             if (has_dlc) { | ||||
|                 titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id, | ||||
|                                     tr("%1 (DLC)").arg(name)); | ||||
|             } | ||||
|             main_window->UninstallTitles(titles); | ||||
|         } | ||||
|     }); | ||||
|     connect(uninstall_game, &QAction::triggered, this, [this, name, media_type, program_id] { | ||||
|         QMessageBox::StandardButton answer = QMessageBox::question( | ||||
|             this, tr("Citra"), tr("Are you sure you want to uninstall '%1'?").arg(name), | ||||
|             QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||
|         if (answer == QMessageBox::Yes) { | ||||
|             std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles; | ||||
|             titles.emplace_back(media_type, program_id, name); | ||||
|             main_window->UninstallTitles(titles); | ||||
|         } | ||||
|     }); | ||||
|     connect(uninstall_update, &QAction::triggered, this, [this, name, update_program_id] { | ||||
|         QMessageBox::StandardButton answer = QMessageBox::question( | ||||
|             this, tr("Citra"), | ||||
|             tr("Are you sure you want to uninstall the update for '%1'?").arg(name), | ||||
|             QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||
|         if (answer == QMessageBox::Yes) { | ||||
|             std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles; | ||||
|             titles.emplace_back(Service::FS::MediaType::SDMC, update_program_id, | ||||
|                                 tr("%1 (Update)").arg(name)); | ||||
|             main_window->UninstallTitles(titles); | ||||
|         } | ||||
|     }); | ||||
|     connect(uninstall_dlc, &QAction::triggered, this, [this, name, dlc_program_id] { | ||||
|         QMessageBox::StandardButton answer = QMessageBox::question( | ||||
|             this, tr("Citra"), tr("Are you sure you want to uninstall all DLC for '%1'?").arg(name), | ||||
|             QMessageBox::Yes | QMessageBox::No, QMessageBox::No); | ||||
|         if (answer == QMessageBox::Yes) { | ||||
|             std::vector<std::tuple<Service::FS::MediaType, u64, QString>> titles; | ||||
|             titles.emplace_back(Service::FS::MediaType::SDMC, dlc_program_id, | ||||
|                                 tr("%1 (DLC)").arg(name)); | ||||
|             main_window->UninstallTitles(titles); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { | ||||
|     UISettings::GameDir& game_dir = | ||||
|  |  | |||
|  | @ -12,6 +12,10 @@ | |||
| #include "common/common_types.h" | ||||
| #include "uisettings.h" | ||||
| 
 | ||||
| namespace Service::FS { | ||||
| enum class MediaType : u32; | ||||
| } | ||||
| 
 | ||||
| class GameListWorker; | ||||
| class GameListDir; | ||||
| class GameListSearchField; | ||||
|  | @ -105,7 +109,8 @@ private: | |||
| 
 | ||||
|     void PopupContextMenu(const QPoint& menu_location); | ||||
|     void PopupHeaderContextMenu(const QPoint& menu_location); | ||||
|     void AddGamePopup(QMenu& context_menu, const QString& path, u64 program_id, u64 extdata_id); | ||||
|     void AddGamePopup(QMenu& context_menu, const QString& path, const QString& name, u64 program_id, | ||||
|                       u64 extdata_id, Service::FS::MediaType media_type); | ||||
|     void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); | ||||
|     void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); | ||||
|     void UpdateColumnVisibility(); | ||||
|  |  | |||
|  | @ -25,6 +25,10 @@ | |||
| #include "common/string_util.h" | ||||
| #include "core/loader/smdh.h" | ||||
| 
 | ||||
| namespace Service::FS { | ||||
| enum class MediaType : u32; | ||||
| } | ||||
| 
 | ||||
| enum class GameListItemType { | ||||
|     Game = QStandardItem::UserType + 1, | ||||
|     CustomDir = QStandardItem::UserType + 2, | ||||
|  | @ -153,14 +157,16 @@ public: | |||
|     static constexpr int ProgramIdRole = SortRole + 3; | ||||
|     static constexpr int ExtdataIdRole = SortRole + 4; | ||||
|     static constexpr int LongTitleRole = SortRole + 5; | ||||
|     static constexpr int MediaTypeRole = SortRole + 6; | ||||
| 
 | ||||
|     GameListItemPath() = default; | ||||
|     GameListItemPath(const QString& game_path, std::span<const u8> smdh_data, u64 program_id, | ||||
|                      u64 extdata_id) { | ||||
|                      u64 extdata_id, Service::FS::MediaType media_type) { | ||||
|         setData(type(), TypeRole); | ||||
|         setData(game_path, FullPathRole); | ||||
|         setData(qulonglong(program_id), ProgramIdRole); | ||||
|         setData(qulonglong(extdata_id), ExtdataIdRole); | ||||
|         setData(quint32(media_type), MediaTypeRole); | ||||
| 
 | ||||
|         if (UISettings::values.game_list_icon_size.GetValue() == | ||||
|             UISettings::GameListIconSize::NoIcon) { | ||||
|  |  | |||
|  | @ -33,10 +33,11 @@ GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs, | |||
| GameListWorker::~GameListWorker() = default; | ||||
| 
 | ||||
| void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, | ||||
|                                              GameListDir* parent_dir) { | ||||
|     const auto callback = [this, recursion, parent_dir](u64* num_entries_out, | ||||
|                                                         const std::string& directory, | ||||
|                                                         const std::string& virtual_name) -> bool { | ||||
|                                              GameListDir* parent_dir, | ||||
|                                              Service::FS::MediaType media_type) { | ||||
|     const auto callback = [this, recursion, parent_dir, | ||||
|                            media_type](u64* num_entries_out, const std::string& directory, | ||||
|                                        const std::string& virtual_name) -> bool { | ||||
|         if (stop_processing) { | ||||
|             // Breaks the callback loop.
 | ||||
|             return false; | ||||
|  | @ -105,7 +106,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
|             emit EntryReady( | ||||
|                 { | ||||
|                     new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id, | ||||
|                                          extdata_id), | ||||
|                                          extdata_id, media_type), | ||||
|                     new GameListItemCompat(compatibility), | ||||
|                     new GameListItemRegion(smdh), | ||||
|                     new GameListItem( | ||||
|  | @ -116,7 +117,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign | |||
| 
 | ||||
|         } else if (is_dir && recursion > 0) { | ||||
|             watch_list.append(QString::fromStdString(physical_name)); | ||||
|             AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir); | ||||
|             AddFstEntriesToGameList(physical_name, recursion - 1, parent_dir, media_type); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|  | @ -144,8 +145,10 @@ void GameListWorker::run() { | |||
|             watch_list.append(demos_path); | ||||
|             auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir); | ||||
|             emit DirEntryReady(game_list_dir); | ||||
|             AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir); | ||||
|             AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir); | ||||
|             AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir, | ||||
|                                     Service::FS::MediaType::SDMC); | ||||
|             AddFstEntriesToGameList(demos_path.toStdString(), 2, game_list_dir, | ||||
|                                     Service::FS::MediaType::SDMC); | ||||
|         } else if (game_dir.path == QStringLiteral("SYSTEM")) { | ||||
|             QString path = | ||||
|                 QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) + | ||||
|  | @ -153,13 +156,14 @@ void GameListWorker::run() { | |||
|             watch_list.append(path); | ||||
|             auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir); | ||||
|             emit DirEntryReady(game_list_dir); | ||||
|             AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir); | ||||
|             AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir, | ||||
|                                     Service::FS::MediaType::NAND); | ||||
|         } else { | ||||
|             watch_list.append(game_dir.path); | ||||
|             auto* const game_list_dir = new GameListDir(game_dir); | ||||
|             emit DirEntryReady(game_list_dir); | ||||
|             AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0, | ||||
|                                     game_list_dir); | ||||
|                                     game_list_dir, Service::FS::MediaType::GameCard); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -15,6 +15,10 @@ | |||
| #include "citra_qt/compatibility_list.h" | ||||
| #include "common/common_types.h" | ||||
| 
 | ||||
| namespace Service::FS { | ||||
| enum class MediaType : u32; | ||||
| } | ||||
| 
 | ||||
| class QStandardItem; | ||||
| 
 | ||||
| /**
 | ||||
|  | @ -52,7 +56,7 @@ signals: | |||
| 
 | ||||
| private: | ||||
|     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion, | ||||
|                                  GameListDir* parent_dir); | ||||
|                                  GameListDir* parent_dir, Service::FS::MediaType media_type); | ||||
| 
 | ||||
|     QVector<UISettings::GameDir>& game_dirs; | ||||
|     const CompatibilityList& compatibility_list; | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ | |||
| #include <QLabel> | ||||
| #include <QMessageBox> | ||||
| #include <QSysInfo> | ||||
| #include <QtConcurrent/QtConcurrentMap> | ||||
| #include <QtConcurrent/QtConcurrentRun> | ||||
| #include <QtGui> | ||||
| #include <QtWidgets> | ||||
|  | @ -1750,6 +1751,57 @@ void GMainWindow::OnCIAInstallFinished() { | |||
|     game_list->PopulateAsync(UISettings::values.game_dirs); | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::UninstallTitles( | ||||
|     const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles) { | ||||
|     if (titles.empty()) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Select the first title in the list as representative.
 | ||||
|     const auto first_name = std::get<QString>(titles[0]); | ||||
| 
 | ||||
|     QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, | ||||
|                              static_cast<int>(titles.size()), this); | ||||
|     progress.setWindowModality(Qt::WindowModal); | ||||
| 
 | ||||
|     QFutureWatcher<void> future_watcher; | ||||
|     QObject::connect(&future_watcher, &QFutureWatcher<void>::finished, &progress, | ||||
|                      &QProgressDialog::reset); | ||||
|     QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, | ||||
|                      &QFutureWatcher<void>::cancel); | ||||
|     QObject::connect(&future_watcher, &QFutureWatcher<void>::progressValueChanged, &progress, | ||||
|                      &QProgressDialog::setValue); | ||||
| 
 | ||||
|     auto failed = false; | ||||
|     QString failed_name; | ||||
| 
 | ||||
|     const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { | ||||
|         const auto name = std::get<QString>(title); | ||||
|         const auto media_type = std::get<Service::FS::MediaType>(title); | ||||
|         const auto program_id = std::get<u64>(title); | ||||
| 
 | ||||
|         const auto result = Service::AM::UninstallProgram(media_type, program_id); | ||||
|         if (result.IsError()) { | ||||
|             LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), | ||||
|                       result.raw); | ||||
|             failed = true; | ||||
|             failed_name = name; | ||||
|             future_watcher.cancel(); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); | ||||
|     progress.exec(); | ||||
|     future_watcher.waitForFinished(); | ||||
| 
 | ||||
|     if (failed) { | ||||
|         QMessageBox::critical(this, tr("Citra"), tr("Failed to uninstall '%1'.").arg(failed_name)); | ||||
|     } else if (!future_watcher.isCanceled()) { | ||||
|         QMessageBox::information(this, tr("Citra"), | ||||
|                                  tr("Successfully uninstalled '%1'.").arg(first_name)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void GMainWindow::OnMenuRecentFile() { | ||||
|     QAction* action = qobject_cast<QAction*>(sender()); | ||||
|     ASSERT(action); | ||||
|  |  | |||
|  | @ -73,6 +73,10 @@ namespace Service::AM { | |||
| enum class InstallStatus : u32; | ||||
| } | ||||
| 
 | ||||
| namespace Service::FS { | ||||
| enum class MediaType : u32; | ||||
| } | ||||
| 
 | ||||
| class GMainWindow : public QMainWindow { | ||||
|     Q_OBJECT | ||||
| 
 | ||||
|  | @ -100,6 +104,9 @@ public: | |||
|     bool DropAction(QDropEvent* event); | ||||
|     void AcceptDropEvent(QDropEvent* event); | ||||
| 
 | ||||
|     void UninstallTitles( | ||||
|         const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles); | ||||
| 
 | ||||
| public slots: | ||||
|     void OnAppFocusStateChanged(Qt::ApplicationState state); | ||||
|     void OnLoadComplete(); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue