diff --git a/docs/user/CustomPlayTime.md b/docs/user/CustomPlayTime.md new file mode 100644 index 0000000000..394f761b0f --- /dev/null +++ b/docs/user/CustomPlayTime.md @@ -0,0 +1,167 @@ +# Custom Play Time Syntax + +This document describes the formatting syntax used to display custom playtime values in the emulator. + +## Overview + +Playtime is internally stored as a total number of seconds. This formatting system allows users to control how that value is displayed by using tokens inside a format string. + +Example: + +``` +{H:02}:{M:02}:{S:02} +``` + +Output: + +``` +02:03:04 +``` + +## Tokens + +The following tokens can be used in format strings. + +| Token | Description | +| ----- | ------------------------ | +| `{d}` | Total days | +| `{h}` | Total hours | +| `{H}` | Hours component (0–23) | +| `{m}` | Total minutes | +| `{M}` | Minutes component (0–59) | +| `{s}` | Total seconds | +| `{S}` | Seconds component (0–59) | + +## Padding + +Tokens may optionally include zero-padding using the syntax `:NN`, where `NN` is the minimum width. + +Example: + +``` +{H:02}:{M:02}:{S:02} +``` + +This ensures each component is displayed with at least two digits. + +Example output: + +``` +02:03:04 +``` + +## Conditional Sections + +Conditional sections allow parts of the format string to appear only when a specific time unit is non-zero. This is useful for hiding units such as `0h`. + +Conditional sections use the following syntax: + +``` +[unit] ... [/unit] +``` + +Where `unit` is one of the following: + +| Unit | Condition | +| ---- | ------------------------------ | +| `d` | Display section if days > 0 | +| `h` | Display section if hours > 0 | +| `m` | Display section if minutes > 0 | + +Example: + +``` +[h]{H}h [/h][m]{M}m [/m]{S}s +``` + +Possible outputs: + +``` +1h 3m 4s +3m 4s +4s +``` + +Conditional sections may contain both tokens and literal text. + +## Escaping Braces + +To include literal braces in the output, they must be escaped using double braces. + +| Input | Output | +| ----- | ------ | +| `{{` | `{` | +| `}}` | `}` | + +Example: + +``` +Playtime: {{ {H}:{M}:{S} }} +``` + +Output: + +``` +Playtime: { 2:3:4 } +``` + +## Literal Text + +Any text outside of tokens or conditional sections is copied directly into the output. + +Example: + +``` +{h}h {M}m {S}s +``` + +Output: + +``` +26h 3m 4s +``` + +## Examples + +### Clock Format + +``` +{H:02}:{M:02}:{S:02} +``` + +Example output: + +``` +02:03:04 +``` + +### Human Readable Format + +``` +{h} hours, {M} minutes, {S} seconds +``` + +Example output: + +``` +26 hours, 3 minutes, 4 seconds +``` + +### Compact Format + +``` +[h]{H}h [/h][m]{M}m [/m][s]{S}s +``` + +Example output: + +``` +3m 4s +``` + +## Notes + +* Playtime values are derived from the total number of elapsed seconds. +* Component tokens (`H`, `M`, `S`) wrap within their normal ranges. +* Total tokens (`h`, `m`, `s`) represent the full accumulated value for that unit. +* This is based on the [fmt syntax](https://fmt.dev/12.0/syntax/). Almost everything there is also supported here. \ No newline at end of file diff --git a/docs/user/README.md b/docs/user/README.md index c1c4cd200a..3085769ff0 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -9,7 +9,7 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/ ## Basics - **[The Basics](Basics.md)** -- **[Quickstart](./QuickStart.md)** +- **[Quickstart](QuickStart.md)** - **[Settings](./Settings.md)** - **[Controllers](./Controllers.md)** - **[Controller profiles](./Controllers.md#configuring-controller-profiles)** @@ -26,6 +26,7 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/ - **[Installing Atmosphere Mods](./InstallingAtmosphereMods.md)** - **[Installing Updates & DLCs](./InstallingUpdatesDLC.md)** - **[Alter Date & Time](./AlterDateTime.md)** +- **[Custom Play Time Syntax](./CustomPlayTime.md)** ## 3rd-party Integration diff --git a/src/frontend_common/play_time_manager.cpp b/src/frontend_common/play_time_manager.cpp index 3315cd713e..82215a41e4 100644 --- a/src/frontend_common/play_time_manager.cpp +++ b/src/frontend_common/play_time_manager.cpp @@ -166,21 +166,4 @@ void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { Save(); } -std::string PlayTimeManager::GetReadablePlayTime(u64 t) { - return t > 0 ? fmt::format("{:02}:{:02}:{:02}", t / 3600, (t / 60) % 60, t % 60) - : std::string{}; -} - -std::string PlayTimeManager::GetPlayTimeHours(u64 time_seconds) { - return fmt::format("{}", time_seconds / 3600); -} - -std::string PlayTimeManager::GetPlayTimeMinutes(u64 time_seconds) { - return fmt::format("{}", (time_seconds % 3600) / 60); -} - -std::string PlayTimeManager::GetPlayTimeSeconds(u64 time_seconds) { - return fmt::format("{}", time_seconds % 60); -} - } // namespace PlayTime diff --git a/src/frontend_common/play_time_manager.h b/src/frontend_common/play_time_manager.h index ea5ddb637b..b14e26d01b 100644 --- a/src/frontend_common/play_time_manager.h +++ b/src/frontend_common/play_time_manager.h @@ -38,11 +38,6 @@ public: void Start(); void Stop(); - static std::string GetReadablePlayTime(u64 time_seconds); - static std::string GetPlayTimeHours(u64 time_seconds); - static std::string GetPlayTimeMinutes(u64 time_seconds); - static std::string GetPlayTimeSeconds(u64 time_seconds); - private: void AutoTimestamp(std::stop_token stop_token); void Save(); diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index a0c4779b73..0ed64763f7 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -373,6 +373,10 @@ std::unique_ptr InitializeTranslations(QObject* parent) { // Ui Multiplayer // Ui Games list + INSERT(UISettings, use_custom_play_time_format, tr("Use custom play time format"), + QString()); + INSERT(UISettings, custom_play_time_format, tr("Custom play time format"), + QString()); #undef INSERT diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h index 4549a36345..1fab8f9611 100644 --- a/src/qt_common/config/uisettings.h +++ b/src/qt_common/config/uisettings.h @@ -233,6 +233,8 @@ struct Values { // Play time Setting show_play_time{linkage, true, "show_play_time", Category::UiGameList}; + Setting use_custom_play_time_format{linkage, false, "use_custom_play_time_format", Category::UiGameList}; + Setting custom_play_time_format{linkage, "", "use_custom_play_time_format", Category::UiGameList}; // misc Setting show_fw_warning{linkage, true, "show_fw_warning", Category::Miscellaneous}; diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp index af8d9fecce..8d4aa5667c 100644 --- a/src/yuzu/configuration/configure_ui.cpp +++ b/src/yuzu/configuration/configure_ui.cpp @@ -16,7 +16,10 @@ #include #include #include +#include #include +#include +#include #include "common/common_types.h" #include "common/fs/path_util.h" @@ -103,7 +106,7 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent) InitializeIconSizeComboBox(); InitializeRowComboBoxes(); - PopulateResolutionComboBox(ui->screenshot_height, this); + PopulateResolutionComboBox(ui->screenshot_size_combobox, this); SetConfiguration(); @@ -142,9 +145,14 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent) } }); - connect(ui->screenshot_height, &QComboBox::currentTextChanged, [this]() { UpdateWidthText(); }); + connect(ui->screenshot_size_combobox, &QComboBox::currentTextChanged, [this]() { UpdateWidthText(); }); UpdateWidthText(); + + connect(ui->show_play_time, &QCheckBox::STATE_CHANGED, [this]() { UpdateCustomPlaytimeGroupBox(); }); + connect(ui->use_custom_play_time_format, &QCheckBox::STATE_CHANGED, [this]() { UpdateCustomPlaytimeGroupBox(); }); + UpdateCustomPlaytimeGroupBox(); + } ConfigureUi::~ConfigureUi() = default; @@ -161,11 +169,14 @@ void ConfigureUi::ApplyConfiguration() { UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); UISettings::values.row_2_text_id = ui->row_2_text_combobox->currentData().toUInt(); + UISettings::values.use_custom_play_time_format = ui->use_custom_play_time_format->isChecked(); + UISettings::values.custom_play_time_format = ui->custom_play_time_edit->text().toStdString(); + UISettings::values.enable_screenshot_save_as = ui->enable_screenshot_save_as->isChecked(); Common::FS::SetEdenPath(Common::FS::EdenPath::ScreenshotsDir, ui->screenshot_path_edit->text().toStdString()); - const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); + const u32 height = ScreenshotDimensionToInt(ui->screenshot_size_combobox->currentText()); UISettings::values.screenshot_height.SetValue(height); RequestGameListUpdate(); @@ -189,6 +200,13 @@ void ConfigureUi::SetConfiguration() { ui->folder_icon_size_combobox->setCurrentIndex( ui->folder_icon_size_combobox->findData(UISettings::values.folder_icon_size.GetValue())); + ui->use_custom_play_time_format->setChecked( + UISettings::values.use_custom_play_time_format.GetValue()); + ui->custom_play_time_edit->setText(QString::fromStdString( + UISettings::values.custom_play_time_format.GetValue())); + + UpdateCustomPlaytimeGroupBox(); + ui->enable_screenshot_save_as->setChecked( UISettings::values.enable_screenshot_save_as.GetValue()); ui->screenshot_path_edit->setText(QString::fromStdString( @@ -196,9 +214,9 @@ void ConfigureUi::SetConfiguration() { const auto height = UISettings::values.screenshot_height.GetValue(); if (height == 0) { - ui->screenshot_height->setCurrentIndex(0); + ui->screenshot_size_combobox->setCurrentIndex(0); } else { - ui->screenshot_height->setCurrentText(QStringLiteral("%1").arg(height)); + ui->screenshot_size_combobox->setCurrentText(QStringLiteral("%1").arg(height)); } } @@ -303,7 +321,7 @@ void ConfigureUi::OnLanguageChanged(int index) { } void ConfigureUi::UpdateWidthText() { - const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); + const u32 height = ScreenshotDimensionToInt(ui->screenshot_size_combobox->currentText()); const u32 width = UISettings::CalculateWidth(height, ratio); if (height == 0) { const auto up_factor = GetUpFactor(resolution_setting); @@ -311,16 +329,27 @@ void ConfigureUi::UpdateWidthText() { const u32 width_docked = UISettings::CalculateWidth(height_docked, ratio); const u32 height_undocked = Layout::ScreenUndocked::Height * up_factor; const u32 width_undocked = UISettings::CalculateWidth(height_undocked, ratio); - ui->screenshot_width->setText(tr("Auto (%1 x %2, %3 x %4)", "Screenshot width value") + ui->screenshot_size_label->setText(tr("Auto (%1 x %2, %3 x %4)", "Screenshot width value") .arg(width_undocked) .arg(height_undocked) .arg(width_docked) .arg(height_docked)); } else { - ui->screenshot_width->setText(QStringLiteral("%1 x").arg(width)); + ui->screenshot_size_label->setText(QStringLiteral("%1 x").arg(width)); } } +void ConfigureUi::UpdateCustomPlaytimeGroupBox() { + bool showPlayTime = ui->show_play_time->isChecked(); + bool useCustomPlayTime = ui->use_custom_play_time_format->isChecked(); + bool enableCheckbox = showPlayTime; + bool enableTextBox = showPlayTime && useCustomPlayTime; + ui->use_custom_play_time_format->setEnabled(enableCheckbox); + ui->custom_play_time_edit->setEnabled(enableTextBox); + ui->custom_play_time_label->setEnabled(enableTextBox); + ui->custom_play_time_help->setEnabled(enableTextBox); +} + void ConfigureUi::UpdateScreenshotInfo(Settings::AspectRatio ratio_, Settings::ResolutionSetup resolution_setting_) { ratio = ratio_; diff --git a/src/yuzu/configuration/configure_ui.h b/src/yuzu/configuration/configure_ui.h index 2a2563a131..924d2effc2 100644 --- a/src/yuzu/configuration/configure_ui.h +++ b/src/yuzu/configuration/configure_ui.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + // SPDX-FileCopyrightText: 2016 Citra Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later @@ -50,6 +53,8 @@ private: void UpdateWidthText(); + void UpdateCustomPlaytimeGroupBox(); + std::unique_ptr ui; Settings::AspectRatio ratio; diff --git a/src/yuzu/configuration/configure_ui.ui b/src/yuzu/configuration/configure_ui.ui index 123068c9e2..1df3bd1a6a 100644 --- a/src/yuzu/configuration/configure_ui.ui +++ b/src/yuzu/configuration/configure_ui.ui @@ -7,7 +7,7 @@ 0 0 363 - 613 + 693 @@ -18,50 +18,46 @@ - + General - + - + + + Note: Changing language will apply your configuration. + + + true + + + + + - + - Note: Changing language will apply your configuration. - - - true + Interface language: - - - - - Interface language: - - - - - - - + + + + + + + + + + Theme: + + - - - - - Theme: - - - - - - - + @@ -69,89 +65,118 @@ - + Game List - + + false + + - + + + Show Compatibility List + + + + + + + Show Add-Ons Column + + + + + + + Show Size Column + + + + + + + Show File Types Column + + + + + + + Show Play Time Column + + + + + - + - Show Compatibility List + Folder Icon Size: - - - Show Add-Ons Column - - + + + + + - + - Show Size Column + Row 1 Text: - - - Show File Types Column - - + + + + + - + - Show Play Time Column + Row 2 Text: - - - - - Folder Icon Size: - - - - - - - + + + + + + + + + + Custom Play Time Format + + + + + + Use Custom Play Time Format + + + + + - - - - - Row 1 Text: - - - - - - - + + + Custom Play Time Format: + + - - - - - Row 2 Text: - - - - - - - + @@ -159,75 +184,71 @@ - + Screenshots - + - + + + Ask Where To Save Screenshots (Windows Only) + + + + + - + - Ask Where To Save Screenshots (Windows Only) + Screenshots Path: - + + + + + + ... + + + + + + + + + 6 + + + - + - Screenshots Path: + TextLabel + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - - - - - ... + + + true - - - - 6 + + + + Resolution: - - - - - - TextLabel - - - Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - - - - - - true - - - - - - - - - Resolution: - - - - + diff --git a/src/yuzu/game/game_list_p.h b/src/yuzu/game/game_list_p.h index f9d714fb8b..769411ade8 100644 --- a/src/yuzu/game/game_list_p.h +++ b/src/yuzu/game/game_list_p.h @@ -86,8 +86,7 @@ public: const auto readable_play_time = play_time > 0 ? QObject::tr("Play Time: %1") - .arg(QString::fromStdString( - PlayTime::PlayTimeManager::GetReadablePlayTime(play_time))) + .arg(QString::fromStdString(GetReadablePlayTime(play_time))) : QObject::tr("Never Played"); const auto enabled_update = [patch_versions]() -> QString { @@ -274,9 +273,8 @@ public: void setData(const QVariant& value, int role) override { qulonglong time_seconds = value.toULongLong(); - GameListItem::setData( - QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)), - Qt::DisplayRole); + GameListItem::setData(QString::fromStdString(GetReadablePlayTime(time_seconds)), + Qt::DisplayRole); GameListItem::setData(value, PlayTimeRole); } diff --git a/src/yuzu/set_play_time_dialog.cpp b/src/yuzu/set_play_time_dialog.cpp index 38876abd6d..9a75de766b 100644 --- a/src/yuzu/set_play_time_dialog.cpp +++ b/src/yuzu/set_play_time_dialog.cpp @@ -4,19 +4,20 @@ #include "frontend_common/play_time_manager.h" #include "ui_set_play_time_dialog.h" #include "yuzu/set_play_time_dialog.h" +#include "yuzu/util/util.h" SetPlayTimeDialog::SetPlayTimeDialog(QWidget* parent, u64 current_play_time) : QDialog(parent), ui{std::make_unique()} { ui->setupUi(this); ui->hoursSpinBox->setValue( - QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeHours(current_play_time)) + QString::fromStdString(GetPlayTimeHours(current_play_time)) .toInt()); ui->minutesSpinBox->setValue( - QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeMinutes(current_play_time)) + QString::fromStdString(GetPlayTimeMinutes(current_play_time)) .toInt()); ui->secondsSpinBox->setValue( - QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeSeconds(current_play_time)) + QString::fromStdString(GetPlayTimeSeconds(current_play_time)) .toInt()); connect(ui->hoursSpinBox, QOverload::of(&QSpinBox::valueChanged), this, diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index a3933d9b63..0ff074c471 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -12,6 +12,7 @@ #include "core/frontend/applets/profile_select.h" #include "core/hle/service/acc/profile_manager.h" #include "frontend_common/data_manager.h" +#include "qt_common/config/uisettings.h" #include "qt_common/qt_common.h" #include "yuzu/util/util.h" @@ -148,6 +149,7 @@ bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) return false; #endif } + const std::optional GetProfileID() { // if there's only a single profile, the user probably wants to use that... right? const auto& profiles = QtCommon::system->GetProfileManager().FindExistingProfileUUIDs(); @@ -185,6 +187,7 @@ const std::optional GetProfileID() { return uuid; } + std::string GetProfileIDString() { const auto uuid = GetProfileID(); if (!uuid) @@ -194,3 +197,88 @@ std::string GetProfileIDString() { return fmt::format("{:016X}{:016X}", user_id[1], user_id[0]); } + +void eraseBetweenStrings(std::string& str, const std::string& start_str, + const std::string& end_str) { + size_t start_pos = std::string::npos; + size_t end_pos = std::string::npos; + + while ((start_pos = str.find(start_str)) != std::string::npos) { + end_pos = str.find(end_str, start_pos + start_str.length()); + + if (end_pos != std::string::npos) { + size_t erase_length = end_pos + end_str.length() - start_pos; + str.erase(start_pos, erase_length); + } else { + break; + } + } +} + +void eraseAll(std::string& str, const std::string& sub_str) { + size_t pos = std::string::npos; + size_t len = sub_str.length(); + while ((pos = str.find(sub_str)) != std::string::npos) { + str.erase(pos, len); + } +} + +std::string GetReadablePlayTime(u64 total_seconds) { + if (total_seconds <= 0) { + return std::string{}; + } + + if (!UISettings::values.use_custom_play_time_format.GetValue()) { + return fmt::format("{:02}:{:02}:{:02}", total_seconds / 3600, (total_seconds % 3600) / 60, + total_seconds % 60); + } + + u64 total_days = total_seconds / 86400; + u64 total_hours = total_seconds / 3600; + u64 total_minutes = total_seconds / 60; + + std::string format = UISettings::values.custom_play_time_format.GetValue(); + + if (total_days <= 0) { + eraseBetweenStrings(format, "[d]", "[/d]"); + } else { + eraseAll(format, "[d]"); + eraseAll(format, "[/d]"); + } + + if (total_hours <= 0) { + eraseBetweenStrings(format, "[h]", "[/h]"); + } else { + eraseAll(format, "[h]"); + eraseAll(format, "[/h]"); + } + + if (total_minutes <= 0) { + eraseBetweenStrings(format, "[m]", "[/m]"); + } else { + eraseAll(format, "[m]"); + eraseAll(format, "[/m]"); + } + + return fmt::format(fmt::runtime(format), + fmt::arg("d", total_days), + fmt::arg("h", total_hours), + fmt::arg("m", total_minutes), + fmt::arg("s", total_seconds), + fmt::arg("H", total_hours % 24), + fmt::arg("M", total_minutes % 60), + fmt::arg("S", total_seconds % 60) + ); +} + +std::string GetPlayTimeHours(u64 total_seconds) { + return fmt::format("{}", total_seconds / 3600); +} + +std::string GetPlayTimeMinutes(u64 total_seconds) { + return fmt::format("{}", (total_seconds % 3600) / 60); +} + +std::string GetPlayTimeSeconds(u64 total_seconds) { + return fmt::format("{}", total_seconds % 60); +} \ No newline at end of file diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 7b482aa11d..0ed343dbeb 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2015 Citra Emulator Project @@ -43,3 +43,8 @@ const std::optional GetProfileID(); * @return A string representation of the selected profile, or an empty string if none were seleeced */ std::string GetProfileIDString(); + +std::string GetReadablePlayTime(u64 time_seconds); +std::string GetPlayTimeHours(u64 time_seconds); +std::string GetPlayTimeMinutes(u64 time_seconds); +std::string GetPlayTimeSeconds(u64 time_seconds); \ No newline at end of file