From 80aff0dfd3d8e5f9b78a1d5fbd75f2b8da210068 Mon Sep 17 00:00:00 2001 From: Christian Moser Date: Tue, 14 Jan 2025 10:43:50 +0100 Subject: [PATCH] 2025.01.14 10:43:50 --- sgbackup/game.py | 22 ++--- sgbackup/gui/_app.py | 16 +++- sgbackup/gui/_gamedialog.py | 33 +++++--- sgbackup/gui/_steam.py | 165 ++++++++++++++++++++++++++++++++++-- sgbackup/gui/appmenu.ui | 6 +- sgbackup/steam.py | 14 ++- 6 files changed, 221 insertions(+), 35 deletions(-) diff --git a/sgbackup/game.py b/sgbackup/game.py index 401ca21..7db46ed 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -1071,11 +1071,10 @@ class Game(GObject): @Property def filename(self)->str|None: - if not self.__id: - return None - if not self.__filename: - GLib.build_filename(settings.gameconf_dir,'.'.join((self.id,'gameconf'))) + if not self.__key: + return None + os.path.join(settings.gameconf_dir,'.'.join((self.key,'gameconf'))) return self.__filename @filename.setter @@ -1172,12 +1171,12 @@ class Game(GObject): def steam_linux(self)->SteamLinuxGame|None: return self.__steam_linux @steam_linux.setter - def steam_windows(self,data:SteamLinuxGame|None): + def steam_linux(self,data:SteamLinuxGame|None): if not data: self.__steam_linux = None else: if not isinstance(data,SteamLinuxGame): - raise TypeError("SteamWindowsGame") + raise TypeError("SteamLinuxGame") self.__steam_linux = data @Property @@ -1189,7 +1188,7 @@ class Game(GObject): self.__steam_macos = None else: if not isinstance(data,SteamMacOSGame): - raise TypeError("SteamWindowsGame") + raise TypeError("SteamMacOSGame") self.__steam_macos = data @Property @@ -1254,9 +1253,12 @@ class Game(GObject): return ret def save(self): - old_path = pathlib.Path(self.filename).resolve() + old_fname = self.filename + if old_fname: + old_path = pathlib.Path(self.filename).resolve() + new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve() - if (str(old_path) != str(new_path)) and old_path.is_file(): + if old_fname and (str(old_path) != str(new_path)) and old_path.is_file(): os.unlink(old_path) if not new_path.parent.is_dir(): os.makedirs(new_path.parent) @@ -1343,7 +1345,7 @@ class GameManager(GObject): def games(self)->dict[str:Game]: return self.__games - @Property(type=object) + @Property def steam_games(self)->dict[int:Game]: return self.__steam_games diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index 670ce1d..1b13c90 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -29,7 +29,7 @@ from ..settings import settings from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog from ..game import Game,GameManager -from ._steam import SteamLibrariesDialog +from ._steam import SteamLibrariesDialog,NewSteamAppsDialog __gtype_name__ = __name__ @@ -555,10 +555,14 @@ class Application(Gtk.Application): action_settings.connect('activate',self._on_action_settings) self.add_action(action_settings) - action_steam_manage_libraries = Gio.SimpleAction.new('steam-manage-libraries') + action_steam_manage_libraries = Gio.SimpleAction.new('steam-manage-libraries',None) action_steam_manage_libraries.connect('activate',self._on_action_steam_manage_libraries) self.add_action(action_steam_manage_libraries) + action_steam_new_apps = Gio.SimpleAction.new('steam-new-apps',None) + action_steam_new_apps.connect('activate',self._on_action_steam_new_apps) + self.add_action(action_steam_new_apps) + # add accels self.set_accels_for_action('app.quit',["q"]) @@ -607,6 +611,14 @@ class Application(Gtk.Application): dialog = SteamLibrariesDialog(self.appwindow) dialog.present() + def _on_action_steam_new_apps(self,action,param): + def on_dialog_response(dialog,response): + self.appwindow.refresh() + + dialog = NewSteamAppsDialog(self.appwindow) + dialog.connect('response',on_dialog_response) + dialog.present() + def new_settings_dialog(self)->SettingsDialog: """ new_settings_dialog Create a new `SettingsDialog`. diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py index 2ddf6b3..8a568ea 100644 --- a/sgbackup/gui/_gamedialog.py +++ b/sgbackup/gui/_gamedialog.py @@ -411,7 +411,7 @@ class GameDialog(Gtk.Dialog): self.__stack_sidebar.append(hbox) - self.reset() + self.__stack_sidebar.select_row(self.__stack_sidebar.get_row_at_index(0)) self.get_content_area().append(paned) @@ -420,6 +420,7 @@ class GameDialog(Gtk.Dialog): self.add_button("Cancel",Gtk.ResponseType.CANCEL) + self.reset() def __add_game_page(self): page = Gtk.ScrolledWindow() @@ -897,11 +898,17 @@ class GameDialog(Gtk.Dialog): self.__steam_macos.ignorematch.columnview.get_model().get_model().remove_all() self.__steam_macos.variables.columnview.get_model().get_model().remove_all() - if self.__game: + if self.__game is not None: self.__active_switch.set_active(self.__game.is_active) self.__live_switch.set_active(self.__game.is_live) self.__name_entry.set_text(self.__game.name) self.__sgname_entry.set_text(self.__game.savegame_name) + model = self.__savegame_type_dropdown.get_model() + for i in range(model.get_n_items()): + if model.get_item(i).savegame_type == self.__game.savegame_type: + self.__savegame_type_dropdown.set_selected(i) + break + for name,value in self.__game.variables.items(): self.__game_variables.get_model().get_model().append(GameVariableData(name,value)) @@ -953,9 +960,9 @@ class GameDialog(Gtk.Dialog): var_model.append(GameVariableData(name,value)) if self.__game.macos: - self.__macos.sgroot_entry.set_text(self.__game.linux.savegame_root) - self.__macos.sgdir_entry.set_text(self.__game.linux.savegame_dir) - self.__macos.binary_entry.set_text(self.__game.linux.binary) + self.__macos.sgroot_entry.set_text(self.__game.macos.savegame_root) + self.__macos.sgdir_entry.set_text(self.__game.macos.savegame_dir) + self.__macos.binary_entry.set_text(self.__game.macos.binary) #filematch fm_model = self.__macos.filematch.columnview.get_model().get_model() @@ -973,8 +980,8 @@ class GameDialog(Gtk.Dialog): if self.__game.steam_windows: self.__steam_windows.sgroot_entry.set_text(self.__game.steam_windows.savegame_root) self.__steam_windows.sgdir_entry.set_text(self.__game.steam_windows.savegame_dir) - self.__steam_windows.appid_entry.set_text(self.__game.steam_windows.appid) - self.__steam_windows.installdir_entry.set_text(self.__game.steam_windows.installdir) + self.__steam_windows.appid_entry.set_text(str(self.__game.steam_windows.appid)) + self.__steam_windows.installdir_entry.set_text(self.__game.steam_windows.installdir if self.__game.steam_windows.installdir else "") #filematch fm_model = self.__steam_windows.filematch.columnview.get_model().get_model() @@ -992,8 +999,8 @@ class GameDialog(Gtk.Dialog): if self.__game.steam_linux: self.__steam_linux.sgroot_entry.set_text(self.__game.steam_linux.savegame_root) self.__steam_linux.sgdir_entry.set_text(self.__game.steam_linux.savegame_dir) - self.__steam_linux.appid_entry.set_text(self.__game.steam_linux.appid) - self.__steam_linux.installdir_entry.set_text(self.__game.steam_linux.installdir) + self.__steam_linux.appid_entry.set_text(str(self.__game.steam_linux.appid)) + self.__steam_linux.installdir_entry.set_text(self.__game.steam_linux.installdir if self.__game.steam_linux.installdir else "") fm_model = self.__steam_linux.filematch.columnview.get_model().get_model() for fm in self.__game.steam_linux.file_matchers: @@ -1010,8 +1017,8 @@ class GameDialog(Gtk.Dialog): if self.__game.steam_macos: self.__steam_macos.sgroot_entry.set_text(self.__game.steam_macos.savegame_root) self.__steam_macos.sgdir_entry.set_text(self.__game.steam_macos.savegame_dir) - self.__steam_macos.appid_entry.set_text(self.__game.steam_macos.appid) - self.__steam_macos.installdir_entry.set_text(self.__game.steam_macos.installdir) + self.__steam_macos.appid_entry.set_text(str(self.__game.steam_macos.appid)) + self.__steam_macos.installdir_entry.set_text(self.__game.steam_macos.installdir if self.__game.steam_macos.installdir else "") fm_model = self.__steam_macos.filematch.columnview.get_model().get_model() for fm in self.__game.steam_macos.file_matchers: @@ -1071,14 +1078,14 @@ class GameDialog(Gtk.Dialog): return variables = {} - var_model = self.__game_variables.get_model().get_model() + var_model = self.__game_variables.columnview.get_model().get_model() for i in range(var_model.get_n_items()): var = var_model.get_item(i) variables[var.name] = var.value key = self.__key_entry.get_text() name = self.__name_entry.get_text() savegame_name = self.__sgname_entry.get_text() - savegame_type = self.__savegame_type_dropdown.get_selected_item().savegame_type() + savegame_type = self.__savegame_type_dropdown.get_selected_item().savegame_type if self.__game: self.__game.key = key self.__game.name = name diff --git a/sgbackup/gui/_steam.py b/sgbackup/gui/_steam.py index b04b0a5..94dc4b2 100644 --- a/sgbackup/gui/_steam.py +++ b/sgbackup/gui/_steam.py @@ -19,9 +19,11 @@ from gi.repository import Gtk,Gio,GLib from gi.repository.GObject import GObject,Property,Signal,BindingFlags -from ..steam import Steam,SteamLibrary -import os +import os +from ..steam import Steam,SteamLibrary,SteamApp,IgnoreSteamApp,PLATFORM_LINUX,PLATFORM_MACOS,PLATFORM_WINDOWS +from ..game import GameManager,Game,SteamLinuxGame,SteamMacOSGame,SteamWindowsGame,SavegameType +from ._gamedialog import GameDialog class SteamLibrariesDialog(Gtk.Dialog): def __init__(self,parent:Gtk.Window|None=None): @@ -186,13 +188,162 @@ class SteamLibrariesDialog(Gtk.Dialog): self.destroy() -class NewSteamAppsData(GObject): - def __init__(self,data): - GObject.__init__(self) - +class NewSteamAppSorter(Gtk.Sorter): + def do_compare(self,item1:SteamApp,item2:SteamApp): + if item1.name < item2.name: + return Gtk.Ordering.SMALLER + elif item1.name > item2.name: + return Gtk.Ordering.LARGER + else: + return Gtk.Ordering.EQUAL class NewSteamAppsDialog(Gtk.Dialog): def __init__(self,parent:Gtk.Window|None=None): Gtk.Dialog.__init__(self) + if parent: + self.set_transient_for(parent) + + self.set_title('sgbackup: New Steam apps') + self.set_default_size(800,600) self.__steam = Steam() - \ No newline at end of file + + self.__listmodel = Gio.ListStore.new(SteamApp) + for app in self.__steam.find_new_steamapps(): + self.__listmodel.append(app) + + sortmodel = Gtk.SortListModel.new(self.__listmodel,NewSteamAppSorter()) + selection = Gtk.SingleSelection.new(sortmodel) + selection.set_can_unselect(True) + selection.set_autoselect(False) + + factory = Gtk.SignalListItemFactory() + factory.connect('setup',self._on_listitem_setup) + factory.connect('bind',self._on_listitem_bind) + + self.__listview = Gtk.ListView.new(selection,factory) + self.__listview.set_vexpand(True) + self.__listview.set_show_separators(True) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_child(self.__listview) + scrolled.set_vexpand(True) + + self.get_content_area().append(scrolled) + + self.add_button("OK",Gtk.ResponseType.OK) + + def _on_listitem_setup(self,factory,item): + grid = Gtk.Grid() + grid.set_hexpand(True) + grid.set_column_spacing(5) + + vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2) + + icon = Gtk.Image.new_from_icon_name('list-add-symbolic') + grid.add_app_button = Gtk.Button() + grid.add_app_button.set_child(icon) + vbox.append(grid.add_app_button) + + icon = Gtk.Image.new_from_icon_name('edit-delete-symbolic') + grid.ignore_app_button = Gtk.Button() + grid.ignore_app_button.set_child(icon) + vbox.append(grid.ignore_app_button) + + grid.attach(vbox,3,0,1,3) + + #label = Gtk.Label.new("Name:") + #label.set_xalign(0.0) + grid.name_label = Gtk.Label() + grid.name_label.set_hexpand(True) + grid.name_label.set_xalign(0.0) + #grid.attach(label,0,0,1,1) + grid.attach(grid.name_label,1,0,1,1) + + label = Gtk.Label.new("AppID:") + label.set_xalign(0.0) + grid.appid_label = Gtk.Label() + grid.appid_label.set_hexpand(True) + grid.appid_label.set_xalign(0.0) + grid.attach(label,0,1,1,1) + grid.attach(grid.appid_label,1,1,1,1) + + label = Gtk.Label.new("Installdir:") + label.set_xalign(0.0) + grid.installdir_label = Gtk.Label() + grid.installdir_label.set_hexpand(True) + grid.installdir_label.set_xalign(0.0) + grid.attach(label,0,2,1,1) + grid.attach(grid.installdir_label,1,2,1,1) + + item.set_child(grid) + + def _on_listitem_bind(self,factory,item): + child = item.get_child() + data = item.get_item() + + child.name_label.set_markup("{}".format(GLib.markup_escape_text(data.name))) + child.appid_label.set_text(str(data.appid)) + child.installdir_label.set_text(data.installdir) + child.add_app_button.connect('clicked',self._on_add_steamapp_button_clicked,data) + child.ignore_app_button.connect('clicked',self._on_ignore_steamapp_button_clicked,data) + + def _on_add_steamapp_button_clicked(self,button,data:SteamApp,*args): + def on_dialog_response(dialog,response): + if response == Gtk.ResponseType.APPLY: + for i in range(self.__listmodel.get_n_items()): + if data.appid == self.__listmodel.get_item(i): + self.__listmodel.remove(i) + break + + game = Game("Enter key",data.name,"") + if PLATFORM_WINDOWS: + game.steam_windows = SteamWindowsGame(data.appid,"","",installdir=data.installdir) + game.steam_linux = SteamLinuxGame(data.appid,"","") + game.steam_macos = SteamMacOSGame(data.appid,"","") + game.savegame_type = SavegameType.STEAM_WINDOWS + elif PLATFORM_LINUX: + game.steam_linux = SteamLinuxGame(data.appid,"","",installdir=data.installdir) + game.steam_windows = SteamWindowsGame(data.appid,"","") + game.steam_macos = SteamMacOSGame(data.appid,"","") + game.savegame_type = SavegameType.STEAM_LINUX + elif PLATFORM_MACOS: + game.steam_macos = SteamMacOSGame(data.appid,"","",installdir=data.installdir) + game.steam_windows = SteamWindowsGame(data.appid,"","") + game.steam_macos = SteamMacOSGame(data.appid,"","") + game.savegame_type = SavegameType.STEAM_MACOS + + dialog = GameDialog(self,game) + dialog.set_title("sgbackup: Add Steam Game") + dialog.connect('response',on_dialog_response) + dialog.present() + + def _on_ignore_steamapp_button_clicked(self,button,data,*args): + def on_dialog_response(dialog,response,data): + if response == Gtk.ResponseType.YES: + ignore = IgnoreSteamApp(data.appid,data.name,dialog.reason_entry.get_text()) + self.__steam.add_ignore_app(ignore) + for i in range(self.__listmodel.get_n_items()): + if data.appid == self.__listmodel.get_item(i).appid: + self.__listmodel.remove(i) + break + dialog.hide() + dialog.destroy() + + dialog = Gtk.MessageDialog(buttons=Gtk.ButtonsType.YES_NO) + dialog.set_transient_for(self) + dialog.set_modal(True) + dialog.props.text = "Do you want to put \"{steamapp}\" on the ignore list?".format(steamapp=data.name) + dialog.props.use_markup = True + + dialog.props.secondary_text = "Please enter the reason for ignoring this app." + dialog.reason_entry = Gtk.Entry() + dialog.reason_entry.set_hexpand(True) + dialog.get_content_area().append(dialog.reason_entry) + + dialog.connect('response',on_dialog_response,data) + dialog.present() + + def do_response(self,response): + self.hide() + self.destroy() + \ No newline at end of file diff --git a/sgbackup/gui/appmenu.ui b/sgbackup/gui/appmenu.ui index be19648..1488f0a 100644 --- a/sgbackup/gui/appmenu.ui +++ b/sgbackup/gui/appmenu.ui @@ -14,9 +14,13 @@ _Steam
- Manage Libraries + Manage libraries app.steam-manage-libraries + + Manage new apps + app.steam-new-apps +
diff --git a/sgbackup/steam.py b/sgbackup/steam.py index ec72a29..cc2d4bb 100644 --- a/sgbackup/steam.py +++ b/sgbackup/steam.py @@ -78,7 +78,7 @@ class AcfFileParser(object): if not os.path.isfile(acf_file): raise FileNotFoundError("File \"{s}\" does not exist!") - with open(acf_file,'r') as ifile: + with open(acf_file,'rt',encoding="utf-8") as ifile: buffer = ifile.read() lines = [l.strip() for l in buffer.split('\n')] if lines[0] == "\"AppState\"" and lines[1] == "{": @@ -281,6 +281,16 @@ class Steam(GObject): @Property def ignore_apps(self)->dict[int:IgnoreSteamApp]: return self.__ignore_apps + @ignore_apps.setter + def ignore_apps(self,apps:dict[int:IgnoreSteamApp]|list[IgnoreSteamApp]|tuple[IgnoreSteamApp]): + if isinstance(apps,dict): + self.__ignore_apps = apps + elif isinstance(apps,list) or isinstance(apps,tuple): + self.__ignore_apps = {} + for app in apps: + self.__ignore_apps[app.appid] = app + else: + raise TypeError("illegal ignore_apps type!") def __write_steamlib_list_file(self): with open(self.steamlib_list_file,'wt',encoding='utf-8') as ofile: @@ -340,7 +350,7 @@ class Steam(GObject): new_apps = [] for lib in self.libraries: for app in lib.steam_apps: - if not app.appid in GameManager.steam_games and not app.appid in self.ignore_apps: + if not app.appid in GameManager.get_global().steam_games and not app.appid in self.ignore_apps: new_apps.append(app) return sorted(new_apps)