mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-03-22 17:46:08 -04:00
feat(ui): Add Editing of Game Metadata for Custom Icons & Game Titles
This commit is contained in:
@@ -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
|
||||
|
||||
122
src/citron/custom_metadata.cpp
Normal file
122
src/citron/custom_metadata.cpp
Normal 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
|
||||
41
src/citron/custom_metadata.h
Normal file
41
src/citron/custom_metadata.h
Normal 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
|
||||
95
src/citron/custom_metadata_dialog.cpp
Normal file
95
src/citron/custom_metadata_dialog.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/citron/custom_metadata_dialog.h
Normal file
38
src/citron/custom_metadata_dialog.h
Normal 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;
|
||||
};
|
||||
131
src/citron/custom_metadata_dialog.ui
Normal file
131
src/citron/custom_metadata_dialog.ui
Normal 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>
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user