[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 <crueter@eden-emu.dev>
This commit is contained in:
crueter 2025-10-08 16:07:02 -04:00
parent 3c6ef765af
commit f6908bba1e
15 changed files with 478 additions and 7 deletions

View file

@ -7,6 +7,7 @@ add_library(frontend_common STATIC
content_manager.h content_manager.h
firmware_manager.h firmware_manager.h
firmware_manager.cpp firmware_manager.cpp
data_manager.h data_manager.cpp
) )
create_target_directory_groups(frontend_common) create_target_directory_groups(frontend_common)

View file

@ -0,0 +1,72 @@
#include "data_manager.h"
#include "common/assert.h"
#include "common/fs/path_util.h"
#include <filesystem>
#include <fmt/format.h>
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<int>(std::log10(size) / std::log10(1024)),
static_cast<int>(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;
}
}

View file

@ -0,0 +1,21 @@
#ifndef DATA_MANAGER_H
#define DATA_MANAGER_H
#include "common/common_types.h"
#include <string>
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

View file

@ -4,9 +4,6 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later # 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 add_library(qt_common STATIC
qt_common.h qt_common.h
qt_common.cpp qt_common.cpp
@ -27,6 +24,7 @@ add_library(qt_common STATIC
qt_rom_util.h qt_rom_util.cpp qt_rom_util.h qt_rom_util.cpp
qt_applet_util.h qt_applet_util.cpp qt_applet_util.h qt_applet_util.cpp
qt_progress_dialog.h qt_progress_dialog.cpp qt_progress_dialog.h qt_progress_dialog.cpp
qt_string_lookup.h
) )
@ -40,6 +38,7 @@ endif()
add_subdirectory(externals) add_subdirectory(externals)
target_link_libraries(qt_common PRIVATE core Qt6::Core SimpleIni::SimpleIni QuaZip::QuaZip) 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) if (NOT APPLE AND ENABLE_OPENGL)
target_compile_definitions(qt_common PUBLIC HAS_OPENGL) target_compile_definitions(qt_common PUBLIC HAS_OPENGL)

View file

@ -17,4 +17,4 @@ AddJsonPackage(quazip)
# frozen # frozen
# TODO(crueter): Qt String Lookup # TODO(crueter): Qt String Lookup
# AddJsonPackage(frozen) AddJsonPackage(frozen)

View file

@ -348,4 +348,29 @@ void FixProfiles()
QtCommon::Game::OpenSaveFolder(); 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 } // namespace QtCommon::Content

View file

@ -6,6 +6,7 @@
#include <QObject> #include <QObject>
#include "common/common_types.h" #include "common/common_types.h"
#include "frontend_common/data_manager.h"
namespace QtCommon::Content { namespace QtCommon::Content {
@ -46,6 +47,8 @@ void InstallKeys();
void VerifyGameContents(const std::string &game_path); void VerifyGameContents(const std::string &game_path);
void VerifyInstalledContents(); void VerifyInstalledContents();
void ClearDataDir(FrontendCommon::DataManager::DataDir dir);
// Profiles // // Profiles //
void FixProfiles(); void FixProfiles();
} }

View file

@ -0,0 +1,35 @@
#pragma once
#include <QString>
#include <frozen/string.h>
#include <frozen/unordered_map.h>
#include <qobjectdefs.h>
#include <qtmetamacros.h>
namespace QtCommon::StringLookup {
Q_NAMESPACE
// TODO(crueter): QML interface
enum StringKey {
SavesTooltip,
ShadersTooltip,
UserNandTooltip,
SysNandTooltip,
ModsTooltip,
};
static constexpr const frozen::unordered_map<StringKey, frozen::string, 5> 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());
}
}

View file

@ -234,6 +234,8 @@ add_executable(yuzu
deps_dialog.cpp deps_dialog.cpp
deps_dialog.h deps_dialog.h
deps_dialog.ui deps_dialog.ui
data_dialog.h data_dialog.cpp data_dialog.ui
) )
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden") set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")

104
src/yuzu/data_dialog.cpp Normal file
View file

@ -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 <QDesktopServices>
#include <QFutureWatcher>
#include <QMenu>
#include <QProgressDialog>
#include <QtConcurrent/qtconcurrentrun.h>
#include <qnamespace.h>
DataDialog::DataDialog(QWidget *parent)
: QDialog(parent)
, ui(std::make_unique<Ui::DataDialog>())
{
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<FrontendCommon::DataManager::DataDir>();
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<FrontendCommon::DataManager::DataDir>();
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<u64>(m_parent);
m_parent->connect(m_watcher, &QFutureWatcher<u64>::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); }));
}

43
src/yuzu/data_dialog.h Normal file
View file

@ -0,0 +1,43 @@
#ifndef DATA_DIALOG_H
#define DATA_DIALOG_H
#include <QDialog>
#include <QFutureWatcher>
#include <QSortFilterProxyModel>
#include <QTableWidgetItem>
#include "frontend_common/data_manager.h"
#include <qnamespace.h>
namespace Ui {
class DataDialog;
}
class DataDialog : public QDialog
{
Q_OBJECT
public:
explicit DataDialog(QWidget *parent = nullptr);
~DataDialog();
private:
std::unique_ptr<Ui::DataDialog> 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<u64> *m_watcher = nullptr;
FrontendCommon::DataManager::DataDir m_dir;
};
#endif // DATA_DIALOG_H

