lime_qt: Track play time

Co-Authored-By: Mario Davó <66087392+mdmrk@users.noreply.github.com>
This commit is contained in:
FearlessTobi 2024-02-08 00:17:16 +01:00 committed by OpenSauce
parent dae2387abf
commit b4662a822b
16 changed files with 394 additions and 8 deletions

View File

@ -53,6 +53,7 @@
#define SHADER_DIR "shaders"
#define STATES_DIR "states"
#define ICONS_DIR "icons"
#define PLAY_TIME_DIR "play_time"
// Filenames
// Files in the directory returned by GetUserPath(UserPath::LogDir)

View File

@ -827,6 +827,7 @@ void SetUserPath(const std::string& path) {
g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP);
g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP);
g_paths.emplace(UserPath::IconsDir, user_path + ICONS_DIR DIR_SEP);
g_paths.emplace(UserPath::PlayTimeDir, user_path + PLAY_TIME_DIR DIR_SEP);
g_default_paths = g_paths;
}

View File

@ -41,6 +41,7 @@ enum class UserPath {
SysDataDir,
UserDir,
IconsDir,
PlayTimeDir,
};
// Replaces install-specific paths with standard placeholders, and back again
@ -344,6 +345,59 @@ public:
return WriteArray(str.data(), str.length());
}
/**
* Reads a span of T data from a file sequentially.
* This function reads from the current position of the file pointer and
* advances it by the (count of T * sizeof(T)) bytes successfully read.
*
* Failures occur when:
* - The file is not open
* - The opened file lacks read permissions
* - Attempting to read beyond the end-of-file
*
* @tparam T Data type
*
* @param data Span of T data
*
* @returns Count of T data successfully read.
*/
template <typename T>
[[nodiscard]] size_t ReadSpan(std::span<T> data) const {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
if (!IsOpen()) {
return 0;
}
return std::fread(data.data(), sizeof(T), data.size(), m_file);
}
/**
* Writes a span of T data to a file sequentially.
* This function writes from the current position of the file pointer and
* advances it by the (count of T * sizeof(T)) bytes successfully written.
*
* Failures occur when:
* - The file is not open
* - The opened file lacks write permissions
*
* @tparam T Data type
*
* @param data Span of T data
*
* @returns Count of T data successfully written.
*/
template <typename T>
[[nodiscard]] size_t WriteSpan(std::span<const T> data) const {
static_assert(std::is_trivially_copyable_v<T>, "Data type must be trivially copyable.");
if (!IsOpen()) {
return 0;
}
return std::fwrite(data.data(), sizeof(T), data.size(), m_file);
}
[[nodiscard]] bool IsOpen() const {
return nullptr != m_file;
}

View File

@ -12,8 +12,11 @@
#ifdef __cpp_lib_jthread
#include <chrono>
#include <condition_variable>
#include <stop_token>
#include <thread>
#include <utility>
namespace Common {
@ -22,11 +25,23 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred&& pred) {
cv.wait(lock, token, std::move(pred));
}
template <typename Rep, typename Period>
bool StoppableTimedWait(std::stop_token token, const std::chrono::duration<Rep, Period>& rel_time) {
std::condition_variable_any cv;
std::mutex m;
// Perform the timed wait.
std::unique_lock lk{m};
return !cv.wait_for(lk, token, rel_time, [&] { return token.stop_requested(); });
}
} // namespace Common
#else
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <functional>
#include <map>
#include <memory>
@ -333,6 +348,30 @@ void CondvarWait(Condvar& cv, Lock& lock, std::stop_token token, Pred pred) {
cv.wait(lock, [&] { return pred() || token.stop_requested(); });
}
template <typename Rep, typename Period>
bool StoppableTimedWait(std::stop_token token, const std::chrono::duration<Rep, Period>& rel_time) {
if (token.stop_requested()) {
return false;
}
bool stop_requested = false;
std::condition_variable cv;
std::mutex m;
std::stop_callback cb(token, [&] {
// Wake up the waiting thread.
{
std::scoped_lock lk{m};
stop_requested = true;
}
cv.notify_one();
});
// Perform the timed wait.
std::unique_lock lk{m};
return !cv.wait_for(lk, rel_time, [&] { return stop_requested; });
}
} // namespace Common
#endif // __cpp_lib_jthread

