Compare commits

..

7 commits

Author SHA1 Message Date
4b0268aea4
[reuse] fix dep5
All checks were successful
eden-license / license-header (pull_request) Successful in 24s
need to do a general fixing of reuse

Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-11 00:14:09 -04:00
7112a1f061
add profile selector for saves
All checks were successful
eden-license / license-header (pull_request) Successful in 24s
Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
cbea2ff397
fix cancel/success mixup, list selector
Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
3836b16beb
[qt_common] asynchronous, cancellable import/export
Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
2071c2acb0
[desktop] feat: import/export data
Currently not the ideal solution. Can't be cancelled due to JlCompress
currently lacking a method of cancellation, but for now this is a good
prototype.

Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
6397a9b410
data dialog rewrite
Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
970c7c1681
[desktop] Initial data manager prototype
Right now, all this adds is a small dialog to the Widgets frontend
showing the user how much space is taken up by their saves, shaders,
NAND, and mods. It also gives them the choice to clear these directories
(with 2x confirmation), OR open the directory in their system file
manager.

In the future, a lot more can be done with this concept. Notably, a
common import/export (a la android) could be added, the UI can obviously
be polished, etc. We could also add this to Android, but I don't think
common import/export is needed *for* Android, and should probably be
left in qt_common.

Signed-off-by: crueter <crueter@eden-emu.dev>
2025-10-10 22:31:36 -04:00
55 changed files with 1584 additions and 160 deletions

View file