152
src/yuzu/data_dialog.ui Normal file
View file

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DataDialog</class>
<widget class="QDialog" name="DataDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>300</width>
<height>350</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>320</height>
</size>
</property>
<property name="windowTitle">
<string>Data Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Right-click on an item to either open it or clear it. Hold your mouse over an item to see more information about it.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="sizes">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::CustomContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>80</number>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<row>
<property name="text">
<string>New Row</string>
</property>
</row>
<row>
<property name="text">
<string>0</string>
</property>
</row>
<row>
<property name="text">
<string>1</string>
</property>
</row>
<row>
<property name="text">
<string>2</string>
</property>
</row>
<row>
<property name="text">
<string>4</string>
</property>
</row>
<column>
<property name="text">
<string>Directory</string>
</property>
</column>
<column>
<property name="text">
<string>Size</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>DataDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>DataDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -156,6 +156,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "yuzu/debugger/console.h" #include "yuzu/debugger/console.h"
#include "yuzu/debugger/controller.h" #include "yuzu/debugger/controller.h"
#include "yuzu/debugger/wait_tree.h" #include "yuzu/debugger/wait_tree.h"
#include "yuzu/data_dialog.h"
#include "yuzu/deps_dialog.h" #include "yuzu/deps_dialog.h"
#include "yuzu/discord.h" #include "yuzu/discord.h"
#include "yuzu/game_list.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_Install_Keys, &GMainWindow::OnInstallDecryptionKeys);
connect_menu(ui->action_About, &GMainWindow::OnAbout); connect_menu(ui->action_About, &GMainWindow::OnAbout);
connect_menu(ui->action_Eden_Dependencies, &GMainWindow::OnEdenDependencies); connect_menu(ui->action_Eden_Dependencies, &GMainWindow::OnEdenDependencies);
connect_menu(ui->action_Data_Manager, &GMainWindow::OnDataDialog);
} }
void GMainWindow::UpdateMenuState() { void GMainWindow::UpdateMenuState() {
@ -3934,6 +3936,11 @@ void GMainWindow::OnEdenDependencies() {
depsDialog.exec(); depsDialog.exec();
} }
void GMainWindow::OnDataDialog() {
DataDialog dataDialog(this);
dataDialog.exec();
}
void GMainWindow::OnToggleFilterBar() { void GMainWindow::OnToggleFilterBar() {
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
if (ui->action_Show_Filter_Bar->isChecked()) { if (ui->action_Show_Filter_Bar->isChecked()) {

View file

@ -387,6 +387,7 @@ private slots:
void OnInstallDecryptionKeys(); void OnInstallDecryptionKeys();
void OnAbout(); void OnAbout();
void OnEdenDependencies(); void OnEdenDependencies();
void OnDataDialog();
void OnToggleFilterBar(); void OnToggleFilterBar();
void OnToggleStatusBar(); void OnToggleStatusBar();
void OnGameListRefresh(); void OnGameListRefresh();

View file

@ -158,7 +158,7 @@
</property> </property>
<widget class="QMenu" name="menu_cabinet_applet"> <widget class="QMenu" name="menu_cabinet_applet">
<property name="title"> <property name="title">
<string>&amp;Amiibo</string> <string>Am&amp;iibo</string>
</property> </property>
<addaction name="action_Load_Cabinet_Nickname_Owner"/> <addaction name="action_Load_Cabinet_Nickname_Owner"/>
<addaction name="action_Load_Cabinet_Eraser"/> <addaction name="action_Load_Cabinet_Eraser"/>
@ -184,7 +184,7 @@
</widget> </widget>
<widget class="QMenu" name="menuInstall_Firmware"> <widget class="QMenu" name="menuInstall_Firmware">
<property name="title"> <property name="title">
<string>Install Firmware</string> <string>Install &amp;Firmware</string>
</property> </property>
<addaction name="action_Firmware_From_Folder"/> <addaction name="action_Firmware_From_Folder"/>
<addaction name="action_Firmware_From_ZIP"/> <addaction name="action_Firmware_From_ZIP"/>
@ -192,6 +192,7 @@
<addaction name="action_Install_Keys"/> <addaction name="action_Install_Keys"/>
<addaction name="menuInstall_Firmware"/> <addaction name="menuInstall_Firmware"/>
<addaction name="action_Verify_installed_contents"/> <addaction name="action_Verify_installed_contents"/>
<addaction name="action_Data_Manager"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="menu_cabinet_applet"/> <addaction name="menu_cabinet_applet"/>
<addaction name="action_Load_Album"/> <addaction name="action_Load_Album"/>
@ -497,7 +498,7 @@
</action> </action>
<action name="action_Install_Keys"> <action name="action_Install_Keys">
<property name="text"> <property name="text">
<string>Install Decryption Keys</string> <string>Install Decryption &amp;Keys</string>
</property> </property>
</action> </action>
<action name="action_Load_Home_Menu"> <action name="action_Load_Home_Menu">
@ -593,6 +594,11 @@
<string>&amp;Eden Dependencies</string> <string>&amp;Eden Dependencies</string>
</property> </property>
</action> </action>
<action name="action_Data_Manager">
<property name="text">
<string>&amp;Data Manager</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="yuzu.qrc"/> <include location="yuzu.qrc"/>