View File

@ -172,6 +172,8 @@ add_executable(lime-qt
multiplayer/state.cpp
multiplayer/state.h
multiplayer/validation.h
play_time_manager.cpp
play_time_manager.h
precompiled_headers.h
uisettings.cpp
uisettings.h

View File

@ -804,6 +804,7 @@ void Config::ReadUIGameListValues() {
ReadBasicSetting(UISettings::values.show_region_column);
ReadBasicSetting(UISettings::values.show_type_column);
ReadBasicSetting(UISettings::values.show_size_column);
ReadBasicSetting(UISettings::values.show_play_time_column);
const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
for (int i = 0; i < favorites_size; i++) {
@ -1293,6 +1294,7 @@ void Config::SaveUIGameListValues() {
WriteBasicSetting(UISettings::values.show_region_column);
WriteBasicSetting(UISettings::values.show_type_column);
WriteBasicSetting(UISettings::values.show_size_column);
WriteBasicSetting(UISettings::values.show_play_time_column);
qt_config->beginWriteArray(QStringLiteral("favorites"));
for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {

View File

@ -306,7 +306,8 @@ void GameList::OnFilterCloseClicked() {
main_window->filterBarSetChecked(false);
}
GameList::GameList(GMainWindow* parent) : QWidget{parent} {
GameList::GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent)
: QWidget{parent}, play_time_manager{play_time_manager_} {
watcher = new QFileSystemWatcher(this);
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory,
Qt::UniqueConnection);
@ -522,7 +523,8 @@ void GameList::PopupHeaderContextMenu(const QPoint& menu_location) {
{tr("Compatibility"), &UISettings::values.show_compat_column},
{tr("Region"), &UISettings::values.show_region_column},
{tr("File type"), &UISettings::values.show_type_column},
{tr("Size"), &UISettings::values.show_size_column}};
{tr("Size"), &UISettings::values.show_size_column},
{tr("Play time"), &UISettings::values.show_play_time_column}};
QActionGroup* column_group = new QActionGroup(this);
column_group->setExclusive(false);
@ -544,6 +546,7 @@ void GameList::UpdateColumnVisibility() {
tree_view->setColumnHidden(COLUMN_REGION, !UISettings::values.show_region_column);
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_type_column);
tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size_column);
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time_column);
}
#ifdef ENABLE_OPENGL
@ -591,6 +594,7 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
QAction* uninstall_update = uninstall_menu->addAction(tr("Update"));
QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC"));
QAction* remove_play_time_data = context_menu.addAction(tr("Remove Play Time Data"));
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
#if !defined(__APPLE__)
@ -712,6 +716,8 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
});
connect(dump_romfs, &QAction::triggered, this,
[this, path, program_id] { emit DumpRomFSRequested(path, program_id); });
connect(remove_play_time_data, &QAction::triggered,
[this, program_id]() { emit RemovePlayTimeRequested(program_id); });
connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
});
@ -933,6 +939,7 @@ void GameList::RetranslateUI() {
item_model->setHeaderData(COLUMN_REGION, Qt::Horizontal, tr("Region"));
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time"));
}
void GameListSearchField::changeEvent(QEvent* event) {
@ -964,7 +971,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
emit ShouldCancelWorker();
GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list);
GameListWorker* worker = new GameListWorker(game_dirs, compatibility_list, play_time_manager);
connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,

View File

