diff --git a/src/citron/CMakeLists.txt b/src/citron/CMakeLists.txt index da419989d..46c82f28e 100644 --- a/src/citron/CMakeLists.txt +++ b/src/citron/CMakeLists.txt @@ -180,6 +180,11 @@ add_executable(citron hotkeys.h hotkey_profile_manager.cpp hotkey_profile_manager.h + custom_metadata.cpp + custom_metadata.h + custom_metadata_dialog.cpp + custom_metadata_dialog.h + custom_metadata_dialog.ui install_dialog.cpp install_dialog.h loading_screen.cpp diff --git a/src/citron/custom_metadata.cpp b/src/citron/custom_metadata.cpp new file mode 100644 index 000000000..2bea22b1f --- /dev/null +++ b/src/citron/custom_metadata.cpp @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include "citron/custom_metadata.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" + +namespace Citron { + +CustomMetadata::CustomMetadata() { + Load(); +} + +CustomMetadata::~CustomMetadata() = default; + +std::optional CustomMetadata::GetCustomTitle(u64 program_id) const { + auto it = metadata.find(program_id); + if (it != metadata.end() && !it->second.title.empty()) { + return it->second.title; + } + return std::nullopt; +} + +std::optional CustomMetadata::GetCustomIconPath(u64 program_id) const { + auto it = metadata.find(program_id); + if (it != metadata.end() && !it->second.icon_path.empty()) { + if (Common::FS::Exists(it->second.icon_path)) { + return it->second.icon_path; + } + } + return std::nullopt; +} + +void CustomMetadata::SetCustomTitle(u64 program_id, const std::string& title) { + metadata[program_id].title = title; + Save(); +} + +void CustomMetadata::SetCustomIcon(u64 program_id, const std::string& icon_path) { + metadata[program_id].icon_path = icon_path; + Save(); +} + +void CustomMetadata::RemoveCustomMetadata(u64 program_id) { + metadata.erase(program_id); + Save(); +} + +void CustomMetadata::Save() { + const auto custom_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "custom_metadata"; + const auto custom_file = Common::FS::PathToUTF8String(custom_dir / "custom_metadata.json"); + + void(Common::FS::CreateParentDirs(custom_file)); + + QJsonObject root; + QJsonArray entries; + + for (const auto& [program_id, data] : metadata) { + QJsonObject entry; + entry[QStringLiteral("program_id")] = QString::number(program_id, 16); + entry[QStringLiteral("title")] = QString::fromStdString(data.title); + entry[QStringLiteral("icon_path")] = QString::fromStdString(data.icon_path); + entries.append(entry); + } + + root[QStringLiteral("entries")] = entries; + + QFile file(QString::fromStdString(custom_file)); + if (file.open(QFile::WriteOnly)) { + const QJsonDocument doc(root); + file.write(doc.toJson()); + } else { + LOG_ERROR(Frontend, "Failed to open custom metadata file for writing: {}", custom_file); + } +} + +void CustomMetadata::Load() { + const auto custom_dir = + Common::FS::GetCitronPath(Common::FS::CitronPath::ConfigDir) / "custom_metadata"; + const auto custom_file = Common::FS::PathToUTF8String(custom_dir / "custom_metadata.json"); + + if (!Common::FS::Exists(custom_file)) { + return; + } + + QFile file(QString::fromStdString(custom_file)); + if (!file.open(QFile::ReadOnly)) { + LOG_ERROR(Frontend, "Failed to open custom metadata file for reading: {}", custom_file); + return; + } + + const QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); + if (!doc.isObject()) { + return; + } + + metadata.clear(); + const QJsonObject root = doc.object(); + const QJsonArray entries = root[QStringLiteral("entries")].toArray(); + + for (const QJsonValue& value : entries) { + const QJsonObject entry = value.toObject(); + const u64 program_id = + entry[QStringLiteral("program_id")].toString().toULongLong(nullptr, 16); + + CustomGameMetadata data; + data.title = entry[QStringLiteral("title")].toString().toStdString(); + data.icon_path = entry[QStringLiteral("icon_path")].toString().toStdString(); + + metadata[program_id] = std::move(data); + } +} + +} // namespace Citron diff --git a/src/citron/custom_metadata.h b/src/citron/custom_metadata.h new file mode 100644 index 000000000..fe4608a06 --- /dev/null +++ b/src/citron/custom_metadata.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include "common/common_types.h" + +namespace Citron { + +struct CustomGameMetadata { + std::string title; + std::string icon_path; +}; + +class CustomMetadata { +public: + static CustomMetadata& GetInstance() { + static CustomMetadata instance; + return instance; + } + + ~CustomMetadata(); + + [[nodiscard]] std::optional GetCustomTitle(u64 program_id) const; + [[nodiscard]] std::optional GetCustomIconPath(u64 program_id) const; + + void SetCustomTitle(u64 program_id, const std::string& title); + void SetCustomIcon(u64 program_id, const std::string& icon_path); + void RemoveCustomMetadata(u64 program_id); + + void Save(); + void Load(); + +private: + explicit CustomMetadata(); + std::unordered_map metadata; +}; + +} // namespace Citron diff --git a/src/citron/custom_metadata_dialog.cpp b/src/citron/custom_metadata_dialog.cpp new file mode 100644 index 000000000..16a7fa775 --- /dev/null +++ b/src/citron/custom_metadata_dialog.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include "citron/custom_metadata.h" +#include "citron/custom_metadata_dialog.h" +#include "ui_custom_metadata_dialog.h" + +CustomMetadataDialog::CustomMetadataDialog(QWidget* parent, u64 program_id_, + const std::string& current_title) + : QDialog(parent), ui(std::make_unique()), program_id(program_id_) { + ui->setupUi(this); + ui->title_edit->setText(QString::fromStdString(current_title)); + + if (auto current_icon_path = + Citron::CustomMetadata::GetInstance().GetCustomIconPath(program_id)) { + icon_path = *current_icon_path; + UpdatePreview(); + } + + connect(ui->select_icon_button, &QPushButton::clicked, this, + &CustomMetadataDialog::OnSelectIcon); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &CustomMetadataDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CustomMetadataDialog::reject); + connect(ui->buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, this, [this] { + was_reset = true; + accept(); + }); +} + +CustomMetadataDialog::~CustomMetadataDialog() = default; + +std::string CustomMetadataDialog::GetTitle() const { + return ui->title_edit->text().toStdString(); +} + +std::string CustomMetadataDialog::GetIconPath() const { + return icon_path; +} + +bool CustomMetadataDialog::WasReset() const { + return was_reset; +} + +void CustomMetadataDialog::OnSelectIcon() { + const QString path = QFileDialog::getOpenFileName(this, tr("Select Icon"), QString(), + tr("Images (*.png *.jpg *.jpeg *.gif)")); + + if (!path.isEmpty()) { + icon_path = path.toStdString(); + UpdatePreview(); + } +} + +void CustomMetadataDialog::UpdatePreview() { + if (movie) { + movie->stop(); + delete movie; + movie = nullptr; + } + + if (icon_path.empty()) { + ui->icon_preview->setPixmap(QPixmap()); + return; + } + + const QString qpath = QString::fromStdString(icon_path); + if (qpath.endsWith(QStringLiteral(".gif"), Qt::CaseInsensitive)) { + movie = new QMovie(qpath, QByteArray(), this); + if (movie->isValid()) { + ui->icon_preview->setMovie(movie); + movie->start(); + } + } else { + QPixmap pixmap(qpath); + if (!pixmap.isNull()) { + QPixmap rounded(pixmap.size()); + rounded.fill(Qt::transparent); + QPainter painter(&rounded); + painter.setRenderHint(QPainter::Antialiasing); + QPainterPath path; + const int radius = pixmap.width() / 6; + path.addRoundedRect(rounded.rect(), radius, radius); + painter.setClipPath(path); + painter.drawPixmap(0, 0, pixmap); + + ui->icon_preview->setPixmap(rounded.scaled( + ui->icon_preview->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + } +} diff --git a/src/citron/custom_metadata_dialog.h b/src/citron/custom_metadata_dialog.h new file mode 100644 index 000000000..218ba2153 --- /dev/null +++ b/src/citron/custom_metadata_dialog.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace Ui { +class CustomMetadataDialog; +} + +class QMovie; + +class CustomMetadataDialog : public QDialog { + Q_OBJECT + +public: + explicit CustomMetadataDialog(QWidget* parent, u64 program_id, + const std::string& current_title); + ~CustomMetadataDialog() override; + + [[nodiscard]] std::string GetTitle() const; + [[nodiscard]] std::string GetIconPath() const; + [[nodiscard]] bool WasReset() const; + +private slots: + void OnSelectIcon(); + +private: + void UpdatePreview(); + + std::unique_ptr ui; + u64 program_id; + std::string icon_path; + QMovie* movie = nullptr; + bool was_reset = false; +}; diff --git a/src/citron/custom_metadata_dialog.ui b/src/citron/custom_metadata_dialog.ui new file mode 100644 index 000000000..2894c3b48 --- /dev/null +++ b/src/citron/custom_metadata_dialog.ui @@ -0,0 +1,131 @@ + + + CustomMetadataDialog + + + + 0 + 0 + 400 + 300 + + + + Edit Game Metadata + + + + + + + + Title: + + + + + + + + + + + + + + Icon: + + + + + + + Select Icon... + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 128 + 128 + + + + + 128 + 128 + + + + + + + false + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset + + + + + + + + diff --git a/src/citron/game_list.cpp b/src/citron/game_list.cpp index a85c6ca93..fce7e00ab 100644 --- a/src/citron/game_list.cpp +++ b/src/citron/game_list.cpp @@ -43,6 +43,8 @@ #include #include #include "citron/compatibility_list.h" +#include "citron/custom_metadata.h" +#include "citron/custom_metadata_dialog.h" #include "citron/game_list.h" #include "citron/game_list_p.h" #include "citron/game_list_worker.h" @@ -1617,8 +1619,28 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri shortcut_menu->addAction(tr("Add to Applications Menu")); #endif context_menu.addSeparator(); + QAction* edit_metadata = context_menu.addAction(tr("Edit Metadata")); QAction* properties = context_menu.addAction(tr("Properties")); + connect(edit_metadata, &QAction::triggered, [this, program_id, game_name] { + CustomMetadataDialog dialog(this, program_id, game_name.toStdString()); + if (dialog.exec() == QDialog::Accepted) { + auto& custom_metadata = Citron::CustomMetadata::GetInstance(); + if (dialog.WasReset()) { + custom_metadata.RemoveCustomMetadata(program_id); + } else { + custom_metadata.SetCustomTitle(program_id, dialog.GetTitle()); + const std::string icon_path = dialog.GetIconPath(); + if (!icon_path.empty()) { + custom_metadata.SetCustomIcon(program_id, icon_path); + } + } + if (main_window) { + main_window->RefreshGameList(); + } + } + }); + favorite->setVisible(program_id != 0); favorite->setCheckable(true); favorite->setChecked(UISettings::values.favorited_ids.contains(program_id)); diff --git a/src/citron/game_list_p.h b/src/citron/game_list_p.h index dbbebdb1b..df4218f0e 100644 --- a/src/citron/game_list_p.h +++ b/src/citron/game_list_p.h @@ -25,7 +25,6 @@ #include "common/logging/log.h" #include "common/string_util.h" - enum class GameListItemType { Game = QStandardItem::UserType + 1, CustomDir = QStandardItem::UserType + 2, @@ -70,8 +69,10 @@ static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) { painter.setClipPath(path); // Draw the scaled pixmap - QPixmap scaled = pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - painter.drawPixmap(0, 0, scaled); + QPixmap scaled = pixmap.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + int x = (size - scaled.width()) / 2; + int y = (size - scaled.height()) / 2; + painter.drawPixmap(x, y, scaled); return rounded; } diff --git a/src/citron/game_list_worker.cpp b/src/citron/game_list_worker.cpp index 894c73508..f14557ca5 100644 --- a/src/citron/game_list_worker.cpp +++ b/src/citron/game_list_worker.cpp @@ -22,6 +22,7 @@ #include #include "citron/compatibility_list.h" +#include "citron/custom_metadata.h" #include "citron/game_list.h" #include "citron/game_list_p.h" #include "citron/game_list_worker.h" @@ -347,11 +348,26 @@ std::pair, std::string> GetGameListCachedObject( void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca, std::vector& icon, std::string& name) { - std::tie(icon, name) = GetGameListCachedObject( - fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] { + const auto program_id = patch_manager.GetTitleID(); + auto& custom_metadata = Citron::CustomMetadata::GetInstance(); + + std::tie(icon, name) = + GetGameListCachedObject(fmt::format("{:016X}", program_id), {}, [&patch_manager, &nca] { const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca); return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName()); }); + + if (auto custom_title = custom_metadata.GetCustomTitle(program_id)) { + name = *custom_title; + } + + if (auto custom_icon_path = custom_metadata.GetCustomIconPath(program_id)) { + QFile icon_file(QString::fromStdString(*custom_icon_path)); + if (icon_file.open(QFile::ReadOnly)) { + const QByteArray data = icon_file.readAll(); + icon.assign(data.begin(), data.end()); + } + } } bool HasSupportedFileExtension(const std::string& file_name) { @@ -645,10 +661,28 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa system.GetContentProvider()}; auto loader = Loader::GetLoader(system, file); if (loader) { - auto entry = MakeGameListEntry(physical_name, cached->title, - cached->file_size, cached->icon, *loader, - cached->program_id, compatibility_list, - play_time_manager, patch, online_stats); + std::string title = cached->title; + std::vector icon = cached->icon; + + auto& custom_metadata = Citron::CustomMetadata::GetInstance(); + if (auto custom_title = + custom_metadata.GetCustomTitle(cached->program_id)) { + title = *custom_title; + } + + if (auto custom_icon_path = + custom_metadata.GetCustomIconPath(cached->program_id)) { + QFile icon_file(QString::fromStdString(*custom_icon_path)); + if (icon_file.open(QFile::ReadOnly)) { + const QByteArray data = icon_file.readAll(); + icon.assign(data.begin(), data.end()); + } + } + + auto entry = + MakeGameListEntry(physical_name, title, cached->file_size, icon, + *loader, cached->program_id, compatibility_list, + play_time_manager, patch, online_stats); RecordEvent( [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); }); } @@ -705,6 +739,20 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa std::string name = " "; loader->ReadIcon(icon); loader->ReadTitle(name); + + auto& custom_metadata = Citron::CustomMetadata::GetInstance(); + if (auto custom_title = custom_metadata.GetCustomTitle(program_id)) { + name = *custom_title; + } + + if (auto custom_icon_path = custom_metadata.GetCustomIconPath(program_id)) { + QFile icon_file(QString::fromStdString(*custom_icon_path)); + if (icon_file.open(QFile::ReadOnly)) { + const QByteArray data = icon_file.readAll(); + icon.assign(data.begin(), data.end()); + } + } + std::size_t file_size = Common::FS::GetSize(physical_name); CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon); diff --git a/src/citron/loading_screen.cpp b/src/citron/loading_screen.cpp index f2b9f7a3f..7b77d6bdc 100644 --- a/src/citron/loading_screen.cpp +++ b/src/citron/loading_screen.cpp @@ -2,12 +2,14 @@ // SPDX-FileCopyrightText: 2025 citron Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "citron/loading_screen.h" #include +#include #include #include #include #include +#include "citron/custom_metadata.h" +#include "citron/loading_screen.h" #include "citron/theme.h" #include "core/frontend/framebuffer_layout.h" #include "core/loader/loader.h" @@ -35,7 +37,8 @@ LoadingScreen::LoadingScreen(QWidget* parent) }); loading_text_animation_timer = new QTimer(this); - connect(loading_text_animation_timer, &QTimer::timeout, this, &LoadingScreen::UpdateLoadingText); + connect(loading_text_animation_timer, &QTimer::timeout, this, + &LoadingScreen::UpdateLoadingText); connect(this, &LoadingScreen::LoadProgress, this, &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); @@ -48,37 +51,92 @@ LoadingScreen::~LoadingScreen() { void LoadingScreen::Prepare(Loader::AppLoader& loader) { QPixmap game_icon_pixmap; - std::vector buffer; - if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) { - game_icon_pixmap.loadFromData(buffer.data(), static_cast(buffer.size())); - } else { - game_icon_pixmap = QPixmap(QStringLiteral(":/icons/scalable/actions/games.svg")); + u64 program_id = 0; + loader.ReadProgramId(program_id); + auto& custom_metadata = Citron::CustomMetadata::GetInstance(); + + bool is_custom_icon = false; + if (auto custom_icon_path = custom_metadata.GetCustomIconPath(program_id)) { + if (custom_icon_path->ends_with(".gif")) { + if (movie) { + movie->stop(); + delete movie; + } + movie = new QMovie(QString::fromStdString(*custom_icon_path), QByteArray(), this); + if (movie->isValid()) { + ui->game_icon->setMovie(movie); + movie->setScaledSize(ui->game_icon->size()); + movie->start(); + is_custom_icon = true; + } + } else { + game_icon_pixmap.load(QString::fromStdString(*custom_icon_path)); + // Custom icons should also be rounded + if (!game_icon_pixmap.isNull()) { + QPixmap rounded_pixmap(game_icon_pixmap.size()); + rounded_pixmap.fill(Qt::transparent); + QPainter painter(&rounded_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + QPainterPath path; + const int radius = game_icon_pixmap.width() / 6; + path.addRoundedRect(rounded_pixmap.rect(), radius, radius); + painter.setClipPath(path); + painter.drawPixmap(0, 0, game_icon_pixmap); + game_icon_pixmap = rounded_pixmap; + is_custom_icon = true; + } + } + } + + if (!is_custom_icon) { + if (movie) { + movie->stop(); + ui->game_icon->setMovie(nullptr); + } + std::vector buffer; + if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) { + game_icon_pixmap.loadFromData(buffer.data(), static_cast(buffer.size())); + } else { + game_icon_pixmap = QPixmap(QStringLiteral(":/icons/scalable/actions/games.svg")); + } } if (!game_icon_pixmap.isNull()) { - QPixmap rounded_pixmap(game_icon_pixmap.size()); - rounded_pixmap.fill(Qt::transparent); - QPainter painter(&rounded_pixmap); - painter.setRenderHint(QPainter::Antialiasing); - QPainterPath path; - const int radius = game_icon_pixmap.width() / 6; - path.addRoundedRect(rounded_pixmap.rect(), radius, radius); - painter.setClipPath(path); - painter.drawPixmap(0, 0, game_icon_pixmap); - ui->game_icon->setPixmap(rounded_pixmap.scaled(ui->game_icon->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); - } else { - ui->game_icon->setPixmap(game_icon_pixmap); + // Apply rounding if not already done (standard icons need this) + if (!is_custom_icon) { + QPixmap rounded_pixmap(game_icon_pixmap.size()); + rounded_pixmap.fill(Qt::transparent); + QPainter painter(&rounded_pixmap); + painter.setRenderHint(QPainter::Antialiasing); + QPainterPath path; + const int radius = game_icon_pixmap.width() / 6; + path.addRoundedRect(rounded_pixmap.rect(), radius, radius); + painter.setClipPath(path); + painter.drawPixmap(0, 0, game_icon_pixmap); + game_icon_pixmap = rounded_pixmap; + } + + ui->game_icon->setPixmap(game_icon_pixmap.scaled(ui->game_icon->size(), Qt::KeepAspectRatio, + Qt::SmoothTransformation)); } std::string title; - if (loader.ReadTitle(title) == Loader::ResultStatus::Success && !title.empty()) { + if (auto custom_title = custom_metadata.GetCustomTitle(program_id)) { + title = *custom_title; + } else { + loader.ReadTitle(title); + } + + if (!title.empty()) { stage_translations = { - {VideoCore::LoadCallbackStage::Prepare, tr("Loading %1").arg(QString::fromStdString(title))}, - {VideoCore::LoadCallbackStage::Build, tr("Loading %1").arg(QString::fromStdString(title))}, + {VideoCore::LoadCallbackStage::Prepare, + tr("Loading %1").arg(QString::fromStdString(title))}, + {VideoCore::LoadCallbackStage::Build, + tr("Loading %1").arg(QString::fromStdString(title))}, {VideoCore::LoadCallbackStage::Complete, tr("Launching...")}, }; } else { - stage_translations = { + stage_translations = { {VideoCore::LoadCallbackStage::Prepare, tr("Loading Game...")}, {VideoCore::LoadCallbackStage::Build, tr("Loading Game...")}, {VideoCore::LoadCallbackStage::Complete, tr("Launching...")}, @@ -129,13 +187,15 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size style = QString::fromUtf8(R"( QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; } QProgressBar::chunk { background-color: %1; border-radius: 4px; } - )").arg(Theme::GetAccentColor()); + )") + .arg(Theme::GetAccentColor()); break; case VideoCore::LoadCallbackStage::Complete: style = QString::fromUtf8(R"( QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; } QProgressBar::chunk { background-color: %1; border-radius: 4px; } - )").arg(Theme::GetAccentColor()); + )") + .arg(Theme::GetAccentColor()); break; default: style = QStringLiteral(""); @@ -187,17 +247,17 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size static_cast(static_cast(total - slow_shader_first_value) / (value - slow_shader_first_value) * diff.count()); estimate = - tr("ETA: %1") - .arg(QTime(0, 0, 0, 0) - .addMSecs(std::max(eta_mseconds - diff.count(), 0)) - .toString(QStringLiteral("mm:ss"))); + tr("ETA: %1").arg(QTime(0, 0, 0, 0) + .addMSecs(std::max(eta_mseconds - diff.count(), 0)) + .toString(QStringLiteral("mm:ss"))); } } ui->shader_stage_label->setText(tr("Building Shaders...")); if (!estimate.isEmpty()) { - ui->shader_value_label->setText(QStringLiteral("%1 / %2 (%3)").arg(value).arg(total).arg(estimate)); + ui->shader_value_label->setText( + QStringLiteral("%1 / %2 (%3)").arg(value).arg(total).arg(estimate)); } else { ui->shader_value_label->setText(QStringLiteral("%1 / %2").arg(value).arg(total)); } diff --git a/src/citron/loading_screen.h b/src/citron/loading_screen.h index 479ddaa4f..87d7dbb17 100644 --- a/src/citron/loading_screen.h +++ b/src/citron/loading_screen.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include + class QGraphicsOpacityEffect; class QPropertyAnimation; @@ -58,6 +60,7 @@ private: QGraphicsOpacityEffect* opacity_effect = nullptr; QPropertyAnimation* fadeout_animation = nullptr; QTimer* loading_text_animation_timer = nullptr; + QMovie* movie = nullptr; std::unordered_map stage_translations; QString base_loading_text; diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 5594f4a67..7f6b6f921 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -37,6 +37,7 @@ #include "applets/qt_profile_select.h" #include "applets/qt_software_keyboard.h" #include "applets/qt_web_browser.h" +#include "citron/custom_metadata.h" #include "citron/multiplayer/state.h" #include "citron/setup_wizard.h" #include "citron/util/controller_navigation.h" @@ -2243,6 +2244,10 @@ void GMainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletP std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())} .filename()); } + if (auto custom_title = Citron::CustomMetadata::GetInstance().GetCustomTitle(title_id)) { + title_name = *custom_title; + } + const bool is_64bit = system->Kernel().ApplicationProcess()->Is64Bit(); const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)"); title_name = tr("%1 %2", "%1 is the title name. %2 indicates if the title is 64-bit or 32-bit")