Browse Source

[ui, docs] add custom play time formatting

Signed-off-by: codeman4033 <codeman4033@eden-emu.dev>
pull/3767/head
codeman4033 2 days ago
parent
commit
af64395c6a
No known key found for this signature in database GPG Key ID: DE2F639DF2D265A3
  1. 167
      docs/user/CustomPlayTime.md
  2. 3
      docs/user/README.md
  3. 17
      src/frontend_common/play_time_manager.cpp
  4. 5
      src/frontend_common/play_time_manager.h
  5. 4
      src/qt_common/config/shared_translation.cpp
  6. 2
      src/qt_common/config/uisettings.h
  7. 45
      src/yuzu/configuration/configure_ui.cpp
  8. 5
      src/yuzu/configuration/configure_ui.h
  9. 67
      src/yuzu/configuration/configure_ui.ui
  10. 6
      src/yuzu/game/game_list_p.h
  11. 7
      src/yuzu/set_play_time_dialog.cpp
  12. 88
      src/yuzu/util/util.cpp
  13. 7
      src/yuzu/util/util.h

167
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.

3
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

17
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

5
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();

4
src/qt_common/config/shared_translation.cpp

@ -373,6 +373,10 @@ std::unique_ptr<TranslationMap> 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

2
src/qt_common/config/uisettings.h

@ -233,6 +233,8 @@ struct Values {
// Play time
Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList};
Setting<bool> use_custom_play_time_format{linkage, false, "use_custom_play_time_format", Category::UiGameList};
Setting<std::string> custom_play_time_format{linkage, "", "use_custom_play_time_format", Category::UiGameList};
// misc
Setting<bool> show_fw_warning{linkage, true, "show_fw_warning", Category::Miscellaneous};

45
src/yuzu/configuration/configure_ui.cpp

@ -16,7 +16,10 @@
#include <QFileDialog>
#include <QString>
#include <QToolButton>
#include <QPushButton>
#include <QVariant>
#include <QDesktopServices>
#include <QUrl>
#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_;

5
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::ConfigureUi> ui;
Settings::AspectRatio ratio;

67
src/yuzu/configuration/configure_ui.ui

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>363</width>
<height>613</height>
<height>693</height>
</rect>
</property>
<property name="windowTitle">
@ -18,13 +18,11 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="general_groupBox">
<widget class="QGroupBox" name="general_group_box">
<property name="title">
<string>General</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="label_change_language_info">
<property name="text">
@ -36,7 +34,7 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="language_qhbox_layout">
<item>
<widget class="QLabel" name="language_label">
<property name="text">
@ -50,7 +48,7 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<layout class="QHBoxLayout" name="theme_qhbox_layout">
<item>
<widget class="QLabel" name="theme_label">
<property name="text">
@ -64,18 +62,17 @@
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="GameListGroupBox">
<widget class="QGroupBox" name="game_list_group_box">
<property name="title">
<string>Game List</string>
</property>
<layout class="QHBoxLayout" name="GameListHorizontalLayout">
<item>
<layout class="QVBoxLayout" name="GeneralVerticalLayout">
<property name="flat">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="show_compat">
<property name="text">
@ -112,7 +109,7 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="folder_icon_size_qhbox_layout_2">
<layout class="QHBoxLayout" name="folder_icon_size_qhbox_layout">
<item>
<widget class="QLabel" name="folder_icon_size_label">
<property name="text">
@ -154,18 +151,44 @@
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="play_time_group_box">
<property name="title">
<string>Custom Play Time Format</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QCheckBox" name="use_custom_play_time_format">
<property name="text">
<string>Use Custom Play Time Format</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="custom_play_time_qhbox_layout">
<item>
<widget class="QLabel" name="custom_play_time_label">
<property name="text">
<string>Custom Play Time Format:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="custom_play_time_edit"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="screenshots_GroupBox">
<widget class="QGroupBox" name="screenshots_group_box">
<property name="title">
<string>Screenshots</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="enable_screenshot_save_as">
<property name="text">
@ -174,7 +197,7 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<layout class="QHBoxLayout" name="screenshot_path_layout">
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -202,7 +225,7 @@
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="screenshot_width">
<widget class="QLabel" name="screenshot_size_label">
<property name="text">
<string>TextLabel</string>
</property>
@ -212,7 +235,7 @@
</widget>
</item>
<item>
<widget class="QComboBox" name="screenshot_height">
<widget class="QComboBox" name="screenshot_size_combobox">
<property name="editable">
<bool>true</bool>
</property>
@ -230,8 +253,6 @@
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>

6
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,8 +273,7 @@ public:
void setData(const QVariant& value, int role) override {
qulonglong time_seconds = value.toULongLong();
GameListItem::setData(
QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)),
GameListItem::setData(QString::fromStdString(GetReadablePlayTime(time_seconds)),
Qt::DisplayRole);
GameListItem::setData(value, PlayTimeRole);
}

7
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::SetPlayTimeDialog>()} {
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<int>::of(&QSpinBox::valueChanged), this,

88
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<Common::UUID> 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<Common::UUID> 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);
}

7
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<Common::UUID> 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);
Loading…
Cancel
Save