From f88b0a49c8ad3570f53f5b790f1edb45052a2bd6 Mon Sep 17 00:00:00 2001 From: tkashkin Date: Tue, 25 Jun 2019 15:57:09 +0300 Subject: [PATCH] Allow multiple selection in games list Add actions for selected games (#262) Batch tag editing for selected games --- po/POTFILES | 2 + po/com.github.tkashkin.gamehub.pot | 168 ++++++---- po/de.po | 168 ++++++---- po/fr.po | 168 ++++++---- po/id.po | 162 +++++---- po/nb_NO.po | 168 ++++++---- po/nl.po | 168 ++++++---- po/pl.po | 174 ++++++---- po/pt_BR.po | 168 ++++++---- po/ru.po | 174 ++++++---- po/tr.po | 168 ++++++---- po/zh_CMN-HANT.po | 162 +++++---- po/zh_Hant.po | 162 +++++---- src/data/Emulator.vala | 4 +- src/data/Runnable.vala | 7 +- src/data/sources/gog/GOGGame.vala | 13 +- src/data/sources/humble/HumbleGame.vala | 11 +- src/data/sources/steam/Steam.vala | 47 +++ src/data/sources/steam/SteamGame.vala | 11 +- src/data/sources/user/UserGame.vala | 11 +- src/meson.build | 2 + src/ui/dialogs/GamePropertiesDialog.vala | 100 +----- src/ui/dialogs/InstallDialog.vala | 17 +- .../pages/emulators/Emulators.vala | 2 +- .../GameDetailsView/GameDetailsPage.vala | 14 +- .../GameDetailsView/GameDetailsView.vala | 52 ++- .../MultipleGamesDetailsView.vala | 281 ++++++++++++++++ src/ui/views/GamesView/GameListRow.vala | 26 +- src/ui/views/GamesView/GamesView.vala | 55 ++- src/ui/widgets/ActionButton.vala | 3 +- src/ui/widgets/GameTagsList.vala | 151 +++++++++ src/ui/widgets/TagRow.vala | 39 ++- src/utils/BinaryVDF.vala | 316 ++++++++++++++---- src/utils/FSUtils.vala | 3 + 34 files changed, 2253 insertions(+), 924 deletions(-) create mode 100644 src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala create mode 100644 src/ui/widgets/GameTagsList.vala diff --git a/po/POTFILES b/po/POTFILES index a5d20e53..3c94f66e 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -69,6 +69,7 @@ src/ui/views/GamesView/FiltersPopover.vala src/ui/views/GamesView/AddGamePopover.vala src/ui/views/GamesView/GameContextMenu.vala src/ui/views/GameDetailsView/GameDetailsView.vala +src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala src/ui/views/GameDetailsView/GameDetailsPage.vala src/ui/views/GameDetailsView/GameDetailsBlock.vala src/ui/views/GameDetailsView/blocks/Achievements.vala @@ -84,6 +85,7 @@ src/ui/widgets/ExtendedStackSwitcher.vala src/ui/widgets/ImagesDownloadPopover.vala src/ui/widgets/CompatToolOptions.vala src/ui/widgets/CompatToolPicker.vala +src/ui/widgets/GameTagsList.vala src/ui/widgets/TagRow.vala src/utils/Utils.vala src/utils/ImageCache.vala diff --git a/po/com.github.tkashkin.gamehub.pot b/po/com.github.tkashkin.gamehub.pot index f8caa72d..37c22551 100644 --- a/po/com.github.tkashkin.gamehub.pot +++ b/po/com.github.tkashkin.gamehub.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-06-23 07:47+0300\n" +"POT-Creation-Date: 2019-06-25 15:41+0300\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -114,12 +114,12 @@ msgstr "" #: src/data/Runnable.vala:93 src/data/Runnable.vala:95 #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:271 -#: src/ui/dialogs/GamePropertiesDialog.vala:218 +#: src/ui/dialogs/GamePropertiesDialog.vala:151 msgid "Select executable" msgstr "" #: src/data/Runnable.vala:93 src/data/Runnable.vala:95 -#: src/data/Runnable.vala:164 src/ui/views/GamesView/GamesView.vala:362 +#: src/data/Runnable.vala:164 src/ui/views/GamesView/GamesView.vala:383 #: src/ui/widgets/FileChooserEntry.vala:55 #: src/ui/widgets/FileChooserEntry.vala:57 msgid "Select" @@ -175,7 +175,7 @@ msgid "" "Please set main executable in game's properties." msgstr "" -#: src/data/Runnable.vala:820 +#: src/data/Runnable.vala:825 msgctxt "platform" msgid "Emulated" msgstr "" @@ -510,8 +510,8 @@ msgstr "" #: src/ui/dialogs/SettingsDialog/SettingsDialog.vala:39 #: src/ui/views/WelcomeView.vala:52 src/ui/views/WelcomeView.vala:77 -#: src/ui/views/GamesView/GamesView.vala:261 -#: src/ui/views/GamesView/GamesView.vala:616 +#: src/ui/views/GamesView/GamesView.vala:265 +#: src/ui/views/GamesView/GamesView.vala:638 msgid "Settings" msgstr "" @@ -779,8 +779,8 @@ msgstr "" #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:271 #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:313 #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:405 -#: src/ui/dialogs/GamePropertiesDialog.vala:214 -#: src/ui/dialogs/GamePropertiesDialog.vala:218 +#: src/ui/dialogs/GamePropertiesDialog.vala:147 +#: src/ui/dialogs/GamePropertiesDialog.vala:151 #: src/ui/views/GamesView/AddGamePopover.vala:67 #: src/ui/views/GamesView/AddGamePopover.vala:77 #: src/ui/views/GamesView/AddGamePopover.vala:143 @@ -795,14 +795,14 @@ msgid "Installer" msgstr "" #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:257 -#: src/ui/dialogs/GamePropertiesDialog.vala:129 -#: src/ui/dialogs/GamePropertiesDialog.vala:134 +#: src/ui/dialogs/GamePropertiesDialog.vala:62 +#: src/ui/dialogs/GamePropertiesDialog.vala:67 #: src/ui/views/GamesView/AddGamePopover.vala:73 msgid "Name" msgstr "" #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:273 -#: src/ui/dialogs/GamePropertiesDialog.vala:237 +#: src/ui/dialogs/GamePropertiesDialog.vala:170 #: src/ui/views/GamesView/AddGamePopover.vala:78 msgid "Arguments" msgstr "" @@ -830,7 +830,7 @@ msgid "Select emulator directory" msgstr "" #: src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala:293 -#: src/ui/dialogs/GamePropertiesDialog.vala:255 +#: src/ui/dialogs/GamePropertiesDialog.vala:188 msgid "Force compatibility mode" msgstr "" @@ -934,81 +934,75 @@ msgctxt "about_link" msgid "Forks" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:55 src/ui/dialogs/InstallDialog.vala:113 +#: src/ui/dialogs/InstallDialog.vala:57 src/ui/dialogs/InstallDialog.vala:115 #: src/ui/views/GamesView/GameContextMenu.vala:50 +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:145 #: src/ui/views/GameDetailsView/GameDetailsPage.vala:223 msgid "Install" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:111 +#: src/ui/dialogs/InstallDialog.vala:113 #: src/ui/dialogs/ImportEmulatedGamesDialog.vala:129 msgid "Import" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:112 +#: src/ui/dialogs/InstallDialog.vala:114 +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:155 msgid "Download" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:209 +#: src/ui/dialogs/InstallDialog.vala:211 msgid "Select installer" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:218 +#: src/ui/dialogs/InstallDialog.vala:220 #, c-format msgid "Installer size: %s" msgstr "" -#: src/ui/dialogs/InstallDialog.vala:317 +#: src/ui/dialogs/InstallDialog.vala:330 msgid "Unknown" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:50 +#: src/ui/dialogs/GamePropertiesDialog.vala:47 #, c-format msgid "%s: Properties" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:65 -#: src/ui/views/GamesView/FiltersPopover.vala:194 -msgid "Tags" -msgstr "" - -#: src/ui/dialogs/GamePropertiesDialog.vala:110 -msgid "Add tag" -msgstr "" - -#: src/ui/dialogs/GamePropertiesDialog.vala:148 +#: src/ui/dialogs/GamePropertiesDialog.vala:81 msgid "Images" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:185 +#: src/ui/dialogs/GamePropertiesDialog.vala:118 +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:162 msgid "Download images" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:192 +#: src/ui/dialogs/GamePropertiesDialog.vala:125 msgid "Image URL" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:200 +#: src/ui/dialogs/GamePropertiesDialog.vala:133 msgid "Icon URL" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:250 +#: src/ui/dialogs/GamePropertiesDialog.vala:183 msgid "Compatibility" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:272 +#: src/ui/dialogs/GamePropertiesDialog.vala:205 msgid "Launch from terminal" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:288 +#: src/ui/dialogs/GamePropertiesDialog.vala:221 msgid "Copy to clipboard" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:290 +#: src/ui/dialogs/GamePropertiesDialog.vala:223 msgid "Add to Steam" msgstr "" -#: src/ui/dialogs/GamePropertiesDialog.vala:291 +#: src/ui/dialogs/GamePropertiesDialog.vala:224 msgid "Add to the Steam library" msgstr "" @@ -1129,93 +1123,93 @@ msgstr "" msgid "Get some games or enable some game sources in settings" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:181 +#: src/ui/views/GamesView/GamesView.vala:185 msgid "Grid view" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:182 +#: src/ui/views/GamesView/GamesView.vala:186 msgid "List view" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:190 +#: src/ui/views/GamesView/GamesView.vala:194 msgid "All games" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:213 +#: src/ui/views/GamesView/GamesView.vala:217 msgid "Downloads" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:239 +#: src/ui/views/GamesView/GamesView.vala:243 msgid "Filters" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:247 +#: src/ui/views/GamesView/GamesView.vala:251 #: src/ui/views/GamesView/AddGamePopover.vala:84 msgid "Add game" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:254 +#: src/ui/views/GamesView/GamesView.vala:258 msgid "Search" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:360 +#: src/ui/views/GamesView/GamesView.vala:381 msgid "Menu" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:361 -#: src/ui/views/GameDetailsView/GameDetailsView.vala:81 +#: src/ui/views/GamesView/GamesView.vala:382 +#: src/ui/views/GameDetailsView/GameDetailsView.vala:105 msgid "Back" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:536 +#: src/ui/views/GamesView/GamesView.vala:558 #, c-format msgid "%u game" msgid_plural "%u games" msgstr[0] "" msgstr[1] "" -#: src/ui/views/GamesView/GamesView.vala:552 +#: src/ui/views/GamesView/GamesView.vala:574 msgid "No user-added games" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:553 +#: src/ui/views/GamesView/GamesView.vala:575 msgid "Add some games using plus button" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:557 +#: src/ui/views/GamesView/GamesView.vala:579 #, c-format msgid "No %s games" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:558 +#: src/ui/views/GamesView/GamesView.vala:580 msgid "Get some Linux-compatible games" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:568 +#: src/ui/views/GamesView/GamesView.vala:590 #, c-format msgid "No games matching “%s”" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:573 +#: src/ui/views/GamesView/GamesView.vala:595 #, c-format msgid "No %1$s games matching “%2$s”" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:614 +#: src/ui/views/GamesView/GamesView.vala:636 msgid "" "No games were loaded from Steam. Set your games list privacy to public or " "use your own Steam API key in settings." msgstr "" -#: src/ui/views/GamesView/GamesView.vala:615 +#: src/ui/views/GamesView/GamesView.vala:637 msgid "Privacy" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:751 +#: src/ui/views/GamesView/GamesView.vala:784 msgid "Downloading images" msgstr "" -#: src/ui/views/GamesView/GamesView.vala:765 +#: src/ui/views/GamesView/GamesView.vala:798 #, c-format msgid "Downloading image: %s" msgstr "" @@ -1239,6 +1233,11 @@ msgstr "" msgid "Sort" msgstr "" +#: src/ui/views/GamesView/FiltersPopover.vala:194 +#: src/ui/widgets/GameTagsList.vala:58 +msgid "Tags" +msgstr "" + #: src/ui/views/GamesView/FiltersPopover.vala:306 msgid "All platforms" msgstr "" @@ -1289,10 +1288,59 @@ msgid "Open bonus collection directory" msgstr "" #: src/ui/views/GamesView/GameContextMenu.vala:167 +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:169 #: src/ui/views/GameDetailsView/GameDetailsPage.vala:230 msgid "Uninstall" msgstr "" +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:102 +#, c-format +msgid "%d game selected" +msgid_plural "%d games selected" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:146 +#, c-format +msgid "%d game will be installed" +msgid_plural "%d games will be installed" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:156 +#, c-format +msgid "%d game will be downloaded" +msgid_plural "%d games will be downloaded" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:163 +#, c-format +msgid "Image for %d game will be searched" +msgid_plural "Images for %d games will be searched" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:170 +#, c-format +msgid "%d game will be uninstalled" +msgid_plural "%d games will be uninstalled" +msgstr[0] "" +msgstr[1] "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:176 +msgid "Refresh" +msgstr "" + +#: src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala:177 +#, c-format +msgid "" +"%d game will be removed from database. Restart GameHub to fetch new data" +msgid_plural "" +"%d games will be removed from database. Restart GameHub to fetch new data" +msgstr[0] "" +msgstr[1] "" + #: src/ui/views/GameDetailsView/GameDetailsPage.vala:229 msgid "Open store page" msgstr "" @@ -1456,6 +1504,10 @@ msgstr "" msgid "Configure" msgstr "" +#: src/ui/widgets/GameTagsList.vala:103 +msgid "Add tag" +msgstr "" + #: src/utils/downloader/Downloader.vala:159 msgctxt "dl_status" msgid "Queued" diff --git a/po/de.po b/po/de.po index 9134ec73..e2de178f 100644 --- a/po/de.po +++ b/po/de.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: com.github.tkashkin.gamehub\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-06-23 07:47+0300\n" +"POT-Creation-Date: 2019-06-25 15:41+0300\n" "PO-Revision-Date: 2019-06-08 23:29+0000\n" "Last-Translator: ssantos \n" "Language-Team: German \n" "Language-Team: French \n" "Language-Team: Indonesian \n" "Language-Team: Norwegian Bokmål \n" "Language-Team: Dutch \n" "Language-Team: Polish \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: Russian \n" "Language-Team: Turkish \n" "Language-Team: Chinese (Simplified) (); installers.add(installer); - var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers, install_mode); wnd.cancelled.connect(() => Idle.add(install.callback)); diff --git a/src/data/Runnable.vala b/src/data/Runnable.vala index 415c1aeb..df80231a 100644 --- a/src/data/Runnable.vala +++ b/src/data/Runnable.vala @@ -72,7 +72,7 @@ namespace GameHub.Data public ArrayList actions { get; protected set; default = new ArrayList(); } - public abstract async void install(); + public abstract async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE); public abstract async void run(); public virtual async void run_with_compat(bool is_opened_from_menu=false) @@ -735,6 +735,11 @@ namespace GameHub.Data return InstallerType.UNKNOWN; } } + + public enum InstallMode + { + INTERACTIVE, AUTOMATIC, AUTOMATIC_DOWNLOAD + } } public abstract class RunnableAction diff --git a/src/data/sources/gog/GOGGame.vala b/src/data/sources/gog/GOGGame.vala index 5dec8394..dae2ae97 100644 --- a/src/data/sources/gog/GOGGame.vala +++ b/src/data/sources/gog/GOGGame.vala @@ -286,13 +286,13 @@ namespace GameHub.Data.Sources.GOG game_info_updating = false; } - public override async void install() + public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { yield update_game_info(); if(installers == null || installers.size < 1) return; - var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers, install_mode); wnd.cancelled.connect(() => Idle.add(install.callback)); @@ -315,8 +315,11 @@ namespace GameHub.Data.Sources.GOG Idle.add(install.callback); }); - wnd.show_all(); - wnd.present(); + if(install_mode == Runnable.Installer.InstallMode.INTERACTIVE) + { + wnd.show_all(); + wnd.present(); + } yield; } @@ -940,7 +943,7 @@ namespace GameHub.Data.Sources.GOG base.update_status(); } - public override async void install() + public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { if(game.install_dir == null || !game.install_dir.query_exists()) return; diff --git a/src/data/sources/humble/HumbleGame.vala b/src/data/sources/humble/HumbleGame.vala index 5f37aab2..0ad683a6 100644 --- a/src/data/sources/humble/HumbleGame.vala +++ b/src/data/sources/humble/HumbleGame.vala @@ -382,13 +382,13 @@ namespace GameHub.Data.Sources.Humble return refresh; } - public override async void install() + public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { yield update_installers(); if(installers.size < 1) return; - var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers, install_mode); wnd.cancelled.connect(() => Idle.add(install.callback)); @@ -408,8 +408,11 @@ namespace GameHub.Data.Sources.Humble Idle.add(install.callback); }); - wnd.show_all(); - wnd.present(); + if(install_mode == Runnable.Installer.InstallMode.INTERACTIVE) + { + wnd.show_all(); + wnd.present(); + } yield; } diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala index e684e72f..ef569be2 100644 --- a/src/data/sources/steam/Steam.vala +++ b/src/data/sources/steam/Steam.vala @@ -60,6 +60,9 @@ namespace GameHub.Data.Sources.Steam private bool? installed = null; + private BinaryVDF.ListNode? appinfo; + private BinaryVDF.ListNode? packageinfo; + public bool is_authenticated_in_steam_client { get @@ -213,6 +216,9 @@ namespace GameHub.Data.Sources.Steam Utils.thread("SteamLoading", () => { _games.clear(); + appinfo = new AppInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.AppInfoVDF)).read(); + packageinfo = new PackageInfoVDF(FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.PackageInfoVDF)).read(); + var cached = Tables.Games.get_all(this); games_count = 0; if(cached.size > 0) @@ -284,6 +290,47 @@ namespace GameHub.Data.Sources.Steam } } + public static BinaryVDF.ListNode? get_appinfo(string appid) + { + if(instance.appinfo != null) + { + return (BinaryVDF.ListNode?) instance.appinfo.get(appid); + } + return null; + } + + public static string[]? get_packages_for_app(string appid) + { + if(instance.packageinfo == null) return null; + string[] pkgs = {}; + foreach(var pkg in instance.packageinfo.nodes.values) + { + if(appid in ((PackageInfoVDF.PackageNode) pkg).appids) + { + pkgs += ((PackageInfoVDF.PackageNode) pkg).id; + } + } + return pkgs; + } + + public static void install_multiple_apps(string[] appids) + { + if(instance.packageinfo == null) return; + var packages = ""; + foreach(var appid in appids) + { + var pkgs = get_packages_for_app(appid); + foreach(var pkg in pkgs) + { + packages += "/" + pkg; + } + } + if(packages.length > 0) + { + Utils.open_uri("steam://subscriptioninstall" + packages); + } + } + private void watch_client_registry() { var regfile = FSUtils.find_case_insensitive(FSUtils.file(FSUtils.Paths.Steam.Home), FSUtils.Paths.Steam.RegistryVDF); diff --git a/src/data/sources/steam/SteamGame.vala b/src/data/sources/steam/SteamGame.vala index ed845457..a0d31793 100644 --- a/src/data/sources/steam/SteamGame.vala +++ b/src/data/sources/steam/SteamGame.vala @@ -132,6 +132,12 @@ namespace GameHub.Data.Sources.Steam Steam.find_app_install_dir(id, out dir); install_dir = dir; + var appinfo = Steam.get_appinfo(id); + if(appinfo != null) + { + //appinfo.show(); + } + if(game_info_updated) { game_info_updating = false; @@ -238,9 +244,10 @@ namespace GameHub.Data.Sources.Steam status = new Game.Status(state, this); } - public override async void install() + public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { - yield run(); + Utils.open_uri(@"steam://install/$(id)"); + update_status(); } public override async void run() diff --git a/src/data/sources/user/UserGame.vala b/src/data/sources/user/UserGame.vala index 89c5b291..83e0e895 100644 --- a/src/data/sources/user/UserGame.vala +++ b/src/data/sources/user/UserGame.vala @@ -126,7 +126,7 @@ namespace GameHub.Data.Sources.User save(); } - public override async void install() + public override async void install(Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { yield update_game_info(); @@ -135,7 +135,7 @@ namespace GameHub.Data.Sources.User var installers = new ArrayList(); installers.add(installer); - var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers); + var wnd = new GameHub.UI.Dialogs.InstallDialog(this, installers, install_mode); wnd.cancelled.connect(() => Idle.add(install.callback)); @@ -146,8 +146,11 @@ namespace GameHub.Data.Sources.User }); }); - wnd.show_all(); - wnd.present(); + if(install_mode == Runnable.Installer.InstallMode.INTERACTIVE) + { + wnd.show_all(); + wnd.present(); + } yield; } diff --git a/src/meson.build b/src/meson.build index c8b93a72..9e2b73f4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -122,6 +122,7 @@ sources = [ 'ui/views/GamesView/GameContextMenu.vala', 'ui/views/GameDetailsView/GameDetailsView.vala', + 'ui/views/GameDetailsView/MultipleGamesDetailsView.vala', 'ui/views/GameDetailsView/GameDetailsPage.vala', 'ui/views/GameDetailsView/GameDetailsBlock.vala', @@ -139,6 +140,7 @@ sources = [ 'ui/widgets/ImagesDownloadPopover.vala', 'ui/widgets/CompatToolOptions.vala', 'ui/widgets/CompatToolPicker.vala', + 'ui/widgets/GameTagsList.vala', 'ui/widgets/TagRow.vala', 'utils/Utils.vala', diff --git a/src/ui/dialogs/GamePropertiesDialog.vala b/src/ui/dialogs/GamePropertiesDialog.vala index 6e019022..90fbfe3e 100644 --- a/src/ui/dialogs/GamePropertiesDialog.vala +++ b/src/ui/dialogs/GamePropertiesDialog.vala @@ -33,9 +33,6 @@ namespace GameHub.UI.Dialogs public Game? game { get; construct; } private Box content; - private ListBox tags_list; - private ScrolledWindow tags_scrolled; - private Entry new_entry; private Entry name_entry; private AutoSizeImage image_view; @@ -60,70 +57,6 @@ namespace GameHub.UI.Dialogs content = new Box(Orientation.HORIZONTAL, 8); content.margin_start = content.margin_end = 6; - var tags_box = new Box(Orientation.VERTICAL, 0); - - var tags_header = new HeaderLabel(_("Tags")); - tags_header.xpad = 8; - tags_box.add(tags_header); - - tags_list = new ListBox(); - tags_list.get_style_context().add_class("tags-list"); - tags_list.selection_mode = SelectionMode.NONE; - - tags_list.set_sort_func((row1, row2) => { - var item1 = row1 as TagRow; - var item2 = row2 as TagRow; - - if(row1 != null && row2 != null) - { - var t1 = item1.tag.id; - var t2 = item2.tag.id; - - var b1 = t1.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); - var b2 = t2.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); - if(b1 && !b2) return -1; - if(!b1 && b2) return 1; - - var u1 = t1.has_prefix(Tables.Tags.Tag.USER_PREFIX); - var u2 = t2.has_prefix(Tables.Tags.Tag.USER_PREFIX); - if(u1 && !u2) return -1; - if(!u1 && u2) return 1; - - return item1.tag.name.collate(item1.tag.name); - } - - return 0; - }); - - tags_scrolled = new ScrolledWindow(null, null); - tags_scrolled.vexpand = true; - #if GTK_3_22 - tags_scrolled.propagate_natural_width = true; - tags_scrolled.propagate_natural_height = true; - tags_scrolled.max_content_height = 320; - #endif - tags_scrolled.add(tags_list); - - tags_box.add(tags_scrolled); - - new_entry = new Entry(); - new_entry.placeholder_text = _("Add tag"); - new_entry.primary_icon_name = "gh-tag-add-symbolic"; - new_entry.primary_icon_activatable = false; - new_entry.secondary_icon_name = "list-add-symbolic"; - new_entry.secondary_icon_activatable = true; - new_entry.margin = 4; - - new_entry.icon_press.connect((icon, event) => { - if(icon == EntryIconPosition.SECONDARY && ((EventButton) event).button == 1) - { - add_tag(); - } - }); - new_entry.activate.connect(add_tag); - - tags_box.add(new_entry); - properties_box = new Box(Orientation.VERTICAL, 0); var name_header = new HeaderLabel(_("Name")); @@ -307,7 +240,7 @@ namespace GameHub.UI.Dialogs } }); - content.add(tags_box); + content.add(new GameTagsList(game)); content.add(new Separator(Orientation.VERTICAL)); content.add(properties_box); @@ -323,40 +256,9 @@ namespace GameHub.UI.Dialogs destroy(); }); - Tables.Tags.instance.tags_updated.connect(update); - - update(); - show_all(); } - private void update() - { - tags_list.foreach(w => w.destroy()); - - foreach(var tag in Tables.Tags.TAGS) - { - if(tag in Tables.Tags.DYNAMIC_TAGS || !tag.enabled) continue; - var row = new TagRow(tag, game); - tags_list.add(row); - } - - tags_list.show_all(); - } - - private void add_tag() - { - var name = new_entry.text.strip(); - if(name.length == 0) return; - - new_entry.text = ""; - - var tag = new Tables.Tags.Tag.from_name(name); - Tables.Tags.add(tag); - game.add_tag(tag); - update(); - } - private void set_image_url(bool replace=false) { var url = image_entry.uri; diff --git a/src/ui/dialogs/InstallDialog.vala b/src/ui/dialogs/InstallDialog.vala index a46a7434..4695f598 100644 --- a/src/ui/dialogs/InstallDialog.vala +++ b/src/ui/dialogs/InstallDialog.vala @@ -50,9 +50,11 @@ namespace GameHub.UI.Dialogs private CompatToolPicker compat_tool_picker; private CompatToolOptions opts_list; - public InstallDialog(Runnable runnable, ArrayList installers) + public Runnable.Installer.InstallMode install_mode { get; construct; } + + public InstallDialog(Runnable runnable, ArrayList installers, Runnable.Installer.InstallMode install_mode=Runnable.Installer.InstallMode.INTERACTIVE) { - Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("Install")); + Object(transient_for: Windows.MainWindow.instance, resizable: false, title: _("Install"), install_mode: install_mode); Game? game = null; @@ -305,6 +307,17 @@ namespace GameHub.UI.Dialogs get_content_area().add(hbox); get_content_area().set_size_request(380, 96); + + if(install_mode != Runnable.Installer.InstallMode.INTERACTIVE) + { + Idle.add(() => { + response(install_mode == Runnable.Installer.InstallMode.AUTOMATIC ? ResponseType.ACCEPT : InstallDialog.RESPONSE_DOWNLOAD); + hide(); + return Source.REMOVE; + }); + return; + } + show_all(); } diff --git a/src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala b/src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala index aa69996c..5f989a31 100644 --- a/src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala +++ b/src/ui/dialogs/SettingsDialog/pages/emulators/Emulators.vala @@ -422,7 +422,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Emulators emulator.installer = new Emulator.Installer(emulator, emulator.executable); emulator.executable = null; - emulator.install.begin((obj, res) => { + emulator.install.begin(Runnable.Installer.InstallMode.INTERACTIVE, (obj, res) => { emulator.install.end(res); sensitive = true; mode.selected = 0; diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index 09148f18..0c603798 100644 --- a/src/ui/views/GameDetailsView/GameDetailsPage.vala +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -216,10 +216,10 @@ namespace GameHub.UI.Views.GameDetailsView stack.add(spinner); stack.add(content_scrolled); - stack.set_visible_child(spinner); - add(stack); + stack.visible_child = spinner; + action_install = add_action("go-down", null, _("Install"), install_game, true); action_run = add_action("media-playback-start", null, _("Run"), run_game, true); action_run_with_compat = add_action("media-playback-start", "platform-windows-symbolic", _("Run with compatibility layer"), run_game_with_compat, true); @@ -328,7 +328,10 @@ namespace GameHub.UI.Views.GameDetailsView if(is_updated) return; - stack.set_visible_child(spinner); + if(spinner.parent == stack) + { + stack.visible_child = spinner; + } if(game == null) return; @@ -374,7 +377,10 @@ namespace GameHub.UI.Views.GameDetailsView icon.load(game.icon, "icon"); no_icon_indicator.visible = game.icon == null || icon.source == null; - stack.set_visible_child(content_scrolled); + if(content_scrolled.parent == stack) + { + stack.visible_child = content_scrolled; + } } private void install_game() diff --git a/src/ui/views/GameDetailsView/GameDetailsView.vala b/src/ui/views/GameDetailsView/GameDetailsView.vala index 632d6fff..43c843a2 100644 --- a/src/ui/views/GameDetailsView/GameDetailsView.vala +++ b/src/ui/views/GameDetailsView/GameDetailsView.vala @@ -31,6 +31,7 @@ namespace GameHub.UI.Views.GameDetailsView public class GameDetailsView: BaseView { private Game? _game; + private ArrayList? _selected_games; public GameSource? preferred_source { get; set; } @@ -42,6 +43,7 @@ namespace GameHub.UI.Views.GameDetailsView set { _game = value; + _selected_games = null; navigation.clear(); navigation.add(game); Idle.add(() => { @@ -51,11 +53,29 @@ namespace GameHub.UI.Views.GameDetailsView } } + public ArrayList? selected_games + { + get { return _selected_games; } + set + { + _selected_games = value; + _game = null; + Idle.add(() => { + update_selected_games(); + return Source.REMOVE; + }); + } + } + public GameDetailsView(Game? game=null) { Object(game: game); } + private Stack root_stack; + public MultipleGamesDetailsView selected_games_view; + private Box game_box; + private Stack stack; private Button back_button; @@ -67,6 +87,10 @@ namespace GameHub.UI.Views.GameDetailsView construct { + root_stack = new Stack(); + root_stack.transition_type = StackTransitionType.NONE; + root_stack.expand = true; + stack = new Stack(); stack.transition_type = StackTransitionType.SLIDE_LEFT_RIGHT; stack.expand = true; @@ -96,6 +120,8 @@ namespace GameHub.UI.Views.GameDetailsView }); }); + game_box = new Box(Orientation.VERTICAL, 0); + actions = new Revealer(); actions.transition_type = RevealerTransitionType.SLIDE_DOWN; actions.reveal_child = false; @@ -107,8 +133,17 @@ namespace GameHub.UI.Views.GameDetailsView actions.add(actionbar); - attach(actions, 0, 0); - attach(stack, 0, 1); + game_box.add(actions); + game_box.add(stack); + + selected_games_view = new MultipleGamesDetailsView(); + + root_stack.add(selected_games_view); + root_stack.add(game_box); + + root_stack.visible_child = game_box; + + add(root_stack); stack.notify["visible-child"].connect(() => { var page = stack.visible_child as GameDetailsPage; @@ -163,6 +198,8 @@ namespace GameHub.UI.Views.GameDetailsView private void update() { + root_stack.visible_child = game_box; + stack_tabs.clear(); back_button.visible = false; @@ -176,8 +213,8 @@ namespace GameHub.UI.Views.GameDetailsView if(g == null) return; - var primary = Settings.UI.Behavior.instance.merge_games ? Tables.Merges.get_primary(game) : null; - var merges = Settings.UI.Behavior.instance.merge_games ? Tables.Merges.get(game) : null; + var primary = Settings.UI.Behavior.instance.merge_games ? Tables.Merges.get_primary(g) : null; + var merges = Settings.UI.Behavior.instance.merge_games ? Tables.Merges.get(g) : null; bool merged = merges != null && merges.size > 0; stack_tabs.visible = merged || primary != null; @@ -226,5 +263,12 @@ namespace GameHub.UI.Views.GameDetailsView page.content.margin = content_margin; stack_tabs.add_tab(page, g.full_id, label, true, g.source.icon); } + + private void update_selected_games() + { + if(selected_games == null) return; + selected_games_view.games = selected_games; + root_stack.visible_child = selected_games_view; + } } } diff --git a/src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala b/src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala new file mode 100644 index 00000000..9a04701f --- /dev/null +++ b/src/ui/views/GameDetailsView/MultipleGamesDetailsView.vala @@ -0,0 +1,281 @@ +/* +This file is part of GameHub. +Copyright (C) 2018-2019 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; +using GameHub.Data; +using GameHub.Data.DB; +using GameHub.Utils; +using GameHub.UI.Widgets; +using WebKit; + +namespace GameHub.UI.Views.GameDetailsView +{ + public class MultipleGamesDetailsView: Grid + { + public signal void download_images(ArrayList games); + + private ArrayList? _games; + public ArrayList? games + { + get { return _games; } + set + { + _games = value; + Idle.add(() => { + update(); + return Source.REMOVE; + }); + } + } + + private Label header; + private Box actions; + private GameTagsList tags; + + private ArrayList? installable; + private ArrayList? downloadable; + private ArrayList? no_images; + private ArrayList? uninstallable; + private ArrayList? refreshable; + + public MultipleGamesDetailsView(ArrayList? games=null) + { + Object(games: games); + } + + construct + { + header = new Label(null); + header.halign = Align.START; + header.wrap = true; + header.xalign = 0; + header.hexpand = true; + header.margin = 24; + header.get_style_context().add_class(Granite.STYLE_CLASS_H2_LABEL); + + var actions_wrapper = new Box(Orientation.VERTICAL, 0); + actions_wrapper.expand = true; + + actions = new Box(Orientation.VERTICAL, 0); + actions.margin = 16; + actions.margin_top = 0; + + actions_wrapper.add(actions); + + tags = new GameTagsList(); + tags.get_style_context().add_class(Gtk.STYLE_CLASS_VIEW); + tags.width_request = 200; + tags.hexpand = false; + + attach(header, 0, 0); + attach(actions_wrapper, 0, 1); + attach(new Separator(Orientation.VERTICAL), 1, 0, 1, 2); + attach(tags, 2, 0, 1, 2); + + show_all(); + } + + private void update() + { + if(games == null) return; + + tags.games = games; + + header.label = ngettext("%d game selected", "%d games selected", games.size).printf(games.size); + + actions.foreach(w => w.destroy()); + + installable = new ArrayList(); + downloadable = new ArrayList(); + no_images = new ArrayList(); + uninstallable = new ArrayList(); + refreshable = new ArrayList(); + + foreach(var g in games) + { + if(g.status.state == Game.State.INSTALLED) + { + uninstallable.add(g); + } + else + { + if(!(g is Sources.User.UserGame)) + { + refreshable.add(g); + if(g.is_installable) + { + if(g.status.state == Game.State.UNINSTALLED) + { + installable.add(g); + } + if(!(g is Sources.Steam.SteamGame)) + { + downloadable.add(g); + } + } + } + } + if(g.image == null) + { + no_images.add(g); + } + } + + if(installable.size > 0) + { + add_action_separator(); + var action_install = add_action("go-down", null, _("Install"), install_games); + action_install.text += "\n" + """%s""".printf(ngettext("%d game will be installed", "%d games will be installed", installable.size).printf(installable.size)); + } + + if(downloadable.size > 0) + { + if(installable.size == 0) + { + add_action_separator(); + } + var action_download = add_action("folder-download", null, _("Download"), download_games); + action_download.text += "\n" + """%s""".printf(ngettext("%d game will be downloaded", "%d games will be downloaded", downloadable.size).printf(downloadable.size)); + } + + if(no_images.size > 0) + { + add_action_separator(); + var action_download_images = add_action("image-x-generic", null, _("Download images"), download_game_images); + action_download_images.text += "\n" + """%s""".printf(ngettext("Image for %d game will be searched", "Images for %d games will be searched", no_images.size).printf(no_images.size)); + } + + if(uninstallable.size > 0) + { + add_action_separator(); + var action_uninstall = add_action("edit-delete", null, _("Uninstall"), uninstall_games); + action_uninstall.text += "\n" + """%s""".printf(ngettext("%d game will be uninstalled", "%d games will be uninstalled", uninstallable.size).printf(uninstallable.size)); + } + + if(refreshable.size > 0) + { + add_action_separator(); + var action_refresh = add_action("view-refresh", null, _("Refresh"), refresh_games); + action_refresh.text += "\n" + """%s""".printf(ngettext("%d game will be removed from database. Restart GameHub to fetch new data", "%d games will be removed from database. Restart GameHub to fetch new data", refreshable.size).printf(refreshable.size)); + } + + actions.show_all(); + } + + private void install_games() + { + if(installable == null || installable.size == 0) return; + + if(Sources.Steam.Steam.instance.enabled) + { + string[] steam_apps = {}; + foreach(var game in installable) + { + if(game is Sources.Steam.SteamGame) + { + steam_apps += game.id; + } + } + if(steam_apps.length > 0) + { + Sources.Steam.Steam.install_multiple_apps(steam_apps); + } + } + + foreach(var game in installable) + { + if(!(game is Sources.Steam.SteamGame)) + { + game.install.begin(Runnable.Installer.InstallMode.AUTOMATIC); + } + } + update(); + } + + private void download_games() + { + if(downloadable == null || downloadable.size == 0) return; + foreach(var game in downloadable) + { + if(!(game is Sources.Steam.SteamGame)) + { + game.install.begin(Runnable.Installer.InstallMode.AUTOMATIC_DOWNLOAD); + } + } + update(); + } + + private void download_game_images() + { + if(no_images == null || no_images.size == 0) return; + download_images(no_images); + update(); + } + + private void uninstall_games() + { + if(uninstallable == null || uninstallable.size == 0) return; + uninstall_games_async.begin(uninstallable); + } + + private void refresh_games() + { + if(refreshable == null || refreshable.size == 0) return; + foreach(var game in refreshable) + { + Tables.Games.remove(game); + } + update(); + } + + private async void uninstall_games_async(ArrayList games) + { + foreach(var game in games) + { + yield game.uninstall(); + } + update(); + } + + private void add_action_separator() + { + if(actions.get_children().length() == 0) return; + var separator = new Separator(Orientation.HORIZONTAL); + separator.margin = 4; + actions.add(separator); + } + + private delegate void Action(); + private ActionButton add_action(string icon, string? icon_overlay, string title, Action action) + { + var ui_settings = Settings.UI.Appearance.instance; + var button = new ActionButton(icon + Settings.UI.Appearance.symbolic_icon_suffix, icon_overlay, title, true, ui_settings.icon_style.is_symbolic()); + button.hexpand = true; + actions.add(button); + button.clicked.connect(() => action()); + ui_settings.notify["icon-style"].connect(() => { + button.icon = icon + Settings.UI.Appearance.symbolic_icon_suffix; + button.compact = ui_settings.icon_style.is_symbolic(); + }); + return button; + } + } +} diff --git a/src/ui/views/GamesView/GameListRow.vala b/src/ui/views/GamesView/GameListRow.vala index 3f81996b..cb11493a 100644 --- a/src/ui/views/GamesView/GameListRow.vala +++ b/src/ui/views/GamesView/GameListRow.vala @@ -64,7 +64,7 @@ namespace GameHub.UI.Views.GamesView public GameListRow(Game? game=null, GamesAdapter? adapter=null) { - Object(game: game, adapter: adapter); + Object(game: game, adapter: adapter, activatable: true, selectable: true); } construct @@ -134,10 +134,28 @@ namespace GameHub.UI.Views.GamesView switch(e.button) { case 1: - activate(); - if(e.type == EventType.2BUTTON_PRESS) + var list = (ListBox) parent; + + if(ModifierType.CONTROL_MASK in e.state) + { + if(is_selected()) + { + list.unselect_row(this); + } + else + { + list.select_row(this); + } + } + else { - game.run_or_install.begin(); + list.unselect_all(); + activate(); + + if(e.type == EventType.2BUTTON_PRESS) + { + game.run_or_install.begin(); + } } break; diff --git a/src/ui/views/GamesView/GamesView.vala b/src/ui/views/GamesView/GamesView.vala index 83973676..01325d12 100644 --- a/src/ui/views/GamesView/GamesView.vala +++ b/src/ui/views/GamesView/GamesView.vala @@ -150,11 +150,15 @@ namespace GameHub.UI.Views.GamesView games_list_paned = new Paned(Orientation.HORIZONTAL); games_list = new ListBox(); - games_list.selection_mode = SelectionMode.BROWSE; + games_list.selection_mode = SelectionMode.MULTIPLE; games_list_details = new GameDetailsView.GameDetailsView(null); games_list_details.content_margin = 16; + games_list_details.selected_games_view.download_images.connect(games => { + download_images_async.begin(games); + }); + var games_list_scrolled = new ScrolledWindow(null, null); games_list_scrolled.hscrollbar_policy = PolicyType.EXTERNAL; games_list_scrolled.add(games_list); @@ -262,9 +266,26 @@ namespace GameHub.UI.Views.GamesView settings.image = new Image.from_icon_name("open-menu" + Settings.UI.Appearance.symbolic_icon_suffix, Settings.UI.Appearance.headerbar_icon_size); settings.action_name = Application.ACTION_PREFIX + Application.ACTION_SETTINGS; - games_list.row_selected.connect(row => { - var item = row as GameListRow; - games_list_details.game = item != null ? item.game : null; + games_list.selected_rows_changed.connect(() => { + var rows = games_list.get_selected_rows(); + + if(rows.length() == 1) + { + games_list_details.game = ((GameListRow) rows.data).game; + } + else if(rows.length() > 1) + { + var selected = new ArrayList(); + foreach(var row in rows) + { + var game = ((GameListRow) row).game; + if(game != null) + { + selected.add(game); + } + } + games_list_details.selected_games = selected; + } }); games_adapter = new GamesAdapter(games_grid, games_list); @@ -304,7 +325,7 @@ namespace GameHub.UI.Views.GamesView update_view(); }); add_game_popover.download_images.connect(() => { - download_images_async.begin(); + download_images_async.begin(null); }); titlebar.pack_start(view); @@ -503,6 +524,7 @@ namespace GameHub.UI.Views.GamesView card.grab_focus(); } } + games_list.unselect_all(); var row = games_list.get_row_at_index(index); if(row != null) { @@ -658,10 +680,21 @@ namespace GameHub.UI.Views.GamesView private void select_first_visible_game() { - var row = games_list.get_selected_row() as GameListRow?; - if(row != null && games_adapter.filter(row.game)) return; + GameListRow? row = null; + + var rows = games_list.get_selected_rows(); + if(rows != null && rows.length() > 0) + { + row = (GameListRow?) rows.data; + if(row != null && games_adapter.filter(row.game)) return; + } + games_list.unselect_all(); + row = games_list.get_row_at_y(32) as GameListRow?; - if(row != null) games_list.select_row(row); + if(row != null) + { + games_list.select_row(row); + } var cards = games_grid.get_selected_children(); var card = cards != null && cards.length() > 0 ? cards.first().data as GameCard? : null; @@ -746,11 +779,11 @@ namespace GameHub.UI.Views.GamesView }, Priority.LOW); } - private async void download_images_async() + private async void download_images_async(ArrayList? games=null) { update_status(_("Downloading images")); - var games = DB.Tables.Games.get_all(); - foreach(var game in games) + var _games = games ?? DB.Tables.Games.get_all(); + foreach(var game in _games) { if(game.image == null) { diff --git a/src/ui/widgets/ActionButton.vala b/src/ui/widgets/ActionButton.vala index da0c7e62..cb4619cf 100644 --- a/src/ui/widgets/ActionButton.vala +++ b/src/ui/widgets/ActionButton.vala @@ -40,10 +40,11 @@ namespace GameHub.UI.Widgets var box = new Box(Orientation.HORIZONTAL, 8); var overlay = new Overlay(); - overlay.valign = Align.START; + overlay.valign = Align.CENTER; overlay.set_size_request(48, 48); var image = new Image.from_icon_name(icon, IconSize.DIALOG); + image.valign = Align.CENTER; overlay.add(image); notify["icon"].connect(() => { diff --git a/src/ui/widgets/GameTagsList.vala b/src/ui/widgets/GameTagsList.vala new file mode 100644 index 00000000..ec43c99c --- /dev/null +++ b/src/ui/widgets/GameTagsList.vala @@ -0,0 +1,151 @@ +/* +This file is part of GameHub. +Copyright (C) 2018-2019 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; +using Granite; + +using GameHub.Data; +using GameHub.Data.DB; + +namespace GameHub.UI.Widgets +{ + public class GameTagsList: Box + { + private ArrayList _games; + public ArrayList games + { + get { return _games; } + set + { + _games = value; + update(); + } + } + + private ListBox list; + private ScrolledWindow scrolled; + private Entry new_entry; + + public GameTagsList(Game? game=null, ArrayList? games=null) + { + Object(orientation: Orientation.VERTICAL, spacing: 0); + _games = games ?? new ArrayList(); + if(game != null && !(game in _games)) + { + _games.add(game); + } + } + + construct + { + var header = new HeaderLabel(_("Tags")); + header.xpad = 8; + add(header); + + list = new ListBox(); + list.get_style_context().add_class("tags-list"); + list.selection_mode = SelectionMode.NONE; + + list.set_sort_func((row1, row2) => { + var item1 = row1 as TagRow; + var item2 = row2 as TagRow; + + if(row1 != null && row2 != null) + { + var t1 = item1.tag.id; + var t2 = item2.tag.id; + + var b1 = t1.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + var b2 = t2.has_prefix(Tables.Tags.Tag.BUILTIN_PREFIX); + if(b1 && !b2) return -1; + if(!b1 && b2) return 1; + + var u1 = t1.has_prefix(Tables.Tags.Tag.USER_PREFIX); + var u2 = t2.has_prefix(Tables.Tags.Tag.USER_PREFIX); + if(u1 && !u2) return -1; + if(!u1 && u2) return 1; + + return item1.tag.name.collate(item1.tag.name); + } + + return 0; + }); + + scrolled = new ScrolledWindow(null, null); + scrolled.vexpand = true; + #if GTK_3_22 + scrolled.propagate_natural_width = true; + scrolled.propagate_natural_height = true; + scrolled.max_content_height = 320; + #endif + scrolled.add(list); + + add(scrolled); + + new_entry = new Entry(); + new_entry.placeholder_text = _("Add tag"); + new_entry.primary_icon_name = "gh-tag-add-symbolic"; + new_entry.primary_icon_activatable = false; + new_entry.secondary_icon_name = "list-add-symbolic"; + new_entry.secondary_icon_activatable = true; + new_entry.margin = 4; + + new_entry.icon_press.connect((icon, event) => { + if(icon == EntryIconPosition.SECONDARY && ((EventButton) event).button == 1) + { + add_tag(); + } + }); + new_entry.activate.connect(add_tag); + + add(new_entry); + + Tables.Tags.instance.tags_updated.connect(update); + update(); + + show_all(); + } + + private void update() + { + list.foreach(w => w.destroy()); + foreach(var tag in Tables.Tags.TAGS) + { + if(tag in Tables.Tags.DYNAMIC_TAGS || !tag.enabled) continue; + list.add(new TagRow(tag, games)); + } + list.show_all(); + } + + private void add_tag() + { + var name = new_entry.text.strip(); + if(name.length == 0) return; + new_entry.text = ""; + var tag = new Tables.Tags.Tag.from_name(name); + Tables.Tags.add(tag); + foreach(var game in games) + { + game.add_tag(tag); + } + update(); + } + } +} diff --git a/src/ui/widgets/TagRow.vala b/src/ui/widgets/TagRow.vala index ebe529b2..27709e24 100644 --- a/src/ui/widgets/TagRow.vala +++ b/src/ui/widgets/TagRow.vala @@ -18,6 +18,7 @@ along with GameHub. If not, see . using Gtk; using Gdk; +using Gee; using Granite; using GameHub.Data; @@ -27,16 +28,16 @@ namespace GameHub.UI.Widgets { public class TagRow: ListBoxRow { - public Game? game; + public ArrayList? games; public Tables.Tags.Tag tag; - public bool toggles_tag_for_game; + public bool toggles_tag_for_games; private CheckButton check; - public TagRow(Tables.Tags.Tag tag, Game? game=null) + public TagRow(Tables.Tags.Tag tag, ArrayList? games=null) { - this.game = game; + this.games = games; this.tag = tag; - this.toggles_tag_for_game = game != null; + this.toggles_tag_for_games = games != null; can_focus = true; @@ -50,9 +51,15 @@ namespace GameHub.UI.Widgets check = new CheckButton(); check.can_focus = false; - if(toggles_tag_for_game) + if(toggles_tag_for_games) { - check.active = game.has_tag(tag); + var have_tag = 0; + foreach(var game in games) + { + if(game.has_tag(tag)) have_tag++; + } + check.active = have_tag > 0; + check.inconsistent = have_tag != 0 && have_tag != games.size; } else { @@ -102,10 +109,22 @@ namespace GameHub.UI.Widgets private void toggle() { - if(toggles_tag_for_game) + if(toggles_tag_for_games) { - game.toggle_tag(tag); - check.active = game.has_tag(tag); + check.active = !check.active; + check.inconsistent = false; + + foreach(var game in games) + { + if(check.active && !game.has_tag(tag)) + { + game.add_tag(tag); + } + else if(!check.active && game.has_tag(tag)) + { + game.remove_tag(tag); + } + } } else { diff --git a/src/utils/BinaryVDF.vala b/src/utils/BinaryVDF.vala index 5390d5f9..b10366ee 100644 --- a/src/utils/BinaryVDF.vala +++ b/src/utils/BinaryVDF.vala @@ -25,40 +25,29 @@ namespace GameHub.Utils public class BinaryVDF { public File? file; + public DataInputStream? stream; - public Node? root_node; + public ListNode? root_node; public BinaryVDF(File? file) { this.file = file; - read(); } - public Node? read() + public virtual ListNode? read() { if(file == null || !file.query_exists()) return null; - root_node = null; try { - var stream = new DataInputStream(file.read()); + stream = new DataInputStream(file.read()); stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); - stream.seek(0, SeekType.END); - var size = stream.tell(); - stream.seek(0, SeekType.SET); - - while(stream.tell() < size) - { - var node = Node.read(stream); - if(root_node == null) - { - root_node = node; - } - } + root_node = Node.read(stream); stream.close(); + stream = null; } catch(Error e) { @@ -78,7 +67,7 @@ namespace GameHub.Utils stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); node.write(stream); - + stream.put_byte(ListNode.END); stream.flush(); @@ -92,66 +81,38 @@ namespace GameHub.Utils public abstract class Node { - private static ArrayQueue? OpenedLists = null; - - public string key; + public string? key; - public static Node? read(DataInputStream stream) throws Error + public static ListNode? read(DataInputStream stream, string? list_key=null) throws Error { - uint8 type = stream.read_byte(); - - if(!(type in new uint8[]{ ListNode.START, ListNode.END, StringNode.START, IntNode.START })) + var list = new ListNode(list_key, stream); + while(true) { - return null; - } + var type = stream.read_byte(); + if(type == ListNode.END) break; - if(type == ListNode.END) - { - if(OpenedLists != null) + string key = stream.read_upto("\0", 1, null); + stream.read_byte(); + + switch(type) { - return OpenedLists.poll_head(); - } - return null; - } + case ListNode.START: + list.add_node(read(stream, key)); + break; - string key = stream.read_upto("\0", 1, null); - stream.read_byte(); + case StringNode.START: + list.add_node(new BinaryVDF.StringNode(key, stream)); + break; - switch(type) - { - case ListNode.START: - if(OpenedLists == null) - { - OpenedLists = new ArrayQueue(); - } - var list = new ListNode(key, stream); - add_node(list); - OpenedLists.offer_head(list); - return list; - - case StringNode.START: - var str = new StringNode(key, stream); - add_node(str); - return str; - - case IntNode.START: - var i = new IntNode(key, stream); - add_node(i); - return i; - } - return null; - } + case IntNode.START: + list.add_node(new BinaryVDF.IntNode(key, stream)); + break; - private static void add_node(Node node) - { - if(OpenedLists != null) - { - var list = OpenedLists.peek_head(); - if(list != null) - { - list.add_node(node); + default: + throw new VDFError.UNKNOWN_NODE_TYPE("Unknown node type: %#04x (at %s)", type, stream.tell().to_string()); } } + return list; } public abstract void write(DataOutputStream stream) throws Error; @@ -174,12 +135,12 @@ namespace GameHub.Utils public HashMap nodes = new HashMap(); - public ListNode(string key, DataInputStream stream) throws Error + public ListNode(string? key, DataInputStream stream) throws Error { this.key = key; } - public ListNode.node(string key) + public ListNode.node(string? key) { this.key = key; } @@ -189,6 +150,29 @@ namespace GameHub.Utils nodes.set(node.key, node); } + public Node? get(string key) + { + return nodes.get(key); + } + + public Node? get_nested(string[] keys, Node? def=null) + { + ListNode? list = this; + foreach(var key in keys) + { + var node = list.get(key); + if(node != null && node is ListNode) + { + list = (ListNode) node; + } + else + { + return node ?? def; + } + } + return def; + } + public override void show(int indent=0) { print_indent(indent); @@ -220,14 +204,14 @@ namespace GameHub.Utils public string value; - public StringNode(string key, DataInputStream stream) throws Error + public StringNode(string? key, DataInputStream stream) throws Error { this.key = key; this.value = stream.read_upto("\0", 1, null); stream.read_byte(); } - public StringNode.node(string key, string value) + public StringNode.node(string? key, string value) { this.key = key; this.value = value; @@ -255,13 +239,13 @@ namespace GameHub.Utils public int32 value; - public IntNode(string key, DataInputStream stream) throws Error + public IntNode(string? key, DataInputStream stream) throws Error { this.key = key; this.value = stream.read_int32(); } - public IntNode.node(string key, int32 value) + public IntNode.node(string? key, int32 value) { this.key = key; this.value = value; @@ -282,4 +266,188 @@ namespace GameHub.Utils } } } + + public class AppInfoVDF: BinaryVDF + { + public const string ROOT = "apps"; + + private const uint16 MAGIC = 0x5644; + + public AppInfoVDF(File? file) + { + base(file); + } + + public override BinaryVDF.ListNode? read() + { + if(file == null || !file.query_exists()) return null; + + var apps = new BinaryVDF.ListNode.node(ROOT); + + try + { + stream = new DataInputStream(file.read()); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + stream.seek(0, SeekType.END); + var size = stream.tell(); + stream.seek(0, SeekType.SET); + + read_header(); + + while(stream.tell() < size) + { + var appid = stream.read_uint32(); + if(appid == 0) break; + + stream.seek(44, SeekType.CUR); + + var app = BinaryVDF.Node.read(stream, appid.to_string()); + if(app != null) + { + apps.add_node(app); + } + } + + stream.close(); + } + catch(Error e) + { + warning("[AppInfoVDF] Error reading `%s`: %s", file.get_path(), e.message); + } + return apps; + } + + private void read_header() throws Error + { + stream.read_byte(); + uint16 magic = stream.read_int16(); + if(magic != MAGIC) + { + throw new AppInfoError.UNSUPPORTED_FILE_TYPE("Unsupported file type: %#06x", magic); + } + stream.seek(5, SeekType.CUR); + } + } + + public class PackageInfoVDF: BinaryVDF + { + public const string ROOT = "packages"; + + private const uint16 MAGIC = 0x5655; + private const uint8[] BYTES_PACKAGEID = { 0x00, 0x02, 0x70, 0x61, 0x63, 0x6B, 0x61, 0x67, 0x65, 0x69, 0x64, 0x00 }; + private const uint8[] BYTES_APPIDS = { 0x08, 0x00, 0x61, 0x70, 0x70, 0x69, 0x64, 0x73, 0x00 }; + + public PackageInfoVDF(File? file) + { + base(file); + } + + public override BinaryVDF.ListNode? read() + { + if(file == null || !file.query_exists()) return null; + + var packages = new BinaryVDF.ListNode.node(ROOT); + + try + { + stream = new DataInputStream(file.read()); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + stream.seek(0, SeekType.END); + var size = stream.tell(); + stream.seek(0, SeekType.SET); + + read_header(); + + while(stream.tell() < size) + { + seek_to(BYTES_PACKAGEID, size); + if(stream.tell() >= size) break; + + var pkgid = stream.read_uint32(); + + seek_to(BYTES_APPIDS, size); + if(stream.tell() >= size) break; + + string[] appids = {}; + + while(stream.read_byte() == 0x02) + { + while(stream.read_byte() != 0x00); + appids += stream.read_uint32().to_string(); + } + + packages.add_node(new PackageNode(pkgid.to_string(), appids)); + } + + stream.close(); + } + catch(Error e) + { + warning("[PackageInfoVDF] Error reading `%s`: %s", file.get_path(), e.message); + } + return packages; + } + + private void read_header() throws Error + { + stream.read_byte(); + uint16 magic = stream.read_int16(); + if(magic != MAGIC) + { + throw new PackageInfoError.UNSUPPORTED_FILE_TYPE("Unsupported file type: %#06x", magic); + } + stream.seek(1, SeekType.CUR); + } + + private void seek_to(uint8[] bytes, int64 size) throws Error + { + int i = 0; + + while(i < bytes.length && stream.tell() < size) + { + uint8 b = stream.read_byte(); + if(b == bytes[i]) + { + i++; + } + else if(i != 0) + { + i = 0; + if(b == bytes[i]) + { + i++; + } + } + } + } + + public class PackageNode: BinaryVDF.Node + { + public string id; + public string[] appids; + + public PackageNode(string pkgid, string[] appids) + { + this.key = this.id = pkgid; + this.appids = appids; + } + + public override void show(int indent=0) + { + print_indent(indent); + print("|- [Package: %s] %s\n", id, string.joinv(", ", appids)); + } + + public override void write(DataOutputStream stream) throws Error + { + throw new PackageInfoError.WRITING_IS_NOT_IMPLEMENTED("Writing is not implemented"); + } + } + } + + public errordomain VDFError { UNKNOWN_NODE_TYPE } + public errordomain AppInfoError { UNSUPPORTED_FILE_TYPE } + public errordomain PackageInfoError { UNSUPPORTED_FILE_TYPE, WRITING_IS_NOT_IMPLEMENTED } } diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index 0fbab96e..0a61c19c 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -83,6 +83,9 @@ namespace GameHub.Utils public const string LibraryFoldersVDF = "libraryfolders.vdf"; public const string RegistryVDF = "registry.vdf"; + + public const string AppInfoVDF = "steam/appcache/appinfo.vdf"; + public const string PackageInfoVDF = "steam/appcache/packageinfo.vdf"; } public class GOG