@ -13,12 +13,13 @@ Copyright: yuzu Emulator Project
License: GPL-2.0-or-later
Files: dist/qt_themes/default/icons/256x256/eden.png
dist/qt_themes/default/icons/256x256/eden_named.png
dist/yuzu.bmp
dist/yuzu.icns
dist/eden.icns
dist/eden.ico
dist/eden.svg
Copyright: yuzu Emulator Project
License: GPL-2.0-or-later
dist/dev.eden_emu.eden.svg
Copyright: 2025 Eden Emulator Project
License: GPL-3.0-or-later
Files: dist/qt_themes/qdarkstyle*/LICENSE.*
dist/qt_themes/qdarkstyle*/style.qrc
@ -155,3 +156,39 @@ License: BSD-3-Clause
Files: src/android/app/debug.keystore
Copyright: 2023 yuzu Emulator Project
License: GPL-3.0-or-later
Files: dist/qt_themes/colorful/icons/48x48/user-trash.png
dist/qt_themes/colorful/icons/48x48/upload.png
dist/qt_themes/colorful/icons/48x48/download.png
Copyright: 2014 Uri Herrera
1996-2025 KDE Software Foundation
License: LGPL-2.0-or-later
Files: dist/qt_themes/default/icons/48x48/user-trash.png
dist/qt_themes/default/icons/48x48/upload.png
dist/qt_themes/default/icons/48x48/download.png
dist/qt_themes/default_dark/icons/48x48/user-trash.png
dist/qt_themes/default_dark/icons/48x48/upload.png
dist/qt_themes/default_dark/icons/48x48/download.png
Copyright: 2025 Fonticons, Inc.
License: CC-BY-4.0
Comment: All of these icons have been modified by crueter <crueter@crueter.xyz>
Files: CMakeModules/CPM.cmake
Copyright: 2019-2023 Lars Melchior
License: MIT
Files: CMakeModules/CPMUtil.cmake
CMakeModules/CPM.cmake
CMakeModules/GetSCMRev.cmake
CMakeModules/DetectArchitecture.cmake
tools/cpm/*
tools/update-cpm.sh
tools/shellcheck.sh
docs/CPMUtil.md
**cpmfile.json
Copyright: 2025 crueter <crueter@crueter.xyz>
License: GPL-3.0-or-later
Comment: CPM.cmake has had additional modifications from crueter to better work with CPMUtil
https://git.crueter.xyz/CMake/CPMUtil
https://git.crueter.xyz/CMake/Modules

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -18,6 +18,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">icons/48x48/download.png</file>
<file alias="48x48/upload.png">icons/48x48/upload.png</file>
<file alias="48x48/list-add.png">icons/48x48/list-add.png</file>
<file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>

View file

@ -11,6 +11,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
<file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">../colorful/icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">../colorful/icons/48x48/download.png</file>
<file alias="48x48/upload.png">../colorful/icons/48x48/upload.png</file>
<file alias="48x48/list-add.png">../colorful/icons/48x48/list-add.png</file>
<file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>
<file alias="256x256/plus_folder.png">../colorful/icons/256x256/plus_folder.png</file>

View file

@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">icons/48x48/download.png</file>
<file alias="48x48/upload.png">icons/48x48/upload.png</file>
<file alias="48x48/list-add.png">icons/48x48/list-add.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>
<file alias="48x48/star.png">icons/48x48/star.png</file>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

View file

@ -13,6 +13,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<file alias="48x48/bad_folder.png">../colorful/icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">../colorful/icons/48x48/chip.png</file>
<file alias="48x48/folder.png">../colorful/icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">../colorful/icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">../colorful/icons/48x48/download.png</file>
<file alias="48x48/upload.png">../colorful/icons/48x48/upload.png</file>
<file alias="48x48/no_avatar.png">../qdarkstyle/icons/48x48/no_avatar.png</file>
<file alias="48x48/list-add.png">../colorful/icons/48x48/list-add.png</file>
<file alias="48x48/sd_card.png">../colorful/icons/48x48/sd_card.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

View file

@ -9,6 +9,9 @@
<file alias="48x48/bad_folder.png">icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">icons/48x48/chip.png</file>
<file alias="48x48/folder.png">icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">icons/48x48/download.png</file>
<file alias="48x48/upload.png">icons/48x48/upload.png</file>
<file alias="48x48/no_avatar.png">icons/48x48/no_avatar.png</file>
<file alias="48x48/list-add.png">icons/48x48/list-add.png</file>
<file alias="48x48/sd_card.png">icons/48x48/sd_card.png</file>

View file

@ -6,6 +6,9 @@
<file alias="48x48/bad_folder.png">../qdarkstyle/icons/48x48/bad_folder.png</file>
<file alias="48x48/chip.png">../qdarkstyle/icons/48x48/chip.png</file>
<file alias="48x48/folder.png">../qdarkstyle/icons/48x48/folder.png</file>
<file alias="48x48/user-trash.png">../qdarkstyle/icons/48x48/user-trash.png</file>
<file alias="48x48/download.png">../qdarkstyle/icons/48x48/download.png</file>
<file alias="48x48/upload.png">../qdarkstyle/icons/48x48/upload.png</file>
<file alias="48x48/no_avatar.png">../qdarkstyle/icons/48x48/no_avatar.png</file>
<file alias="48x48/list-add.png">../qdarkstyle/icons/48x48/list-add.png</file>
<file alias="48x48/sd_card.png">../qdarkstyle/icons/48x48/sd_card.png</file>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Before After
Before After

View file

@ -161,7 +161,7 @@ struct Values {
Category::LibraryApplet};
Setting<AppletMode> photo_viewer_applet_mode{
linkage, AppletMode::LLE, "photo_viewer_applet_mode", Category::LibraryApplet};
Setting<AppletMode> offline_web_applet_mode{linkage, AppletMode::LLE, "offline_web_applet_mode",
Setting<AppletMode> offline_web_applet_mode{linkage, AppletMode::HLE, "offline_web_applet_mode",
Category::LibraryApplet};
Setting<AppletMode> login_share_applet_mode{linkage, AppletMode::HLE, "login_share_applet_mode",
Category::LibraryApplet};

View file

@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -71,6 +68,42 @@ std::string ResolveURL(const std::string& url) {
return url.substr(0, index) + "lp1" + url.substr(index + 1);
}
WebArgInputTLVMap ReadWebArgs(const std::vector<u8>& web_arg, WebArgHeader& web_arg_header) {
std::memcpy(&web_arg_header, web_arg.data(), sizeof(WebArgHeader));
if (web_arg.size() == sizeof(WebArgHeader)) {
return {};
}
WebArgInputTLVMap input_tlv_map;
u64 current_offset = sizeof(WebArgHeader);
for (std::size_t i = 0; i < web_arg_header.total_tlv_entries; ++i) {
if (web_arg.size() < current_offset + sizeof(WebArgInputTLV)) {
return input_tlv_map;
}
WebArgInputTLV input_tlv;
std::memcpy(&input_tlv, web_arg.data() + current_offset, sizeof(WebArgInputTLV));
current_offset += sizeof(WebArgInputTLV);
if (web_arg.size() < current_offset + input_tlv.arg_data_size) {
return input_tlv_map;
}
std::vector<u8> data(input_tlv.arg_data_size);
std::memcpy(data.data(), web_arg.data() + current_offset, input_tlv.arg_data_size);
current_offset += input_tlv.arg_data_size;
input_tlv_map.insert_or_assign(input_tlv.input_tlv_type, std::move(data));
}
return input_tlv_map;
}
FileSys::VirtualFile GetOfflineRomFS(Core::System& system, u64 title_id,
FileSys::ContentRecordType nca_type) {
if (nca_type == FileSys::ContentRecordType::Data) {
@ -111,43 +144,6 @@ FileSys::VirtualFile GetOfflineRomFS(Core::System& system, u64 title_id,
}
}
#ifdef YUZU_USE_QT_WEB_ENGINE
WebArgInputTLVMap ReadWebArgs(const std::vector<u8>& web_arg, WebArgHeader& web_arg_header) {
std::memcpy(&web_arg_header, web_arg.data(), sizeof(WebArgHeader));
if (web_arg.size() == sizeof(WebArgHeader)) {
return {};
}
WebArgInputTLVMap input_tlv_map;
u64 current_offset = sizeof(WebArgHeader);
for (std::size_t i = 0; i < web_arg_header.total_tlv_entries; ++i) {
if (web_arg.size() < current_offset + sizeof(WebArgInputTLV)) {
return input_tlv_map;
}
WebArgInputTLV input_tlv;
std::memcpy(&input_tlv, web_arg.data() + current_offset, sizeof(WebArgInputTLV));
current_offset += sizeof(WebArgInputTLV);
if (web_arg.size() < current_offset + input_tlv.arg_data_size) {
return input_tlv_map;
}
std::vector<u8> data(input_tlv.arg_data_size);
std::memcpy(data.data(), web_arg.data() + current_offset, input_tlv.arg_data_size);
current_offset += input_tlv.arg_data_size;
input_tlv_map.insert_or_assign(input_tlv.input_tlv_type, std::move(data));
}
return input_tlv_map;
}
void ExtractSharedFonts(Core::System& system) {
static constexpr std::array<const char*, 7> DECRYPTED_SHARED_FONTS{
"FontStandard.ttf",
@ -225,7 +221,6 @@ void ExtractSharedFonts(Core::System& system) {
FileSys::VfsRawCopy(decrypted_font, out_file);
}
}
#endif
} // namespace
@ -237,7 +232,6 @@ WebBrowser::WebBrowser(Core::System& system_, std::shared_ptr<Applet> applet_,
WebBrowser::~WebBrowser() = default;
void WebBrowser::Initialize() {
#ifdef YUZU_USE_QT_WEB_ENGINE
FrontendApplet::Initialize();
LOG_INFO(Service_AM, "Initializing Web Browser Applet.");
@ -290,7 +284,6 @@ void WebBrowser::Initialize() {
ASSERT_MSG(false, "Invalid ShimKind={}", web_arg_header.shim_kind);
break;
}
#endif
}
Result WebBrowser::GetStatus() const {
@ -302,7 +295,6 @@ void WebBrowser::ExecuteInteractive() {
}
void WebBrowser::Execute() {
#ifdef YUZU_USE_QT_WEB_ENGINE
switch (web_arg_header.shim_kind) {
case ShimKind::Shop:
ExecuteShop();
@ -330,10 +322,6 @@ void WebBrowser::Execute() {
WebBrowserExit(WebExitReason::EndButtonPressed);
break;
}
#else
LOG_INFO(Service_AM, "Web Browser Applet disabled, skipping.");
WebBrowserExit(WebExitReason::EndButtonPressed);
#endif
}
void WebBrowser::ExtractOfflineRomFS() {

View file

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

View file

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

View file

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

View file

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

View file

@ -16,5 +16,4 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
AddJsonPackage(quazip)
# frozen
# TODO(crueter): Qt String Lookup
# AddJsonPackage(frozen)
AddJsonPackage(frozen)

View file

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

View file

@ -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);
}

View file

@ -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,183 @@ 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,
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"),
QStringLiteral("export.zip"),
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

View file

@ -6,6 +6,7 @@
#include <QObject>
#include "common/common_types.h"
#include "frontend_common/data_manager.h"
namespace QtCommon::Content {
@ -46,6 +47,10 @@ 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 = "", std::function<void()> callback = {});
void ImportDataDir(FrontendCommon::DataManager::DataDir dir, const std::string &user_id = "", std::function<void()> callback = {});
// Profiles //
void FixProfiles();
}

View file

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

View file

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

View file

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

View file

@ -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)};

View file

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

View file

@ -386,9 +386,11 @@ void BufferCache<P>::BindHostComputeBuffers() {
template <class P>
void BufferCache<P>::SetUniformBuffersState(const std::array<u32, NUM_STAGES>& mask,
const UniformBufferSizes* sizes) {
if (channel_state->enabled_uniform_buffer_masks != mask) {
channel_state->fast_bound_uniform_buffers.fill(0);
if constexpr (HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS) {
if constexpr (HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS) {
if (channel_state->enabled_uniform_buffer_masks != mask) {
if constexpr (IS_OPENGL) {
channel_state->fast_bound_uniform_buffers.fill(0);
}
channel_state->dirty_uniform_buffers.fill(~u32{0});
channel_state->uniform_buffer_binding_sizes.fill({});
}
@ -804,7 +806,7 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
channel_state->uniform_buffer_binding_sizes[stage][binding_index] != size;
if (should_fast_bind) {
// We only have to bind when the currently bound buffer is not the fast version
channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
channel_state->fast_bound_uniform_buffers[stage] |= 1U << binding_index;
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
runtime.BindFastUniformBuffer(stage, binding_index, size);
}
@ -813,8 +815,10 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
return;
}
}
channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
if constexpr (IS_OPENGL) {
channel_state->fast_bound_uniform_buffers[stage] |= 1U << binding_index;
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
}
// Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan
const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
device_memory.ReadBlockUnsafe(device_addr, span.data(), size);
@ -835,6 +839,9 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
}
const u32 offset = buffer.Offset(device_addr);
if constexpr (IS_OPENGL) {
// Fast buffer will be unbound
channel_state->fast_bound_uniform_buffers[stage] &= ~(1U << binding_index);
// Mark the index as dirty if offset doesn't match
const bool is_copy_bind = offset != 0 && !runtime.SupportsNonZeroUniformOffset();
channel_state->dirty_uniform_buffers[stage] |= (is_copy_bind ? 1U : 0U) << index;
@ -848,7 +855,6 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
} else {
runtime.BindUniformBuffer(buffer, offset, size);
}
channel_state->fast_bound_uniform_buffers[stage] &= ~(1u << binding_index);
}
template <class P>
@ -1783,7 +1789,12 @@ std::span<u8> BufferCache<P>::ImmediateBuffer(size_t wanted_capacity) {
template <class P>
bool BufferCache<P>::HasFastUniformBufferBound(size_t stage, u32 binding_index) const noexcept {
return ((channel_state->fast_bound_uniform_buffers[stage] >> binding_index) & 1u) != 0;
if constexpr (IS_OPENGL) {
return ((channel_state->fast_bound_uniform_buffers[stage] >> binding_index) & 1) != 0;
} else {
// Only OpenGL has fast uniform buffers
return false;
}
}
template <class P>

View file

@ -54,8 +54,6 @@ constexpr u32 NUM_STORAGE_BUFFERS = 16;
constexpr u32 NUM_TEXTURE_BUFFERS = 32;
constexpr u32 NUM_STAGES = 5;
static_assert(NUM_GRAPHICS_UNIFORM_BUFFERS <= 32, "fast bitmask must fit u32");
using UniformBufferSizes = std::array<std::array<u32, NUM_GRAPHICS_UNIFORM_BUFFERS>, NUM_STAGES>;
using ComputeUniformBufferSizes = std::array<u32, NUM_COMPUTE_UNIFORM_BUFFERS>;
@ -139,8 +137,8 @@ public:
u32 written_compute_texture_buffers = 0;
u32 image_compute_texture_buffers = 0;
std::array<u32, NUM_GRAPHICS_UNIFORM_BUFFERS> uniform_cache_hits{};
std::array<u32, NUM_GRAPHICS_UNIFORM_BUFFERS> uniform_cache_shots{};
std::array<u32, 16> uniform_cache_hits{};
std::array<u32, 16> uniform_cache_shots{};
u32 uniform_buffer_skip_cache_size = DEFAULT_SKIP_CACHE_SIZE;

View file

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

View file

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

View file

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

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

@ -0,0 +1,165 @@
// 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_content_util.h"
#include "qt_common/qt_frontend_util.h"
#include "qt_common/qt_progress_dialog.h"
#include "qt_common/qt_string_lookup.h"
#include "ui_data_dialog.h"
#include <QDesktopServices>
#include <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, \
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,
QWidget *parent)
: QWidget(parent)
, ui(std::make_unique<Ui::DataWidget>())
, m_dir(data_dir)
{
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);
}
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]);
}

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

@ -0,0 +1,52 @@
// 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,
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;
std::string selectProfile();
};
#endif // DATA_DIALOG_H

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

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

205
src/yuzu/data_widget.ui Normal file
View file

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

View file

@ -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());

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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