Browse Source

[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>
pull/2700/head
crueter 5 months ago
parent
commit
23da9022a0
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 4
      src/frontend_common/CMakeLists.txt
  2. 75
      src/frontend_common/data_manager.cpp
  3. 24
      src/frontend_common/data_manager.h
  4. 5
      src/qt_common/CMakeLists.txt
  5. 2
      src/qt_common/externals/CMakeLists.txt
  6. 25
      src/qt_common/qt_content_util.cpp
  7. 3
      src/qt_common/qt_content_util.h
  8. 38
      src/qt_common/qt_string_lookup.h
  9. 2
      src/yuzu/CMakeLists.txt
  10. 107
      src/yuzu/data_dialog.cpp
  11. 46
      src/yuzu/data_dialog.h
  12. 152
      src/yuzu/data_dialog.ui
  13. 7
      src/yuzu/main.cpp
  14. 1
      src/yuzu/main.h
  15. 12
      src/yuzu/main.ui

4
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)

75
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 <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;
}
}

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

5
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)

2
src/qt_common/externals/CMakeLists.txt

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

25
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

3
src/qt_common/qt_content_util.h

@ -6,6 +6,7 @@
#include <QObject>
#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();
}

38
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 <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());
}
}

2
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")

107
src/yuzu/data_dialog.cpp

@ -0,0 +1,107 @@
// 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_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); }));
}

46
src/yuzu/data_dialog.h

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#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

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

7
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()) {

1
src/yuzu/main.h

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

12
src/yuzu/main.ui

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

Loading…
Cancel
Save