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 + +