@ -10,6 +10,7 @@
#include <QWidget>
#include "common/common_types.h"
#include "lime_qt/compatibility_list.h"
#include "lime_qt/play_time_manager.h"
#include "uisettings.h"
namespace Service::FS {
@ -60,10 +61,11 @@ public:
COLUMN_REGION,
COLUMN_FILE_TYPE,
COLUMN_SIZE,
COLUMN_PLAY_TIME,
COLUMN_COUNT, // Number of columns
};
explicit GameList(GMainWindow* parent = nullptr);
explicit GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent = nullptr);
~GameList() override;
QString GetLastFilterResultItem() const;
@ -97,6 +99,7 @@ signals:
void OpenFolderRequested(u64 program_id, GameListOpenTarget target);
void CreateShortcut(u64 program_id, const std::string& game_path,
GameListShortcutTarget target);
void RemovePlayTimeRequested(u64 program_id);
void NavigateToGamedbEntryRequested(u64 program_id,
const CompatibilityList& compatibility_list);
void OpenPerGameGeneralRequested(const QString file);
@ -142,6 +145,8 @@ private:
CompatibilityList compatibility_list;
friend class GameListSearchField;
const PlayTime::PlayTimeManager& play_time_manager;
};
Q_DECLARE_METATYPE(GameListOpenTarget);

View File

@ -22,6 +22,7 @@
#include "common/logging/log.h"
#include "common/string_util.h"
#include "core/loader/smdh.h"
#include "lime_qt/play_time_manager.h"
#include "lime_qt/uisettings.h"
#include "lime_qt/util/util.h"
@ -362,6 +363,31 @@ public:
}
};
/**
* GameListItem for Play Time values.
* This object stores the play time of a game in seconds, and its readable
* representation in minutes/hours
*/
class GameListItemPlayTime : public GameListItem {
public:
static constexpr int PlayTimeRole = SortRole;
GameListItemPlayTime() = default;
explicit GameListItemPlayTime(const qulonglong time_seconds) {
setData(time_seconds, PlayTimeRole);
}
void setData(const QVariant& value, int role) override {
qulonglong time_seconds = value.toULongLong();
GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole);
GameListItem::setData(value, PlayTimeRole);
}
bool operator<(const QStandardItem& other) const override {
return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong();
}
};
class GameListDir : public GameListItem {
public:
static constexpr int GameDirRole = Qt::UserRole + 2;

View File

@ -27,8 +27,10 @@ bool HasSupportedFileExtension(const std::string& file_name) {
} // Anonymous namespace
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
const CompatibilityList& compatibility_list)
: game_dirs(game_dirs), compatibility_list(compatibility_list) {}
const CompatibilityList& compatibility_list,
const PlayTime::PlayTimeManager& play_time_manager_)
: game_dirs(game_dirs), compatibility_list(compatibility_list),
play_time_manager{play_time_manager_} {}
GameListWorker::~GameListWorker() = default;
@ -112,6 +114,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
new GameListItem(
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
new GameListItemSize(FileUtil::GetSize(physical_name)),
new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
},
parent_dir);

View File

@ -14,6 +14,7 @@
#include <QVector>
#include "common/common_types.h"
#include "lime_qt/compatibility_list.h"
#include "lime_qt/play_time_manager.h"
namespace Service::FS {
enum class MediaType : u32;
@ -30,7 +31,8 @@ class GameListWorker : public QObject, public QRunnable {
public:
GameListWorker(QVector<UISettings::GameDir>& game_dirs,
const CompatibilityList& compatibility_list);
const CompatibilityList& compatibility_list,
const PlayTime::PlayTimeManager& play_time_manager_);
~GameListWorker() override;
/// Starts the processing of directory tree information.
@ -60,6 +62,7 @@ private:
QVector<UISettings::GameDir>& game_dirs;
const CompatibilityList& compatibility_list;
const PlayTime::PlayTimeManager& play_time_manager;
QStringList watch_list;
std::atomic_bool stop_processing;

View File

@ -71,6 +71,7 @@
#include "lime_qt/movie/movie_play_dialog.h"
#include "lime_qt/movie/movie_record_dialog.h"
#include "lime_qt/multiplayer/state.h"
#include "lime_qt/play_time_manager.h"
#include "lime_qt/qt_image_interface.h"
#include "lime_qt/uisettings.h"
#include "lime_qt/updater/updater.h"
@ -188,6 +189,8 @@ GMainWindow::GMainWindow(Core::System& system_)
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
discord_rpc->Update();
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
Network::Init();
movie.SetPlaybackCompletionCallback([this] {
@ -339,7 +342,7 @@ void GMainWindow::InitializeWidgets() {
secondary_window->hide();
secondary_window->setParent(nullptr);
game_list = new GameList(this);
game_list = new GameList(*play_time_manager, this);
ui->horizontalLayout->addWidget(game_list);
game_list_placeholder = new GameListPlaceholder(this);
@ -825,6 +828,8 @@ void GMainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile);
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
connect(game_list, &GameList::RemovePlayTimeRequested, this,
&GMainWindow::OnGameListRemovePlayTimeData);
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
&GMainWindow::OnGameListNavigateToGamedbEntry);
connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);
@ -1223,7 +1228,11 @@ bool GMainWindow::LoadROM(const QString& filename) {
game_title_long = QString::fromStdString(title_long);
UpdateWindowTitle();
u64 title_id;
system.GetAppLoader().ReadProgramId(title_id);
game_path = filename;
game_title_id = title_id;
return true;
}
@ -1445,6 +1454,7 @@ void GMainWindow::ShutdownGame() {
UpdateWindowTitle();
game_path.clear();
game_title_id = 0;
// Update the GUI
UpdateMenuState();
@ -1632,6 +1642,17 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) {
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
}
void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes) {
return;
}
play_time_manager->ResetProgramPlayTime(program_id);
game_list->PopulateAsync(UISettings::values.game_dirs);
}
void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list) {
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
@ -2165,6 +2186,9 @@ void GMainWindow::OnStartGame() {
UpdateMenuState();
play_time_manager->SetProgramId(game_title_id);
play_time_manager->Start();
discord_rpc->Update();
#ifdef __unix__
@ -2187,6 +2211,8 @@ void GMainWindow::OnPauseGame() {
emu_thread->SetRunning(false);
qt_cameras->PauseCameras();
play_time_manager->Stop();
UpdateMenuState();
AllowOSSleep();
@ -2206,6 +2232,10 @@ void GMainWindow::OnPauseContinueGame() {
}
void GMainWindow::OnStopGame() {
play_time_manager->Stop();
// Update game list to show new play time
game_list->PopulateAsync(UISettings::values.game_dirs);
ShutdownGame();
graphics_api_button->setEnabled(true);
Settings::RestoreGlobalState(false);

View File

@ -64,6 +64,10 @@ namespace DiscordRPC {
class DiscordInterface;
}
namespace PlayTime {
class PlayTimeManager;
}
namespace Core {
class Movie;
}
@ -94,6 +98,7 @@ public:
~GMainWindow();
GameList* game_list;
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
bool DropAction(QDropEvent* event);
@ -224,6 +229,7 @@ private slots:
/// Called whenever a user selects a game in the game list widget.
void OnGameListLoadFile(QString game_path);
void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target);
void OnGameListRemovePlayTimeData(u64 program_id);
void OnGameListNavigateToGamedbEntry(u64 program_id,
const CompatibilityList& compatibility_list);
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
@ -298,6 +304,7 @@ private:
void UpdateWindowTitle();
void UpdateUISettings();
void RetranslateStatusBar();
void RemovePlayTimeData(u64 program_id);
void InstallCIA(QStringList filepaths);
void HideMouseCursor();
void ShowMouseCursor();
@ -343,6 +350,8 @@ private:
QString game_title_long;
// The path to the game currently running
QString game_path;
// The title id of the game currently running
u64 game_title_id;
bool auto_paused = false;
bool auto_muted = false;

View File

@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: 2024 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include "common/alignment.h"
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "common/thread.h"
#include "lime_qt/play_time_manager.h"
namespace PlayTime {
namespace {
struct PlayTimeElement {
ProgramId program_id;
PlayTime play_time;
};
std::string GetCurrentUserPlayTimePath() {
return FileUtil::GetUserPath(FileUtil::UserPath::PlayTimeDir) + DIR_SEP + "play_time.bin";
}
[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) {
const auto filename = GetCurrentUserPlayTimePath();
out_play_time_db.clear();
if (FileUtil::Exists(filename)) {
FileUtil::IOFile file{filename, "rb"};
if (!file.IsOpen()) {
LOG_ERROR(Frontend, "Failed to open play time file: {}", filename);
return false;
}
const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement);
std::vector<PlayTimeElement> elements(num_elements);
if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) {
return false;
}
for (const auto& [program_id, play_time] : elements) {
if (program_id != 0) {
out_play_time_db[program_id] = play_time;
}
}
}
return true;
}
[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) {
const auto filename = GetCurrentUserPlayTimePath();
FileUtil::IOFile file{filename, "wb"};
if (!file.IsOpen()) {
LOG_ERROR(Frontend, "Failed to open play time file: {}", filename);
return false;
}
std::vector<PlayTimeElement> elements;
elements.reserve(play_time_db.size());
for (auto& [program_id, play_time] : play_time_db) {
if (program_id != 0) {
elements.push_back(PlayTimeElement{program_id, play_time});
}
}
return file.WriteSpan<PlayTimeElement>(elements) == elements.size();
}
} // namespace
PlayTimeManager::PlayTimeManager() {
if (!ReadPlayTimeFile(database)) {
LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
}
}
PlayTimeManager::~PlayTimeManager() {
Save();
}
void PlayTimeManager::SetProgramId(u64 program_id) {
running_program_id = program_id;
}
void PlayTimeManager::Start() {
play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); });
}
void PlayTimeManager::Stop() {
play_time_thread = {};
}
void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
Common::SetCurrentThreadName("PlayTimeReport");
using namespace std::literals::chrono_literals;
using std::chrono::seconds;
using std::chrono::steady_clock;
auto timestamp = steady_clock::now();
const auto GetDuration = [&]() -> u64 {
const auto last_timestamp = std::exchange(timestamp, steady_clock::now());
const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp);
return static_cast<u64>(duration.count());
};
while (!stop_token.stop_requested()) {
Common::StoppableTimedWait(stop_token, 30s);
database[running_program_id] += GetDuration();
Save();
}
}
void PlayTimeManager::Save() {
if (!WritePlayTimeFile(database)) {
LOG_ERROR(Frontend, "Failed to update play time database!");
}
}
u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
auto it = database.find(program_id);
if (it != database.end()) {
return it->second;
} else {
return 0;
}
}
void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
database.erase(program_id);
Save();
}
QString ReadablePlayTime(qulonglong time_seconds) {
if (time_seconds == 0) {
return {};
}
const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0);
const auto time_hours = static_cast<double>(time_seconds) / 3600;
const bool is_minutes = time_minutes < 60;
const char* unit = is_minutes ? "m" : "h";
const auto value = is_minutes ? time_minutes : time_hours;
return QStringLiteral("%L1 %2")
.arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0)
.arg(QString::fromUtf8(unit));
}
} // namespace PlayTime

