diff --git a/dist/qt_themes/colorful/icons/48x48/trash.png b/dist/qt_themes/colorful/icons/48x48/trash.png new file mode 100644 index 0000000000..e60dce2e32 Binary files /dev/null and b/dist/qt_themes/colorful/icons/48x48/trash.png differ diff --git a/dist/qt_themes/colorful/style.qrc b/dist/qt_themes/colorful/style.qrc index 82cd367be9..fc85922d6b 100644 --- a/dist/qt_themes/colorful/style.qrc +++ b/dist/qt_themes/colorful/style.qrc @@ -18,6 +18,7 @@ SPDX-License-Identifier: GPL-2.0-or-later icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/trash.png icons/48x48/list-add.png icons/48x48/no_avatar.png icons/48x48/sd_card.png diff --git a/dist/qt_themes/colorful_midnight_blue/style.qrc b/dist/qt_themes/colorful_midnight_blue/style.qrc index b9821c6722..2aee8ea1e1 100644 --- a/dist/qt_themes/colorful_midnight_blue/style.qrc +++ b/dist/qt_themes/colorful_midnight_blue/style.qrc @@ -11,6 +11,7 @@ SPDX-License-Identifier: GPL-2.0-or-later ../colorful/icons/48x48/bad_folder.png ../colorful/icons/48x48/chip.png ../colorful/icons/48x48/folder.png + ../colorful/icons/48x48/trash.png ../colorful/icons/48x48/list-add.png ../colorful/icons/48x48/sd_card.png ../colorful/icons/256x256/plus_folder.png diff --git a/dist/qt_themes/default/default.qrc b/dist/qt_themes/default/default.qrc index 0d8af20fc1..45a91ef2dc 100644 --- a/dist/qt_themes/default/default.qrc +++ b/dist/qt_themes/default/default.qrc @@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/trash.png icons/48x48/list-add.png icons/48x48/sd_card.png icons/48x48/star.png diff --git a/dist/qt_themes/default/icons/48x48/trash.png b/dist/qt_themes/default/icons/48x48/trash.png new file mode 100644 index 0000000000..547b821de9 Binary files /dev/null and b/dist/qt_themes/default/icons/48x48/trash.png differ diff --git a/dist/qt_themes/default_dark/style.qrc b/dist/qt_themes/default_dark/style.qrc index 7de4737c2c..ac6c9fe4e9 100644 --- a/dist/qt_themes/default_dark/style.qrc +++ b/dist/qt_themes/default_dark/style.qrc @@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later ../colorful/icons/48x48/bad_folder.png ../colorful/icons/48x48/chip.png ../colorful/icons/48x48/folder.png + ../colorful/icons/48x48/trash.png ../qdarkstyle/icons/48x48/no_avatar.png ../colorful/icons/48x48/list-add.png ../colorful/icons/48x48/sd_card.png diff --git a/dist/qt_themes/qdarkstyle/icons/48x48/trash.png b/dist/qt_themes/qdarkstyle/icons/48x48/trash.png new file mode 100644 index 0000000000..e59fe85c03 Binary files /dev/null and b/dist/qt_themes/qdarkstyle/icons/48x48/trash.png differ diff --git a/dist/qt_themes/qdarkstyle/style.qrc b/dist/qt_themes/qdarkstyle/style.qrc index a89fb26c68..2b5ca31e72 100644 --- a/dist/qt_themes/qdarkstyle/style.qrc +++ b/dist/qt_themes/qdarkstyle/style.qrc @@ -9,6 +9,7 @@ icons/48x48/bad_folder.png icons/48x48/chip.png icons/48x48/folder.png + icons/48x48/trash.png icons/48x48/no_avatar.png icons/48x48/list-add.png icons/48x48/sd_card.png diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index 70e142bb0c..aa44740596 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +# SPDX-License-Identifier: GPL-3.0-or-later + # SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later @@ -7,6 +10,7 @@ add_library(frontend_common STATIC content_manager.h firmware_manager.h firmware_manager.cpp + data_manager.h data_manager.cpp ) create_target_directory_groups(frontend_common) diff --git a/src/frontend_common/data_manager.cpp b/src/frontend_common/data_manager.cpp new file mode 100644 index 0000000000..4a63ed9e28 --- /dev/null +++ b/src/frontend_common/data_manager.cpp @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "data_manager.h" +#include "common/assert.h" +#include "common/fs/path_util.h" +#include +#include + +namespace FrontendCommon::DataManager { + +namespace fs = std::filesystem; + +const std::string GetDataDir(DataDir dir) +{ + const fs::path nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); + + switch (dir) { + case DataDir::Saves: + return (nand_dir / "user" / "save" / "0000000000000000").string(); + case DataDir::UserNand: + return (nand_dir / "user" / "Contents" / "registered").string(); + case DataDir::SysNand: + return (nand_dir / "system").string(); + case DataDir::Mods: + return Common::FS::GetEdenPathString(Common::FS::EdenPath::LoadDir); + case DataDir::Shaders: + return Common::FS::GetEdenPathString(Common::FS::EdenPath::ShaderDir); + default: + UNIMPLEMENTED(); + } + + return ""; +} + +u64 ClearDir(DataDir dir) +{ + fs::path data_dir = GetDataDir(dir); + u64 result = fs::remove_all(data_dir); + + // mkpath at the end just so it actually exists + fs::create_directories(data_dir); + return result; +} + +const std::string ReadableBytesSize(u64 size) +{ + static constexpr std::array units{"B", "KiB", "MiB", "GiB", "TiB", "PiB"}; + if (size == 0) { + return "0 B"; + } + + const int digit_groups = (std::min) (static_cast(std::log10(size) / std::log10(1024)), + static_cast(units.size())); + return fmt::format("{:.1f} {}", size / std::pow(1024, digit_groups), units[digit_groups]); +} + +u64 DataDirSize(DataDir dir) +{ + fs::path data_dir = GetDataDir(dir); + u64 size = 0; + + if (!fs::exists(data_dir)) + return 0; + + for (const auto& entry : fs::recursive_directory_iterator(data_dir)) { + if (!entry.is_directory()) { + size += entry.file_size(); + } + } + + return size; +} + +} diff --git a/src/frontend_common/data_manager.h b/src/frontend_common/data_manager.h new file mode 100644 index 0000000000..f6ae836c14 --- /dev/null +++ b/src/frontend_common/data_manager.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DATA_MANAGER_H +#define DATA_MANAGER_H + +#include "common/common_types.h" +#include + +namespace FrontendCommon::DataManager { + +enum class DataDir { Saves, UserNand, SysNand, Mods, Shaders }; + +const std::string GetDataDir(DataDir dir); + +u64 ClearDir(DataDir dir); + +const std::string ReadableBytesSize(u64 size); + +u64 DataDirSize(DataDir dir); + +}; // namespace FrontendCommon::DataManager + +#endif // DATA_MANAGER_H diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt index aa931f113e..fe728e0377 100644 --- a/src/qt_common/CMakeLists.txt +++ b/src/qt_common/CMakeLists.txt @@ -4,9 +4,6 @@ # SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-License-Identifier: GPL-2.0-or-later -find_package(Qt6 REQUIRED COMPONENTS Core) -find_package(Qt6 REQUIRED COMPONENTS Core) - add_library(qt_common STATIC qt_common.h qt_common.cpp @@ -27,6 +24,7 @@ add_library(qt_common STATIC qt_rom_util.h qt_rom_util.cpp qt_applet_util.h qt_applet_util.cpp qt_progress_dialog.h qt_progress_dialog.cpp + qt_string_lookup.h ) @@ -40,6 +38,7 @@ endif() add_subdirectory(externals) target_link_libraries(qt_common PRIVATE core Qt6::Core SimpleIni::SimpleIni QuaZip::QuaZip) +target_link_libraries(qt_common PUBLIC frozen::frozen) if (NOT APPLE AND ENABLE_OPENGL) target_compile_definitions(qt_common PUBLIC HAS_OPENGL) diff --git a/src/qt_common/externals/CMakeLists.txt b/src/qt_common/externals/CMakeLists.txt index e7b2e7b3e6..189a52c0a6 100644 --- a/src/qt_common/externals/CMakeLists.txt +++ b/src/qt_common/externals/CMakeLists.txt @@ -17,4 +17,4 @@ AddJsonPackage(quazip) # frozen # TODO(crueter): Qt String Lookup -# AddJsonPackage(frozen) +AddJsonPackage(frozen) diff --git a/src/qt_common/qt_content_util.cpp b/src/qt_common/qt_content_util.cpp index 2f659cf1b2..9cbd1caf02 100644 --- a/src/qt_common/qt_content_util.cpp +++ b/src/qt_common/qt_content_util.cpp @@ -348,4 +348,29 @@ void FixProfiles() QtCommon::Game::OpenSaveFolder(); } +void ClearDataDir(FrontendCommon::DataManager::DataDir dir) { + auto result = QtCommon::Frontend::Warning("Really clear data?", + "Important data may be lost!", + QMessageBox::Yes | QMessageBox::No); + + if (result != QMessageBox::Yes) + return; + + result = QtCommon::Frontend::Warning( + "Are you REALLY sure?", + "Once deleted, your data will NOT come back!\n" + "Only do this if you're 100% sure you want to delete this data.", + QMessageBox::Yes | QMessageBox::No); + + if (result != QMessageBox::Yes) + return; + + QtCommon::Frontend::QtProgressDialog dialog(tr("Clearing..."), QString(), 0, 0); + dialog.show(); + + FrontendCommon::DataManager::ClearDir(dir); + + dialog.close(); +} + } // namespace QtCommon::Content diff --git a/src/qt_common/qt_content_util.h b/src/qt_common/qt_content_util.h index b95e78c0a0..b2443829ab 100644 --- a/src/qt_common/qt_content_util.h +++ b/src/qt_common/qt_content_util.h @@ -6,6 +6,7 @@ #include #include "common/common_types.h" +#include "frontend_common/data_manager.h" namespace QtCommon::Content { @@ -46,6 +47,8 @@ void InstallKeys(); void VerifyGameContents(const std::string &game_path); void VerifyInstalledContents(); +void ClearDataDir(FrontendCommon::DataManager::DataDir dir); + // Profiles // void FixProfiles(); } diff --git a/src/qt_common/qt_string_lookup.h b/src/qt_common/qt_string_lookup.h new file mode 100644 index 0000000000..de6acac8a1 --- /dev/null +++ b/src/qt_common/qt_string_lookup.h @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +namespace QtCommon::StringLookup { + +Q_NAMESPACE + +// TODO(crueter): QML interface +enum StringKey { + SavesTooltip, + ShadersTooltip, + UserNandTooltip, + SysNandTooltip, + ModsTooltip, +}; + +static constexpr const frozen::unordered_map strings = { + {SavesTooltip, "Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!"}, + {ShadersTooltip, "Contains Vulkan and OpenGL pipeline caches. Generally safe to remove."}, + {UserNandTooltip, "Contains updates and DLC for games."}, + {SysNandTooltip, "Contains firmware and applet data."}, + {ModsTooltip, "Contains game mods, patches, and cheats."}, +}; + +static inline const QString Lookup(StringKey key) +{ + return QString::fromStdString(strings.at(key).data()); +} + +} diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index c03f7a3abf..f4669d0914 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -234,6 +234,9 @@ add_executable(yuzu deps_dialog.cpp deps_dialog.h deps_dialog.ui + + data_dialog.h data_dialog.cpp data_dialog.ui + data_widget.ui ) set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") diff --git a/src/yuzu/data_dialog.cpp b/src/yuzu/data_dialog.cpp new file mode 100644 index 0000000000..87d81e4f43 --- /dev/null +++ b/src/yuzu/data_dialog.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "data_dialog.h" +#include "frontend_common/data_manager.h" +#include "qt_common/qt_content_util.h" +#include "qt_common/qt_string_lookup.h" +#include "ui_data_dialog.h" + +#include +#include +#include + +DataDialog::DataDialog(QWidget *parent) + : QDialog(parent) + , ui(std::make_unique()) +{ + ui->setupUi(this); + + // TODO: Should we make this a single widget that pulls data from a model? +#define WIDGET(name) \ + ui->page->addWidget(new DataWidget(FrontendCommon::DataManager::DataDir::name, \ + QtCommon::StringLookup::name##Tooltip, \ + this)); + + WIDGET(Saves) + WIDGET(Shaders) + WIDGET(UserNand) + WIDGET(SysNand) + WIDGET(Mods) + +#undef WIDGET + + connect(ui->labels, &QListWidget::itemSelectionChanged, this, [this]() { + ui->page->setCurrentIndex(ui->labels->currentRow()); + }); +} + +DataDialog::~DataDialog() = default; + +DataWidget::DataWidget(FrontendCommon::DataManager::DataDir data_dir, + QtCommon::StringLookup::StringKey tooltip, + QWidget *parent) + : QWidget(parent) + , ui(std::make_unique()) + , m_dir(data_dir) +{ + ui->setupUi(this); + + ui->tooltip->setText(QtCommon::StringLookup::Lookup(tooltip)); + + ui->clear->setIcon(QIcon::fromTheme(QStringLiteral("trash"))); + ui->open->setIcon(QIcon::fromTheme(QStringLiteral("folder"))); + + connect(ui->clear, &QPushButton::clicked, this, &DataWidget::clear); + connect(ui->open, &QPushButton::clicked, this, &DataWidget::open); + + scan(); +} + +void DataWidget::clear() { + QtCommon::Content::ClearDataDir(m_dir); + scan(); +} + +void DataWidget::open() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FrontendCommon::DataManager::GetDataDir(m_dir)))); +} + +void DataWidget::scan() { + ui->size->setText(tr("Calculating...")); + + QFutureWatcher *watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcher::finished, this, [=, this]() { + u64 size = watcher->result(); + ui->size->setText( + QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size))); + watcher->deleteLater(); + }); + + watcher->setFuture( + QtConcurrent::run([this]() { return FrontendCommon::DataManager::DataDirSize(m_dir); })); +} diff --git a/src/yuzu/data_dialog.h b/src/yuzu/data_dialog.h new file mode 100644 index 0000000000..9f367d6049 --- /dev/null +++ b/src/yuzu/data_dialog.h @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DATA_DIALOG_H +#define DATA_DIALOG_H + +#include +#include "frontend_common/data_manager.h" +#include "qt_common/qt_string_lookup.h" + +#include "ui_data_widget.h" + +namespace Ui { +class DataDialog; +} + +class DataDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DataDialog(QWidget *parent = nullptr); + ~DataDialog(); + +private: + std::unique_ptr ui; +}; + +class DataWidget : public QWidget +{ + Q_OBJECT +public: + explicit DataWidget(FrontendCommon::DataManager::DataDir data_dir, + QtCommon::StringLookup::StringKey tooltip, + QWidget *parent = nullptr); + +public slots: + void clear(); + void open(); + void scan(); + +private: + std::unique_ptr ui; + FrontendCommon::DataManager::DataDir m_dir; +}; + +#endif // DATA_DIALOG_H diff --git a/src/yuzu/data_dialog.ui b/src/yuzu/data_dialog.ui new file mode 100644 index 0000000000..06751e2fb1 --- /dev/null +++ b/src/yuzu/data_dialog.ui @@ -0,0 +1,148 @@ + + + DataDialog + + + + 0 + 0 + 480 + 320 + + + + + 0 + 0 + + + + + 300 + 320 + + + + Data Manager + + + + + + + + + 0 + 0 + + + + + Saves + + + + + Shaders + + + + + UserNAND + + + + + SysNAND + + + + + Mods + + + + + + + + + 0 + 0 + + + + + 275 + 200 + + + + -1 + + + + + + + + + 10 + + + + + Deleting ANY data is IRREVERSABLE! + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Ok + + + + + + + + + + + buttonBox + accepted() + DataDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DataDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/yuzu/data_widget.ui b/src/yuzu/data_widget.ui new file mode 100644 index 0000000000..ed67078fa1 --- /dev/null +++ b/src/yuzu/data_widget.ui @@ -0,0 +1,147 @@ + + + DataWidget + + + + 0 + 0 + 275 + 200 + + + + Form + + + + + + + + Tooltip + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + + 10 + true + + + + Size + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + + + QLayout::SizeConstraint::SetFixedSize + + + 25 + + + + + + 1 + 1 + + + + + 52 + 42 + + + + Open with your system file manager + + + QPushButton { + border-style: solid; + border-width:1px; + border-radius:25px; + border-color: transparent; + max-width:50px; + max-height:40px; + min-width:50px; + min-height:40px; +} + + + + + + + 24 + 24 + + + + + + + + + 1 + 1 + + + + + 52 + 42 + + + + Delete all data in this directory. THIS IS 100% IRREVERSABLE! + + + QPushButton { + border-style: solid; + border-width:1px; + border-radius:25px; + border-color: transparent; + max-width:50px; + max-height:40px; + min-width:50px; + min-height:40px; +} + + + + + + + 24 + 24 + + + + + + + + + + + diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 44ed29f141..b6dded447c 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -156,6 +156,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "yuzu/debugger/console.h" #include "yuzu/debugger/controller.h" #include "yuzu/debugger/wait_tree.h" +#include "yuzu/data_dialog.h" #include "yuzu/deps_dialog.h" #include "yuzu/discord.h" #include "yuzu/game_list.h" @@ -1705,6 +1706,7 @@ void GMainWindow::ConnectMenuEvents() { connect_menu(ui->action_Install_Keys, &GMainWindow::OnInstallDecryptionKeys); connect_menu(ui->action_About, &GMainWindow::OnAbout); connect_menu(ui->action_Eden_Dependencies, &GMainWindow::OnEdenDependencies); + connect_menu(ui->action_Data_Manager, &GMainWindow::OnDataDialog); } void GMainWindow::UpdateMenuState() { @@ -3934,6 +3936,11 @@ void GMainWindow::OnEdenDependencies() { depsDialog.exec(); } +void GMainWindow::OnDataDialog() { + DataDialog dataDialog(this); + dataDialog.exec(); +} + void GMainWindow::OnToggleFilterBar() { game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); if (ui->action_Show_Filter_Bar->isChecked()) { diff --git a/src/yuzu/main.h b/src/yuzu/main.h index e3922759b0..a3e99c05fe 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -387,6 +387,7 @@ private slots: void OnInstallDecryptionKeys(); void OnAbout(); void OnEdenDependencies(); + void OnDataDialog(); void OnToggleFilterBar(); void OnToggleStatusBar(); void OnGameListRefresh(); diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 5f56c9e6d1..12ff4efdf1 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -158,13 +158,23 @@ - &Amiibo + Am&iibo + + + &Applets + + + + + + + &TAS @@ -184,7 +194,7 @@ - Install Firmware + Install &Firmware @@ -192,13 +202,10 @@ + - - - - - + @@ -497,12 +504,12 @@ - Install Decryption Keys + Install Decryption &Keys - Open Home Menu + Open &Home Menu @@ -593,6 +600,11 @@ &Eden Dependencies + + + &Data Manager + +