diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/common/fs/fs_paths.h | 2 | ||||
| -rw-r--r-- | src/common/fs/path_util.cpp | 2 | ||||
| -rw-r--r-- | src/common/fs/path_util.h | 2 | ||||
| -rw-r--r-- | src/yuzu/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_ui.cpp | 5 | ||||
| -rw-r--r-- | src/yuzu/configuration/configure_ui.ui | 7 | ||||
| -rw-r--r-- | src/yuzu/game_list.cpp | 18 | ||||
| -rw-r--r-- | src/yuzu/game_list.h | 7 | ||||
| -rw-r--r-- | src/yuzu/game_list_p.h | 26 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.cpp | 23 | ||||
| -rw-r--r-- | src/yuzu/game_list_worker.h | 6 | ||||
| -rw-r--r-- | src/yuzu/main.cpp | 110 | ||||
| -rw-r--r-- | src/yuzu/main.h | 7 | ||||
| -rw-r--r-- | src/yuzu/play_time_manager.cpp | 179 | ||||
| -rw-r--r-- | src/yuzu/play_time_manager.h | 44 | ||||
| -rw-r--r-- | src/yuzu/uisettings.h | 3 | ||||
| -rw-r--r-- | src/yuzu/util/util.cpp | 77 | ||||
| -rw-r--r-- | src/yuzu/util/util.h | 14 |
18 files changed, 494 insertions, 40 deletions
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 61bac9eba..441c8af97 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h | |||
| @@ -18,10 +18,12 @@ | |||
| 18 | #define LOAD_DIR "load" | 18 | #define LOAD_DIR "load" |
| 19 | #define LOG_DIR "log" | 19 | #define LOG_DIR "log" |
| 20 | #define NAND_DIR "nand" | 20 | #define NAND_DIR "nand" |
| 21 | #define PLAY_TIME_DIR "play_time" | ||
| 21 | #define SCREENSHOTS_DIR "screenshots" | 22 | #define SCREENSHOTS_DIR "screenshots" |
| 22 | #define SDMC_DIR "sdmc" | 23 | #define SDMC_DIR "sdmc" |
| 23 | #define SHADER_DIR "shader" | 24 | #define SHADER_DIR "shader" |
| 24 | #define TAS_DIR "tas" | 25 | #define TAS_DIR "tas" |
| 26 | #define ICONS_DIR "icons" | ||
| 25 | 27 | ||
| 26 | // yuzu-specific files | 28 | // yuzu-specific files |
| 27 | 29 | ||
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index dce219fcf..0abd81a45 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp | |||
| @@ -124,10 +124,12 @@ public: | |||
| 124 | GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); | 124 | GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); |
| 125 | GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); | 125 | GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); |
| 126 | GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR); | 126 | GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR); |
| 127 | GenerateYuzuPath(YuzuPath::PlayTimeDir, yuzu_path / PLAY_TIME_DIR); | ||
| 127 | GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); | 128 | GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); |
| 128 | GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); | 129 | GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); |
| 129 | GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); | 130 | GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); |
| 130 | GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); | 131 | GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); |
| 132 | GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); | ||
| 131 | } | 133 | } |
| 132 | 134 | ||
| 133 | private: | 135 | private: |
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index ba28964d0..63801c924 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h | |||
| @@ -20,10 +20,12 @@ enum class YuzuPath { | |||
| 20 | LoadDir, // Where cheat/mod files are stored. | 20 | LoadDir, // Where cheat/mod files are stored. |
| 21 | LogDir, // Where log files are stored. | 21 | LogDir, // Where log files are stored. |
| 22 | NANDDir, // Where the emulated NAND is stored. | 22 | NANDDir, // Where the emulated NAND is stored. |
| 23 | PlayTimeDir, // Where play time data is stored. | ||
| 23 | ScreenshotsDir, // Where yuzu screenshots are stored. | 24 | ScreenshotsDir, // Where yuzu screenshots are stored. |
| 24 | SDMCDir, // Where the emulated SDMC is stored. | 25 | SDMCDir, // Where the emulated SDMC is stored. |
| 25 | ShaderDir, // Where shaders are stored. | 26 | ShaderDir, // Where shaders are stored. |
| 26 | TASDir, // Where TAS scripts are stored. | 27 | TASDir, // Where TAS scripts are stored. |
| 28 | IconsDir, // Where Icons for Windows shortcuts are stored. | ||
| 27 | }; | 29 | }; |
| 28 | 30 | ||
| 29 | /** | 31 | /** |
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 8f86a1553..9ebece907 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt | |||
| @@ -195,6 +195,8 @@ add_executable(yuzu | |||
| 195 | multiplayer/state.cpp | 195 | multiplayer/state.cpp |
| 196 | multiplayer/state.h | 196 | multiplayer/state.h |
| 197 | multiplayer/validation.h | 197 | multiplayer/validation.h |
| 198 | play_time_manager.cpp | ||
| 199 | play_time_manager.h | ||
| 198 | precompiled_headers.h | 200 | precompiled_headers.h |
| 199 | qt_common.cpp | 201 | qt_common.cpp |
| 200 | qt_common.h | 202 | qt_common.h |
diff --git a/src/yuzu/configuration/configure_ui.cpp b/src/yuzu/configuration/configure_ui.cpp index a9fde9f4f..82f3b6e78 100644 --- a/src/yuzu/configuration/configure_ui.cpp +++ b/src/yuzu/configuration/configure_ui.cpp | |||
| @@ -123,6 +123,8 @@ ConfigureUi::ConfigureUi(Core::System& system_, QWidget* parent) | |||
| 123 | connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); | 123 | connect(ui->show_compat, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); |
| 124 | connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); | 124 | connect(ui->show_size, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); |
| 125 | connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); | 125 | connect(ui->show_types, &QCheckBox::stateChanged, this, &ConfigureUi::RequestGameListUpdate); |
| 126 | connect(ui->show_play_time, &QCheckBox::stateChanged, this, | ||
| 127 | &ConfigureUi::RequestGameListUpdate); | ||
| 126 | connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, | 128 | connect(ui->game_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, |
| 127 | &ConfigureUi::RequestGameListUpdate); | 129 | &ConfigureUi::RequestGameListUpdate); |
| 128 | connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), | 130 | connect(ui->folder_icon_size_combobox, QOverload<int>::of(&QComboBox::currentIndexChanged), |
| @@ -167,6 +169,7 @@ void ConfigureUi::ApplyConfiguration() { | |||
| 167 | UISettings::values.show_compat = ui->show_compat->isChecked(); | 169 | UISettings::values.show_compat = ui->show_compat->isChecked(); |
| 168 | UISettings::values.show_size = ui->show_size->isChecked(); | 170 | UISettings::values.show_size = ui->show_size->isChecked(); |
| 169 | UISettings::values.show_types = ui->show_types->isChecked(); | 171 | UISettings::values.show_types = ui->show_types->isChecked(); |
| 172 | UISettings::values.show_play_time = ui->show_play_time->isChecked(); | ||
| 170 | UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt(); | 173 | UISettings::values.game_icon_size = ui->game_icon_size_combobox->currentData().toUInt(); |
| 171 | UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt(); | 174 | UISettings::values.folder_icon_size = ui->folder_icon_size_combobox->currentData().toUInt(); |
| 172 | UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); | 175 | UISettings::values.row_1_text_id = ui->row_1_text_combobox->currentData().toUInt(); |
| @@ -179,6 +182,7 @@ void ConfigureUi::ApplyConfiguration() { | |||
| 179 | const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); | 182 | const u32 height = ScreenshotDimensionToInt(ui->screenshot_height->currentText()); |
| 180 | UISettings::values.screenshot_height.SetValue(height); | 183 | UISettings::values.screenshot_height.SetValue(height); |
| 181 | 184 | ||
| 185 | RequestGameListUpdate(); | ||
| 182 | system.ApplySettings(); | 186 | system.ApplySettings(); |
| 183 | } | 187 | } |
| 184 | 188 | ||
| @@ -194,6 +198,7 @@ void ConfigureUi::SetConfiguration() { | |||
| 194 | ui->show_compat->setChecked(UISettings::values.show_compat.GetValue()); | 198 | ui->show_compat->setChecked(UISettings::values.show_compat.GetValue()); |
| 195 | ui->show_size->setChecked(UISettings::values.show_size.GetValue()); | 199 | ui->show_size->setChecked(UISettings::values.show_size.GetValue()); |
| 196 | ui->show_types->setChecked(UISettings::values.show_types.GetValue()); | 200 | ui->show_types->setChecked(UISettings::values.show_types.GetValue()); |
| 201 | ui->show_play_time->setChecked(UISettings::values.show_play_time.GetValue()); | ||
| 197 | ui->game_icon_size_combobox->setCurrentIndex( | 202 | ui->game_icon_size_combobox->setCurrentIndex( |
| 198 | ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue())); | 203 | ui->game_icon_size_combobox->findData(UISettings::values.game_icon_size.GetValue())); |
| 199 | ui->folder_icon_size_combobox->setCurrentIndex( | 204 | ui->folder_icon_size_combobox->setCurrentIndex( |
diff --git a/src/yuzu/configuration/configure_ui.ui b/src/yuzu/configuration/configure_ui.ui index cb66ef104..b8e648381 100644 --- a/src/yuzu/configuration/configure_ui.ui +++ b/src/yuzu/configuration/configure_ui.ui | |||
| @@ -105,6 +105,13 @@ | |||
| 105 | </widget> | 105 | </widget> |
| 106 | </item> | 106 | </item> |
| 107 | <item> | 107 | <item> |
| 108 | <widget class="QCheckBox" name="show_play_time"> | ||
| 109 | <property name="text"> | ||
| 110 | <string>Show Play Time Column</string> | ||
| 111 | </property> | ||
| 112 | </widget> | ||
| 113 | </item> | ||
| 114 | <item> | ||
| 108 | <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2"> | 115 | <layout class="QHBoxLayout" name="game_icon_size_qhbox_layout_2"> |
| 109 | <item> | 116 | <item> |
| 110 | <widget class="QLabel" name="game_icon_size_label"> | 117 | <widget class="QLabel" name="game_icon_size_label"> |
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index f254c1e1c..74f48031a 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp | |||
| @@ -312,8 +312,10 @@ void GameList::OnFilterCloseClicked() { | |||
| 312 | } | 312 | } |
| 313 | 313 | ||
| 314 | GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, | 314 | GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvider* provider_, |
| 315 | Core::System& system_, GMainWindow* parent) | 315 | PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, |
| 316 | : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, system{system_} { | 316 | GMainWindow* parent) |
| 317 | : QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_}, | ||
| 318 | play_time_manager{play_time_manager_}, system{system_} { | ||
| 317 | watcher = new QFileSystemWatcher(this); | 319 | watcher = new QFileSystemWatcher(this); |
| 318 | connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory); | 320 | connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory); |
| 319 | 321 | ||
| @@ -340,6 +342,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid | |||
| 340 | 342 | ||
| 341 | tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); | 343 | tree_view->setColumnHidden(COLUMN_ADD_ONS, !UISettings::values.show_add_ons); |
| 342 | tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); | 344 | tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); |
| 345 | tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); | ||
| 343 | item_model->setSortRole(GameListItemPath::SortRole); | 346 | item_model->setSortRole(GameListItemPath::SortRole); |
| 344 | 347 | ||
| 345 | connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); | 348 | connect(main_window, &GMainWindow::UpdateThemedIcons, this, &GameList::OnUpdateThemedIcons); |
| @@ -548,6 +551,7 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 548 | QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); | 551 | QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update")); |
| 549 | QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC")); | 552 | QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC")); |
| 550 | QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration")); | 553 | QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration")); |
| 554 | QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data")); | ||
| 551 | QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage")); | 555 | QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage")); |
| 552 | QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache")); | 556 | QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache")); |
| 553 | QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache")); | 557 | QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache")); |
| @@ -560,9 +564,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 560 | QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); | 564 | QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); |
| 561 | QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); | 565 | QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); |
| 562 | QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); | 566 | QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); |
| 563 | #ifndef WIN32 | ||
| 564 | QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); | 567 | QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); |
| 565 | QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); | 568 | QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); |
| 569 | #ifndef WIN32 | ||
| 566 | QAction* create_applications_menu_shortcut = | 570 | QAction* create_applications_menu_shortcut = |
| 567 | shortcut_menu->addAction(tr("Add to Applications Menu")); | 571 | shortcut_menu->addAction(tr("Add to Applications Menu")); |
| 568 | #endif | 572 | #endif |
| @@ -622,6 +626,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 622 | connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { | 626 | connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { |
| 623 | emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path); | 627 | emit RemoveFileRequested(program_id, GameListRemoveTarget::CustomConfiguration, path); |
| 624 | }); | 628 | }); |
| 629 | connect(remove_play_time_data, &QAction::triggered, | ||
| 630 | [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); | ||
| 625 | connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { | 631 | connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { |
| 626 | emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path); | 632 | emit RemoveFileRequested(program_id, GameListRemoveTarget::CacheStorage, path); |
| 627 | }); | 633 | }); |
| @@ -638,10 +644,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri | |||
| 638 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { | 644 | connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { |
| 639 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); | 645 | emit NavigateToGamedbEntryRequested(program_id, compatibility_list); |
| 640 | }); | 646 | }); |
| 641 | #ifndef WIN32 | ||
| 642 | connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { | 647 | connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { |
| 643 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); | 648 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); |
| 644 | }); | 649 | }); |
| 650 | #ifndef WIN32 | ||
| 645 | connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { | 651 | connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { |
| 646 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); | 652 | emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); |
| 647 | }); | 653 | }); |
| @@ -790,6 +796,7 @@ void GameList::RetranslateUI() { | |||
| 790 | item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons")); | 796 | item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons")); |
| 791 | item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); | 797 | item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); |
| 792 | item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); | 798 | item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); |
| 799 | item_model->setHeaderData(COLUMN_PLAY_TIME, Qt::Horizontal, tr("Play time")); | ||
| 793 | } | 800 | } |
| 794 | 801 | ||
| 795 | void GameListSearchField::changeEvent(QEvent* event) { | 802 | void GameListSearchField::changeEvent(QEvent* event) { |
| @@ -817,6 +824,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { | |||
| 817 | tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); | 824 | tree_view->setColumnHidden(COLUMN_COMPATIBILITY, !UISettings::values.show_compat); |
| 818 | tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); | 825 | tree_view->setColumnHidden(COLUMN_FILE_TYPE, !UISettings::values.show_types); |
| 819 | tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); | 826 | tree_view->setColumnHidden(COLUMN_SIZE, !UISettings::values.show_size); |
| 827 | tree_view->setColumnHidden(COLUMN_PLAY_TIME, !UISettings::values.show_play_time); | ||
| 820 | 828 | ||
| 821 | // Delete any rows that might already exist if we're repopulating | 829 | // Delete any rows that might already exist if we're repopulating |
| 822 | item_model->removeRows(0, item_model->rowCount()); | 830 | item_model->removeRows(0, item_model->rowCount()); |
| @@ -825,7 +833,7 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) { | |||
| 825 | emit ShouldCancelWorker(); | 833 | emit ShouldCancelWorker(); |
| 826 | 834 | ||
| 827 | GameListWorker* worker = | 835 | GameListWorker* worker = |
| 828 | new GameListWorker(vfs, provider, game_dirs, compatibility_list, system); | 836 | new GameListWorker(vfs, provider, game_dirs, compatibility_list, play_time_manager, system); |
| 829 | 837 | ||
| 830 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); | 838 | connect(worker, &GameListWorker::EntryReady, this, &GameList::AddEntry, Qt::QueuedConnection); |
| 831 | connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, | 839 | connect(worker, &GameListWorker::DirEntryReady, this, &GameList::AddDirEntry, |
diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 1fcbbf0ba..712570cea 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h | |||
| @@ -18,6 +18,7 @@ | |||
| 18 | #include "core/core.h" | 18 | #include "core/core.h" |
| 19 | #include "uisettings.h" | 19 | #include "uisettings.h" |
| 20 | #include "yuzu/compatibility_list.h" | 20 | #include "yuzu/compatibility_list.h" |
| 21 | #include "yuzu/play_time_manager.h" | ||
| 21 | 22 | ||
| 22 | namespace Core { | 23 | namespace Core { |
| 23 | class System; | 24 | class System; |
| @@ -75,11 +76,13 @@ public: | |||
| 75 | COLUMN_ADD_ONS, | 76 | COLUMN_ADD_ONS, |
| 76 | COLUMN_FILE_TYPE, | 77 | COLUMN_FILE_TYPE, |
| 77 | COLUMN_SIZE, | 78 | COLUMN_SIZE, |
| 79 | COLUMN_PLAY_TIME, | ||
| 78 | COLUMN_COUNT, // Number of columns | 80 | COLUMN_COUNT, // Number of columns |
| 79 | }; | 81 | }; |
| 80 | 82 | ||
| 81 | explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_, | 83 | explicit GameList(std::shared_ptr<FileSys::VfsFilesystem> vfs_, |
| 82 | FileSys::ManualContentProvider* provider_, Core::System& system_, | 84 | FileSys::ManualContentProvider* provider_, |
| 85 | PlayTime::PlayTimeManager& play_time_manager_, Core::System& system_, | ||
| 83 | GMainWindow* parent = nullptr); | 86 | GMainWindow* parent = nullptr); |
| 84 | ~GameList() override; | 87 | ~GameList() override; |
| 85 | 88 | ||
| @@ -113,6 +116,7 @@ signals: | |||
| 113 | void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type); | 116 | void RemoveInstalledEntryRequested(u64 program_id, InstalledEntryType type); |
| 114 | void RemoveFileRequested(u64 program_id, GameListRemoveTarget target, | 117 | void RemoveFileRequested(u64 program_id, GameListRemoveTarget target, |
| 115 | const std::string& game_path); | 118 | const std::string& game_path); |
| 119 | void RemovePlayTimeRequested(u64 program_id); | ||
| 116 | void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); | 120 | void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); |
| 117 | void VerifyIntegrityRequested(const std::string& game_path); | 121 | void VerifyIntegrityRequested(const std::string& game_path); |
| 118 | void CopyTIDRequested(u64 program_id); | 122 | void CopyTIDRequested(u64 program_id); |
| @@ -168,6 +172,7 @@ private: | |||
| 168 | 172 | ||
| 169 | friend class GameListSearchField; | 173 | friend class GameListSearchField; |
| 170 | 174 | ||
| 175 | const PlayTime::PlayTimeManager& play_time_manager; | ||
| 171 | Core::System& system; | 176 | Core::System& system; |
| 172 | }; | 177 | }; |
| 173 | 178 | ||
diff --git a/src/yuzu/game_list_p.h b/src/yuzu/game_list_p.h index 1800f090f..86a0c41d9 100644 --- a/src/yuzu/game_list_p.h +++ b/src/yuzu/game_list_p.h | |||
| @@ -18,6 +18,7 @@ | |||
| 18 | #include "common/common_types.h" | 18 | #include "common/common_types.h" |
| 19 | #include "common/logging/log.h" | 19 | #include "common/logging/log.h" |
| 20 | #include "common/string_util.h" | 20 | #include "common/string_util.h" |
| 21 | #include "yuzu/play_time_manager.h" | ||
| 21 | #include "yuzu/uisettings.h" | 22 | #include "yuzu/uisettings.h" |
| 22 | #include "yuzu/util/util.h" | 23 | #include "yuzu/util/util.h" |
| 23 | 24 | ||
| @@ -221,6 +222,31 @@ public: | |||
| 221 | } | 222 | } |
| 222 | }; | 223 | }; |
| 223 | 224 | ||
| 225 | /** | ||
| 226 | * GameListItem for Play Time values. | ||
| 227 | * This object stores the play time of a game in seconds, and its readable | ||
| 228 | * representation in minutes/hours | ||
| 229 | */ | ||
| 230 | class GameListItemPlayTime : public GameListItem { | ||
| 231 | public: | ||
| 232 | static constexpr int PlayTimeRole = SortRole; | ||
| 233 | |||
| 234 | GameListItemPlayTime() = default; | ||
| 235 | explicit GameListItemPlayTime(const qulonglong time_seconds) { | ||
| 236 | setData(time_seconds, PlayTimeRole); | ||
| 237 | } | ||
| 238 | |||
| 239 | void setData(const QVariant& value, int role) override { | ||
| 240 | qulonglong time_seconds = value.toULongLong(); | ||
| 241 | GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole); | ||
| 242 | GameListItem::setData(value, PlayTimeRole); | ||
| 243 | } | ||
| 244 | |||
| 245 | bool operator<(const QStandardItem& other) const override { | ||
| 246 | return data(PlayTimeRole).toULongLong() < other.data(PlayTimeRole).toULongLong(); | ||
| 247 | } | ||
| 248 | }; | ||
| 249 | |||
| 224 | class GameListDir : public GameListItem { | 250 | class GameListDir : public GameListItem { |
| 225 | public: | 251 | public: |
| 226 | static constexpr int GameDirRole = Qt::UserRole + 2; | 252 | static constexpr int GameDirRole = Qt::UserRole + 2; |
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index e7fb8a282..588f1dd6e 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp | |||
| @@ -194,6 +194,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri | |||
| 194 | const std::size_t size, const std::vector<u8>& icon, | 194 | const std::size_t size, const std::vector<u8>& icon, |
| 195 | Loader::AppLoader& loader, u64 program_id, | 195 | Loader::AppLoader& loader, u64 program_id, |
| 196 | const CompatibilityList& compatibility_list, | 196 | const CompatibilityList& compatibility_list, |
| 197 | const PlayTime::PlayTimeManager& play_time_manager, | ||
| 197 | const FileSys::PatchManager& patch) { | 198 | const FileSys::PatchManager& patch) { |
| 198 | const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); | 199 | const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); |
| 199 | 200 | ||
| @@ -212,6 +213,7 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri | |||
| 212 | new GameListItemCompat(compatibility), | 213 | new GameListItemCompat(compatibility), |
| 213 | new GameListItem(file_type_string), | 214 | new GameListItem(file_type_string), |
| 214 | new GameListItemSize(size), | 215 | new GameListItemSize(size), |
| 216 | new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), | ||
| 215 | }; | 217 | }; |
| 216 | 218 | ||
| 217 | const auto patch_versions = GetGameListCachedObject( | 219 | const auto patch_versions = GetGameListCachedObject( |
| @@ -227,9 +229,12 @@ QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::stri | |||
| 227 | GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, | 229 | GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_, |
| 228 | FileSys::ManualContentProvider* provider_, | 230 | FileSys::ManualContentProvider* provider_, |
| 229 | QVector<UISettings::GameDir>& game_dirs_, | 231 | QVector<UISettings::GameDir>& game_dirs_, |
| 230 | const CompatibilityList& compatibility_list_, Core::System& system_) | 232 | const CompatibilityList& compatibility_list_, |
| 233 | const PlayTime::PlayTimeManager& play_time_manager_, | ||
| 234 | Core::System& system_) | ||
| 231 | : vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_}, | 235 | : vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_}, |
| 232 | compatibility_list{compatibility_list_}, system{system_} {} | 236 | compatibility_list{compatibility_list_}, |
| 237 | play_time_manager{play_time_manager_}, system{system_} {} | ||
| 233 | 238 | ||
| 234 | GameListWorker::~GameListWorker() = default; | 239 | GameListWorker::~GameListWorker() = default; |
| 235 | 240 | ||
| @@ -280,7 +285,7 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) { | |||
| 280 | } | 285 | } |
| 281 | 286 | ||
| 282 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, | 287 | emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader, |
| 283 | program_id, compatibility_list, patch), | 288 | program_id, compatibility_list, play_time_manager, patch), |
| 284 | parent_dir); | 289 | parent_dir); |
| 285 | } | 290 | } |
| 286 | } | 291 | } |
| @@ -357,7 +362,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 357 | 362 | ||
| 358 | emit EntryReady(MakeGameListEntry(physical_name, name, | 363 | emit EntryReady(MakeGameListEntry(physical_name, name, |
| 359 | Common::FS::GetSize(physical_name), icon, | 364 | Common::FS::GetSize(physical_name), icon, |
| 360 | *loader, id, compatibility_list, patch), | 365 | *loader, id, compatibility_list, |
| 366 | play_time_manager, patch), | ||
| 361 | parent_dir); | 367 | parent_dir); |
| 362 | } | 368 | } |
| 363 | } else { | 369 | } else { |
| @@ -370,10 +376,11 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa | |||
| 370 | const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), | 376 | const FileSys::PatchManager patch{program_id, system.GetFileSystemController(), |
| 371 | system.GetContentProvider()}; | 377 | system.GetContentProvider()}; |
| 372 | 378 | ||
| 373 | emit EntryReady( | 379 | emit EntryReady(MakeGameListEntry(physical_name, name, |
| 374 | MakeGameListEntry(physical_name, name, Common::FS::GetSize(physical_name), | 380 | Common::FS::GetSize(physical_name), icon, |
| 375 | icon, *loader, program_id, compatibility_list, patch), | 381 | *loader, program_id, compatibility_list, |
| 376 | parent_dir); | 382 | play_time_manager, patch), |
| 383 | parent_dir); | ||
| 377 | } | 384 | } |
| 378 | } | 385 | } |
| 379 | } else if (is_dir) { | 386 | } else if (is_dir) { |
diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 24a4e92c3..2bb0a0cb6 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h | |||
| @@ -13,6 +13,7 @@ | |||
| 13 | #include <QString> | 13 | #include <QString> |
| 14 | 14 | ||
| 15 | #include "yuzu/compatibility_list.h" | 15 | #include "yuzu/compatibility_list.h" |
| 16 | #include "yuzu/play_time_manager.h" | ||
| 16 | 17 | ||
| 17 | namespace Core { | 18 | namespace Core { |
| 18 | class System; | 19 | class System; |
| @@ -36,7 +37,9 @@ public: | |||
| 36 | explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_, | 37 | explicit GameListWorker(std::shared_ptr<FileSys::VfsFilesystem> vfs_, |
| 37 | FileSys::ManualContentProvider* provider_, | 38 | FileSys::ManualContentProvider* provider_, |
| 38 | QVector<UISettings::GameDir>& game_dirs_, | 39 | QVector<UISettings::GameDir>& game_dirs_, |
| 39 | const CompatibilityList& compatibility_list_, Core::System& system_); | 40 | const CompatibilityList& compatibility_list_, |
| 41 | const PlayTime::PlayTimeManager& play_time_manager_, | ||
| 42 | Core::System& system_); | ||
| 40 | ~GameListWorker() override; | 43 | ~GameListWorker() override; |
| 41 | 44 | ||
| 42 | /// Starts the processing of directory tree information. | 45 | /// Starts the processing of directory tree information. |
| @@ -76,6 +79,7 @@ private: | |||
| 76 | FileSys::ManualContentProvider* provider; | 79 | FileSys::ManualContentProvider* provider; |
| 77 | QVector<UISettings::GameDir>& game_dirs; | 80 | QVector<UISettings::GameDir>& game_dirs; |
| 78 | const CompatibilityList& compatibility_list; | 81 | const CompatibilityList& compatibility_list; |
| 82 | const PlayTime::PlayTimeManager& play_time_manager; | ||
| 79 | 83 | ||
| 80 | QStringList watch_list; | 84 | QStringList watch_list; |
| 81 | std::atomic_bool stop_processing; | 85 | std::atomic_bool stop_processing; |
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 1753fec12..89361fa3f 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp | |||
| @@ -98,6 +98,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | |||
| 98 | #include "common/scm_rev.h" | 98 | #include "common/scm_rev.h" |
| 99 | #include "common/scope_exit.h" | 99 | #include "common/scope_exit.h" |
| 100 | #ifdef _WIN32 | 100 | #ifdef _WIN32 |
| 101 | #include <shlobj.h> | ||
| 101 | #include "common/windows/timer_resolution.h" | 102 | #include "common/windows/timer_resolution.h" |
| 102 | #endif | 103 | #endif |
| 103 | #ifdef ARCHITECTURE_x86_64 | 104 | #ifdef ARCHITECTURE_x86_64 |
| @@ -150,6 +151,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | |||
| 150 | #include "yuzu/install_dialog.h" | 151 | #include "yuzu/install_dialog.h" |
| 151 | #include "yuzu/loading_screen.h" | 152 | #include "yuzu/loading_screen.h" |
| 152 | #include "yuzu/main.h" | 153 | #include "yuzu/main.h" |
| 154 | #include "yuzu/play_time_manager.h" | ||
| 153 | #include "yuzu/startup_checks.h" | 155 | #include "yuzu/startup_checks.h" |
| 154 | #include "yuzu/uisettings.h" | 156 | #include "yuzu/uisettings.h" |
| 155 | #include "yuzu/util/clickable_label.h" | 157 | #include "yuzu/util/clickable_label.h" |
| @@ -338,6 +340,8 @@ GMainWindow::GMainWindow(std::unique_ptr<Config> config_, bool has_broken_vulkan | |||
| 338 | SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); | 340 | SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); |
| 339 | discord_rpc->Update(); | 341 | discord_rpc->Update(); |
| 340 | 342 | ||
| 343 | play_time_manager = std::make_unique<PlayTime::PlayTimeManager>(); | ||
| 344 | |||
| 341 | system->GetRoomNetwork().Init(); | 345 | system->GetRoomNetwork().Init(); |
| 342 | 346 | ||
| 343 | RegisterMetaTypes(); | 347 | RegisterMetaTypes(); |
| @@ -986,7 +990,7 @@ void GMainWindow::InitializeWidgets() { | |||
| 986 | render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system); | 990 | render_window = new GRenderWindow(this, emu_thread.get(), input_subsystem, *system); |
| 987 | render_window->hide(); | 991 | render_window->hide(); |
| 988 | 992 | ||
| 989 | game_list = new GameList(vfs, provider.get(), *system, this); | 993 | game_list = new GameList(vfs, provider.get(), *play_time_manager, *system, this); |
| 990 | ui->horizontalLayout->addWidget(game_list); | 994 | ui->horizontalLayout->addWidget(game_list); |
| 991 | 995 | ||
| 992 | game_list_placeholder = new GameListPlaceholder(this); | 996 | game_list_placeholder = new GameListPlaceholder(this); |
| @@ -1461,6 +1465,8 @@ void GMainWindow::ConnectWidgetEvents() { | |||
| 1461 | connect(game_list, &GameList::RemoveInstalledEntryRequested, this, | 1465 | connect(game_list, &GameList::RemoveInstalledEntryRequested, this, |
| 1462 | &GMainWindow::OnGameListRemoveInstalledEntry); | 1466 | &GMainWindow::OnGameListRemoveInstalledEntry); |
| 1463 | connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); | 1467 | connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile); |
| 1468 | connect(game_list, &GameList::RemovePlayTimeRequested, this, | ||
| 1469 | &GMainWindow::OnGameListRemovePlayTimeData); | ||
| 1464 | connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); | 1470 | connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); |
| 1465 | connect(game_list, &GameList::VerifyIntegrityRequested, this, | 1471 | connect(game_list, &GameList::VerifyIntegrityRequested, this, |
| 1466 | &GMainWindow::OnGameListVerifyIntegrity); | 1472 | &GMainWindow::OnGameListVerifyIntegrity); |
| @@ -2535,6 +2541,17 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, GameListRemoveTarget targ | |||
| 2535 | } | 2541 | } |
| 2536 | } | 2542 | } |
| 2537 | 2543 | ||
| 2544 | void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { | ||
| 2545 | if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), | ||
| 2546 | QMessageBox::Yes | QMessageBox::No, | ||
| 2547 | QMessageBox::No) != QMessageBox::Yes) { | ||
| 2548 | return; | ||
| 2549 | } | ||
| 2550 | |||
| 2551 | play_time_manager->ResetProgramPlayTime(program_id); | ||
| 2552 | game_list->PopulateAsync(UISettings::values.game_dirs); | ||
| 2553 | } | ||
| 2554 | |||
| 2538 | void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) { | 2555 | void GMainWindow::RemoveTransferableShaderCache(u64 program_id, GameListRemoveTarget target) { |
| 2539 | const auto target_file_name = [target] { | 2556 | const auto target_file_name = [target] { |
| 2540 | switch (target) { | 2557 | switch (target) { |
| @@ -2826,7 +2843,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2826 | const QStringList args = QApplication::arguments(); | 2843 | const QStringList args = QApplication::arguments(); |
| 2827 | std::filesystem::path yuzu_command = args[0].toStdString(); | 2844 | std::filesystem::path yuzu_command = args[0].toStdString(); |
| 2828 | 2845 | ||
| 2829 | #if defined(__linux__) || defined(__FreeBSD__) | ||
| 2830 | // If relative path, make it an absolute path | 2846 | // If relative path, make it an absolute path |
| 2831 | if (yuzu_command.c_str()[0] == '.') { | 2847 | if (yuzu_command.c_str()[0] == '.') { |
| 2832 | yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; | 2848 | yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; |
| @@ -2849,12 +2865,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2849 | UISettings::values.shortcut_already_warned = true; | 2865 | UISettings::values.shortcut_already_warned = true; |
| 2850 | } | 2866 | } |
| 2851 | #endif // __linux__ | 2867 | #endif // __linux__ |
| 2852 | #endif // __linux__ || __FreeBSD__ | ||
| 2853 | 2868 | ||
| 2854 | std::filesystem::path target_directory{}; | 2869 | std::filesystem::path target_directory{}; |
| 2855 | // Determine target directory for shortcut | 2870 | // Determine target directory for shortcut |
| 2856 | #if defined(__linux__) || defined(__FreeBSD__) | 2871 | #if defined(WIN32) |
| 2872 | const char* home = std::getenv("USERPROFILE"); | ||
| 2873 | #else | ||
| 2857 | const char* home = std::getenv("HOME"); | 2874 | const char* home = std::getenv("HOME"); |
| 2875 | #endif | ||
| 2858 | const std::filesystem::path home_path = (home == nullptr ? "~" : home); | 2876 | const std::filesystem::path home_path = (home == nullptr ? "~" : home); |
| 2859 | const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); | 2877 | const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); |
| 2860 | 2878 | ||
| @@ -2864,7 +2882,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2864 | QMessageBox::critical( | 2882 | QMessageBox::critical( |
| 2865 | this, tr("Create Shortcut"), | 2883 | this, tr("Create Shortcut"), |
| 2866 | tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") | 2884 | tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") |
| 2867 | .arg(QString::fromStdString(target_directory)), | 2885 | .arg(QString::fromStdString(target_directory.generic_string())), |
| 2868 | QMessageBox::StandardButton::Ok); | 2886 | QMessageBox::StandardButton::Ok); |
| 2869 | return; | 2887 | return; |
| 2870 | } | 2888 | } |
| @@ -2872,15 +2890,15 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2872 | target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / | 2890 | target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / |
| 2873 | "applications"; | 2891 | "applications"; |
| 2874 | if (!Common::FS::CreateDirs(target_directory)) { | 2892 | if (!Common::FS::CreateDirs(target_directory)) { |
| 2875 | QMessageBox::critical(this, tr("Create Shortcut"), | 2893 | QMessageBox::critical( |
| 2876 | tr("Cannot create shortcut in applications menu. Path \"%1\" " | 2894 | this, tr("Create Shortcut"), |
| 2877 | "does not exist and cannot be created.") | 2895 | tr("Cannot create shortcut in applications menu. Path \"%1\" " |
| 2878 | .arg(QString::fromStdString(target_directory)), | 2896 | "does not exist and cannot be created.") |
| 2879 | QMessageBox::StandardButton::Ok); | 2897 | .arg(QString::fromStdString(target_directory.generic_string())), |
| 2898 | QMessageBox::StandardButton::Ok); | ||
| 2880 | return; | 2899 | return; |
| 2881 | } | 2900 | } |
| 2882 | } | 2901 | } |
| 2883 | #endif | ||
| 2884 | 2902 | ||
| 2885 | const std::string game_file_name = std::filesystem::path(game_path).filename().string(); | 2903 | const std::string game_file_name = std::filesystem::path(game_path).filename().string(); |
| 2886 | // Determine full paths for icon and shortcut | 2904 | // Determine full paths for icon and shortcut |
| @@ -2902,9 +2920,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2902 | const std::filesystem::path shortcut_path = | 2920 | const std::filesystem::path shortcut_path = |
| 2903 | target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) | 2921 | target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) |
| 2904 | : fmt::format("yuzu-{:016X}.desktop", program_id)); | 2922 | : fmt::format("yuzu-{:016X}.desktop", program_id)); |
| 2923 | #elif defined(WIN32) | ||
| 2924 | std::filesystem::path icons_path = | ||
| 2925 | Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); | ||
| 2926 | std::filesystem::path icon_path = | ||
| 2927 | icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) | ||
| 2928 | : fmt::format("yuzu-{:016X}.ico", program_id))); | ||
| 2905 | #else | 2929 | #else |
| 2906 | const std::filesystem::path icon_path{}; | 2930 | std::string icon_extension; |
| 2907 | const std::filesystem::path shortcut_path{}; | ||
| 2908 | #endif | 2931 | #endif |
| 2909 | 2932 | ||
| 2910 | // Get title from game file | 2933 | // Get title from game file |
| @@ -2929,29 +2952,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga | |||
| 2929 | LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); | 2952 | LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); |
| 2930 | } | 2953 | } |
| 2931 | 2954 | ||
| 2932 | QImage icon_jpeg = | 2955 | QImage icon_data = |
| 2933 | QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); | 2956 | QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); |
| 2934 | #if defined(__linux__) || defined(__FreeBSD__) | 2957 | #if defined(__linux__) || defined(__FreeBSD__) |
| 2935 | // Convert and write the icon as a PNG | 2958 | // Convert and write the icon as a PNG |
| 2936 | if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) { | 2959 | if (!icon_data.save(QString::fromStdString(icon_path.string()))) { |
| 2937 | LOG_ERROR(Frontend, "Could not write icon as PNG to file"); | 2960 | LOG_ERROR(Frontend, "Could not write icon as PNG to file"); |
| 2938 | } else { | 2961 | } else { |
| 2939 | LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); | 2962 | LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); |
| 2940 | } | 2963 | } |
| 2964 | #elif defined(WIN32) | ||
| 2965 | if (!SaveIconToFile(icon_path.string(), icon_data)) { | ||
| 2966 | LOG_ERROR(Frontend, "Could not write icon to file"); | ||
| 2967 | return; | ||
| 2968 | } | ||
| 2941 | #endif // __linux__ | 2969 | #endif // __linux__ |
| 2942 | 2970 | ||
| 2943 | #if defined(__linux__) || defined(__FreeBSD__) | 2971 | #ifdef _WIN32 |
| 2972 | // Replace characters that are illegal in Windows filenames by a dash | ||
| 2973 | const std::string illegal_chars = "<>:\"/\\|?*"; | ||
| 2974 | for (char c : illegal_chars) { | ||
| 2975 | std::replace(title.begin(), title.end(), c, '_'); | ||
| 2976 | } | ||
| 2977 | const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); | ||
| 2978 | #endif | ||
| 2979 | |||
| 2944 | const std::string comment = | 2980 | const std::string comment = |
| 2945 | tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); | 2981 | tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); |
| 2946 | const std::string arguments = fmt::format("-g \"{:s}\"", game_path); | 2982 | const std::string arguments = fmt::format("-g \"{:s}\"", game_path); |
| 2947 | const std::string categories = "Game;Emulator;Qt;"; | 2983 | const std::string categories = "Game;Emulator;Qt;"; |
| 2948 | const std::string keywords = "Switch;Nintendo;"; | 2984 | const std::string keywords = "Switch;Nintendo;"; |
| 2949 | #else | 2985 | |
| 2950 | const std::string comment{}; | ||
| 2951 | const std::string arguments{}; | ||
| 2952 | const std::string categories{}; | ||
| 2953 | const std::string keywords{}; | ||
| 2954 | #endif | ||
| 2955 | if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), | 2986 | if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), |
| 2956 | yuzu_command.string(), arguments, categories, keywords)) { | 2987 | yuzu_command.string(), arguments, categories, keywords)) { |
| 2957 | QMessageBox::critical(this, tr("Create Shortcut"), | 2988 | QMessageBox::critical(this, tr("Create Shortcut"), |
| @@ -3358,6 +3389,9 @@ void GMainWindow::OnStartGame() { | |||
| 3358 | UpdateMenuState(); | 3389 | UpdateMenuState(); |
| 3359 | OnTasStateChanged(); | 3390 | OnTasStateChanged(); |
| 3360 | 3391 | ||
| 3392 | play_time_manager->SetProgramId(system->GetApplicationProcessProgramID()); | ||
| 3393 | play_time_manager->Start(); | ||
| 3394 | |||
| 3361 | discord_rpc->Update(); | 3395 | discord_rpc->Update(); |
| 3362 | } | 3396 | } |
| 3363 | 3397 | ||
| @@ -3373,6 +3407,7 @@ void GMainWindow::OnRestartGame() { | |||
| 3373 | 3407 | ||
| 3374 | void GMainWindow::OnPauseGame() { | 3408 | void GMainWindow::OnPauseGame() { |
| 3375 | emu_thread->SetRunning(false); | 3409 | emu_thread->SetRunning(false); |
| 3410 | play_time_manager->Stop(); | ||
| 3376 | UpdateMenuState(); | 3411 | UpdateMenuState(); |
| 3377 | AllowOSSleep(); | 3412 | AllowOSSleep(); |
| 3378 | } | 3413 | } |
| @@ -3393,6 +3428,9 @@ void GMainWindow::OnStopGame() { | |||
| 3393 | return; | 3428 | return; |
| 3394 | } | 3429 | } |
| 3395 | 3430 | ||
| 3431 | play_time_manager->Stop(); | ||
| 3432 | // Update game list to show new play time | ||
| 3433 | game_list->PopulateAsync(UISettings::values.game_dirs); | ||
| 3396 | if (OnShutdownBegin()) { | 3434 | if (OnShutdownBegin()) { |
| 3397 | OnShutdownBeginDialog(); | 3435 | OnShutdownBeginDialog(); |
| 3398 | } else { | 3436 | } else { |
| @@ -3966,6 +4004,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st | |||
| 3966 | shortcut_stream.close(); | 4004 | shortcut_stream.close(); |
| 3967 | 4005 | ||
| 3968 | return true; | 4006 | return true; |
| 4007 | #elif defined(WIN32) | ||
| 4008 | IShellLinkW* shell_link; | ||
| 4009 | auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, | ||
| 4010 | (void**)&shell_link); | ||
| 4011 | if (FAILED(hres)) { | ||
| 4012 | return false; | ||
| 4013 | } | ||
| 4014 | shell_link->SetPath( | ||
| 4015 | Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to | ||
| 4016 | shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); | ||
| 4017 | shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); | ||
| 4018 | shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); | ||
| 4019 | |||
| 4020 | IPersistFile* persist_file; | ||
| 4021 | hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); | ||
| 4022 | if (FAILED(hres)) { | ||
| 4023 | return false; | ||
| 4024 | } | ||
| 4025 | |||
| 4026 | hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); | ||
| 4027 | if (FAILED(hres)) { | ||
| 4028 | return false; | ||
| 4029 | } | ||
| 4030 | |||
| 4031 | persist_file->Release(); | ||
| 4032 | shell_link->Release(); | ||
| 4033 | |||
| 4034 | return true; | ||
| 3969 | #endif | 4035 | #endif |
| 3970 | return false; | 4036 | return false; |
| 3971 | } | 4037 | } |
diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 52028234c..c1872ecd4 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h | |||
| @@ -81,6 +81,10 @@ namespace DiscordRPC { | |||
| 81 | class DiscordInterface; | 81 | class DiscordInterface; |
| 82 | } | 82 | } |
| 83 | 83 | ||
| 84 | namespace PlayTime { | ||
| 85 | class PlayTimeManager; | ||
| 86 | } | ||
| 87 | |||
| 84 | namespace FileSys { | 88 | namespace FileSys { |
| 85 | class ContentProvider; | 89 | class ContentProvider; |
| 86 | class ManualContentProvider; | 90 | class ManualContentProvider; |
| @@ -323,6 +327,7 @@ private slots: | |||
| 323 | void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); | 327 | void OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryType type); |
| 324 | void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, | 328 | void OnGameListRemoveFile(u64 program_id, GameListRemoveTarget target, |
| 325 | const std::string& game_path); | 329 | const std::string& game_path); |
| 330 | void OnGameListRemovePlayTimeData(u64 program_id); | ||
| 326 | void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); | 331 | void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target); |
| 327 | void OnGameListVerifyIntegrity(const std::string& game_path); | 332 | void OnGameListVerifyIntegrity(const std::string& game_path); |
| 328 | void OnGameListCopyTID(u64 program_id); | 333 | void OnGameListCopyTID(u64 program_id); |
| @@ -389,6 +394,7 @@ private: | |||
| 389 | void RemoveVulkanDriverPipelineCache(u64 program_id); | 394 | void RemoveVulkanDriverPipelineCache(u64 program_id); |
| 390 | void RemoveAllTransferableShaderCaches(u64 program_id); | 395 | void RemoveAllTransferableShaderCaches(u64 program_id); |
| 391 | void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); | 396 | void RemoveCustomConfiguration(u64 program_id, const std::string& game_path); |
| 397 | void RemovePlayTimeData(u64 program_id); | ||
| 392 | void RemoveCacheStorage(u64 program_id); | 398 | void RemoveCacheStorage(u64 program_id); |
| 393 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, | 399 | bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, |
| 394 | u64* selected_title_id, u8* selected_content_record_type); | 400 | u64* selected_title_id, u8* selected_content_record_type); |
| @@ -428,6 +434,7 @@ private: | |||
| 428 | 434 | ||
| 429 | std::unique_ptr<Core::System> system; | 435 | std::unique_ptr<Core::System> system; |
| 430 | std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; | 436 | std::unique_ptr<DiscordRPC::DiscordInterface> discord_rpc; |
| 437 | std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager; | ||
| 431 | std::shared_ptr<InputCommon::InputSubsystem> input_subsystem; | 438 | std::shared_ptr<InputCommon::InputSubsystem> input_subsystem; |
| 432 | 439 | ||
| 433 | MultiplayerState* multiplayer_state = nullptr; | 440 | MultiplayerState* multiplayer_state = nullptr; |
diff --git a/src/yuzu/play_time_manager.cpp b/src/yuzu/play_time_manager.cpp new file mode 100644 index 000000000..155c36b7d --- /dev/null +++ b/src/yuzu/play_time_manager.cpp | |||
| @@ -0,0 +1,179 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #include "common/alignment.h" | ||
| 5 | #include "common/fs/file.h" | ||
| 6 | #include "common/fs/fs.h" | ||
| 7 | #include "common/fs/path_util.h" | ||
| 8 | #include "common/logging/log.h" | ||
| 9 | #include "common/settings.h" | ||
| 10 | #include "common/thread.h" | ||
| 11 | #include "core/hle/service/acc/profile_manager.h" | ||
| 12 | #include "yuzu/play_time_manager.h" | ||
| 13 | |||
| 14 | namespace PlayTime { | ||
| 15 | |||
| 16 | namespace { | ||
| 17 | |||
| 18 | struct PlayTimeElement { | ||
| 19 | ProgramId program_id; | ||
| 20 | PlayTime play_time; | ||
| 21 | }; | ||
| 22 | |||
| 23 | std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() { | ||
| 24 | const Service::Account::ProfileManager manager; | ||
| 25 | const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user)); | ||
| 26 | if (!uuid.has_value()) { | ||
| 27 | return std::nullopt; | ||
| 28 | } | ||
| 29 | return Common::FS::GetYuzuPath(Common::FS::YuzuPath::PlayTimeDir) / | ||
| 30 | uuid->RawString().append(".bin"); | ||
| 31 | } | ||
| 32 | |||
| 33 | [[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) { | ||
| 34 | const auto filename = GetCurrentUserPlayTimePath(); | ||
| 35 | |||
| 36 | if (!filename.has_value()) { | ||
| 37 | LOG_ERROR(Frontend, "Failed to get current user path"); | ||
| 38 | return false; | ||
| 39 | } | ||
| 40 | |||
| 41 | out_play_time_db.clear(); | ||
| 42 | |||
| 43 | if (Common::FS::Exists(filename.value())) { | ||
| 44 | Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Read, | ||
| 45 | Common::FS::FileType::BinaryFile}; | ||
| 46 | if (!file.IsOpen()) { | ||
| 47 | LOG_ERROR(Frontend, "Failed to open play time file: {}", | ||
| 48 | Common::FS::PathToUTF8String(filename.value())); | ||
| 49 | return false; | ||
| 50 | } | ||
| 51 | |||
| 52 | const size_t num_elements = file.GetSize() / sizeof(PlayTimeElement); | ||
| 53 | std::vector<PlayTimeElement> elements(num_elements); | ||
| 54 | |||
| 55 | if (file.ReadSpan<PlayTimeElement>(elements) != num_elements) { | ||
| 56 | return false; | ||
| 57 | } | ||
| 58 | |||
| 59 | for (const auto& [program_id, play_time] : elements) { | ||
| 60 | if (program_id != 0) { | ||
| 61 | out_play_time_db[program_id] = play_time; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | return true; | ||
| 67 | } | ||
| 68 | |||
| 69 | [[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) { | ||
| 70 | const auto filename = GetCurrentUserPlayTimePath(); | ||
| 71 | |||
| 72 | if (!filename.has_value()) { | ||
| 73 | LOG_ERROR(Frontend, "Failed to get current user path"); | ||
| 74 | return false; | ||
| 75 | } | ||
| 76 | |||
| 77 | Common::FS::IOFile file{filename.value(), Common::FS::FileAccessMode::Write, | ||
| 78 | Common::FS::FileType::BinaryFile}; | ||
| 79 | if (!file.IsOpen()) { | ||
| 80 | LOG_ERROR(Frontend, "Failed to open play time file: {}", | ||
| 81 | Common::FS::PathToUTF8String(filename.value())); | ||
| 82 | return false; | ||
| 83 | } | ||
| 84 | |||
| 85 | std::vector<PlayTimeElement> elements; | ||
| 86 | elements.reserve(play_time_db.size()); | ||
| 87 | |||
| 88 | for (auto& [program_id, play_time] : play_time_db) { | ||
| 89 | if (program_id != 0) { | ||
| 90 | elements.push_back(PlayTimeElement{program_id, play_time}); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | return file.WriteSpan<PlayTimeElement>(elements) == elements.size(); | ||
| 95 | } | ||
| 96 | |||
| 97 | } // namespace | ||
| 98 | |||
| 99 | PlayTimeManager::PlayTimeManager() { | ||
| 100 | if (!ReadPlayTimeFile(database)) { | ||
| 101 | LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default."); | ||
| 102 | } | ||
| 103 | } | ||
| 104 | |||
| 105 | PlayTimeManager::~PlayTimeManager() { | ||
| 106 | Save(); | ||
| 107 | } | ||
| 108 | |||
| 109 | void PlayTimeManager::SetProgramId(u64 program_id) { | ||
| 110 | running_program_id = program_id; | ||
| 111 | } | ||
| 112 | |||
| 113 | void PlayTimeManager::Start() { | ||
| 114 | play_time_thread = std::jthread([&](std::stop_token stop_token) { AutoTimestamp(stop_token); }); | ||
| 115 | } | ||
| 116 | |||
| 117 | void PlayTimeManager::Stop() { | ||
| 118 | play_time_thread = {}; | ||
| 119 | } | ||
| 120 | |||
| 121 | void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) { | ||
| 122 | Common::SetCurrentThreadName("PlayTimeReport"); | ||
| 123 | |||
| 124 | using namespace std::literals::chrono_literals; | ||
| 125 | using std::chrono::seconds; | ||
| 126 | using std::chrono::steady_clock; | ||
| 127 | |||
| 128 | auto timestamp = steady_clock::now(); | ||
| 129 | |||
| 130 | const auto GetDuration = [&]() -> u64 { | ||
| 131 | const auto last_timestamp = std::exchange(timestamp, steady_clock::now()); | ||
| 132 | const auto duration = std::chrono::duration_cast<seconds>(timestamp - last_timestamp); | ||
| 133 | return static_cast<u64>(duration.count()); | ||
| 134 | }; | ||
| 135 | |||
| 136 | while (!stop_token.stop_requested()) { | ||
| 137 | Common::StoppableTimedWait(stop_token, 30s); | ||
| 138 | |||
| 139 | database[running_program_id] += GetDuration(); | ||
| 140 | Save(); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | void PlayTimeManager::Save() { | ||
| 145 | if (!WritePlayTimeFile(database)) { | ||
| 146 | LOG_ERROR(Frontend, "Failed to update play time database!"); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | u64 PlayTimeManager::GetPlayTime(u64 program_id) const { | ||
| 151 | auto it = database.find(program_id); | ||
| 152 | if (it != database.end()) { | ||
| 153 | return it->second; | ||
| 154 | } else { | ||
| 155 | return 0; | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | void PlayTimeManager::ResetProgramPlayTime(u64 program_id) { | ||
| 160 | database.erase(program_id); | ||
| 161 | Save(); | ||
| 162 | } | ||
| 163 | |||
| 164 | QString ReadablePlayTime(qulonglong time_seconds) { | ||
| 165 | if (time_seconds == 0) { | ||
| 166 | return {}; | ||
| 167 | } | ||
| 168 | const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60, 1.0); | ||
| 169 | const auto time_hours = static_cast<double>(time_seconds) / 3600; | ||
| 170 | const bool is_minutes = time_minutes < 60; | ||
| 171 | const char* unit = is_minutes ? "m" : "h"; | ||
| 172 | const auto value = is_minutes ? time_minutes : time_hours; | ||
| 173 | |||
| 174 | return QStringLiteral("%L1 %2") | ||
| 175 | .arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0) | ||
| 176 | .arg(QString::fromUtf8(unit)); | ||
| 177 | } | ||
| 178 | |||
| 179 | } // namespace PlayTime | ||
diff --git a/src/yuzu/play_time_manager.h b/src/yuzu/play_time_manager.h new file mode 100644 index 000000000..5f96f3447 --- /dev/null +++ b/src/yuzu/play_time_manager.h | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | // SPDX-FileCopyrightText: 2023 yuzu Emulator Project | ||
| 2 | // SPDX-License-Identifier: GPL-2.0-or-later | ||
| 3 | |||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include <QString> | ||
| 7 | |||
| 8 | #include <map> | ||
| 9 | |||
| 10 | #include "common/common_funcs.h" | ||
| 11 | #include "common/common_types.h" | ||
| 12 | #include "common/polyfill_thread.h" | ||
| 13 | |||
| 14 | namespace PlayTime { | ||
| 15 | |||
| 16 | using ProgramId = u64; | ||
| 17 | using PlayTime = u64; | ||
| 18 | using PlayTimeDatabase = std::map<ProgramId, PlayTime>; | ||
| 19 | |||
| 20 | class PlayTimeManager { | ||
| 21 | public: | ||
| 22 | explicit PlayTimeManager(); | ||
| 23 | ~PlayTimeManager(); | ||
| 24 | |||
| 25 | YUZU_NON_COPYABLE(PlayTimeManager); | ||
| 26 | YUZU_NON_MOVEABLE(PlayTimeManager); | ||
| 27 | |||
| 28 | u64 GetPlayTime(u64 program_id) const; | ||
| 29 | void ResetProgramPlayTime(u64 program_id); | ||
| 30 | void SetProgramId(u64 program_id); | ||
| 31 | void Start(); | ||
| 32 | void Stop(); | ||
| 33 | |||
| 34 | private: | ||
| 35 | PlayTimeDatabase database; | ||
| 36 | u64 running_program_id; | ||
| 37 | std::jthread play_time_thread; | ||
| 38 | void AutoTimestamp(std::stop_token stop_token); | ||
| 39 | void Save(); | ||
| 40 | }; | ||
| 41 | |||
| 42 | QString ReadablePlayTime(qulonglong time_seconds); | ||
| 43 | |||
| 44 | } // namespace PlayTime | ||
diff --git a/src/yuzu/uisettings.h b/src/yuzu/uisettings.h index 8a2caa9dd..975008159 100644 --- a/src/yuzu/uisettings.h +++ b/src/yuzu/uisettings.h | |||
| @@ -183,6 +183,9 @@ struct Values { | |||
| 183 | Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList}; | 183 | Setting<bool> show_size{linkage, true, "show_size", Category::UiGameList}; |
| 184 | Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList}; | 184 | Setting<bool> show_types{linkage, true, "show_types", Category::UiGameList}; |
| 185 | 185 | ||
| 186 | // Play time | ||
| 187 | Setting<bool> show_play_time{linkage, true, "show_play_time", Category::UiGameList}; | ||
| 188 | |||
| 186 | bool configuration_applied; | 189 | bool configuration_applied; |
| 187 | bool reset_to_defaults; | 190 | bool reset_to_defaults; |
| 188 | bool shortcut_already_warned{false}; | 191 | bool shortcut_already_warned{false}; |
diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 5c3e4589e..61cf00176 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp | |||
| @@ -5,6 +5,10 @@ | |||
| 5 | #include <cmath> | 5 | #include <cmath> |
| 6 | #include <QPainter> | 6 | #include <QPainter> |
| 7 | #include "yuzu/util/util.h" | 7 | #include "yuzu/util/util.h" |
| 8 | #ifdef _WIN32 | ||
| 9 | #include <windows.h> | ||
| 10 | #include "common/fs/file.h" | ||
| 11 | #endif | ||
| 8 | 12 | ||
| 9 | QFont GetMonospaceFont() { | 13 | QFont GetMonospaceFont() { |
| 10 | QFont font(QStringLiteral("monospace")); | 14 | QFont font(QStringLiteral("monospace")); |
| @@ -37,3 +41,76 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { | |||
| 37 | painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); | 41 | painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); |
| 38 | return circle_pixmap; | 42 | return circle_pixmap; |
| 39 | } | 43 | } |
| 44 | |||
| 45 | bool SaveIconToFile(const std::string_view path, const QImage& image) { | ||
| 46 | #if defined(WIN32) | ||
| 47 | #pragma pack(push, 2) | ||
| 48 | struct IconDir { | ||
| 49 | WORD id_reserved; | ||
| 50 | WORD id_type; | ||
| 51 | WORD id_count; | ||
| 52 | }; | ||
| 53 | |||
| 54 | struct IconDirEntry { | ||
| 55 | BYTE width; | ||
| 56 | BYTE height; | ||
| 57 | BYTE color_count; | ||
| 58 | BYTE reserved; | ||
| 59 | WORD planes; | ||
| 60 | WORD bit_count; | ||
| 61 | DWORD bytes_in_res; | ||
| 62 | DWORD image_offset; | ||
| 63 | }; | ||
| 64 | #pragma pack(pop) | ||
| 65 | |||
| 66 | QImage source_image = image.convertToFormat(QImage::Format_RGB32); | ||
| 67 | constexpr int bytes_per_pixel = 4; | ||
| 68 | const int image_size = source_image.width() * source_image.height() * bytes_per_pixel; | ||
| 69 | |||
| 70 | BITMAPINFOHEADER info_header{}; | ||
| 71 | info_header.biSize = sizeof(BITMAPINFOHEADER), info_header.biWidth = source_image.width(), | ||
| 72 | info_header.biHeight = source_image.height() * 2, info_header.biPlanes = 1, | ||
| 73 | info_header.biBitCount = bytes_per_pixel * 8, info_header.biCompression = BI_RGB; | ||
| 74 | |||
| 75 | const IconDir icon_dir{.id_reserved = 0, .id_type = 1, .id_count = 1}; | ||
| 76 | const IconDirEntry icon_entry{.width = static_cast<BYTE>(source_image.width()), | ||
| 77 | .height = static_cast<BYTE>(source_image.height() * 2), | ||
| 78 | .color_count = 0, | ||
| 79 | .reserved = 0, | ||
| 80 | .planes = 1, | ||
| 81 | .bit_count = bytes_per_pixel * 8, | ||
| 82 | .bytes_in_res = | ||
| 83 | static_cast<DWORD>(sizeof(BITMAPINFOHEADER) + image_size), | ||
| 84 | .image_offset = sizeof(IconDir) + sizeof(IconDirEntry)}; | ||
| 85 | |||
| 86 | Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, | ||
| 87 | Common::FS::FileType::BinaryFile); | ||
| 88 | if (!icon_file.IsOpen()) { | ||
| 89 | return false; | ||
| 90 | } | ||
| 91 | |||
| 92 | if (!icon_file.Write(icon_dir)) { | ||
| 93 | return false; | ||
| 94 | } | ||
| 95 | if (!icon_file.Write(icon_entry)) { | ||
| 96 | return false; | ||
| 97 | } | ||
| 98 | if (!icon_file.Write(info_header)) { | ||
| 99 | return false; | ||
| 100 | } | ||
| 101 | |||
| 102 | for (int y = 0; y < image.height(); y++) { | ||
| 103 | const auto* line = source_image.scanLine(source_image.height() - 1 - y); | ||
| 104 | std::vector<u8> line_data(source_image.width() * bytes_per_pixel); | ||
| 105 | std::memcpy(line_data.data(), line, line_data.size()); | ||
| 106 | if (!icon_file.Write(line_data)) { | ||
| 107 | return false; | ||
| 108 | } | ||
| 109 | } | ||
| 110 | icon_file.Close(); | ||
| 111 | |||
| 112 | return true; | ||
| 113 | #else | ||
| 114 | return false; | ||
| 115 | #endif | ||
| 116 | } | ||
diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 39dd2d895..09c14ce3f 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h | |||
| @@ -7,14 +7,22 @@ | |||
| 7 | #include <QString> | 7 | #include <QString> |
| 8 | 8 | ||
| 9 | /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. | 9 | /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. |
| 10 | QFont GetMonospaceFont(); | 10 | [[nodiscard]] QFont GetMonospaceFont(); |
| 11 | 11 | ||
| 12 | /// Convert a size in bytes into a readable format (KiB, MiB, etc.) | 12 | /// Convert a size in bytes into a readable format (KiB, MiB, etc.) |
| 13 | QString ReadableByteSize(qulonglong size); | 13 | [[nodiscard]] QString ReadableByteSize(qulonglong size); |
| 14 | 14 | ||
| 15 | /** | 15 | /** |
| 16 | * Creates a circle pixmap from a specified color | 16 | * Creates a circle pixmap from a specified color |
| 17 | * @param color The color the pixmap shall have | 17 | * @param color The color the pixmap shall have |
| 18 | * @return QPixmap circle pixmap | 18 | * @return QPixmap circle pixmap |
| 19 | */ | 19 | */ |
| 20 | QPixmap CreateCirclePixmapFromColor(const QColor& color); | 20 | [[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color); |
| 21 | |||
| 22 | /** | ||
| 23 | * Saves a windows icon to a file | ||
| 24 | * @param path The icons path | ||
| 25 | * @param image The image to save | ||
| 26 | * @return bool If the operation succeeded | ||
| 27 | */ | ||
| 28 | [[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); | ||