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.cpp
|
||||||
multiplayer/state.h
|
multiplayer/state.h
|
||||||
multiplayer/validation.h
|
multiplayer/validation.h
|
||||||
|
play_time_manager.cpp
|
||||||
|
play_time_manager.h
|
||||||
precompiled_headers.h
|
precompiled_headers.h
|
||||||
uisettings.cpp
|
uisettings.cpp
|
||||||
uisettings.h
|
uisettings.h
|
||||||
|
|
|
@ -790,6 +790,7 @@ void Config::ReadUIGameListValues() {
|
||||||
ReadBasicSetting(UISettings::values.show_region_column);
|
ReadBasicSetting(UISettings::values.show_region_column);
|
||||||
ReadBasicSetting(UISettings::values.show_type_column);
|
ReadBasicSetting(UISettings::values.show_type_column);
|
||||||
ReadBasicSetting(UISettings::values.show_size_column);
|
ReadBasicSetting(UISettings::values.show_size_column);
|
||||||
|
ReadBasicSetting(UISettings::values.show_play_time_column);
|
||||||
|
|
||||||
const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
|
const int favorites_size = qt_config->beginReadArray(QStringLiteral("favorites"));
|
||||||
for (int i = 0; i < favorites_size; i++) {
|
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_region_column);
|
||||||
WriteBasicSetting(UISettings::values.show_type_column);
|
WriteBasicSetting(UISettings::values.show_type_column);
|
||||||
WriteBasicSetting(UISettings::values.show_size_column);
|
WriteBasicSetting(UISettings::values.show_size_column);
|
||||||
|
WriteBasicSetting(UISettings::values.show_play_time_column);
|
||||||
|
|
||||||
qt_config->beginWriteArray(QStringLiteral("favorites"));
|
qt_config->beginWriteArray(QStringLiteral("favorites"));
|
||||||
for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {
|
for (int i = 0; i < UISettings::values.favorited_ids.size(); i++) {
|
||||||
|
|
|
@ -306,7 +306,8 @@ void GameList::OnFilterCloseClicked() {
|
||||||
main_window->filterBarSetChecked(false);
|
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);
|
watcher = new QFileSystemWatcher(this);
|
||||||
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory,
|
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory,
|
||||||
Qt::UniqueConnection);
|
Qt::UniqueConnection);
|
||||||
|
@ -522,7 +523,8 @@ void GameList::PopupHeaderContextMenu(const QPoint& menu_location) {
|
||||||
{tr("Compatibility"), &UISettings::values.show_compat_column},
|
{tr("Compatibility"), &UISettings::values.show_compat_column},
|
||||||
{tr("Region"), &UISettings::values.show_region_column},
|
{tr("Region"), &UISettings::values.show_region_column},
|
||||||
{tr("File type"), &UISettings::values.show_type_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);
|
QActionGroup* column_group = new QActionGroup(this);
|
||||||
column_group->setExclusive(false);
|
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_REGION, !UISettings::values.show_region_column);
|
||||||
tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_type_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_SIZE, !UISettings::values.show_size_column);
|
||||||
|
tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time_column);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_OPENGL
|
#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_update = uninstall_menu->addAction(tr("Update"));
|
||||||
QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC"));
|
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"));
|
QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
|
||||||
|
|
||||||
#if !defined(__APPLE__)
|
#if !defined(__APPLE__)
|
||||||
|
@ -712,6 +716,8 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr
|
||||||
});
|
});
|
||||||
connect(dump_romfs, &QAction::triggered, this,
|
connect(dump_romfs, &QAction::triggered, this,
|
||||||
[this, path, program_id] { emit DumpRomFSRequested(path, program_id); });
|
[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]() {
|
connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() {
|
||||||
emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
|
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_REGION, Qt::Horizontal, tr("Region"));
|
||||||
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
|
item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type"));
|
||||||
item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size"));
|
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) {
|
void GameListSearchField::changeEvent(QEvent* event) {
|
||||||
|
@ -964,7 +971,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||||
|
|
||||||
emit ShouldCancelWorker();
|
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::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection);
|
||||||
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
|
connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry,
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include "citra_qt/compatibility_list.h"
|
#include "citra_qt/compatibility_list.h"
|
||||||
|
#include "citra_qt/play_time_manager.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "uisettings.h"
|
#include "uisettings.h"
|
||||||
|
|
||||||
|
@ -60,10 +61,11 @@ public:
|
||||||
COLUMN_REGION,
|
COLUMN_REGION,
|
||||||
COLUMN_FILE_TYPE,
|
COLUMN_FILE_TYPE,
|
||||||
COLUMN_SIZE,
|
COLUMN_SIZE,
|
||||||
|
COLUMN_PLAY_TIME,
|
||||||
COLUMN_COUNT, // Number of columns
|
COLUMN_COUNT, // Number of columns
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit GameList(GMainWindow* parent = nullptr);
|
explicit GameList(PlayTime::PlayTimeManager& play_time_manager_, GMainWindow* parent = nullptr);
|
||||||
~GameList() override;
|
~GameList() override;
|
||||||
|
|
||||||
QString GetLastFilterResultItem() const;
|
QString GetLastFilterResultItem() const;
|
||||||
|
@ -97,6 +99,7 @@ signals:
|
||||||
void OpenFolderRequested(u64 program_id, GameListOpenTarget target);
|
void OpenFolderRequested(u64 program_id, GameListOpenTarget target);
|
||||||
void CreateShortcut(u64 program_id, const std::string& game_path,
|
void CreateShortcut(u64 program_id, const std::string& game_path,
|
||||||
GameListShortcutTarget target);
|
GameListShortcutTarget target);
|
||||||
|
void RemovePlayTimeRequested(u64 program_id);
|
||||||
void NavigateToGamedbEntryRequested(u64 program_id,
|
void NavigateToGamedbEntryRequested(u64 program_id,
|
||||||
const CompatibilityList& compatibility_list);
|
const CompatibilityList& compatibility_list);
|
||||||
void OpenPerGameGeneralRequested(const QString file);
|
void OpenPerGameGeneralRequested(const QString file);
|
||||||
|
@ -142,6 +145,8 @@ private:
|
||||||
CompatibilityList compatibility_list;
|
CompatibilityList compatibility_list;
|
||||||
|
|
||||||
friend class GameListSearchField;
|
friend class GameListSearchField;
|
||||||
|
|
||||||
|
const PlayTime::PlayTimeManager& play_time_manager;
|
||||||
};
|
};
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(GameListOpenTarget);
|
Q_DECLARE_METATYPE(GameListOpenTarget);
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
#include <QStandardItem>
|
#include <QStandardItem>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
#include "citra_qt/play_time_manager.h"
|
||||||
#include "citra_qt/uisettings.h"
|
#include "citra_qt/uisettings.h"
|
||||||
#include "citra_qt/util/util.h"
|
#include "citra_qt/util/util.h"
|
||||||
#include "common/file_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 {
|
class GameListDir : public GameListItem {
|
||||||
public:
|
public:
|
||||||
static constexpr int GameDirRole = Qt::UserRole + 2;
|
static constexpr int GameDirRole = Qt::UserRole + 2;
|
||||||
|
|
|
@ -27,8 +27,10 @@ bool HasSupportedFileExtension(const std::string& file_name) {
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
|
GameListWorker::GameListWorker(QVector<UISettings::GameDir>& game_dirs,
|
||||||
const CompatibilityList& compatibility_list)
|
const CompatibilityList& compatibility_list,
|
||||||
: game_dirs(game_dirs), compatibility_list(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;
|
GameListWorker::~GameListWorker() = default;
|
||||||
|
|
||||||
|
@ -112,6 +114,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign
|
||||||
new GameListItem(
|
new GameListItem(
|
||||||
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
|
QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))),
|
||||||
new GameListItemSize(FileUtil::GetSize(physical_name)),
|
new GameListItemSize(FileUtil::GetSize(physical_name)),
|
||||||
|
new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
|
||||||
},
|
},
|
||||||
parent_dir);
|
parent_dir);
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include "citra_qt/compatibility_list.h"
|
#include "citra_qt/compatibility_list.h"
|
||||||
|
#include "citra_qt/play_time_manager.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace Service::FS {
|
namespace Service::FS {
|
||||||
|
@ -30,7 +31,8 @@ class GameListWorker : public QObject, public QRunnable {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GameListWorker(QVector<UISettings::GameDir>& game_dirs,
|
GameListWorker(QVector<UISettings::GameDir>& game_dirs,
|
||||||
const CompatibilityList& compatibility_list);
|
const CompatibilityList& compatibility_list,
|
||||||
|
const PlayTime::PlayTimeManager& play_time_manager_);
|
||||||
~GameListWorker() override;
|
~GameListWorker() override;
|
||||||
|
|
||||||
/// Starts the processing of directory tree information.
|
/// Starts the processing of directory tree information.
|
||||||
|
@ -60,6 +62,7 @@ private:
|
||||||
|
|
||||||
QVector<UISettings::GameDir>& game_dirs;
|
QVector<UISettings::GameDir>& game_dirs;
|
||||||
const CompatibilityList& compatibility_list;
|
const CompatibilityList& compatibility_list;
|
||||||
|
const PlayTime::PlayTimeManager& play_time_manager;
|
||||||
|
|
||||||
QStringList watch_list;
|
QStringList watch_list;
|
||||||
std::atomic_bool stop_processing;
|
std::atomic_bool stop_processing;
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
#include "citra_qt/movie/movie_play_dialog.h"
|
#include "citra_qt/movie/movie_play_dialog.h"
|
||||||
#include "citra_qt/movie/movie_record_dialog.h"
|
#include "citra_qt/movie/movie_record_dialog.h"
|
||||||
#include "citra_qt/multiplayer/state.h"
|
#include "citra_qt/multiplayer/state.h"
|
||||||
|
#include "citra_qt/play_time_manager.h"
|
||||||
#include "citra_qt/qt_image_interface.h"
|
#include "citra_qt/qt_image_interface.h"
|
||||||
#include "citra_qt/uisettings.h"
|
#include "citra_qt/uisettings.h"
|
||||||
#include "citra_qt/updater/updater.h"
|
#include "citra_qt/updater/updater.h"
|
||||||
|
@ -210,6 +211,8 @@ GMainWindow::GMainWindow(Core::System& system_)
|
||||||
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
|
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
|
||||||
discord_rpc->Update();
|
discord_rpc->Update();
|
||||||
|
|
||||||
|
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
|
||||||
|
|
||||||
Network::Init();
|
Network::Init();
|
||||||
|
|
||||||
movie.SetPlaybackCompletionCallback([this] {
|
movie.SetPlaybackCompletionCallback([this] {
|
||||||
|
@ -364,7 +367,7 @@ void GMainWindow::InitializeWidgets() {
|
||||||
secondary_window->hide();
|
secondary_window->hide();
|
||||||
secondary_window->setParent(nullptr);
|
secondary_window->setParent(nullptr);
|
||||||
|
|
||||||
game_list = new GameList(this);
|
game_list = new GameList(*play_time_manager, this);
|
||||||
ui->horizontalLayout->addWidget(game_list);
|
ui->horizontalLayout->addWidget(game_list);
|
||||||
|
|
||||||
game_list_placeholder = new GameListPlaceholder(this);
|
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::GameChosen, this, &GMainWindow::OnGameListLoadFile);
|
||||||
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
|
connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory);
|
||||||
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
|
connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder);
|
||||||
|
connect(game_list, &GameList::RemovePlayTimeRequested, this,
|
||||||
|
&GMainWindow::OnGameListRemovePlayTimeData);
|
||||||
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
|
connect(game_list, &GameList::NavigateToGamedbEntryRequested, this,
|
||||||
&GMainWindow::OnGameListNavigateToGamedbEntry);
|
&GMainWindow::OnGameListNavigateToGamedbEntry);
|
||||||
connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);
|
connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut);
|
||||||
|
@ -1238,7 +1243,11 @@ bool GMainWindow::LoadROM(const QString& filename) {
|
||||||
game_title = QString::fromStdString(title);
|
game_title = QString::fromStdString(title);
|
||||||
UpdateWindowTitle();
|
UpdateWindowTitle();
|
||||||
|
|
||||||
|
u64 title_id;
|
||||||
|
system.GetAppLoader().ReadProgramId(title_id);
|
||||||
|
|
||||||
game_path = filename;
|
game_path = filename;
|
||||||
|
game_title_id = title_id;
|
||||||
|
|
||||||
system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
|
system.TelemetrySession().AddField(Common::Telemetry::FieldType::App, "Frontend", "Qt");
|
||||||
return true;
|
return true;
|
||||||
|
@ -1460,6 +1469,7 @@ void GMainWindow::ShutdownGame() {
|
||||||
UpdateWindowTitle();
|
UpdateWindowTitle();
|
||||||
|
|
||||||
game_path.clear();
|
game_path.clear();
|
||||||
|
game_title_id = 0;
|
||||||
|
|
||||||
// Update the GUI
|
// Update the GUI
|
||||||
UpdateMenuState();
|
UpdateMenuState();
|
||||||
|
@ -1647,6 +1657,17 @@ void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) {
|
||||||
QDesktopServices::openUrl(QUrl::fromLocalFile(qpath));
|
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,
|
void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id,
|
||||||
const CompatibilityList& compatibility_list) {
|
const CompatibilityList& compatibility_list) {
|
||||||
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
|
auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
|
||||||
|
@ -2179,6 +2200,9 @@ void GMainWindow::OnStartGame() {
|
||||||
|
|
||||||
UpdateMenuState();
|
UpdateMenuState();
|
||||||
|
|
||||||
|
play_time_manager->SetProgramId(game_title_id);
|
||||||
|
play_time_manager->Start();
|
||||||
|
|
||||||
discord_rpc->Update();
|
discord_rpc->Update();
|
||||||
|
|
||||||
#ifdef __unix__
|
#ifdef __unix__
|
||||||
|
@ -2201,6 +2225,8 @@ void GMainWindow::OnPauseGame() {
|
||||||
emu_thread->SetRunning(false);
|
emu_thread->SetRunning(false);
|
||||||
qt_cameras->PauseCameras();
|
qt_cameras->PauseCameras();
|
||||||
|
|
||||||
|
play_time_manager->Stop();
|
||||||
|
|
||||||
UpdateMenuState();
|
UpdateMenuState();
|
||||||
AllowOSSleep();
|
AllowOSSleep();
|
||||||
|
|
||||||
|
@ -2220,6 +2246,10 @@ void GMainWindow::OnPauseContinueGame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnStopGame() {
|
void GMainWindow::OnStopGame() {
|
||||||
|
play_time_manager->Stop();
|
||||||
|
// Update game list to show new play time
|
||||||
|
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||||
|
|
||||||
ShutdownGame();
|
ShutdownGame();
|
||||||
graphics_api_button->setEnabled(true);
|
graphics_api_button->setEnabled(true);
|
||||||
Settings::RestoreGlobalState(false);
|
Settings::RestoreGlobalState(false);
|
||||||
|
|
|
@ -64,6 +64,10 @@ namespace DiscordRPC {
|
||||||
class DiscordInterface;
|
class DiscordInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace PlayTime {
|
||||||
|
class PlayTimeManager;
|
||||||
|
}
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
class Movie;
|
class Movie;
|
||||||
}
|
}
|
||||||
|
@ -94,6 +98,7 @@ public:
|
||||||
~GMainWindow();
|
~GMainWindow();
|
||||||
|
|
||||||
GameList* game_list;
|
GameList* game_list;
|
||||||
|
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
||||||
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
|
std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc;
|
||||||
|
|
||||||
bool DropAction(QDropEvent* event);
|
bool DropAction(QDropEvent* event);
|
||||||
|
@ -225,6 +230,7 @@ private slots:
|
||||||
/// Called whenever a user selects a game in the game list widget.
|
/// Called whenever a user selects a game in the game list widget.
|
||||||
void OnGameListLoadFile(QString game_path);
|
void OnGameListLoadFile(QString game_path);
|
||||||
void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target);
|
void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target);
|
||||||
|
void OnGameListRemovePlayTimeData(u64 program_id);
|
||||||
void OnGameListNavigateToGamedbEntry(u64 program_id,
|
void OnGameListNavigateToGamedbEntry(u64 program_id,
|
||||||
const CompatibilityList& compatibility_list);
|
const CompatibilityList& compatibility_list);
|
||||||
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
|
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
|
||||||
|
@ -299,6 +305,7 @@ private:
|
||||||
void UpdateWindowTitle();
|
void UpdateWindowTitle();
|
||||||
void UpdateUISettings();
|
void UpdateUISettings();
|
||||||
void RetranslateStatusBar();
|
void RetranslateStatusBar();
|
||||||
|
void RemovePlayTimeData(u64 program_id);
|
||||||
void InstallCIA(QStringList filepaths);
|
void InstallCIA(QStringList filepaths);
|
||||||
void HideMouseCursor();
|
void HideMouseCursor();
|
||||||
void ShowMouseCursor();
|
void ShowMouseCursor();
|
||||||
|
@ -343,6 +350,8 @@ private:
|
||||||
QString game_title;
|
QString game_title;
|
||||||
// The path to the game currently running
|
// The path to the game currently running
|
||||||
QString game_path;
|
QString game_path;
|
||||||
|
// The title id of the game currently running
|
||||||
|
u64 game_title_id;
|
||||||
|
|
||||||
bool auto_paused = false;
|
bool auto_paused = false;
|
||||||
bool auto_muted = 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_region_column{true, "show_region_column"};
|
||||||
Settings::Setting<bool> show_type_column{true, "show_type_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_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::Setting<u16> screenshot_resolution_factor{0, "screenshot_resolution_factor"};
|
||||||
Settings::SwitchableSetting<std::string> screenshot_path{"", "screenshotPath"};
|
Settings::SwitchableSetting<std::string> screenshot_path{"", "screenshotPath"};
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
#define SHADER_DIR "shaders"
|
#define SHADER_DIR "shaders"
|
||||||
#define STATES_DIR "states"
|
#define STATES_DIR "states"
|
||||||
#define ICONS_DIR "icons"
|
#define ICONS_DIR "icons"
|
||||||
|
#define PLAY_TIME_DIR "play_time"
|
||||||
|
|
||||||
// Filenames
|
// Filenames
|
||||||
// Files in the directory returned by GetUserPath(UserPath::LogDir)
|
// 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::LoadDir, user_path + LOAD_DIR DIR_SEP);
|
||||||
g_paths.emplace(UserPath::StatesDir, user_path + STATES_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::IconsDir, user_path + ICONS_DIR DIR_SEP);
|
||||||
|
g_paths.emplace(UserPath::PlayTimeDir, user_path + PLAY_TIME_DIR DIR_SEP);
|
||||||
g_default_paths = g_paths;
|
g_default_paths = g_paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ enum class UserPath {
|
||||||
SysDataDir,
|
SysDataDir,
|
||||||
UserDir,
|
UserDir,
|
||||||
IconsDir,
|
IconsDir,
|
||||||
|
PlayTimeDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Replaces install-specific paths with standard placeholders, and back again
|
// Replaces install-specific paths with standard placeholders, and back again
|
||||||
|
@ -347,6 +348,59 @@ public:
|
||||||
return WriteArray(str.data(), str.length());
|
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 {
|
[[nodiscard]] bool IsOpen() const {
|
||||||
return nullptr != m_file;
|
return nullptr != m_file;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,11 @@
|
||||||
|
|
||||||
#ifdef __cpp_lib_jthread
|
#ifdef __cpp_lib_jthread
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <stop_token>
|
#include <stop_token>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace Common {
|
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));
|
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
|
} // namespace Common
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#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(); });
|
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
|
} // namespace Common
|
||||||
|
|
||||||
#endif // __cpp_lib_jthread
|
#endif // __cpp_lib_jthread
|
||||||
|
|
Loading…
Reference in a new issue