View File

@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2024 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QString>
#include <map>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/polyfill_thread.h"
namespace PlayTime {
using ProgramId = u64;
using PlayTime = u64;
using PlayTimeDatabase = std::map<ProgramId, PlayTime>;
class PlayTimeManager {
public:
explicit PlayTimeManager();
~PlayTimeManager();
PlayTimeManager(const PlayTimeManager&) = delete;
PlayTimeManager& operator=(const PlayTimeManager&) = delete;
u64 GetPlayTime(u64 program_id) const;
void ResetProgramPlayTime(u64 program_id);
void SetProgramId(u64 program_id);
void Start();
void Stop();
private:
void AutoTimestamp(std::stop_token stop_token);
void Save();
PlayTimeDatabase database;
u64 running_program_id;
std::jthread play_time_thread;
};
QString ReadablePlayTime(qulonglong time_seconds);
} // namespace PlayTime

View File

@ -103,6 +103,7 @@ struct Values {
Settings::Setting<bool> show_region_column{true, "show_region_column"};
Settings::Setting<bool> show_type_column{true, "show_type_column"};
Settings::Setting<bool> show_size_column{true, "show_size_column"};
Settings::Setting<bool> show_play_time_column{true, "show_play_time_column"};
Settings::Setting<u16> screenshot_resolution_factor{0, "screenshot_resolution_factor"};
Settings::SwitchableSetting<std::string> screenshot_path{"", "screenshotPath"};