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
|
@ -16,6 +16,7 @@
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QModelIndex>
|
#include <QModelIndex>
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
|
@ -459,8 +460,11 @@ void GameList::PopupContextMenu(const QPoint& menu_location) {
|
||||||
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
|
switch (selected.data(GameListItem::TypeRole).value<GameListItemType>()) {
|
||||||
case GameListItemType::Game:
|
case GameListItemType::Game:
|
||||||
AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(),
|
AddGamePopup(context_menu, selected.data(GameListItemPath::FullPathRole).toString(),
|
||||||
|
selected.data(GameListItemPath::TitleRole).toString(),
|
||||||
selected.data(GameListItemPath::ProgramIdRole).toULongLong(),
|
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;
|
break;
|
||||||
case GameListItemType::CustomDir:
|
case GameListItemType::CustomDir:
|
||||||
AddPermDirPopup(context_menu, selected);
|
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,
|
void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QString& name,
|
||||||
u64 extdata_id) {
|
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_save_location = context_menu.addAction(tr("Open Save Data Location"));
|
||||||
QAction* open_extdata_location = context_menu.addAction(tr("Open Extra 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_application_location = context_menu.addAction(tr("Open Application Location"));
|
||||||
QAction* open_update_location = context_menu.addAction(tr("Open Update Data 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_dump_location = context_menu.addAction(tr("Open Texture Dump Location"));
|
||||||
QAction* open_texture_load_location =
|
QAction* open_texture_load_location =
|
||||||
context_menu.addAction(tr("Open Custom Texture Location"));
|
context_menu.addAction(tr("Open Custom Texture Location"));
|
||||||
QAction* open_mods_location = context_menu.addAction(tr("Open Mods 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* 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"));
|
QAction* open_shader_cache_location = shader_menu->addAction(tr("Open Shader Cache Location"));
|
||||||
shader_menu->addSeparator();
|
shader_menu->addSeparator();
|
||||||
QAction* delete_opengl_disk_shader_cache =
|
QAction* delete_opengl_disk_shader_cache =
|
||||||
shader_menu->addAction(tr("Delete OpenGL 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 u32 program_id_high = (program_id >> 32) & 0xFFFFFFFF;
|
||||||
const bool is_application = program_id_high == 0x00040000 || program_id_high == 0x00040010;
|
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);
|
open_extdata_location->setVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto media_type = Service::AM::GetTitleMediaType(program_id);
|
const auto update_program_id = program_id | 0xE00000000;
|
||||||
open_application_location->setEnabled(path.toStdString() ==
|
const auto dlc_program_id = program_id | 0x8C00000000;
|
||||||
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 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_dump_location->setEnabled(is_application);
|
||||||
open_texture_load_location->setEnabled(is_application);
|
open_texture_load_location->setEnabled(is_application);
|
||||||
open_mods_location->setEnabled(is_application);
|
open_mods_location->setEnabled(is_application);
|
||||||
open_dlc_location->setEnabled(is_application);
|
|
||||||
dump_romfs->setEnabled(is_application);
|
dump_romfs->setEnabled(is_application);
|
||||||
|
|
||||||
delete_opengl_disk_shader_cache->setEnabled(opengl_cache_exists);
|
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());
|
navigate_to_gamedb_entry->setVisible(it != compatibility_list.end());
|
||||||
|
|
||||||
connect(open_save_location, &QAction::triggered, this, [this, program_id] {
|
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] {
|
connect(delete_opengl_disk_shader_cache, &QAction::triggered, this, [program_id] {
|
||||||
ForEachOpenGLCacheFile(program_id, [](QFile& file) { file.remove(); });
|
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) {
|
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
|
||||||
UISettings::GameDir& game_dir =
|
UISettings::GameDir& game_dir =
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "uisettings.h"
|
#include "uisettings.h"
|
||||||
|
|
||||||
|
namespace Service::FS {
|
||||||
|
enum class MediaType : u32;
|
||||||
|
}
|
||||||
|
|
||||||
class GameListWorker;
|
class GameListWorker;
|
||||||
class GameListDir;
|
class GameListDir;
|
||||||
class GameListSearchField;
|
class GameListSearchField;
|
||||||
|
@ -105,7 +109,8 @@ private:
|
||||||
|
|
||||||
void PopupContextMenu(const QPoint& menu_location);
|
void PopupContextMenu(const QPoint& menu_location);
|
||||||
void PopupHeaderContextMenu(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 AddCustomDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||||
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
|
void AddPermDirPopup(QMenu& context_menu, QModelIndex selected);
|
||||||
void UpdateColumnVisibility();
|
void UpdateColumnVisibility();
|
||||||
|
|
|
@ -25,6 +25,10 @@
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "core/loader/smdh.h"
|
#include "core/loader/smdh.h"
|
||||||
|
|
||||||
|
namespace Service::FS {
|
||||||
|
enum class MediaType : u32;
|
||||||
|
}
|
||||||
|
|
||||||
enum class GameListItemType {
|
enum class GameListItemType {
|
||||||
Game = QStandardItem::UserType + 1,
|
Game = QStandardItem::UserType + 1,
|
||||||
CustomDir = QStandardItem::UserType + 2,
|
CustomDir = QStandardItem::UserType + 2,
|
||||||
|
@ -153,14 +157,16 @@ public:
|
||||||
static constexpr int ProgramIdRole = SortRole + 3;
|
static constexpr int ProgramIdRole = SortRole + 3;
|
||||||
static constexpr int ExtdataIdRole = SortRole + 4;
|
static constexpr int ExtdataIdRole = SortRole + 4;
|
||||||
static constexpr int LongTitleRole = SortRole + 5;
|
static constexpr int LongTitleRole = SortRole + 5;
|
||||||
|
static constexpr int MediaTypeRole = SortRole + 6;
|
||||||
|
|
||||||
GameListItemPath() = default;
|
GameListItemPath() = default;
|
||||||
GameListItemPath(const QString& game_path, std::span<const u8> smdh_data, u64 program_id,
|
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(type(), TypeRole);
|
||||||
setData(game_path, FullPathRole);
|
setData(game_path, FullPathRole);
|
||||||
setData(qulonglong(program_id), ProgramIdRole);
|
setData(qulonglong(program_id), ProgramIdRole);
|
||||||
setData(qulonglong(extdata_id), ExtdataIdRole);
|
setData(qulonglong(extdata_id), ExtdataIdRole);
|
||||||
|
setData(quint32(media_type), MediaTypeRole);
|
||||||
|
|
||||||
if (UISettings::values.game_list_icon_size.GetValue() ==
|
if (UISettings::values.game_list_icon_size.GetValue() ==
|
||||||
UISettings::GameListIconSize::NoIcon) {
|
UISettings::GameListIconSize::NoIcon) {
|
||||||
|
|
|
@ -33,10 +33,11 @@ GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
|
||||||
GameListWorker::~GameListWorker() = default;
|
GameListWorker::~GameListWorker() = default;
|
||||||
|
|
||||||
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
|
void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
|
||||||
GameListDir* parent_dir) {
|
GameListDir* parent_dir,
|
||||||
const auto callback = [this, recursion, parent_dir](u64* num_entries_out,
|
Service::FS::MediaType media_type) {
|
||||||
const std::string& directory,
|
const auto callback = [this, recursion, parent_dir,
|
||||||
const std::string& virtual_name) -> bool {
|
media_type](u64* num_entries_out, const std::string& directory,
|
||||||
|
const std::string& virtual_name) -> bool {
|
||||||
if (stop_processing) {
|
if (stop_processing) {
|
||||||
// Breaks the callback loop.
|
// Breaks the callback loop.
|
||||||
return false;
|
return false;
|
||||||
|
@ -105,7 +106,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
|
||||||
emit EntryReady(
|
emit EntryReady(
|
||||||
{
|
{
|
||||||
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id,
|
new GameListItemPath(QString::fromStdString(physical_name), smdh, program_id,
|
||||||
extdata_id),
|
extdata_id, media_type),
|
||||||
new GameListItemCompat(compatibility),
|
new GameListItemCompat(compatibility),
|
||||||
new GameListItemRegion(smdh),
|
new GameListItemRegion(smdh),
|
||||||
new GameListItem(
|
new GameListItem(
|
||||||
|
@ -116,7 +117,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
|
||||||
|
|
||||||
} else if (is_dir && recursion > 0) {
|
} else if (is_dir && recursion > 0) {
|
||||||
watch_list.append(QString::fromStdString(physical_name));
|
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;
|
return true;
|
||||||
|
@ -144,8 +145,10 @@ void GameListWorker::run() {
|
||||||
watch_list.append(demos_path);
|
watch_list.append(demos_path);
|
||||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
|
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::InstalledDir);
|
||||||
emit DirEntryReady(game_list_dir);
|
emit DirEntryReady(game_list_dir);
|
||||||
AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir);
|
AddFstEntriesToGameList(games_path.toStdString(), 2, game_list_dir,
|
||||||
AddFstEntriesToGameList(demos_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")) {
|
} else if (game_dir.path == QStringLiteral("SYSTEM")) {
|
||||||
QString path =
|
QString path =
|
||||||
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) +
|
QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir)) +
|
||||||
|
@ -153,13 +156,14 @@ void GameListWorker::run() {
|
||||||
watch_list.append(path);
|
watch_list.append(path);
|
||||||
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
|
auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SystemDir);
|
||||||
emit DirEntryReady(game_list_dir);
|
emit DirEntryReady(game_list_dir);
|
||||||
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir);
|
AddFstEntriesToGameList(path.toStdString(), 2, game_list_dir,
|
||||||
|
Service::FS::MediaType::NAND);
|
||||||
} else {
|
} else {
|
||||||
watch_list.append(game_dir.path);
|
watch_list.append(game_dir.path);
|
||||||
auto* const game_list_dir = new GameListDir(game_dir);
|
auto* const game_list_dir = new GameListDir(game_dir);
|
||||||
emit DirEntryReady(game_list_dir);
|
emit DirEntryReady(game_list_dir);
|
||||||
AddFstEntriesToGameList(game_dir.path.toStdString(), game_dir.deep_scan ? 256 : 0,
|
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 "citra_qt/compatibility_list.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Service::FS {
|
||||||
|
enum class MediaType : u32;
|
||||||
|
}
|
||||||
|
|
||||||
class QStandardItem;
|
class QStandardItem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,7 +56,7 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion,
|
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;
|
QVector<UISettings::GameDir>& game_dirs;
|
||||||
const CompatibilityList& compatibility_list;
|
const CompatibilityList& compatibility_list;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
|
#include <QtConcurrent/QtConcurrentMap>
|
||||||
#include <QtConcurrent/QtConcurrentRun>
|
#include <QtConcurrent/QtConcurrentRun>
|
||||||
#include <QtGui>
|
#include <QtGui>
|
||||||
#include <QtWidgets>
|
#include <QtWidgets>
|
||||||
|
@ -1750,6 +1751,57 @@ void GMainWindow::OnCIAInstallFinished() {
|
||||||
game_list->PopulateAsync(UISettings::values.game_dirs);
|
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() {
|
void GMainWindow::OnMenuRecentFile() {
|
||||||
QAction* action = qobject_cast<QAction*>(sender());
|
QAction* action = qobject_cast<QAction*>(sender());
|
||||||
ASSERT(action);
|
ASSERT(action);
|
||||||
|
|
|
@ -73,6 +73,10 @@ namespace Service::AM {
|
||||||
enum class InstallStatus : u32;
|
enum class InstallStatus : u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace Service::FS {
|
||||||
|
enum class MediaType : u32;
|
||||||
|
}
|
||||||
|
|
||||||
class GMainWindow : public QMainWindow {
|
class GMainWindow : public QMainWindow {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
@ -100,6 +104,9 @@ public:
|
||||||
bool DropAction(QDropEvent* event);
|
bool DropAction(QDropEvent* event);
|
||||||
void AcceptDropEvent(QDropEvent* event);
|
void AcceptDropEvent(QDropEvent* event);
|
||||||
|
|
||||||
|
void UninstallTitles(
|
||||||
|
const std::vector<std::tuple<Service::FS::MediaType, u64, QString>>& titles);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void OnAppFocusStateChanged(Qt::ApplicationState state);
|
void OnAppFocusStateChanged(Qt::ApplicationState state);
|
||||||
void OnLoadComplete();
|
void OnLoadComplete();
|
||||||
|
|
|
@ -1557,24 +1557,33 @@ void Module::Interface::GetRequiredSizeFromCia(Kernel::HLERequestContext& ctx) {
|
||||||
rb.Push(container.GetTitleMetadata().GetContentSizeByIndex(FileSys::TMDContentIndex::Main));
|
rb.Push(container.GetTitleMetadata().GetContentSizeByIndex(FileSys::TMDContentIndex::Main));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id) {
|
||||||
|
// Use the content folder so we don't delete the user's save data.
|
||||||
|
const auto path = GetTitlePath(media_type, title_id) + "content/";
|
||||||
|
if (!FileUtil::Exists(path)) {
|
||||||
|
return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent};
|
||||||
|
}
|
||||||
|
if (!FileUtil::DeleteDirRecursively(path)) {
|
||||||
|
// TODO: Determine the right error code for this.
|
||||||
|
return {ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
|
||||||
|
ErrorLevel::Permanent};
|
||||||
|
}
|
||||||
|
return RESULT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
void Module::Interface::DeleteProgram(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::DeleteProgram(Kernel::HLERequestContext& ctx) {
|
||||||
IPC::RequestParser rp(ctx);
|
IPC::RequestParser rp(ctx);
|
||||||
auto media_type = rp.PopEnum<FS::MediaType>();
|
const auto media_type = rp.PopEnum<FS::MediaType>();
|
||||||
u64 title_id = rp.Pop<u64>();
|
const auto title_id = rp.Pop<u64>();
|
||||||
LOG_INFO(Service_AM, "Deleting title 0x{:016x}", title_id);
|
|
||||||
std::string path = GetTitlePath(media_type, title_id);
|
LOG_INFO(Service_AM, "called, title={:016x}", title_id);
|
||||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
|
||||||
if (!FileUtil::Exists(path)) {
|
const auto result = UninstallProgram(media_type, title_id);
|
||||||
rb.Push(ResultCode(ErrorDescription::NotFound, ErrorModule::AM, ErrorSummary::InvalidState,
|
|
||||||
ErrorLevel::Permanent));
|
|
||||||
LOG_ERROR(Service_AM, "Title not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bool success = FileUtil::DeleteDirRecursively(path);
|
|
||||||
am->ScanForAllTitles();
|
am->ScanForAllTitles();
|
||||||
rb.Push(RESULT_SUCCESS);
|
|
||||||
if (!success)
|
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||||
LOG_ERROR(Service_AM, "FileUtil::DeleteDirRecursively unexpectedly failed");
|
rb.Push(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Module::Interface::GetSystemUpdaterMutex(Kernel::HLERequestContext& ctx) {
|
void Module::Interface::GetSystemUpdaterMutex(Kernel::HLERequestContext& ctx) {
|
||||||
|
|
|
@ -172,6 +172,14 @@ std::string GetTitlePath(Service::FS::MediaType media_type, u64 tid);
|
||||||
*/
|
*/
|
||||||
std::string GetMediaTitlePath(Service::FS::MediaType media_type);
|
std::string GetMediaTitlePath(Service::FS::MediaType media_type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls the specified title.
|
||||||
|
* @param media_type the storage medium the title is installed to
|
||||||
|
* @param title_id the title ID to uninstall
|
||||||
|
* @return result of the uninstall operation
|
||||||
|
*/
|
||||||
|
ResultCode UninstallProgram(const FS::MediaType media_type, const u64 title_id);
|
||||||
|
|
||||||
class Module final {
|
class Module final {
|
||||||
public:
|
public:
|
||||||
explicit Module(Core::System& system);
|
explicit Module(Core::System& system);
|
||||||
|
|
Reference in New Issue