Browse Source
[desktop] Initial data manager prototype
[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
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
15 changed files with 496 additions and 7 deletions
-
4src/frontend_common/CMakeLists.txt
-
75src/frontend_common/data_manager.cpp
-
24src/frontend_common/data_manager.h
-
5src/qt_common/CMakeLists.txt
-
2src/qt_common/externals/CMakeLists.txt
-
25src/qt_common/qt_content_util.cpp
-
3src/qt_common/qt_content_util.h
-
38src/qt_common/qt_string_lookup.h
-
2src/yuzu/CMakeLists.txt
-
107src/yuzu/data_dialog.cpp
-
46src/yuzu/data_dialog.h
-
152src/yuzu/data_dialog.ui
-
7src/yuzu/main.cpp
-
1src/yuzu/main.h
-
12src/yuzu/main.ui
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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 |
||||
@ -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()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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); })); |
||||
|
} |
||||
@ -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 |
||||
@ -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> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue