From 221d9c71acf630747a046435aa20d3ae8cc27848 Mon Sep 17 00:00:00 2001 From: crueter Date: Wed, 8 Oct 2025 16:07:02 -0400 Subject: [PATCH] [desktop] Initial data manager prototype Right now, all this adds is a small dialog to the Widgets frontend showing the user how much space is taken up by their saves, shaders, NAND, and mods. It also gives them the choice to clear these directories (with 2x confirmation), OR open the directory in their system file manager. In the future, a lot more can be done with this concept. Notably, a common import/export (a la android) could be added, the UI can obviously be polished, etc. We could also add this to Android, but I don't think common import/export is needed *for* Android, and should probably be left in qt_common. Signed-off-by: crueter --- src/frontend_common/CMakeLists.txt | 1 + src/frontend_common/data_manager.cpp | 72 ++++++++++++ src/frontend_common/data_manager.h | 21 ++++ src/qt_common/CMakeLists.txt | 5 +- src/qt_common/externals/CMakeLists.txt | 2 +- src/qt_common/qt_content_util.cpp | 25 ++++ src/qt_common/qt_content_util.h | 3 + src/qt_common/qt_string_lookup.h | 35 ++++++ src/yuzu/CMakeLists.txt | 2 + src/yuzu/data_dialog.cpp | 104 +++++++++++++++++ src/yuzu/data_dialog.h | 43 +++++++ src/yuzu/data_dialog.ui | 152 +++++++++++++++++++++++++ src/yuzu/main.cpp | 7 ++ src/yuzu/main.h | 1 + src/yuzu/main.ui | 12 +- 15 files changed, 478 insertions(+), 7 deletions(-) create mode 100644 src/frontend_common/data_manager.cpp create mode 100644 src/frontend_common/data_manager.h create mode 100644 src/qt_common/qt_string_lookup.h create mode 100644 src/yuzu/data_dialog.cpp create mode 100644 src/yuzu/data_dialog.h create mode 100644 src/yuzu/data_dialog.ui diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt index 70e142bb0c..83638476bf 100644 --- a/src/frontend_common/CMakeLists.txt +++ b/src/frontend_common/CMakeLists.txt @@ -7,6 +7,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..21e2422a42 --- /dev/null +++ b/src/frontend_common/data_manager.cpp @@ -0,0 +1,72 @@ +#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..70ac2ca47e --- /dev/null +++ b/src/frontend_common/data_manager.h @@ -0,0 +1,21 @@ +#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..4898516b72 --- /dev/null +++ b/src/qt_common/qt_string_lookup.h @@ -0,0 +1,35 @@ +#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, "DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!"}, + {ShadersTooltip, "Shader pipeline caches. Generally safe to remove."}, + {UserNandTooltip, "Contains updates and DLC for games."}, + {SysNandTooltip, "Contains firmware and applet data."}, + {ModsTooltip, "Contains all of your mod data."}, +}; + +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..9663d12bf0 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -234,6 +234,8 @@ add_executable(yuzu deps_dialog.cpp deps_dialog.h deps_dialog.ui + + data_dialog.h data_dialog.cpp data_dialog.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..af2d858125 --- /dev/null +++ b/src/yuzu/data_dialog.cpp @@ -0,0 +1,104 @@ +#include "data_dialog.h" +#include "frontend_common/data_manager.h" +#include "qt_common/qt_content_util.h" +#include "qt_common/qt_frontend_util.h" +#include "qt_common/qt_progress_dialog.h" +#include "qt_common/qt_string_lookup.h" +#include "ui_data_dialog.h" + +#include +#include +#include +#include +#include +#include + +DataDialog::DataDialog(QWidget *parent) + : QDialog(parent) + , ui(std::make_unique()) +{ + ui->setupUi(this); + + std::size_t row = 0; +#define TABLE_ITEM(label, name, data_dir) \ + QTableWidgetItem *name##Label = new QTableWidgetItem(tr(label)); \ + name##Label->setToolTip( \ + QtCommon::StringLookup::Lookup(QtCommon::StringLookup::data_dir##Tooltip)); \ + ui->sizes->setItem(row, 0, name##Label); \ + DataItem *name##Item = new DataItem(FrontendCommon::DataManager::DataDir::data_dir, this); \ + ui->sizes->setItem(row, 1, name##Item); \ + ++row; + + TABLE_ITEM("Saves", save, Saves) + TABLE_ITEM("Shaders", shaders, Shaders) + TABLE_ITEM("UserNAND", user, UserNand) + TABLE_ITEM("SysNAND", sys, SysNand) + TABLE_ITEM("Mods", mods, Mods) + +#undef TABLE_ITEM + + QObject::connect(ui->sizes, &QTableWidget::customContextMenuRequested, this, [this]() { + auto items = ui->sizes->selectedItems(); + if (items.empty()) + return; + + QTableWidgetItem *selected = items.at(0); + DataItem *item = (DataItem *) ui->sizes->item(selected->row(), 1); + + QMenu *menu = new QMenu(this); + QAction *open = menu->addAction(tr("Open")); + QObject::connect(open, &QAction::triggered, this, [item]() { + auto data_dir + = item->data(DataItem::DATA_DIR).value(); + + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FrontendCommon::DataManager::GetDataDir(data_dir)))); + }); + + QAction *clear = menu->addAction(tr("Clear")); + QObject::connect(clear, &QAction::triggered, this, [item]() { + auto data_dir + = item->data(DataItem::DATA_DIR).value(); + + QtCommon::Content::ClearDataDir(data_dir); + + item->scan(); + }); + + menu->exec(QCursor::pos()); + }); +} + +DataDialog::~DataDialog() = default; + +DataItem::DataItem(FrontendCommon::DataManager::DataDir data_dir, QWidget *parent) + : QTableWidgetItem(QObject::tr("Calculating")) + , m_parent(parent) + , m_dir(data_dir) +{ + setData(DataItem::DATA_DIR, QVariant::fromValue(m_dir)); + scan(); +} + +bool DataItem::operator<(const QTableWidgetItem &other) const +{ + return this->data(DataRole::SIZE).toULongLong() < other.data(DataRole::SIZE).toULongLong(); +} + +void DataItem::reset() { + setText(QStringLiteral("0 B")); + setData(DataItem::SIZE, QVariant::fromValue(0ULL)); +} + +void DataItem::scan() { + m_watcher = new QFutureWatcher(m_parent); + + m_parent->connect(m_watcher, &QFutureWatcher::finished, m_parent, [=, this]() { + u64 size = m_watcher->result(); + setText(QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size))); + setData(DataItem::SIZE, QVariant::fromValue(size)); + }); + + m_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..51bd0a4353 --- /dev/null +++ b/src/yuzu/data_dialog.h @@ -0,0 +1,43 @@ +#ifndef DATA_DIALOG_H +#define DATA_DIALOG_H + +#include +#include +#include +#include +#include "frontend_common/data_manager.h" +#include + +namespace Ui { +class DataDialog; +} + +class DataDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DataDialog(QWidget *parent = nullptr); + ~DataDialog(); + +private: + std::unique_ptr ui; +}; + +class DataItem : public QTableWidgetItem +{ +public: + DataItem(FrontendCommon::DataManager::DataDir data_dir, QWidget *parent); + enum DataRole { SIZE = Qt::UserRole + 1, DATA_DIR }; + + bool operator<(const QTableWidgetItem &other) const; + void reset(); + void scan(); + +private: + QWidget *m_parent; + QFutureWatcher *m_watcher = nullptr; + 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..d757a48bd6 --- /dev/null +++ b/src/yuzu/data_dialog.ui @@ -0,0 +1,152 @@ + + + DataDialog + + + + 0 + 0 + 300 + 350 + + + + + 0 + 0 + + + + + 300 + 320 + + + + Data Manager + + + + + + Right-click on an item to either open it or clear it. Hold your mouse over an item to see more information about it. + + + true + + + + + + + Qt::ContextMenuPolicy::CustomContextMenu + + + QAbstractItemView::EditTrigger::NoEditTriggers + + + true + + + QAbstractItemView::SelectionMode::SingleSelection + + + true + + + false + + + 80 + + + true + + + false + + + + New Row + + + + + 0 + + + + + 1 + + + + + 2 + + + + + 4 + + + + + Directory + + + + + Size + + + + + + + + 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/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..b7f9d3b1e3 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -158,7 +158,7 @@ - &Amiibo + Am&iibo @@ -184,7 +184,7 @@ - Install Firmware + Install &Firmware @@ -192,6 +192,7 @@ + @@ -497,7 +498,7 @@ - Install Decryption Keys + Install Decryption &Keys @@ -593,6 +594,11 @@ &Eden Dependencies + + + &Data Manager + +