aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcrueter <crueter@eden-emu.dev>2025-10-15 04:54:41 +0200
committercrueter <crueter@eden-emu.dev>2025-10-15 04:54:41 +0200
commit5f9dba40a0fe71ccd7014ae37a2fbfd8f225396e (patch)
tree4a0258d989bb795dd68fc2067fb01a7d7703868b /src
parent0a54ac63f00782605b74044e1661569d931354fe (diff)
[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>
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/legacy/drawable/ic_icon_bg.pngbin39026 -> 37636 bytes
-rw-r--r--src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.pngbin448880 -> 249669 bytes
-rw-r--r--src/android/app/src/main/res/drawable/ic_icon_bg.pngbin21355 -> 21310 bytes
-rw-r--r--src/android/app/src/main/res/drawable/ic_icon_bg_orig.pngbin115013 -> 114970 bytes
-rw-r--r--src/frontend_common/CMakeLists.txt4
-rw-r--r--src/frontend_common/data_manager.cpp77
-rw-r--r--src/frontend_common/data_manager.h24
-rw-r--r--src/qt_common/CMakeLists.txt27
-rw-r--r--src/qt_common/externals/CMakeLists.txt3
-rw-r--r--src/qt_common/qt_compress.cpp354
-rw-r--r--src/qt_common/qt_compress.h71
-rw-r--r--src/qt_common/qt_content_util.cpp246
-rw-r--r--src/qt_common/qt_content_util.h8
-rw-r--r--src/qt_common/qt_frontend_util.cpp11
-rw-r--r--src/qt_common/qt_frontend_util.h29
-rw-r--r--src/qt_common/qt_game_util.cpp8
-rw-r--r--src/qt_common/qt_path_util.cpp7
-rw-r--r--src/qt_common/qt_string_lookup.h38
-rw-r--r--src/yuzu/CMakeLists.txt24
-rw-r--r--src/yuzu/compatdb.cpp5
-rw-r--r--src/yuzu/configuration/configure_web.cpp2
-rw-r--r--src/yuzu/data_dialog.cpp167
-rw-r--r--src/yuzu/data_dialog.h54
-rw-r--r--src/yuzu/data_dialog.ui148
-rw-r--r--src/yuzu/data_widget.ui205
-rw-r--r--src/yuzu/main.cpp17
-rw-r--r--src/yuzu/main.h1
-rw-r--r--src/yuzu/main.ui30
-rw-r--r--src/yuzu/multiplayer/chat_room.cpp2
-rw-r--r--src/yuzu/multiplayer/client_room.cpp2
-rw-r--r--src/yuzu/multiplayer/direct_connect.cpp2
-rw-r--r--src/yuzu/multiplayer/host_room.cpp2
-rw-r--r--src/yuzu/multiplayer/lobby.cpp2
33 files changed, 1475 insertions, 95 deletions
diff --git a/src/android/app/src/main/legacy/drawable/ic_icon_bg.png b/src/android/app/src/main/legacy/drawable/ic_icon_bg.png
index 3327014f8f..f3d4c55dea 100644
--- a/src/android/app/src/main/legacy/drawable/ic_icon_bg.png
+++ b/src/android/app/src/main/legacy/drawable/ic_icon_bg.png
Binary files differ
diff --git a/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png b/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png
index a9fc55a4f5..5859c43f86 100644
--- a/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png
+++ b/src/android/app/src/main/legacy/drawable/ic_icon_bg_orig.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg.png b/src/android/app/src/main/res/drawable/ic_icon_bg.png
index 7da2c6a1a3..db7e53410d 100644
--- a/src/android/app/src/main/res/drawable/ic_icon_bg.png
+++ b/src/android/app/src/main/res/drawable/ic_icon_bg.png
Binary files differ
diff --git a/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png b/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png
index 18325c031a..ddb43b349c 100644
--- a/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png
+++ b/src/android/app/src/main/res/drawable/ic_icon_bg_orig.png
Binary files differ
diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt
index 70e142bb0c..aa44740596 100644
--- a/src/frontend_common/CMakeLists.txt
+++ b/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)
diff --git a/src/frontend_common/data_manager.cpp b/src/frontend_common/data_manager.cpp
new file mode 100644
index 0000000000..83eccbde17
--- /dev/null
+++ b/src/frontend_common/data_manager.cpp
@@ -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;
+}
+
+}
diff --git a/src/frontend_common/data_manager.h b/src/frontend_common/data_manager.h
new file mode 100644
index 0000000000..d67bd20467
--- /dev/null
+++ b/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, 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
diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt
index 2c47a37526..cdad6679d9 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,8 @@ 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
+ qt_compress.h qt_compress.cpp
)
@@ -37,9 +36,29 @@ if (ENABLE_QT)
target_link_libraries(qt_common PRIVATE Qt6::Widgets)
endif()
+target_compile_definitions(qt_common PUBLIC
+ # Use QStringBuilder for string concatenation to reduce
+ # the overall number of temporary strings created.
+ QT_USE_QSTRINGBUILDER
+
+ # Disable implicit conversions from/to C strings
+ QT_NO_CAST_FROM_ASCII
+ QT_NO_CAST_TO_ASCII
+
+ # Disable implicit type narrowing in signal/slot connect() calls.
+ QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
+
+ # Disable unsafe overloads of QProcess' start() function.
+ QT_NO_PROCESS_COMBINED_ARGUMENT_START
+
+ # Disable implicit QString->QUrl conversions to enforce use of proper resolving functions.
+ QT_NO_URL_CAST_FROM_STRING
+)
+
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 Qt6::Concurrent 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..b998090d90 100644
--- a/src/qt_common/externals/CMakeLists.txt
+++ b/src/qt_common/externals/CMakeLists.txt
@@ -16,5 +16,4 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
AddJsonPackage(quazip)
# frozen
-# TODO(crueter): Qt String Lookup
-# AddJsonPackage(frozen)
+AddJsonPackage(frozen)
diff --git a/src/qt_common/qt_compress.cpp b/src/qt_common/qt_compress.cpp
new file mode 100644
index 0000000000..1fc5c4bb2c
--- /dev/null
+++ b/src/qt_common/qt_compress.cpp
@@ -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
diff --git a/src/qt_common/qt_compress.h b/src/qt_common/qt_compress.h
new file mode 100644
index 0000000000..003a87ac87
--- /dev/null
+++ b/src/qt_common/qt_compress.h
@@ -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);
+
+}
diff --git a/src/qt_common/qt_content_util.cpp b/src/qt_common/qt_content_util.cpp
index 2f659cf1b2..f0a6a3ad80 100644
--- a/src/qt_common/qt_content_util.cpp
+++ b/src/qt_common/qt_content_util.cpp
@@ -1,17 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
-#include "qt_common/qt_game_util.h"
#include "qt_content_util.h"
#include "common/fs/fs.h"
#include "core/hle/service/acc/profile_manager.h"
#include "frontend_common/content_manager.h"
+#include "frontend_common/data_manager.h"
#include "frontend_common/firmware_manager.h"
#include "qt_common/qt_common.h"
+#include "qt_common/qt_compress.h"
+#include "qt_common/qt_game_util.h"
#include "qt_common/qt_progress_dialog.h"
#include "qt_frontend_util.h"
+#include <QFuture>
+#include <QtConcurrentRun>
#include <JlCompress.h>
+#include <qfuturewatcher.h>
namespace QtCommon::Content {
@@ -21,10 +26,10 @@ bool CheckGameFirmware(u64 program_id, QObject* parent)
&& !FirmwareManager::CheckFirmwarePresence(*system)) {
auto result = QtCommon::Frontend::ShowMessage(
QMessageBox::Warning,
- "Game Requires Firmware",
- "The game you are trying to launch requires firmware to boot or to get past the "
- "opening menu. Please <a href='https://yuzu-mirror.github.io/help/quickstart'>"
- "dump and install firmware</a>, or press \"OK\" to launch anyways.",
+ tr("Game Requires Firmware"),
+ tr("The game you are trying to launch requires firmware to boot or to get past the "
+ "opening menu. Please <a href='https://yuzu-mirror.github.io/help/quickstart'>"
+ "dump and install firmware</a>, or press \"OK\" to launch anyways."),
QMessageBox::Ok | QMessageBox::Cancel,
parent);
@@ -60,8 +65,8 @@ void InstallFirmware(const QString& location, bool recursive)
const auto ShowMessage = [&]() {
QtCommon::Frontend::ShowMessage(icon,
- failedTitle,
- GetFirmwareInstallResultString(result));
+ tr(failedTitle),
+ tr(GetFirmwareInstallResultString(result)));
};
LOG_INFO(Frontend, "Installing firmware from {}", location.toStdString());
@@ -125,8 +130,8 @@ void InstallFirmware(const QString& location, bool recursive)
i++;
auto firmware_src_vfile = vfs->OpenFile(firmware_src_path.generic_string(),
FileSys::OpenMode::Read);
- auto firmware_dst_vfile = firmware_vdir
- ->CreateFileRelative(firmware_src_path.filename().string());
+ auto firmware_dst_vfile = firmware_vdir->CreateFileRelative(
+ firmware_src_path.filename().string());
if (!VfsRawCopy(firmware_src_vfile, firmware_dst_vfile)) {
LOG_ERROR(Frontend,
@@ -168,9 +173,9 @@ void InstallFirmware(const QString& location, bool recursive)
const auto failed_names = QString::fromStdString(
fmt::format("{}", fmt::join(results, "\n")));
progress.close();
- QtCommon::Frontend::Critical(tr("Firmware integrity verification failed!"),
- tr("Verification failed for the following files:\n\n%1")
- .arg(failed_names));
+ QtCommon::Frontend::Critical(
+ tr("Firmware integrity verification failed!"),
+ tr("Verification failed for the following files:\n\n%1").arg(failed_names));
return;
}
@@ -181,10 +186,10 @@ void InstallFirmware(const QString& location, bool recursive)
const std::string display_version(firmware_data.display_version.data());
result = FirmwareInstallResult::Success;
- QtCommon::Frontend::Information(rootObject,
- tr(successTitle),
- tr(GetFirmwareInstallResultString(result))
- .arg(QString::fromStdString(display_version)));
+ QtCommon::Frontend::Information(
+ rootObject,
+ tr(successTitle),
+ tr(GetFirmwareInstallResultString(result)).arg(QString::fromStdString(display_version)));
}
QString UnzipFirmwareToTmp(const QString& location)
@@ -193,7 +198,7 @@ QString UnzipFirmwareToTmp(const QString& location)
fs::path tmp{fs::temp_directory_path()};
if (!fs::create_directories(tmp / "eden" / "firmware")) {
- return "";
+ return QString();
}
tmp /= "eden";
@@ -205,7 +210,7 @@ QString UnzipFirmwareToTmp(const QString& location)
QStringList result = JlCompress::extractDir(&zip, qCacheDir);
if (result.isEmpty()) {
- return "";
+ return QString();
}
return qCacheDir;
@@ -264,9 +269,8 @@ void InstallKeys()
return;
}
- FirmwareManager::KeyInstallResult result = FirmwareManager::InstallKeys(key_source_location
- .toStdString(),
- "keys");
+ FirmwareManager::KeyInstallResult result
+ = FirmwareManager::InstallKeys(key_source_location.toStdString(), "keys");
system->GetFileSystemController().CreateFactories(*QtCommon::vfs);
@@ -282,9 +286,14 @@ void InstallKeys()
}
}
-void VerifyInstalledContents() {
+void VerifyInstalledContents()
+{
// Initialize a progress dialog.
- QtCommon::Frontend::QtProgressDialog progress(tr("Verifying integrity..."), tr("Cancel"), 0, 100, QtCommon::rootObject);
+ QtCommon::Frontend::QtProgressDialog progress(tr("Verifying integrity..."),
+ tr("Cancel"),
+ 0,
+ 100,
+ QtCommon::rootObject);
progress.setWindowModality(Qt::WindowModal);
progress.setMinimumDuration(100);
progress.setAutoClose(false);
@@ -296,16 +305,17 @@ void VerifyInstalledContents() {
return progress.wasCanceled();
};
- const std::vector<std::string> result =
- ContentManager::VerifyInstalledContents(*QtCommon::system, *QtCommon::provider, QtProgressCallback);
+ const std::vector<std::string> result
+ = ContentManager::VerifyInstalledContents(*QtCommon::system,
+ *QtCommon::provider,
+ QtProgressCallback);
progress.close();
if (result.empty()) {
QtCommon::Frontend::Information(tr("Integrity verification succeeded!"),
tr("The operation completed successfully."));
} else {
- const auto failed_names =
- QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
+ const auto failed_names = QString::fromStdString(fmt::format("{}", fmt::join(result, "\n")));
QtCommon::Frontend::Critical(
tr("Integrity verification failed!"),
tr("Verification failed for the following files:\n\n%1").arg(failed_names));
@@ -332,7 +342,7 @@ void FixProfiles()
qorphaned.reserve(8 * 33);
for (const std::string& s : orphaned) {
- qorphaned += "\n" + QString::fromStdString(s);
+ qorphaned = qorphaned % QStringLiteral("\n") % QString::fromStdString(s);
}
QtCommon::Frontend::Critical(
@@ -348,4 +358,184 @@ void FixProfiles()
QtCommon::Game::OpenSaveFolder();
}
+void ClearDataDir(FrontendCommon::DataManager::DataDir dir, const std::string& user_id)
+{
+ auto result = QtCommon::Frontend::Warning(tr("Really clear data?"),
+ tr("Important data may be lost!"),
+ QMessageBox::Yes | QMessageBox::No);
+
+ if (result != QMessageBox::Yes)
+ return;
+
+ result = QtCommon::Frontend::Warning(
+ tr("Are you REALLY sure?"),
+ tr("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, user_id);
+
+ dialog.close();
+}
+
+void ExportDataDir(FrontendCommon::DataManager::DataDir data_dir,
+ const std::string& user_id,
+ const QString& name,
+ std::function<void()> callback)
+{
+ using namespace QtCommon::Frontend;
+ const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
+
+ const QString zip_dump_location = GetSaveFileName(tr("Select Export Location"),
+ tr("%1.zip").arg(name),
+ tr("Zipped Archives (*.zip)"));
+
+ if (zip_dump_location.isEmpty())
+ return;
+
+ QtProgressDialog* progress = new QtProgressDialog(
+ tr("Exporting data. This may take a while..."), tr("Cancel"), 0, 100, rootObject);
+
+ progress->setWindowTitle(tr("Exporting"));
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(100);
+ progress->setAutoClose(false);
+ progress->setAutoReset(false);
+ progress->show();
+
+ QGuiApplication::processEvents();
+
+ auto progress_callback = [=](size_t total_size, size_t processed_size) {
+ QMetaObject::invokeMethod(progress,
+ &QtProgressDialog::setValue,
+ static_cast<int>((processed_size * 100) / total_size));
+
+ return !progress->wasCanceled();
+ };
+
+ QFuture<bool> future = QtConcurrent::run([=]() {
+ return QtCommon::Compress::compressDir(zip_dump_location,
+ QString::fromStdString(dir),
+ JlCompress::Options(),
+ progress_callback);
+ });
+
+ QFutureWatcher<bool>* watcher = new QFutureWatcher<bool>(rootObject);
+
+ QObject::connect(watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
+ progress->close();
+
+ if (watcher->result()) {
+ Information(tr("Exported Successfully"), tr("Data was exported successfully."));
+ } else if (progress->wasCanceled()) {
+ Information(tr("Export Cancelled"), tr("Export was cancelled by the user."));
+ } else {
+ Critical(
+ tr("Export Failed"),
+ tr("Ensure you have write permissions on the targeted directory and try again."));
+ }
+
+ progress->deleteLater();
+ watcher->deleteLater();
+ if (callback)
+ callback();
+ });
+
+ watcher->setFuture(future);
+}
+
+void ImportDataDir(FrontendCommon::DataManager::DataDir data_dir,
+ const std::string& user_id,
+ std::function<void()> callback)
+{
+ const std::string dir = FrontendCommon::DataManager::GetDataDir(data_dir, user_id);
+
+ using namespace QtCommon::Frontend;
+
+ const QString zip_dump_location = GetOpenFileName(tr("Select Import Location"),
+ {},
+ tr("Zipped Archives (*.zip)"));
+
+ if (zip_dump_location.isEmpty())
+ return;
+
+ StandardButton button = Warning(
+ tr("Import Warning"),
+ tr("All previous data in this directory will be deleted. Are you sure you wish to "
+ "proceed?"),
+ StandardButton::Yes | StandardButton::No);
+
+ if (button != QMessageBox::Yes)
+ return;
+
+ QtProgressDialog* progress = new QtProgressDialog(
+ tr("Importing data. This may take a while..."), tr("Cancel"), 0, 100, rootObject);
+
+ progress->setWindowTitle(tr("Importing"));
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(100);
+ progress->setAutoClose(false);
+ progress->setAutoReset(false);
+ progress->show();
+ progress->setValue(0);
+
+ QGuiApplication::processEvents();
+
+ // to prevent GUI mangling we have to run this in a thread as well
+ QFuture<bool> delete_future = QtConcurrent::run([=]() {
+ FrontendCommon::DataManager::ClearDir(data_dir, user_id);
+ return !progress->wasCanceled();
+ });
+
+ QFutureWatcher<bool>* delete_watcher = new QFutureWatcher<bool>(rootObject);
+ delete_watcher->setFuture(delete_future);
+
+ QObject::connect(delete_watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
+ auto progress_callback = [=](size_t total_size, size_t processed_size) {
+ QMetaObject::invokeMethod(progress,
+ &QtProgressDialog::setValue,
+ static_cast<int>((processed_size * 100) / total_size));
+
+ return !progress->wasCanceled();
+ };
+
+ QFuture<bool> future = QtConcurrent::run([=]() {
+ return !QtCommon::Compress::extractDir(zip_dump_location,
+ QString::fromStdString(dir),
+ progress_callback)
+ .empty();
+ });
+
+ QFutureWatcher<bool>* watcher = new QFutureWatcher<bool>(rootObject);
+
+ QObject::connect(watcher, &QFutureWatcher<bool>::finished, rootObject, [=]() {
+ progress->close();
+
+ // this sucks
+ if (watcher->result()) {
+ Information(tr("Imported Successfully"), tr("Data was imported successfully."));
+ } else if (progress->wasCanceled()) {
+ Information(tr("Import Cancelled"), tr("Import was cancelled by the user."));
+ } else {
+ Critical(tr("Import Failed"),
+ tr("Ensure you have read permissions on the targeted directory and try "
+ "again."));
+ }
+
+ progress->deleteLater();
+ watcher->deleteLater();
+ if (callback)
+ callback();
+ });
+
+ watcher->setFuture(future);
+ });
+}
+
} // namespace QtCommon::Content
diff --git a/src/qt_common/qt_content_util.h b/src/qt_common/qt_content_util.h
index b95e78c0a0..29d45d5766 100644
--- a/src/qt_common/qt_content_util.h
+++ b/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,13 @@ void InstallKeys();
void VerifyGameContents(const std::string &game_path);
void VerifyInstalledContents();
+void ClearDataDir(FrontendCommon::DataManager::DataDir dir, const std::string &user_id = "");
+void ExportDataDir(FrontendCommon::DataManager::DataDir dir,
+ const std::string &user_id = "",
+ const QString &name = QStringLiteral("export"),
+ std::function<void()> callback = {});
+void ImportDataDir(FrontendCommon::DataManager::DataDir dir, const std::string &user_id = "", std::function<void()> callback = {});
+
// Profiles //
void FixProfiles();
}
diff --git a/src/qt_common/qt_frontend_util.cpp b/src/qt_common/qt_frontend_util.cpp
index d519669ad5..3fe0ba0a80 100644
--- a/src/qt_common/qt_frontend_util.cpp
+++ b/src/qt_common/qt_frontend_util.cpp
@@ -32,4 +32,15 @@ const QString GetOpenFileName(const QString &title,
#endif
}
+const QString GetSaveFileName(const QString &title,
+ const QString &dir,
+ const QString &filter,
+ QString *selectedFilter,
+ Options options)
+{
+#ifdef YUZU_QT_WIDGETS
+ return QFileDialog::getSaveFileName((QWidget *) rootObject, title, dir, filter, selectedFilter, options);
+#endif
+}
+
} // namespace QtCommon::Frontend
diff --git a/src/qt_common/qt_frontend_util.h b/src/qt_common/qt_frontend_util.h
index f86b9e1357..59e8f15a4e 100644
--- a/src/qt_common/qt_frontend_util.h
+++ b/src/qt_common/qt_frontend_util.h
@@ -97,10 +97,10 @@ Q_ENUM_NS(Icon)
// TODO(crueter) widgets-less impl, choices et al.
StandardButton ShowMessage(Icon icon,
- const QString &title,
- const QString &text,
- StandardButtons buttons = StandardButton::NoButton,
- QObject *parent = nullptr);
+ const QString &title,
+ const QString &text,
+ StandardButtons buttons = StandardButton::NoButton,
+ QObject *parent = nullptr);
#define UTIL_OVERRIDES(level) \
inline StandardButton level(QObject *parent, \
@@ -110,21 +110,6 @@ StandardButton ShowMessage(Icon icon,
{ \
return ShowMessage(Icon::level, title, text, buttons, parent); \
} \
- inline StandardButton level(QObject *parent, \
- const char *title, \
- const char *text, \
- StandardButtons buttons \
- = StandardButton::Ok) \
- { \
- return ShowMessage(Icon::level, tr(title), tr(text), buttons, parent); \
- } \
- inline StandardButton level(const char *title, \
- const char *text, \
- StandardButtons buttons \
- = StandardButton::Ok) \
- { \
- return ShowMessage(Icon::level, tr(title), tr(text), buttons, rootObject); \
- } \
inline StandardButton level(const QString title, \
const QString &text, \
StandardButtons buttons \
@@ -144,5 +129,11 @@ const QString GetOpenFileName(const QString &title,
QString *selectedFilter = nullptr,
Options options = Options());
+const QString GetSaveFileName(const QString &title,
+ const QString &dir,
+ const QString &filter,
+ QString *selectedFilter = nullptr,
+ Options options = Options());
+
} // namespace QtCommon::Frontend
#endif // QT_FRONTEND_UTIL_H
diff --git a/src/qt_common/qt_game_util.cpp b/src/qt_common/qt_game_util.cpp
index ac922ea967..0eddd10fe1 100644
--- a/src/qt_common/qt_game_util.cpp
+++ b/src/qt_common/qt_game_util.cpp
@@ -220,8 +220,8 @@ void RemoveBaseContent(u64 program_id, InstalledEntryType type)
program_id);
if (res) {
QtCommon::Frontend::Information(rootObject,
- "Successfully Removed",
- "Successfully removed the installed base game.");
+ tr("Successfully Removed"),
+ tr("Successfully removed the installed base game."));
} else {
QtCommon::Frontend::Warning(
rootObject,
@@ -235,8 +235,8 @@ void RemoveUpdateContent(u64 program_id, InstalledEntryType type)
const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
if (res) {
QtCommon::Frontend::Information(rootObject,
- "Successfully Removed",
- "Successfully removed the installed update.");
+ tr("Successfully Removed"),
+ tr("Successfully removed the installed update."));
} else {
QtCommon::Frontend::Warning(rootObject,
GetGameListErrorRemoving(type),
diff --git a/src/qt_common/qt_path_util.cpp b/src/qt_common/qt_path_util.cpp
index 761e6e8405..632424e0ca 100644
--- a/src/qt_common/qt_path_util.cpp
+++ b/src/qt_common/qt_path_util.cpp
@@ -17,7 +17,12 @@ bool OpenShaderCache(u64 program_id, QObject *parent)
const auto shader_cache_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::ShaderDir);
const auto shader_cache_folder_path{shader_cache_dir / fmt::format("{:016x}", program_id)};
if (!Common::FS::CreateDirs(shader_cache_folder_path)) {
- QtCommon::Frontend::ShowMessage(QMessageBox::Warning, "Error Opening Shader Cache", "Failed to create or open shader cache for this title, ensure your app data directory has write permissions.", QMessageBox::Ok, parent);
+ QtCommon::Frontend::ShowMessage(QMessageBox::Warning,
+ tr("Error Opening Shader Cache"),
+ tr("Failed to create or open shader cache for this title, "
+ "ensure your app data directory has write permissions."),
+ QMessageBox::Ok,
+ parent);
}
const auto shader_path_string{Common::FS::PathToUTF8String(shader_cache_folder_path)};
diff --git a/src/qt_common/qt_string_lookup.h b/src/qt_common/qt_string_lookup.h
new file mode 100644
index 0000000000..de6acac8a1
--- /dev/null
+++ b/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, "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());
+}
+
+}
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index f6f752af3b..481d7c2f48 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -234,6 +234,9 @@ add_executable(yuzu
deps_dialog.cpp
deps_dialog.h
deps_dialog.ui
+
+ data_dialog.h data_dialog.cpp data_dialog.ui
+ data_widget.ui
)
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
@@ -390,32 +393,13 @@ endif()
target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common)
-target_link_libraries(yuzu PRIVATE Boost::headers glad Qt6::Widgets)
+target_link_libraries(yuzu PRIVATE Boost::headers glad Qt6::Widgets Qt6::Concurrent)
target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
if (UNIX AND NOT APPLE)
target_link_libraries(yuzu PRIVATE Qt6::DBus)
endif()
-target_compile_definitions(yuzu PRIVATE
- # Use QStringBuilder for string concatenation to reduce
- # the overall number of temporary strings created.
- QT_USE_QSTRINGBUILDER
-
- # Disable implicit conversions from/to C strings
- QT_NO_CAST_FROM_ASCII
- QT_NO_CAST_TO_ASCII
-
- # Disable implicit type narrowing in signal/slot connect() calls.
- QT_NO_NARROWING_CONVERSIONS_IN_CONNECT
-
- # Disable unsafe overloads of QProcess' start() function.
- QT_NO_PROCESS_COMBINED_ARGUMENT_START
-
- # Disable implicit QString->QUrl conversions to enforce use of proper resolving functions.
- QT_NO_URL_CAST_FROM_STRING
-)
-
if (YUZU_ENABLE_COMPATIBILITY_REPORTING)
target_compile_definitions(yuzu PRIVATE YUZU_ENABLE_COMPATIBILITY_REPORTING)
endif()
diff --git a/src/yuzu/compatdb.cpp b/src/yuzu/compatdb.cpp
index 0e58f17405..6d5d263e16 100644
--- a/src/yuzu/compatdb.cpp
+++ b/src/yuzu/compatdb.cpp
@@ -1,10 +1,13 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QButtonGroup>
#include <QMessageBox>
#include <QPushButton>
-#include <QtConcurrent/qtconcurrentrun.h>
+#include <qtconcurrentrun.h>
#include "common/logging/log.h"
#include "ui_compatdb.h"
#include "yuzu/compatdb.h"
diff --git a/src/yuzu/configuration/configure_web.cpp b/src/yuzu/configuration/configure_web.cpp
index 15a0029901..7a693be10c 100644
--- a/src/yuzu/configuration/configure_web.cpp
+++ b/src/yuzu/configuration/configure_web.cpp
@@ -14,7 +14,7 @@
#include <QRegExpValidator>
#endif
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/settings.h"
#include "ui_configure_web.h"
#include "qt_common/uisettings.h"
diff --git a/src/yuzu/data_dialog.cpp b/src/yuzu/data_dialog.cpp
new file mode 100644
index 0000000000..bae49ff351
--- /dev/null
+++ b/src/yuzu/data_dialog.cpp
@@ -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]);
+}
diff --git a/src/yuzu/data_dialog.h b/src/yuzu/data_dialog.h
new file mode 100644
index 0000000000..70714be7cb
--- /dev/null
+++ b/src/yuzu/data_dialog.h
@@ -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
diff --git a/src/yuzu/data_dialog.ui b/src/yuzu/data_dialog.ui
new file mode 100644
index 0000000000..06751e2fb1
--- /dev/null
+++ b/src/yuzu/data_dialog.ui
@@ -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>
diff --git a/src/yuzu/data_widget.ui b/src/yuzu/data_widget.ui
new file mode 100644
index 0000000000..99010d8856
--- /dev/null
+++ b/src/yuzu/data_widget.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>
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 2c991b9208..77bfe5db9f 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() {
@@ -2406,7 +2408,6 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target
ASSERT_MSG(has_user_save != has_device_save, "Game uses both user and device savedata?");
- // TODO(alekpop): It returns the wrong user
switch (target) {
case GameListOpenTarget::SaveData: {
open_target = tr("Save Data");
@@ -2792,6 +2793,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
}
void GMainWindow::OnGameListOpenDirectory(const QString& directory) {
+ // TODO(crueter): QtCommon
std::filesystem::path fs_path;
if (directory == QStringLiteral("SDMC")) {
fs_path =
@@ -3934,6 +3936,15 @@ void GMainWindow::OnEdenDependencies() {
depsDialog.exec();
}
+void GMainWindow::OnDataDialog() {
+ DataDialog dataDialog(this);
+ dataDialog.exec();
+
+ // refresh stuff in case it was cleared
+ OnGameListRefresh();
+
+}
+
void GMainWindow::OnToggleFilterBar() {
game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked());
if (ui->action_Show_Filter_Bar->isChecked()) {
@@ -4474,11 +4485,15 @@ void GMainWindow::SetFirmwareVersion() {
if (result.IsError() || !CheckFirmwarePresence()) {
LOG_INFO(Frontend, "Installed firmware: No firmware available");
+ ui->menu_Applets->setEnabled(false);
+ ui->menu_Create_Shortcuts->setEnabled(false);
firmware_label->setVisible(false);
return;
}
firmware_label->setVisible(true);
+ ui->menu_Applets->setEnabled(true);
+ ui->menu_Create_Shortcuts->setEnabled(true);
const std::string display_version(firmware_data.display_version.data());
const std::string display_title(firmware_data.display_title.data());
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..12ff4efdf1 100644
--- a/src/yuzu/main.ui
+++ b/src/yuzu/main.ui
@@ -158,13 +158,23 @@
</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"/>
<addaction name="action_Load_Cabinet_Restorer"/>
<addaction name="action_Load_Cabinet_Formatter"/>
</widget>
+ <widget class="QMenu" name="menu_Applets">
+ <property name="title">
+ <string>&amp;Applets</string>
+ </property>
+ <addaction name="action_Load_Home_Menu"/>
+ <addaction name="action_Load_Album"/>
+ <addaction name="action_Load_Mii_Edit"/>
+ <addaction name="action_Open_Controller_Menu"/>
+ <addaction name="action_Open_Setup"/>
+ </widget>
<widget class="QMenu" name="menuTAS">
<property name="title">
<string>&amp;TAS</string>
@@ -184,7 +194,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,13 +202,10 @@
<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"/>
- <addaction name="action_Load_Mii_Edit"/>
- <addaction name="action_Open_Controller_Menu"/>
- <addaction name="action_Load_Home_Menu"/>
- <addaction name="action_Open_Setup"/>
+ <addaction name="menu_Applets"/>
<addaction name="menu_Create_Shortcuts"/>
<addaction name="separator"/>
<addaction name="action_Capture_Screenshot"/>
@@ -497,12 +504,12 @@
</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">
<property name="text">
- <string>Open Home Menu</string>
+ <string>Open &amp;Home Menu</string>
</property>
</action>
<action name="action_Discord">
@@ -593,6 +600,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"/>
diff --git a/src/yuzu/multiplayer/chat_room.cpp b/src/yuzu/multiplayer/chat_room.cpp
index 4c2c41ea2a..53beda0f8e 100644
--- a/src/yuzu/multiplayer/chat_room.cpp
+++ b/src/yuzu/multiplayer/chat_room.cpp
@@ -16,7 +16,7 @@
#include <QMetaType>
#include <QTime>
#include <QUrl>
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/logging/log.h"
#include "network/announce_multiplayer_session.h"
#include "ui_chat_room.h"
diff --git a/src/yuzu/multiplayer/client_room.cpp b/src/yuzu/multiplayer/client_room.cpp
index 93d6662c1e..4e995c044f 100644
--- a/src/yuzu/multiplayer/client_room.cpp
+++ b/src/yuzu/multiplayer/client_room.cpp
@@ -10,7 +10,7 @@
#include <QLocale>
#include <QMetaType>
#include <QTime>
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/logging/log.h"
#include "network/announce_multiplayer_session.h"
#include "ui_client_room.h"
diff --git a/src/yuzu/multiplayer/direct_connect.cpp b/src/yuzu/multiplayer/direct_connect.cpp
index deac3b9e59..6291979fe6 100644
--- a/src/yuzu/multiplayer/direct_connect.cpp
+++ b/src/yuzu/multiplayer/direct_connect.cpp
@@ -9,7 +9,7 @@
#include <QIntValidator>
#include <QRegularExpressionValidator>
#include <QString>
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/settings.h"
#include "core/core.h"
#include "core/internal_network/network_interface.h"
diff --git a/src/yuzu/multiplayer/host_room.cpp b/src/yuzu/multiplayer/host_room.cpp
index 4dd3958550..cc163a5420 100644
--- a/src/yuzu/multiplayer/host_room.cpp
+++ b/src/yuzu/multiplayer/host_room.cpp
@@ -12,7 +12,7 @@
#include <QMessageBox>
#include <QMetaType>
#include <QTime>
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
diff --git a/src/yuzu/multiplayer/lobby.cpp b/src/yuzu/multiplayer/lobby.cpp
index 84723041df..e8daa0c6eb 100644
--- a/src/yuzu/multiplayer/lobby.cpp
+++ b/src/yuzu/multiplayer/lobby.cpp
@@ -6,7 +6,7 @@
#include <QInputDialog>
#include <QList>
-#include <QtConcurrent/QtConcurrentRun>
+#include <QtConcurrentRun>
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"