citra_qt: Track play time
Co-Authored-By: Mario Davó <66087392+mdmrk@users.noreply.github.com>
This commit is contained in:
parent
4abe3f3436
commit
a4b16bfb1f
16 changed files with 394 additions and 8 deletions
|
@ -172,6 +172,8 @@ add_executable(citra-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
|
||||
|
|
|
@ -790,6 +790,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++) {
|
||||
|
@ -1272,6 +1273,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++) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <QVector>
|
||||
#include <QWidget>
|
||||
#include "citra_qt/compatibility_list.h"
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#include "common/common_types.h"
|
||||
#include "uisettings.h"
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <QStandardItem>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#include "citra_qt/uisettings.h"
|
||||
#include "citra_qt/util/util.h"
|
||||
#include "common/file_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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include <QString>
|
||||
#include <QVector>
|
||||
#include "citra_qt/compatibility_list.h"
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Service::FS {
|
||||
|
@ -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;
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
#include "citra_qt/movie/movie_play_dialog.h"
|
||||
#include "citra_qt/movie/movie_record_dialog.h"
|
||||
#include "citra_qt/multiplayer/state.h"
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#include "citra_qt/qt_image_interface.h"
|
||||
#include "citra_qt/uisettings.h"
|
||||
#include "citra_qt/updater/updater.h"
|
||||
|
@ -210,6 +211,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] {
|
||||
|
@ -364,7 +367,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);
|
||||
|
@ -843,6 +846,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);
|
||||
|
@ -1238,7 +1243,11 @@ bool GMainWindow::LoadROM(const QString& filename) {
|
|||
game_title = QString::fromStdString(title);
|
||||
UpdateWindowTitle();
|
||||
|
||||
u64 title_id;
|
||||
system.GetAppLoader().ReadProgramId(title_id);
|
||||
|
||||
game_path = filename;
|
||||
game_title_id = title_id;
|
||||
|
||||
system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
|
||||
return true;
|
||||
|
@ -1460,6 +1469,7 @@ void GMainWindow::ShutdownGame() {
|
|||
UpdateWindowTitle();
|
||||
|
||||
game_path.clear();
|
||||
game_title_id = 0;
|
||||
|
||||
// Update the GUI
|
||||
UpdateMenuState();
|
||||
|
@ -1647,6 +1657,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);
|
||||
|
@ -2179,6 +2200,9 @@ void GMainWindow::OnStartGame() {
|
|||
|
||||
UpdateMenuState();
|
||||
|
||||
play_time_manager->SetProgramId(game_title_id);
|
||||
play_time_manager->Start();
|
||||
|
||||
discord_rpc->Update();
|
||||
|
||||
#ifdef __unix__
|
||||
|
@ -2201,6 +2225,8 @@ void GMainWindow::OnPauseGame() {
|
|||
emu_thread->SetRunning(false);
|
||||
qt_cameras->PauseCameras();
|
||||
|
||||
play_time_manager->Stop();
|
||||
|
||||
UpdateMenuState();
|
||||
AllowOSSleep();
|
||||
|
||||
|
@ -2220,6 +2246,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);
|
||||
|
|
|
@ -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);
|
||||
|
@ -225,6 +230,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,
|
||||
|
@ -299,6 +305,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;
|
||||
// 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;
|
||||
|
|
158
src/citra_qt/play_time_manager.cpp
Normal file
158
src/citra_qt/play_time_manager.cpp
Normal file
|
@ -0,0 +1,158 @@
|
|||
// SPDX-FileCopyrightText: 2024 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <filesystem>
|
||||
#include "citra_qt/play_time_manager.h"
|
||||
#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"
|
||||
|
||||
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
|
45
src/citra_qt/play_time_manager.h
Normal file
45
src/citra_qt/play_time_manager.h
Normal 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
|
|
@ -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"};
|
||||
|
|
|
@ -54,6 +54,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)
|
||||
|
|
|
@ -818,6 +818,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ enum class UserPath {
|
|||
SysDataDir,
|
||||
UserDir,
|
||||
IconsDir,
|
||||
PlayTimeDir,
|
||||
};
|
||||
|
||||
// Replaces install-specific paths with standard placeholders, and back again
|
||||
|
@ -347,6 +348,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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue