feat(ui): Add Editing of Game Metadata for Custom Icons & Game Titles

This commit is contained in:
Collecting
2026-02-15 02:01:09 -05:00
parent a56c7a515c
commit 637418dbaf
12 changed files with 610 additions and 39 deletions

View File

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

View File

@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <fstream>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#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<std::string> 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<std::string> 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

View File

@@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <optional>
#include <string>
#include <unordered_map>
#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<std::string> GetCustomTitle(u64 program_id) const;
[[nodiscard]] std::optional<std::string> 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<u64, CustomGameMetadata> metadata;
};
} // namespace Citron

View File

@@ -0,0 +1,95 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QFileDialog>
#include <QMovie>
#include <QPainter>
#include <QPainterPath>
#include <QPixmap>
#include <QPushButton>
#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<Ui::CustomMetadataDialog>()), 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));
}
}
}

View File

@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <QDialog>
#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::CustomMetadataDialog> ui;
u64 program_id;
std::string icon_path;
QMovie* movie = nullptr;
bool was_reset = false;
};

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CustomMetadataDialog</class>
<widget class="QDialog" name="CustomMetadataDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Game Metadata</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="title_edit"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Icon:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="select_icon_button">
<property name="text">
<string>Select Icon...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="previewLayout">
<item>
<spacer name="leftSpacer">
<property name="orientation">
<set>Qt::Horizontal</set>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="icon_preview">
<property name="minimumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="rightSpacer">
<property name="orientation">
<set>Qt::Horizontal</set>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<set>Qt::Vertical</set>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<set>Qt::Horizontal</set>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -43,6 +43,8 @@
#include <QtConcurrent/QtConcurrent>
#include <fmt/format.h>
#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));

View File

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

View File

@@ -22,6 +22,7 @@
#include <QStandardPaths>
#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::vector<u8>, std::string> GetGameListCachedObject(
void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
std::vector<u8>& 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<u8> 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);

View File

@@ -2,12 +2,14 @@
// SPDX-FileCopyrightText: 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "citron/loading_screen.h"
#include <QGraphicsOpacityEffect>
#include <QMovie>
#include <QPainter>
#include <QPropertyAnimation>
#include <QStyleOption>
#include <QTime>
#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<u8> buffer;
if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) {
game_icon_pixmap.loadFromData(buffer.data(), static_cast<uint>(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<u8> buffer;
if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) {
game_icon_pixmap.loadFromData(buffer.data(), static_cast<uint>(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<long>(static_cast<double>(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<long>(eta_mseconds - diff.count(), 0))
.toString(QStringLiteral("mm:ss")));
tr("ETA: %1").arg(QTime(0, 0, 0, 0)
.addMSecs(std::max<long>(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));
}

View File

@@ -7,6 +7,7 @@
#include <chrono>
#include <memory>
#include <unordered_map>
#include <QMovie>
#include <QPainterPath>
#include <QPixmap>
#include <QString>
@@ -14,6 +15,7 @@
#include <QWidget>
#include <QtGlobal>
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<VideoCore::LoadCallbackStage, QString> stage_translations;
QString base_loading_text;

View File

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