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
|
hotkeys.h
|
||||||
hotkey_profile_manager.cpp
|
hotkey_profile_manager.cpp
|
||||||
hotkey_profile_manager.h
|
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.cpp
|
||||||
install_dialog.h
|
install_dialog.h
|
||||||
loading_screen.cpp
|
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 <QtConcurrent/QtConcurrent>
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include "citron/compatibility_list.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.h"
|
||||||
#include "citron/game_list_p.h"
|
#include "citron/game_list_p.h"
|
||||||
#include "citron/game_list_worker.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"));
|
shortcut_menu->addAction(tr("Add to Applications Menu"));
|
||||||
#endif
|
#endif
|
||||||
context_menu.addSeparator();
|
context_menu.addSeparator();
|
||||||
|
QAction* edit_metadata = context_menu.addAction(tr("Edit Metadata"));
|
||||||
QAction* properties = context_menu.addAction(tr("Properties"));
|
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->setVisible(program_id != 0);
|
||||||
favorite->setCheckable(true);
|
favorite->setCheckable(true);
|
||||||
favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
|
favorite->setChecked(UISettings::values.favorited_ids.contains(program_id));
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
|
|
||||||
|
|
||||||
enum class GameListItemType {
|
enum class GameListItemType {
|
||||||
Game = QStandardItem::UserType + 1,
|
Game = QStandardItem::UserType + 1,
|
||||||
CustomDir = QStandardItem::UserType + 2,
|
CustomDir = QStandardItem::UserType + 2,
|
||||||
@@ -70,8 +69,10 @@ static QPixmap CreateRoundIcon(const QPixmap& pixmap, u32 size) {
|
|||||||
painter.setClipPath(path);
|
painter.setClipPath(path);
|
||||||
|
|
||||||
// Draw the scaled pixmap
|
// Draw the scaled pixmap
|
||||||
QPixmap scaled = pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
QPixmap scaled = pixmap.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
painter.drawPixmap(0, 0, scaled);
|
int x = (size - scaled.width()) / 2;
|
||||||
|
int y = (size - scaled.height()) / 2;
|
||||||
|
painter.drawPixmap(x, y, scaled);
|
||||||
|
|
||||||
return rounded;
|
return rounded;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
||||||
#include "citron/compatibility_list.h"
|
#include "citron/compatibility_list.h"
|
||||||
|
#include "citron/custom_metadata.h"
|
||||||
#include "citron/game_list.h"
|
#include "citron/game_list.h"
|
||||||
#include "citron/game_list_p.h"
|
#include "citron/game_list_p.h"
|
||||||
#include "citron/game_list_worker.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,
|
void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
|
||||||
std::vector<u8>& icon, std::string& name) {
|
std::vector<u8>& icon, std::string& name) {
|
||||||
std::tie(icon, name) = GetGameListCachedObject(
|
const auto program_id = patch_manager.GetTitleID();
|
||||||
fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
|
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);
|
const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
|
||||||
return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
|
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) {
|
bool HasSupportedFileExtension(const std::string& file_name) {
|
||||||
@@ -645,9 +661,27 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
|||||||
system.GetContentProvider()};
|
system.GetContentProvider()};
|
||||||
auto loader = Loader::GetLoader(system, file);
|
auto loader = Loader::GetLoader(system, file);
|
||||||
if (loader) {
|
if (loader) {
|
||||||
auto entry = MakeGameListEntry(physical_name, cached->title,
|
std::string title = cached->title;
|
||||||
cached->file_size, cached->icon, *loader,
|
std::vector<u8> icon = cached->icon;
|
||||||
cached->program_id, compatibility_list,
|
|
||||||
|
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);
|
play_time_manager, patch, online_stats);
|
||||||
RecordEvent(
|
RecordEvent(
|
||||||
[=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
|
[=](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 = " ";
|
std::string name = " ";
|
||||||
loader->ReadIcon(icon);
|
loader->ReadIcon(icon);
|
||||||
loader->ReadTitle(name);
|
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);
|
std::size_t file_size = Common::FS::GetSize(physical_name);
|
||||||
|
|
||||||
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
|
CacheGameMetadata(physical_name, program_id, file_type, file_size, name, icon);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
// SPDX-FileCopyrightText: 2025 citron Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include "citron/loading_screen.h"
|
|
||||||
#include <QGraphicsOpacityEffect>
|
#include <QGraphicsOpacityEffect>
|
||||||
|
#include <QMovie>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPropertyAnimation>
|
#include <QPropertyAnimation>
|
||||||
#include <QStyleOption>
|
#include <QStyleOption>
|
||||||
#include <QTime>
|
#include <QTime>
|
||||||
|
#include "citron/custom_metadata.h"
|
||||||
|
#include "citron/loading_screen.h"
|
||||||
#include "citron/theme.h"
|
#include "citron/theme.h"
|
||||||
#include "core/frontend/framebuffer_layout.h"
|
#include "core/frontend/framebuffer_layout.h"
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
@@ -35,7 +37,8 @@ LoadingScreen::LoadingScreen(QWidget* parent)
|
|||||||
});
|
});
|
||||||
|
|
||||||
loading_text_animation_timer = new QTimer(this);
|
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,
|
connect(this, &LoadingScreen::LoadProgress, this, &LoadingScreen::OnLoadProgress,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@@ -48,13 +51,27 @@ LoadingScreen::~LoadingScreen() {
|
|||||||
|
|
||||||
void LoadingScreen::Prepare(Loader::AppLoader& loader) {
|
void LoadingScreen::Prepare(Loader::AppLoader& loader) {
|
||||||
QPixmap game_icon_pixmap;
|
QPixmap game_icon_pixmap;
|
||||||
std::vector<u8> buffer;
|
u64 program_id = 0;
|
||||||
if (loader.ReadIcon(buffer) == Loader::ResultStatus::Success) {
|
loader.ReadProgramId(program_id);
|
||||||
game_icon_pixmap.loadFromData(buffer.data(), static_cast<uint>(buffer.size()));
|
auto& custom_metadata = Citron::CustomMetadata::GetInstance();
|
||||||
} else {
|
|
||||||
game_icon_pixmap = QPixmap(QStringLiteral(":/icons/scalable/actions/games.svg"));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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()) {
|
if (!game_icon_pixmap.isNull()) {
|
||||||
QPixmap rounded_pixmap(game_icon_pixmap.size());
|
QPixmap rounded_pixmap(game_icon_pixmap.size());
|
||||||
rounded_pixmap.fill(Qt::transparent);
|
rounded_pixmap.fill(Qt::transparent);
|
||||||
@@ -65,16 +82,57 @@ void LoadingScreen::Prepare(Loader::AppLoader& loader) {
|
|||||||
path.addRoundedRect(rounded_pixmap.rect(), radius, radius);
|
path.addRoundedRect(rounded_pixmap.rect(), radius, radius);
|
||||||
painter.setClipPath(path);
|
painter.setClipPath(path);
|
||||||
painter.drawPixmap(0, 0, game_icon_pixmap);
|
painter.drawPixmap(0, 0, game_icon_pixmap);
|
||||||
ui->game_icon->setPixmap(rounded_pixmap.scaled(ui->game_icon->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
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 {
|
} else {
|
||||||
ui->game_icon->setPixmap(game_icon_pixmap);
|
game_icon_pixmap = QPixmap(QStringLiteral(":/icons/scalable/actions/games.svg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!game_icon_pixmap.isNull()) {
|
||||||
|
// 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;
|
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 = {
|
stage_translations = {
|
||||||
{VideoCore::LoadCallbackStage::Prepare, tr("Loading %1").arg(QString::fromStdString(title))},
|
{VideoCore::LoadCallbackStage::Prepare,
|
||||||
{VideoCore::LoadCallbackStage::Build, tr("Loading %1").arg(QString::fromStdString(title))},
|
tr("Loading %1").arg(QString::fromStdString(title))},
|
||||||
|
{VideoCore::LoadCallbackStage::Build,
|
||||||
|
tr("Loading %1").arg(QString::fromStdString(title))},
|
||||||
{VideoCore::LoadCallbackStage::Complete, tr("Launching...")},
|
{VideoCore::LoadCallbackStage::Complete, tr("Launching...")},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -129,13 +187,15 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size
|
|||||||
style = QString::fromUtf8(R"(
|
style = QString::fromUtf8(R"(
|
||||||
QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; }
|
QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; }
|
||||||
QProgressBar::chunk { background-color: %1; border-radius: 4px; }
|
QProgressBar::chunk { background-color: %1; border-radius: 4px; }
|
||||||
)").arg(Theme::GetAccentColor());
|
)")
|
||||||
|
.arg(Theme::GetAccentColor());
|
||||||
break;
|
break;
|
||||||
case VideoCore::LoadCallbackStage::Complete:
|
case VideoCore::LoadCallbackStage::Complete:
|
||||||
style = QString::fromUtf8(R"(
|
style = QString::fromUtf8(R"(
|
||||||
QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; }
|
QProgressBar { background-color: #3a3a3a; border: none; border-radius: 4px; }
|
||||||
QProgressBar::chunk { background-color: %1; border-radius: 4px; }
|
QProgressBar::chunk { background-color: %1; border-radius: 4px; }
|
||||||
)").arg(Theme::GetAccentColor());
|
)")
|
||||||
|
.arg(Theme::GetAccentColor());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
style = QStringLiteral("");
|
style = QStringLiteral("");
|
||||||
@@ -187,8 +247,7 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size
|
|||||||
static_cast<long>(static_cast<double>(total - slow_shader_first_value) /
|
static_cast<long>(static_cast<double>(total - slow_shader_first_value) /
|
||||||
(value - slow_shader_first_value) * diff.count());
|
(value - slow_shader_first_value) * diff.count());
|
||||||
estimate =
|
estimate =
|
||||||
tr("ETA: %1")
|
tr("ETA: %1").arg(QTime(0, 0, 0, 0)
|
||||||
.arg(QTime(0, 0, 0, 0)
|
|
||||||
.addMSecs(std::max<long>(eta_mseconds - diff.count(), 0))
|
.addMSecs(std::max<long>(eta_mseconds - diff.count(), 0))
|
||||||
.toString(QStringLiteral("mm:ss")));
|
.toString(QStringLiteral("mm:ss")));
|
||||||
}
|
}
|
||||||
@@ -197,7 +256,8 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size
|
|||||||
ui->shader_stage_label->setText(tr("Building Shaders..."));
|
ui->shader_stage_label->setText(tr("Building Shaders..."));
|
||||||
|
|
||||||
if (!estimate.isEmpty()) {
|
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 {
|
} else {
|
||||||
ui->shader_value_label->setText(QStringLiteral("%1 / %2").arg(value).arg(total));
|
ui->shader_value_label->setText(QStringLiteral("%1 / %2").arg(value).arg(total));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <QMovie>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
|
||||||
class QGraphicsOpacityEffect;
|
class QGraphicsOpacityEffect;
|
||||||
class QPropertyAnimation;
|
class QPropertyAnimation;
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ private:
|
|||||||
QGraphicsOpacityEffect* opacity_effect = nullptr;
|
QGraphicsOpacityEffect* opacity_effect = nullptr;
|
||||||
QPropertyAnimation* fadeout_animation = nullptr;
|
QPropertyAnimation* fadeout_animation = nullptr;
|
||||||
QTimer* loading_text_animation_timer = nullptr;
|
QTimer* loading_text_animation_timer = nullptr;
|
||||||
|
QMovie* movie = nullptr;
|
||||||
|
|
||||||
std::unordered_map<VideoCore::LoadCallbackStage, QString> stage_translations;
|
std::unordered_map<VideoCore::LoadCallbackStage, QString> stage_translations;
|
||||||
QString base_loading_text;
|
QString base_loading_text;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "applets/qt_profile_select.h"
|
#include "applets/qt_profile_select.h"
|
||||||
#include "applets/qt_software_keyboard.h"
|
#include "applets/qt_software_keyboard.h"
|
||||||
#include "applets/qt_web_browser.h"
|
#include "applets/qt_web_browser.h"
|
||||||
|
#include "citron/custom_metadata.h"
|
||||||
#include "citron/multiplayer/state.h"
|
#include "citron/multiplayer/state.h"
|
||||||
#include "citron/setup_wizard.h"
|
#include "citron/setup_wizard.h"
|
||||||
#include "citron/util/controller_navigation.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())}
|
std::filesystem::path{Common::U16StringFromBuffer(filename.utf16(), filename.size())}
|
||||||
.filename());
|
.filename());
|
||||||
}
|
}
|
||||||
|
if (auto custom_title = Citron::CustomMetadata::GetInstance().GetCustomTitle(title_id)) {
|
||||||
|
title_name = *custom_title;
|
||||||
|
}
|
||||||
|
|
||||||
const bool is_64bit = system->Kernel().ApplicationProcess()->Is64Bit();
|
const bool is_64bit = system->Kernel().ApplicationProcess()->Is64Bit();
|
||||||
const auto instruction_set_suffix = is_64bit ? tr("(64-bit)") : tr("(32-bit)");
|
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")
|
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