From c5b263c71e56dcbbbdb2a3e9d6708b82b1453883 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Sun, 19 Mar 2023 01:06:00 +0100 Subject: [PATCH] Prepare for Windows builds --- data/gtk/preferences.blp | 8 +- data/gtk/window.blp | 1 + src/main.py | 4 + src/preferences.py | 8 + src/utils/heroic_parser.py | 5 + src/utils/run_command.py | 9 +- src/utils/steam_parser.py | 301 +++++++++++++++++++++---------------- 7 files changed, 198 insertions(+), 138 deletions(-) diff --git a/data/gtk/preferences.blp b/data/gtk/preferences.blp index 71208a1..30fda1f 100644 --- a/data/gtk/preferences.blp +++ b/data/gtk/preferences.blp @@ -5,7 +5,7 @@ template PreferencesWindow : Adw.PreferencesWindow { search-enabled: false; default-height: 500; - Adw.PreferencesPage { + Adw.PreferencesPage page { Adw.PreferencesGroup { title: _("General"); @@ -18,7 +18,7 @@ template PreferencesWindow : Adw.PreferencesWindow { } } - Adw.PreferencesGroup { + Adw.PreferencesGroup steam_group { title: "Steam"; Adw.ActionRow { @@ -32,7 +32,7 @@ template PreferencesWindow : Adw.PreferencesWindow { } } - Adw.PreferencesGroup { + Adw.PreferencesGroup heroic_group { title: "Heroic"; Adw.ActionRow { @@ -70,7 +70,7 @@ template PreferencesWindow : Adw.PreferencesWindow { } } - Adw.PreferencesGroup { + Adw.PreferencesGroup bottles_group { title: "Bottles"; Adw.ActionRow { diff --git a/data/gtk/window.blp b/data/gtk/window.blp index e24d27b..041750e 100644 --- a/data/gtk/window.blp +++ b/data/gtk/window.blp @@ -392,6 +392,7 @@ menu add_games { item { label: _("Bottles"); action: "app.bottles_import"; + hidden-when: "action-disabled"; } } } diff --git a/src/main.py b/src/main.py index 5e376d6..e8db59e 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import os import sys import time @@ -58,6 +59,9 @@ class CartridgesApplication(Adw.Application): self.create_action("add_game", self.on_add_game_action, ["n"]) self.create_action("remove_game", self.on_remove_game_action) + if os.name == "nt": + self.lookup_action("bottles_import").set_enabled(False) + self.win = None def do_activate(self): diff --git a/src/preferences.py b/src/preferences.py index 5711db7..3daac44 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -17,6 +17,8 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import os + from gi.repository import Adw, Gio, GLib, Gtk @@ -24,6 +26,9 @@ from gi.repository import Adw, Gio, GLib, Gtk class PreferencesWindow(Adw.PreferencesWindow): __gtype_name__ = "PreferencesWindow" + page = Gtk.Template.Child() + bottles_group = Gtk.Template.Child() + exit_after_launch_switch = Gtk.Template.Child() import_epic_games_switch = Gtk.Template.Child() import_gog_games_switch = Gtk.Template.Child() @@ -102,3 +107,6 @@ class PreferencesWindow(Adw.PreferencesWindow): self.bottles_file_chooser_button.connect( "clicked", choose_folder, set_bottles_dir ) + + if os.name == "nt": + self.page.remove(self.bottles_group) diff --git a/src/utils/heroic_parser.py b/src/utils/heroic_parser.py index 319e2f6..a0f353b 100644 --- a/src/utils/heroic_parser.py +++ b/src/utils/heroic_parser.py @@ -57,6 +57,11 @@ def heroic_parser(parent_widget, action): ), ) action(None, None) + elif os.path.exists(os.path.join(os.getenv("appdata"), "heroic")): + schema.set_string( + "heroic-location", os.path.join(os.getenv("appdata"), "heroic") + ) + action(None, None) else: filechooser = Gtk.FileDialog.new() diff --git a/src/utils/run_command.py b/src/utils/run_command.py index 006e9fd..78306b6 100644 --- a/src/utils/run_command.py +++ b/src/utils/run_command.py @@ -25,12 +25,13 @@ from gi.repository import Gio def run_command(executable): - with subprocess.Popen( + subprocess.Popen( ["flatpak-spawn --host " + executable] if os.getenv("FLATPAK_ID") == "hu.kramo.Cartridges" else [executable], shell=True, start_new_session=True, - ): - if Gio.Settings.new("hu.kramo.Cartridges").get_boolean("exit-after-launch"): - sys.exit() + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if os.name == "nt" else 0, + ) + if Gio.Settings.new("hu.kramo.Cartridges").get_boolean("exit-after-launch"): + sys.exit() diff --git a/src/utils/steam_parser.py b/src/utils/steam_parser.py index 053f5f1..9198d2c 100644 --- a/src/utils/steam_parser.py +++ b/src/utils/steam_parser.py @@ -21,14 +21,172 @@ import json import os import re import time +import urllib.request -from gi.repository import Gio, GLib, Gtk, Adw +from gi.repository import Adw, Gio, GLib, Gtk from .create_dialog import create_dialog from .save_cover import save_cover from .save_games import save_games +def upadte_values_from_data(content, values): + basic_data = json.loads(content)[values["appid"]] + if not basic_data["success"]: + values["blacklisted"] = True + else: + data = basic_data["data"] + values["developer"] = ", ".join(data["developers"]) + + if data["type"] != "game": + values["blacklisted"] = True + + return values + + +def get_game(task, datatypes, current_time, parent_widget, appmanifest, steam_dir): + values = {} + + with open(appmanifest, "r") as open_file: + data = open_file.read() + open_file.close() + for datatype in datatypes: + value = re.findall('"' + datatype + '"\t\t"(.*)"\n', data) + values[datatype] = value[0] + + values["game_id"] = "steam_" + values["appid"] + + if ( + values["game_id"] in parent_widget.games + and not parent_widget.games[values["game_id"]].removed + ): + task.return_value(None) + return + + values["executable"] = "xdg-open steam://rungameid/" + values["appid"] + values["hidden"] = False + values["source"] = "steam" + values["added"] = current_time + values["last_played"] = 0 + + url = "https://store.steampowered.com/api/appdetails?appids=" + values["appid"] + + # On Linux the request is made through gvfs so the app can run without network permissions + if os.name == "nt": + try: + with urllib.request.urlopen(url, timeout=10) as open_file: + content = open_file.read().decode("utf-8") + except urllib.error.URLError: + content = None + else: + open_file = Gio.File.new_for_uri(url) + try: + content = open_file.load_contents()[1] + except GLib.GError: + content = None + + if content: + values = upadte_values_from_data(content, values) + + if os.path.isfile( + os.path.join( + steam_dir, + "appcache", + "librarycache", + values["appid"] + "_library_600x900.jpg", + ) + ): + save_cover( + values, + parent_widget, + os.path.join( + steam_dir, + "appcache", + "librarycache", + values["appid"] + "_library_600x900.jpg", + ), + ) + + task.return_value(values) + return + + +def get_games_async(parent_widget, appmanifests, steam_dir, import_dialog): + datatypes = ["appid", "name"] + current_time = int(time.time()) + + steam_games = {} + queue = 0 + + # Wrap the function with another one as Gio.Task.run_in_thread does not allow for passing args + def create_func(datatypes, current_time, parent_widget, appmanifest, steam_dir): + def wrapper(task, *_unused): + get_game( + task, datatypes, current_time, parent_widget, appmanifest, steam_dir + ) + + return wrapper + + def update_games(_task, result, parent_widget): + nonlocal queue + nonlocal import_dialog + + queue -= 1 + + try: + final_values = result.propagate_value()[1] + steam_games[final_values["game_id"]] = final_values + except (TypeError, GLib.GError): + pass + + if queue == 0: + save_games(steam_games) + parent_widget.update_games(steam_games) + import_dialog.close() + games_no = len( + { + game_id: final_values + for game_id, final_values in steam_games.items() + if "blacklisted" not in final_values.keys() + } + ) + + if games_no == 0: + create_dialog( + parent_widget, + _("No Games Found"), + _("No new games were found in the Steam library."), + ) + elif games_no == 1: + create_dialog( + parent_widget, + _("Steam Games Imported"), + _("Successfully imported 1 game."), + ) + elif games_no > 1: + create_dialog( + parent_widget, + _("Steam Games Imported"), + _("Successfully imported") + + " " + + str(games_no) + + " " + + _("games."), + ) + + for appmanifest in appmanifests: + queue += 1 + + cancellable = Gio.Cancellable.new() + GLib.timeout_add_seconds(5, cancellable.cancel) + + task = Gio.Task.new(None, cancellable, update_games, parent_widget) + task.set_return_on_cancel(True) + task.run_in_thread( + create_func(datatypes, current_time, parent_widget, appmanifest, steam_dir) + ) + + def steam_parser(parent_widget, action): schema = parent_widget.schema steam_dir = os.path.expanduser(schema.get_string("steam-location")) @@ -44,6 +202,11 @@ def steam_parser(parent_widget, action): elif os.path.exists(os.path.expanduser("~/.steam/steam/")): schema.set_string("steam-location", "~/.steam/steam/") action(None, None) + elif os.path.exists(os.path.join(os.getenv("programfiles(x86)"), "Steam")): + schema.set_string( + "steam-location", os.path.join(os.getenv("programfiles(x86)"), "Steam") + ) + action(None, None) else: filechooser = Gtk.FileDialog.new() @@ -84,16 +247,6 @@ def steam_parser(parent_widget, action): steam_dir = os.path.expanduser(schema.get_string("steam-location")) - appmanifests = [] - datatypes = ["appid", "name"] - steam_games = {} - current_time = int(time.time()) - - for open_file in os.listdir(os.path.join(steam_dir, "steamapps")): - path = os.path.join(steam_dir, "steamapps", open_file) - if os.path.isfile(path) and "appmanifest" in open_file: - appmanifests.append(path) - import_statuspage = Adw.StatusPage( title=_("Importing Games..."), description=_("Talking to Steam"), @@ -108,125 +261,13 @@ def steam_parser(parent_widget, action): deletable=False, ) - queue = [] - cancellables = [] + import_dialog.present() - for appmanifest in appmanifests: - values = {} - with open(appmanifest, "r") as open_file: - data = open_file.read() - open_file.close() - for datatype in datatypes: - value = re.findall('"' + datatype + '"\t\t"(.*)"\n', data) - values[datatype] = value[0] + appmanifests = [] - values["game_id"] = "steam_" + values["appid"] + for open_file in os.listdir(os.path.join(steam_dir, "steamapps")): + path = os.path.join(steam_dir, "steamapps", open_file) + if os.path.isfile(path) and "appmanifest" in open_file: + appmanifests.append(path) - if ( - values["game_id"] in parent_widget.games - and not parent_widget.games[values["game_id"]].removed - ): - continue - - values["executable"] = "xdg-open steam://rungameid/" + values["appid"] - values["hidden"] = False - values["source"] = "steam" - values["added"] = current_time - values["last_played"] = 0 - - def steam_api_callback(current_file, result, values): - try: - _success, content, _etag = current_file.load_contents_finish(result) - basic_data = json.loads(content)[values["appid"]] - - if not basic_data["success"]: - steam_games[values["game_id"]]["blacklisted"] = True - else: - data = basic_data["data"] - steam_games[values["game_id"]]["developer"] = ", ".join( - data["developers"] - ) - - if data["type"] != "game": - steam_games[values["game_id"]]["blacklisted"] = True - - except GLib.GError: - pass - - queue.remove(values["appid"]) - if not queue: - import_dialog.close() - - games_no = len( - { - game_id: final_values - for game_id, final_values in steam_games.items() - if "blacklisted" not in final_values.keys() - } - ) - - if games_no == 1: - create_dialog( - parent_widget, - _("Steam Games Imported"), - _("Successfully imported 1 game."), - ) - elif games_no > 1: - create_dialog( - parent_widget, - _("Steam Games Imported"), - _("Successfully imported") - + " " - + str(games_no) - + " " - + _("games."), - ) - save_games(steam_games) - parent_widget.update_games(steam_games.keys()) - - open_file = Gio.File.new_for_uri( - "https://store.steampowered.com/api/appdetails?appids=" + values["appid"] - ) - - if not import_dialog.is_visible(): - import_dialog.show() - - queue.append(values["appid"]) - - cancellables.append(Gio.Cancellable()) - open_file.load_contents_async(cancellables[-1], steam_api_callback, values) - - GLib.timeout_add_seconds(10, timeout, cancellables[-1]) - - if os.path.isfile( - os.path.join( - steam_dir, - "appcache", - "librarycache", - values["appid"] + "_library_600x900.jpg", - ) - ): - save_cover( - values, - parent_widget, - os.path.join( - steam_dir, - "appcache", - "librarycache", - values["appid"] + "_library_600x900.jpg", - ), - ) - - steam_games[values["game_id"]] = values - - if not steam_games: - create_dialog( - parent_widget, - _("No Games Found"), - _("No new games were found in the Steam library."), - ) - - -def timeout(cancellable): - cancellable.cancel() - return False + get_games_async(parent_widget, appmanifests, steam_dir, import_dialog)