[desktop] Data Manager, data import/export (#2700)
This adds a "Data Manager" dialog to the Tools menu. The Data Manager allows for the following operations: - Open w/ system file manager - Clear - Export - Import On any of the following directories: - Save (w/ profile selector) - UserNAND - SysNAND - Mods - Shaders TODO for the future: - "Cleanup" for each directory - TitleID -> Game name--let users clean data for a specific game if applicable Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2700 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev>pull/2735/head
-
45.reuse/dep5
-
BINdist/qt_themes/colorful/icons/256x256/plus_folder.png
-
BINdist/qt_themes/colorful/icons/48x48/download.png
-
BINdist/qt_themes/colorful/icons/48x48/upload.png
-
BINdist/qt_themes/colorful/icons/48x48/user-trash.png
-
3dist/qt_themes/colorful/style.qrc
-
3dist/qt_themes/colorful_midnight_blue/style.qrc
-
3dist/qt_themes/default/default.qrc
-
BINdist/qt_themes/default/icons/256x256/eden.png
-
BINdist/qt_themes/default/icons/48x48/download.png
-
BINdist/qt_themes/default/icons/48x48/upload.png
-
BINdist/qt_themes/default/icons/48x48/user-trash.png
-
3dist/qt_themes/default_dark/style.qrc
-
BINdist/qt_themes/qdarkstyle/icons/48x48/download.png
-
BINdist/qt_themes/qdarkstyle/icons/48x48/upload.png
-
BINdist/qt_themes/qdarkstyle/icons/48x48/user-trash.png
-
3dist/qt_themes/qdarkstyle/style.qrc
-
3dist/qt_themes/qdarkstyle_midnight_blue/style.qrc
-
BINsrc/android/app/src/main/legacy/drawable/ic_icon_bg.png
-
BINsrc/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png
-
BINsrc/android/app/src/main/res/drawable/ic_icon_bg.png
-
BINsrc/android/app/src/main/res/drawable/ic_icon_bg_orig.png
-
4src/frontend_common/CMakeLists.txt
-
77src/frontend_common/data_manager.cpp
-
24src/frontend_common/data_manager.h
-
27src/qt_common/CMakeLists.txt
-
3src/qt_common/externals/CMakeLists.txt
-
354src/qt_common/qt_compress.cpp
-
71src/qt_common/qt_compress.h
-
246src/qt_common/qt_content_util.cpp
-
8src/qt_common/qt_content_util.h
-
11src/qt_common/qt_frontend_util.cpp
-
29src/qt_common/qt_frontend_util.h
-
8src/qt_common/qt_game_util.cpp
-
7src/qt_common/qt_path_util.cpp
-
38src/qt_common/qt_string_lookup.h
-
24src/yuzu/CMakeLists.txt
-
5src/yuzu/compatdb.cpp
-
2src/yuzu/configuration/configure_web.cpp
-
167src/yuzu/data_dialog.cpp
-
54src/yuzu/data_dialog.h
-
148src/yuzu/data_dialog.ui
-
205src/yuzu/data_widget.ui
-
17src/yuzu/main.cpp
-
1src/yuzu/main.h
-
30src/yuzu/main.ui
-
2src/yuzu/multiplayer/chat_room.cpp
-
2src/yuzu/multiplayer/client_room.cpp
-
2src/yuzu/multiplayer/direct_connect.cpp
-
2src/yuzu/multiplayer/host_room.cpp
-
2src/yuzu/multiplayer/lobby.cpp
|
Before Width: 256 | Height: 256 | Size: 3.5 KiB After Width: 256 | Height: 256 | Size: 3.5 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
After Width: 48 | Height: 48 | Size: 1.4 KiB |
|
Before Width: 256 | Height: 256 | Size: 4.9 KiB After Width: 256 | Height: 256 | Size: 4.9 KiB |
|
After Width: 48 | Height: 48 | Size: 853 B |
|
After Width: 48 | Height: 48 | Size: 820 B |
|
After Width: 48 | Height: 48 | Size: 584 B |
|
After Width: 48 | Height: 48 | Size: 883 B |
|
After Width: 48 | Height: 48 | Size: 853 B |
|
After Width: 48 | Height: 48 | Size: 584 B |
|
Before Width: 768 | Height: 768 | Size: 38 KiB After Width: 768 | Height: 768 | Size: 37 KiB |
|
Before Width: 1949 | Height: 1949 | Size: 438 KiB After Width: 1949 | Height: 1949 | Size: 244 KiB |
|
Before Width: 768 | Height: 768 | Size: 21 KiB After Width: 768 | Height: 768 | Size: 21 KiB |
|
Before Width: 2598 | Height: 2598 | Size: 112 KiB After Width: 2598 | Height: 2598 | Size: 112 KiB |
@ -0,0 +1,77 @@ |
|||
// 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 std::string &user_id) |
|||
{ |
|||
const fs::path nand_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir); |
|||
|
|||
switch (dir) { |
|||
case DataDir::Saves: |
|||
return (nand_dir / "user" / "save" / "0000000000000000" / user_id).string(); |
|||
case DataDir::UserNand: |
|||
return (nand_dir / "user" / "Contents" / "registered").string(); |
|||
case DataDir::SysNand: |
|||
// NB: do NOT delete save
|
|||
// that contains profile data and other stuff
|
|||
return (nand_dir / "system" / "Contents" / "registered").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, const std::string &user_id) |
|||
{ |
|||
fs::path data_dir = GetDataDir(dir, user_id); |
|||
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, const std::string &user_id = ""); |
|||
|
|||
u64 ClearDir(DataDir dir, const std::string &user_id = ""); |
|||
|
|||
const std::string ReadableBytesSize(u64 size); |
|||
|
|||
u64 DataDirSize(DataDir dir); |
|||
|
|||
}; // namespace FrontendCommon::DataManager |
|||
|
|||
#endif // DATA_MANAGER_H |
|||
@ -0,0 +1,354 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
#include "qt_compress.h"
|
|||
#include "quazipfileinfo.h"
|
|||
|
|||
#include <QDirIterator>
|
|||
|
|||
/** This is a modified version of JlCompress **/ |
|||
namespace QtCommon::Compress { |
|||
|
|||
bool compressDir(QString fileCompressed, |
|||
QString dir, |
|||
const JlCompress::Options &options, |
|||
QtCommon::QtProgressCallback callback) |
|||
{ |
|||
// Create zip
|
|||
QuaZip zip(fileCompressed); |
|||
QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); |
|||
if (!zip.open(QuaZip::mdCreate)) { |
|||
QFile::remove(fileCompressed); |
|||
return false; |
|||
} |
|||
|
|||
// See how big the overall fs structure is
|
|||
// good approx. of total progress
|
|||
// TODO(crueter): QDirListing impl
|
|||
QDirIterator iter(dir, |
|||
QDir::NoDotAndDotDot | QDir::Hidden | QDir::Files, |
|||
QDirIterator::Subdirectories); |
|||
|
|||
std::size_t total = 0; |
|||
while (iter.hasNext()) { |
|||
total += iter.nextFileInfo().size(); |
|||
} |
|||
|
|||
std::size_t progress = 0; |
|||
callback(total, progress); |
|||
|
|||
// Add the files and subdirectories
|
|||
if (!compressSubDir(&zip, dir, dir, options, total, progress, callback)) { |
|||
QFile::remove(fileCompressed); |
|||
return false; |
|||
} |
|||
|
|||
// Close zip
|
|||
zip.close(); |
|||
if (zip.getZipError() != 0) { |
|||
QFile::remove(fileCompressed); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
bool compressSubDir(QuaZip *zip, |
|||
QString dir, |
|||
QString origDir, |
|||
const JlCompress::Options &options, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtProgressCallback callback) |
|||
{ |
|||
// zip: object where to add the file
|
|||
// dir: current real directory
|
|||
// origDir: original real directory
|
|||
// (path(dir)-path(origDir)) = path inside the zip object
|
|||
|
|||
if (!zip) |
|||
return false; |
|||
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend |
|||
&& zip->getMode() != QuaZip::mdAdd) |
|||
return false; |
|||
|
|||
QDir directory(dir); |
|||
if (!directory.exists()) |
|||
return false; |
|||
|
|||
|
|||
QDir origDirectory(origDir); |
|||
if (dir != origDir) { |
|||
QuaZipFile dirZipFile(zip); |
|||
std::unique_ptr<QuaZipNewInfo> qzni; |
|||
if (options.getDateTime().isNull()) { |
|||
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir) |
|||
+ QLatin1String("/"), |
|||
dir); |
|||
} else { |
|||
qzni = std::make_unique<QuaZipNewInfo>(origDirectory.relativeFilePath(dir) |
|||
+ QLatin1String("/"), |
|||
dir, |
|||
options.getDateTime()); |
|||
} |
|||
if (!dirZipFile.open(QIODevice::WriteOnly, *qzni, nullptr, 0, 0)) { |
|||
return false; |
|||
} |
|||
dirZipFile.close(); |
|||
} |
|||
|
|||
// For each subfolder
|
|||
QFileInfoList subfiles = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot |
|||
| QDir::Hidden | QDir::Dirs); |
|||
for (const auto &file : std::as_const(subfiles)) { |
|||
if (!compressSubDir( |
|||
zip, file.absoluteFilePath(), origDir, options, total, progress, callback)) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
// For each file in directory
|
|||
QFileInfoList files = directory.entryInfoList(QDir::Hidden | QDir::Files); |
|||
for (const auto &file : std::as_const(files)) { |
|||
// If it's not a file or it's the compressed file being created
|
|||
if (!file.isFile() || file.absoluteFilePath() == zip->getZipName()) |
|||
continue; |
|||
|
|||
// Create relative name for the compressed file
|
|||
QString filename = origDirectory.relativeFilePath(file.absoluteFilePath()); |
|||
|
|||
// Compress the file
|
|||
if (!compressFile(zip, file.absoluteFilePath(), filename, options, total, progress, callback)) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
bool compressFile(QuaZip *zip, |
|||
QString fileName, |
|||
QString fileDest, |
|||
const JlCompress::Options &options, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback) |
|||
{ |
|||
// zip: object where to add the file
|
|||
// fileName: real file name
|
|||
// fileDest: file name inside the zip object
|
|||
|
|||
if (!zip) |
|||
return false; |
|||
if (zip->getMode() != QuaZip::mdCreate && zip->getMode() != QuaZip::mdAppend |
|||
&& zip->getMode() != QuaZip::mdAdd) |
|||
return false; |
|||
|
|||
QuaZipFile outFile(zip); |
|||
if (options.getDateTime().isNull()) { |
|||
if (!outFile.open(QIODevice::WriteOnly, |
|||
QuaZipNewInfo(fileDest, fileName), |
|||
nullptr, |
|||
0, |
|||
options.getCompressionMethod(), |
|||
options.getCompressionLevel())) |
|||
return false; |
|||
} else { |
|||
if (!outFile.open(QIODevice::WriteOnly, |
|||
QuaZipNewInfo(fileDest, fileName, options.getDateTime()), |
|||
nullptr, |
|||
0, |
|||
options.getCompressionMethod(), |
|||
options.getCompressionLevel())) |
|||
return false; |
|||
} |
|||
|
|||
QFileInfo input(fileName); |
|||
if (quazip_is_symlink(input)) { |
|||
// Not sure if we should use any specialized codecs here.
|
|||
// After all, a symlink IS just a byte array. And
|
|||
// this is mostly for Linux, where UTF-8 is ubiquitous these days.
|
|||
QString path = quazip_symlink_target(input); |
|||
QString relativePath = input.dir().relativeFilePath(path); |
|||
outFile.write(QFile::encodeName(relativePath)); |
|||
} else { |
|||
QFile inFile; |
|||
inFile.setFileName(fileName); |
|||
if (!inFile.open(QIODevice::ReadOnly)) { |
|||
return false; |
|||
} |
|||
if (!copyData(inFile, outFile, total, progress, callback) || outFile.getZipError() != UNZ_OK) { |
|||
return false; |
|||
} |
|||
inFile.close(); |
|||
} |
|||
|
|||
outFile.close(); |
|||
return outFile.getZipError() == UNZ_OK; |
|||
} |
|||
|
|||
bool copyData(QIODevice &inFile, |
|||
QIODevice &outFile, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtProgressCallback callback) |
|||
{ |
|||
while (!inFile.atEnd()) { |
|||
char buf[4096]; |
|||
qint64 readLen = inFile.read(buf, 4096); |
|||
if (readLen <= 0) |
|||
return false; |
|||
if (outFile.write(buf, readLen) != readLen) |
|||
return false; |
|||
|
|||
progress += readLen; |
|||
if (!callback(total, progress)) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback) |
|||
{ |
|||
// Open zip
|
|||
QuaZip zip(fileCompressed); |
|||
return extractDir(zip, dir, callback); |
|||
} |
|||
|
|||
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback) |
|||
{ |
|||
if (!zip.open(QuaZip::mdUnzip)) { |
|||
return QStringList(); |
|||
} |
|||
QString cleanDir = QDir::cleanPath(dir); |
|||
QDir directory(cleanDir); |
|||
QString absCleanDir = directory.absolutePath(); |
|||
if (!absCleanDir.endsWith(QLatin1Char('/'))) // It only ends with / if it's the FS root.
|
|||
absCleanDir += QLatin1Char('/'); |
|||
QStringList extracted; |
|||
if (!zip.goToFirstFile()) { |
|||
return QStringList(); |
|||
} |
|||
|
|||
std::size_t total = 0; |
|||
for (const QuaZipFileInfo64 &info : zip.getFileInfoList64()) { |
|||
total += info.uncompressedSize; |
|||
} |
|||
|
|||
std::size_t progress = 0; |
|||
callback(total, progress); |
|||
|
|||
do { |
|||
QString name = zip.getCurrentFileName(); |
|||
QString absFilePath = directory.absoluteFilePath(name); |
|||
QString absCleanPath = QDir::cleanPath(absFilePath); |
|||
if (!absCleanPath.startsWith(absCleanDir)) |
|||
continue; |
|||
if (!extractFile(&zip, QLatin1String(""), absFilePath, total, progress, callback)) { |
|||
removeFile(extracted); |
|||
return QStringList(); |
|||
} |
|||
extracted.append(absFilePath); |
|||
} while (zip.goToNextFile()); |
|||
|
|||
// Close zip
|
|||
zip.close(); |
|||
if (zip.getZipError() != 0) { |
|||
removeFile(extracted); |
|||
return QStringList(); |
|||
} |
|||
|
|||
return extracted; |
|||
} |
|||
|
|||
bool extractFile(QuaZip *zip, |
|||
QString fileName, |
|||
QString fileDest, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback) |
|||
{ |
|||
// zip: object where to add the file
|
|||
// filename: real file name
|
|||
// fileincompress: file name of the compressed file
|
|||
|
|||
if (!zip) |
|||
return false; |
|||
if (zip->getMode() != QuaZip::mdUnzip) |
|||
return false; |
|||
|
|||
if (!fileName.isEmpty()) |
|||
zip->setCurrentFile(fileName); |
|||
QuaZipFile inFile(zip); |
|||
if (!inFile.open(QIODevice::ReadOnly) || inFile.getZipError() != UNZ_OK) |
|||
return false; |
|||
|
|||
// Check existence of resulting file
|
|||
QDir curDir; |
|||
if (fileDest.endsWith(QLatin1String("/"))) { |
|||
if (!curDir.mkpath(fileDest)) { |
|||
return false; |
|||
} |
|||
} else { |
|||
if (!curDir.mkpath(QFileInfo(fileDest).absolutePath())) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
QuaZipFileInfo64 info; |
|||
if (!zip->getCurrentFileInfo(&info)) |
|||
return false; |
|||
|
|||
QFile::Permissions srcPerm = info.getPermissions(); |
|||
if (fileDest.endsWith(QLatin1String("/")) && QFileInfo(fileDest).isDir()) { |
|||
if (srcPerm != 0) { |
|||
QFile(fileDest).setPermissions(srcPerm); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
if (info.isSymbolicLink()) { |
|||
QString target = QFile::decodeName(inFile.readAll()); |
|||
return QFile::link(target, fileDest); |
|||
} |
|||
|
|||
// Open resulting file
|
|||
QFile outFile; |
|||
outFile.setFileName(fileDest); |
|||
if (!outFile.open(QIODevice::WriteOnly)) |
|||
return false; |
|||
|
|||
// Copy data
|
|||
if (!copyData(inFile, outFile, total, progress, callback) || inFile.getZipError() != UNZ_OK) { |
|||
outFile.close(); |
|||
removeFile(QStringList(fileDest)); |
|||
return false; |
|||
} |
|||
outFile.close(); |
|||
|
|||
// Close file
|
|||
inFile.close(); |
|||
if (inFile.getZipError() != UNZ_OK) { |
|||
removeFile(QStringList(fileDest)); |
|||
return false; |
|||
} |
|||
|
|||
if (srcPerm != 0) { |
|||
outFile.setPermissions(srcPerm); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
bool removeFile(QStringList listFile) |
|||
{ |
|||
bool ret = true; |
|||
// For each file
|
|||
for (int i = 0; i < listFile.count(); i++) { |
|||
// Remove
|
|||
ret = ret && QFile::remove(listFile.at(i)); |
|||
} |
|||
return ret; |
|||
} |
|||
|
|||
} // namespace QtCommon::Compress
|
|||
@ -0,0 +1,71 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <QDir> |
|||
#include <QString> |
|||
#include <JlCompress.h> |
|||
#include "qt_common/qt_common.h" |
|||
|
|||
/** This is a modified version of JlCompress **/ |
|||
namespace QtCommon::Compress { |
|||
|
|||
/** |
|||
* @brief Compress an entire directory and report its progress. |
|||
* @param fileCompressed Destination file |
|||
* @param dir The directory to compress |
|||
* @param options Compression level, etc |
|||
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled. |
|||
*/ |
|||
bool compressDir(QString fileCompressed, |
|||
QString dir, |
|||
const JlCompress::Options& options = JlCompress::Options(), |
|||
QtCommon::QtProgressCallback callback = {}); |
|||
|
|||
// Internal // |
|||
bool compressSubDir(QuaZip *zip, |
|||
QString dir, |
|||
QString origDir, |
|||
const JlCompress::Options &options, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback); |
|||
|
|||
bool compressFile(QuaZip *zip, |
|||
QString fileName, |
|||
QString fileDest, |
|||
const JlCompress::Options &options, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback); |
|||
|
|||
bool copyData(QIODevice &inFile, |
|||
QIODevice &outFile, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback); |
|||
|
|||
// Extract // |
|||
|
|||
/** |
|||
* @brief Extract a zip file and report its progress. |
|||
* @param fileCompressed Compressed file |
|||
* @param dir The directory to push the results to |
|||
* @param callback Callback that takes in two std::size_t (total, progress) and returns false if the current operation should be cancelled. |
|||
*/ |
|||
QStringList extractDir(QString fileCompressed, QString dir, QtCommon::QtProgressCallback callback = {}); |
|||
|
|||
// Internal // |
|||
QStringList extractDir(QuaZip &zip, const QString &dir, QtCommon::QtProgressCallback callback); |
|||
|
|||
bool extractFile(QuaZip *zip, |
|||
QString fileName, |
|||
QString fileDest, |
|||
std::size_t total, |
|||
std::size_t &progress, |
|||
QtCommon::QtProgressCallback callback); |
|||
|
|||
bool removeFile(QStringList listFile); |
|||
|
|||
} |
|||
@ -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, "Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU'RE DOING!"}, |
|||
{ShadersTooltip, "Contains Vulkan and OpenGL pipeline caches. Generally safe to remove."}, |
|||
{UserNandTooltip, "Contains updates and DLC for games."}, |
|||
{SysNandTooltip, "Contains firmware and applet data."}, |
|||
{ModsTooltip, "Contains game mods, patches, and cheats."}, |
|||
}; |
|||
|
|||
static inline const QString Lookup(StringKey key) |
|||
{ |
|||
return QString::fromStdString(strings.at(key).data()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
#include "data_dialog.h"
|
|||
#include "core/hle/service/acc/profile_manager.h"
|
|||
#include "frontend_common/data_manager.h"
|
|||
#include "qt_common/qt_common.h"
|
|||
#include "qt_common/qt_content_util.h"
|
|||
#include "qt_common/qt_string_lookup.h"
|
|||
#include "ui_data_dialog.h"
|
|||
|
|||
#include <QDesktopServices>
|
|||
#include <QFileDialog>
|
|||
#include <QFutureWatcher>
|
|||
#include <QProgressDialog>
|
|||
#include <QtConcurrentRun>
|
|||
|
|||
#include <core/frontend/applets/profile_select.h>
|
|||
|
|||
#include <applets/qt_profile_select.h>
|
|||
|
|||
DataDialog::DataDialog(QWidget *parent) |
|||
: QDialog(parent) |
|||
, ui(std::make_unique<Ui::DataDialog>()) |
|||
{ |
|||
ui->setupUi(this); |
|||
|
|||
// TODO: Should we make this a single widget that pulls data from a model?
|
|||
#define WIDGET(name) \
|
|||
ui->page->addWidget(new DataWidget(FrontendCommon::DataManager::DataDir::name, \ |
|||
QtCommon::StringLookup::name##Tooltip, \ |
|||
QStringLiteral(#name), \ |
|||
this)); |
|||
|
|||
WIDGET(Saves) |
|||
WIDGET(Shaders) |
|||
WIDGET(UserNand) |
|||
WIDGET(SysNand) |
|||
WIDGET(Mods) |
|||
|
|||
#undef WIDGET
|
|||
|
|||
connect(ui->labels, &QListWidget::itemSelectionChanged, this, [this]() { |
|||
const auto items = ui->labels->selectedItems(); |
|||
if (items.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
ui->page->setCurrentIndex(ui->labels->row(items[0])); |
|||
}); |
|||
} |
|||
|
|||
DataDialog::~DataDialog() = default; |
|||
|
|||
DataWidget::DataWidget(FrontendCommon::DataManager::DataDir data_dir, |
|||
QtCommon::StringLookup::StringKey tooltip, |
|||
const QString &exportName, |
|||
QWidget *parent) |
|||
: QWidget(parent) |
|||
, ui(std::make_unique<Ui::DataWidget>()) |
|||
, m_dir(data_dir) |
|||
, m_exportName(exportName) |
|||
{ |
|||
ui->setupUi(this); |
|||
|
|||
ui->tooltip->setText(QtCommon::StringLookup::Lookup(tooltip)); |
|||
|
|||
ui->clear->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"))); |
|||
ui->open->setIcon(QIcon::fromTheme(QStringLiteral("folder"))); |
|||
ui->upload->setIcon(QIcon::fromTheme(QStringLiteral("upload"))); |
|||
ui->download->setIcon(QIcon::fromTheme(QStringLiteral("download"))); |
|||
|
|||
connect(ui->clear, &QPushButton::clicked, this, &DataWidget::clear); |
|||
connect(ui->open, &QPushButton::clicked, this, &DataWidget::open); |
|||
connect(ui->upload, &QPushButton::clicked, this, &DataWidget::upload); |
|||
connect(ui->download, &QPushButton::clicked, this, &DataWidget::download); |
|||
|
|||
scan(); |
|||
} |
|||
|
|||
void DataWidget::clear() |
|||
{ |
|||
std::string user_id{}; |
|||
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
|||
user_id = selectProfile(); |
|||
} |
|||
QtCommon::Content::ClearDataDir(m_dir, user_id); |
|||
scan(); |
|||
} |
|||
|
|||
void DataWidget::open() |
|||
{ |
|||
std::string user_id{}; |
|||
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
|||
user_id = selectProfile(); |
|||
} |
|||
QDesktopServices::openUrl(QUrl::fromLocalFile( |
|||
QString::fromStdString(FrontendCommon::DataManager::GetDataDir(m_dir, user_id)))); |
|||
} |
|||
|
|||
void DataWidget::upload() |
|||
{ |
|||
std::string user_id{}; |
|||
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
|||
user_id = selectProfile(); |
|||
} |
|||
QtCommon::Content::ExportDataDir(m_dir, user_id, m_exportName); |
|||
} |
|||
|
|||
void DataWidget::download() |
|||
{ |
|||
std::string user_id{}; |
|||
if (m_dir == FrontendCommon::DataManager::DataDir::Saves) { |
|||
user_id = selectProfile(); |
|||
} |
|||
QtCommon::Content::ImportDataDir(m_dir, user_id, std::bind(&DataWidget::scan, this)); |
|||
} |
|||
|
|||
void DataWidget::scan() { |
|||
ui->size->setText(tr("Calculating...")); |
|||
|
|||
QFutureWatcher<u64> *watcher = new QFutureWatcher<u64>(this); |
|||
|
|||
connect(watcher, &QFutureWatcher<u64>::finished, this, [=, this]() { |
|||
u64 size = watcher->result(); |
|||
ui->size->setText( |
|||
QString::fromStdString(FrontendCommon::DataManager::ReadableBytesSize(size))); |
|||
watcher->deleteLater(); |
|||
}); |
|||
|
|||
watcher->setFuture( |
|||
QtConcurrent::run([this]() { return FrontendCommon::DataManager::DataDirSize(m_dir); })); |
|||
} |
|||
|
|||
std::string DataWidget::selectProfile() |
|||
{ |
|||
const auto select_profile = [this] { |
|||
const Core::Frontend::ProfileSelectParameters parameters{ |
|||
.mode = Service::AM::Frontend::UiMode::UserSelector, |
|||
.invalid_uid_list = {}, |
|||
.display_options = {}, |
|||
.purpose = Service::AM::Frontend::UserSelectionPurpose::General, |
|||
}; |
|||
QtProfileSelectionDialog dialog(*QtCommon::system, this, parameters); |
|||
dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint |
|||
| Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); |
|||
dialog.setWindowModality(Qt::WindowModal); |
|||
|
|||
if (dialog.exec() == QDialog::Rejected) { |
|||
return -1; |
|||
} |
|||
|
|||
return dialog.GetIndex(); |
|||
}; |
|||
|
|||
const auto index = select_profile(); |
|||
if (index == -1) { |
|||
return ""; |
|||
} |
|||
|
|||
const auto uuid = QtCommon::system->GetProfileManager().GetUser(static_cast<std::size_t>(index)); |
|||
ASSERT(uuid); |
|||
|
|||
const auto user_id = uuid->AsU128(); |
|||
|
|||
return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// 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 "frontend_common/data_manager.h" |
|||
#include "qt_common/qt_string_lookup.h" |
|||
|
|||
#include "ui_data_widget.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 DataWidget : public QWidget |
|||
{ |
|||
Q_OBJECT |
|||
public: |
|||
explicit DataWidget(FrontendCommon::DataManager::DataDir data_dir, |
|||
QtCommon::StringLookup::StringKey tooltip, |
|||
const QString &exportName, |
|||
QWidget *parent = nullptr); |
|||
|
|||
public slots: |
|||
void clear(); |
|||
void open(); |
|||
void upload(); |
|||
void download(); |
|||
|
|||
void scan(); |
|||
|
|||
private: |
|||
std::unique_ptr<Ui::DataWidget> ui; |
|||
FrontendCommon::DataManager::DataDir m_dir; |
|||
const QString m_exportName; |
|||
|
|||
std::string selectProfile(); |
|||
}; |
|||
|
|||
#endif // DATA_DIALOG_H |
|||
@ -0,0 +1,148 @@ |
|||
<?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>480</width> |
|||
<height>320</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> |
|||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,1"> |
|||
<item> |
|||
<widget class="QListWidget" name="labels"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<item> |
|||
<property name="text"> |
|||
<string>Saves</string> |
|||
</property> |
|||
</item> |
|||
<item> |
|||
<property name="text"> |
|||
<string>Shaders</string> |
|||
</property> |
|||
</item> |
|||
<item> |
|||
<property name="text"> |
|||
<string>UserNAND</string> |
|||
</property> |
|||
</item> |
|||
<item> |
|||
<property name="text"> |
|||
<string>SysNAND</string> |
|||
</property> |
|||
</item> |
|||
<item> |
|||
<property name="text"> |
|||
<string>Mods</string> |
|||
</property> |
|||
</item> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QStackedWidget" name="page"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>275</width> |
|||
<height>200</height> |
|||
</size> |
|||
</property> |
|||
<property name="currentIndex"> |
|||
<number>-1</number> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item> |
|||
<layout class="QHBoxLayout" name="horizontalLayout_2"> |
|||
<property name="leftMargin"> |
|||
<number>10</number> |
|||
</property> |
|||
<item> |
|||
<widget class="QLabel" name="label"> |
|||
<property name="text"> |
|||
<string>Deleting ANY data is IRREVERSABLE!</string> |
|||
</property> |
|||
</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> |
|||
</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> |
|||
@ -0,0 +1,205 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<ui version="4.0"> |
|||
<class>DataWidget</class> |
|||
<widget class="QWidget" name="DataWidget"> |
|||
<property name="geometry"> |
|||
<rect> |
|||
<x>0</x> |
|||
<y>0</y> |
|||
<width>275</width> |
|||
<height>200</height> |
|||
</rect> |
|||
</property> |
|||
<property name="windowTitle"> |
|||
<string>Form</string> |
|||
</property> |
|||
<layout class="QGridLayout" name="gridLayout"> |
|||
<item row="0" column="0"> |
|||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="3,2"> |
|||
<item> |
|||
<widget class="QLabel" name="tooltip"> |
|||
<property name="text"> |
|||
<string>Tooltip</string> |
|||
</property> |
|||
<property name="alignment"> |
|||
<set>Qt::AlignmentFlag::AlignCenter</set> |
|||
</property> |
|||
<property name="wordWrap"> |
|||
<bool>true</bool> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QLabel" name="size"> |
|||
<property name="font"> |
|||
<font> |
|||
<pointsize>10</pointsize> |
|||
<bold>true</bold> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string notr="true">Size</string> |
|||
</property> |
|||
<property name="alignment"> |
|||
<set>Qt::AlignmentFlag::AlignCenter</set> |
|||
</property> |
|||
<property name="wordWrap"> |
|||
<bool>true</bool> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item row="1" column="0"> |
|||
<layout class="QHBoxLayout" name="horizontalLayout_2"> |
|||
<item> |
|||
<widget class="QPushButton" name="open"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>1</horstretch> |
|||
<verstretch>1</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>52</width> |
|||
<height>47</height> |
|||
</size> |
|||
</property> |
|||
<property name="toolTip"> |
|||
<string>Open with your system file manager</string> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">QPushButton { |
|||
max-width: 50px; |
|||
max-height: 45px; |
|||
min-width: 50px; |
|||
min-height: 45px; |
|||
}</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
<property name="iconSize"> |
|||
<size> |
|||
<width>28</width> |
|||
<height>28</height> |
|||
</size> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QPushButton" name="clear"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>1</horstretch> |
|||
<verstretch>1</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>52</width> |
|||
<height>47</height> |
|||
</size> |
|||
</property> |
|||
<property name="toolTip"> |
|||
<string>Delete all data in this directory. THIS IS 100% IRREVERSABLE!</string> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">QPushButton { |
|||
max-width: 50px; |
|||
max-height: 45px; |
|||
min-width: 50px; |
|||
min-height: 45px; |
|||
}</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
<property name="iconSize"> |
|||
<size> |
|||
<width>28</width> |
|||
<height>28</height> |
|||
</size> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QPushButton" name="upload"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>1</horstretch> |
|||
<verstretch>1</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>52</width> |
|||
<height>47</height> |
|||
</size> |
|||
</property> |
|||
<property name="toolTip"> |
|||
<string>Export all data in this directory. This may take a while!</string> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">QPushButton { |
|||
max-width: 50px; |
|||
max-height: 45px; |
|||
min-width: 50px; |
|||
min-height: 45px; |
|||
}</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
<property name="iconSize"> |
|||
<size> |
|||
<width>28</width> |
|||
<height>28</height> |
|||
</size> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item> |
|||
<widget class="QPushButton" name="download"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
|||
<horstretch>1</horstretch> |
|||
<verstretch>1</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>52</width> |
|||
<height>47</height> |
|||
</size> |
|||
</property> |
|||
<property name="toolTip"> |
|||
<string>Import data for this directory. This may take a while, and will delete ALL EXISTING DATA!</string> |
|||
</property> |
|||
<property name="styleSheet"> |
|||
<string notr="true">QPushButton { |
|||
max-width: 50px; |
|||
max-height: 45px; |
|||
min-width: 50px; |
|||
min-height: 45px; |
|||
}</string> |
|||
</property> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
<property name="iconSize"> |
|||
<size> |
|||
<width>28</width> |
|||
<height>28</height> |
|||
</size> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
<resources/> |
|||
<connections/> |
|||
</ui> |
|||