diff --git a/PO/SOURCES b/PO/SOURCES index 0b875d5..f47f8c9 100644 --- a/PO/SOURCES +++ b/PO/SOURCES @@ -7,7 +7,9 @@ sgbackup/commands/help.py sgbackup/commands/__init__.py sgbackup/curses/__init__.py sgbackup/curses/__main__.py +sgbackup/epic.py sgbackup/game.py +sgbackup/gog.py sgbackup/gui/_app.py sgbackup/gui/_backupdialog.py sgbackup/gui/_dialogs.py diff --git a/PO/de.po b/PO/de.po index 2ceeb28..86f26cd 100644 --- a/PO/de.po +++ b/PO/de.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-03-05 10:15+0100\n" -"PO-Revision-Date: 2025-03-05 10:19+0100\n" +"POT-Creation-Date: 2025-03-11 02:01+0100\n" +"PO-Revision-Date: 2025-03-11 15:31+0100\n" "Last-Translator: Christian Moser \n" "Language-Team: \n" "Language: de\n" @@ -18,54 +18,59 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.5\n" -#: sgbackup/gui/_app.py:224 +#: sgbackup/epic.py:162 +#, python-brace-format +msgid "Unable to load Epic manifest \"{manifest}\"! ({error})" +msgstr "Kann Epic Manifest \"{manifest}\" nicht laden! ({error})" + +#: sgbackup/gui/_app.py:228 msgid "Add a new game." msgstr "Neues Spiel hinzufügen." -#: sgbackup/gui/_app.py:232 +#: sgbackup/gui/_app.py:236 msgid "Manage new Steam-Apps." msgstr "Neue Steam-Apps verwalten." -#: sgbackup/gui/_app.py:242 +#: sgbackup/gui/_app.py:246 msgid "Backup all active and live Games." msgstr "Alle aktiven und live Spiele sichern." -#: sgbackup/gui/_app.py:276 +#: sgbackup/gui/_app.py:280 msgid "Key" msgstr "Schlüssel" -#: sgbackup/gui/_app.py:282 +#: sgbackup/gui/_app.py:286 msgid "Name" msgstr "Name" -#: sgbackup/gui/_app.py:290 +#: sgbackup/gui/_app.py:294 msgid "Active" msgstr "Aktiv" -#: sgbackup/gui/_app.py:296 sgbackup/gui/_app.py:864 +#: sgbackup/gui/_app.py:300 sgbackup/gui/_app.py:868 msgid "Live" msgstr "Live" -#: sgbackup/gui/_app.py:455 +#: sgbackup/gui/_app.py:459 msgid "No icon-name for sgtype {}" msgstr "Kein Icon-Name für sgtype {}" -#: sgbackup/gui/_app.py:543 +#: sgbackup/gui/_app.py:547 #, python-brace-format msgid "Do you want to create a new savegame for {game}?" msgstr "Willst du ein neues SaveGame für {game} erstellen?" -#: sgbackup/gui/_app.py:545 +#: sgbackup/gui/_app.py:549 msgid "The new savegame is added to the finsihed savegames for the game." msgstr "" "Das Neue SaveGame wird zu den beendeten Speicherständen für dieses Spiel " "hinzugefügt." -#: sgbackup/gui/_app.py:557 +#: sgbackup/gui/_app.py:561 msgid "Backup the SaveGames for this game." msgstr "Sichere das SaveGame für dieses Spiel." -#: sgbackup/gui/_app.py:569 +#: sgbackup/gui/_app.py:573 msgid "" "Remove this game.\n" "This also deletes the game configuration file!!!Dies wird auch die Konfigurationsdatei des Spiels " "löschen!!!" -#: sgbackup/gui/_app.py:663 +#: sgbackup/gui/_app.py:667 #, python-brace-format msgid "" "Do you really want to remove the game {game}?" msgstr "Wills du wirklich das Spiel {game} löschen?" -#: sgbackup/gui/_app.py:666 +#: sgbackup/gui/_app.py:670 msgid "Removing games cannot be undone!!!" msgstr "Das Löschen von Spielen kann nicht Rückgängig gemacht werden!!!" -#: sgbackup/gui/_app.py:859 +#: sgbackup/gui/_app.py:863 msgid "OS" msgstr "OS" -#: sgbackup/gui/_app.py:869 +#: sgbackup/gui/_app.py:873 msgid "Savegame name" msgstr "Speicherstandname" -#: sgbackup/gui/_app.py:875 +#: sgbackup/gui/_app.py:879 msgid "Timestamp" msgstr "Zeitstempel" -#: sgbackup/gui/_app.py:880 +#: sgbackup/gui/_app.py:884 msgid "Size" msgstr "Größe" -#: sgbackup/gui/_app.py:1066 +#: sgbackup/gui/_app.py:1072 msgid "%m.%d.%Y %H:%M:%S" msgstr "%d.%m.%Y %H:%M:%S" -#: sgbackup/gui/_app.py:1099 +#: sgbackup/gui/_app.py:1105 msgid "Restore the SaveGameBackup." msgstr "Stelle das Speicherstandbackup wieder her." -#: sgbackup/gui/_app.py:1104 +#: sgbackup/gui/_app.py:1110 msgid "Convert to another SaveGameBackup." -msgstr "Konvertiere zu anderen Speicherstandbackup" +msgstr "Konvertiere zu anderen Speicherstandbackup." -#: sgbackup/gui/_app.py:1113 +#: sgbackup/gui/_app.py:1119 msgid "Delete this SaveGameBackup." msgstr "Lösche dieses Speicherstandbackup." -#: sgbackup/gui/_app.py:1170 +#: sgbackup/gui/_app.py:1176 msgid "SGBackup" msgstr "SGBackup" -#: sgbackup/gui/_app.py:1232 sgbackup/gui/_app.py:1300 +#: sgbackup/gui/_app.py:1238 sgbackup/gui/_app.py:1307 #, python-brace-format msgid "" "{games} Games -- {active} Games active -- {live} Games live -- {finished} " "Games finished" msgstr "" -"{games} Spile -- {active} Spiele aktiv -- {live} Spiele live -- {finished} " +"{games} Spiele -- {active} Spiele aktiv -- {live} Spiele live -- {finished} " "Spiele beendet" -#: sgbackup/gui/appmenu.ui:6 -msgid "_Game" -msgstr "_Spiel" +#: sgbackup/gui/_dialogs.py:38 +msgid "There are no games to backup!" +msgstr "Es gibt keine Spiele zu sichern!" -#: sgbackup/gui/appmenu.ui:9 +#: sgbackup/gui/_dialogs.py:53 +msgid "There were no games to backup found!" +msgstr "Es wurden keine Spiele zum Sichern gefunden!" + +#: sgbackup/gui/_gamedialog.py:248 +msgid "Name:" +msgstr "Name:" + +#: sgbackup/gui/_gamedialog.py:256 +msgid "Value:" +msgstr "Wert:" + +#: sgbackup/gui/_gamedialog.py:332 +msgid "Filename" +msgstr "Dateiname" + +#: sgbackup/gui/_gamedialog.py:333 +msgid "Glob" +msgstr "Glob" + +#: sgbackup/gui/_gamedialog.py:334 +msgid "Regular expression" +msgstr "Regulärer Ausdruck" + +#: sgbackup/gui/_gamedialog.py:403 sgbackup/gui/_gamedialog.py:531 +#: sgbackup/gui/_gamedialog.py:688 sgbackup/gui/_gamedialog.py:767 +msgid "Windows" +msgstr "Windows" + +#: sgbackup/gui/_gamedialog.py:404 +msgid "Steam Windows" +msgstr "Steam Windows" + +#: sgbackup/gui/_gamedialog.py:405 +msgid "Epic Windows" +msgstr "Epic Windows" + +#. (SavegameType.GOG_WINDOWS,"GoG Windows","object-select-symbolic"), +#: sgbackup/gui/_gamedialog.py:408 +msgid "Linux native" +msgstr "Linux" + +#: sgbackup/gui/_gamedialog.py:409 +msgid "Steam Linux" +msgstr "Steam Linux" + +#. (SavegameType.EPIC_LINUX,_("Epic Linux"),"epic-games-svgrepo-com-symbolic"), +#. (SavegameType.GOG_LINUX,"GoG Linux","object-select-symbolic"), +#: sgbackup/gui/_gamedialog.py:413 sgbackup/gui/_gamedialog.py:618 +msgid "MacOS" +msgstr "Mac OS" + +#: sgbackup/gui/_gamedialog.py:414 +msgid "Steam MacOS" +msgstr "Steam Mac OS" + +#: sgbackup/gui/_gamedialog.py:422 +msgid "Is active?" +msgstr "Ist Aktiv?" + +#: sgbackup/gui/_gamedialog.py:429 +msgid "Is live?" +msgstr "Ist live?" + +#: sgbackup/gui/_gamedialog.py:437 +msgid "ID:" +msgstr "ID:" + +#: sgbackup/gui/_gamedialog.py:443 +msgid "Key:" +msgstr "Schlüssel:" + +#: sgbackup/gui/_gamedialog.py:458 +msgid "Savegame Type:" +msgstr "Speicherstandtyp:" + +#: sgbackup/gui/_gamedialog.py:462 +msgid "Game name:" +msgstr "Spielname:" + +#: sgbackup/gui/_gamedialog.py:469 +msgid "Savegame name:" +msgstr "Speicherstandname:" + +#: sgbackup/gui/_gamedialog.py:482 +msgid "Game" +msgstr "Spiel" + +#: sgbackup/gui/_gamedialog.py:494 sgbackup/gui/_gamedialog.py:544 +#: sgbackup/gui/_gamedialog.py:587 sgbackup/gui/_gamedialog.py:638 +#: sgbackup/gui/_gamedialog.py:718 +msgid "Root directory:" +msgstr "Wurzelverzeichnis:" + +#: sgbackup/gui/_gamedialog.py:501 sgbackup/gui/_gamedialog.py:551 +#: sgbackup/gui/_gamedialog.py:594 sgbackup/gui/_gamedialog.py:645 +#: sgbackup/gui/_gamedialog.py:725 +msgid "Backup directory:" +msgstr "Speicherverzeichnis:" + +#: sgbackup/gui/_gamedialog.py:515 +msgid "Match Files" +msgstr "Dateiübereinstimmung" + +#: sgbackup/gui/_gamedialog.py:518 +msgid "Ignore Files" +msgstr "Ignoriere Dateien" + +#: sgbackup/gui/_gamedialog.py:521 +msgid "Lookup Registry keys" +msgstr "Registrierungsschlüssel nachschlagen" + +#: sgbackup/gui/_gamedialog.py:524 +msgid "Installations directory Registry keys" +msgstr "Registrierungsschlüssel für Installationsverzeichnis" + +#: sgbackup/gui/_gamedialog.py:558 +msgid "Executable:" +msgstr "Ausführbare Datei:" + +#: sgbackup/gui/_gamedialog.py:575 sgbackup/gui/_gamedialog.py:691 +msgid "Linux" +msgstr "Linux" + +#: sgbackup/gui/_gamedialog.py:601 +msgid "Executable" +msgstr "Ausführbare Datei" + +#: sgbackup/gui/_gamedialog.py:652 sgbackup/gui/_gamedialog.py:732 +msgid "Installation directory:" +msgstr "Installationsverzeichnis:" + +#: sgbackup/gui/_gamedialog.py:694 +msgid "Mac OS" +msgstr "Mac OS" + +#: sgbackup/gui/_gamedialog.py:756 +msgid "AppName:" +msgstr "AppName:" + +#: sgbackup/gui/_steam.py:60 +msgid "Steam libraries" +msgstr "Steam Bibliotheken" + +#: sgbackup/gui/_steam.py:82 +msgid "Apply" +msgstr "Anwenden" + +#: sgbackup/gui/_steam.py:83 +msgid "Cancel" +msgstr "Abbrechen" + +#: sgbackup/gui/_steam.py:101 +msgid "SGBackup: Select Steam library path" +msgstr "SGBackup: Wähle Steam Bibliothekspfad" + +#: sgbackup/gui/_steam.py:130 +msgid "SGBackup: Change Steam library path" +msgstr "SGBackup: Ändere Steam Bibliothekspfad" + +#: sgbackup/gui/_steam.py:216 +msgid "SGBackup: New Steam apps" +msgstr "Neue Steam-Apps" + +#: sgbackup/gui/_steam.py:286 +msgid "Add SteamApp as new Game." +msgstr "Füge Steam-App als neues Spiel hinzu." + +#: sgbackup/gui/_steam.py:293 +msgid "Add SteamApp to ignore list." +msgstr "Füge Steam-App zur Ignorieren-Liste hinzu." + +#: sgbackup/gui/_steam.py:300 +msgid "Lookup SteamApp for already registered game." +msgstr "Schlage Steam-App unter den bereits registrierten Spielen nach." + +#: sgbackup/gui/_steam.py:307 +msgid "Lookup SteamApp online." +msgstr "Suche Steam-App online." + +#: sgbackup/gui/_steam.py:366 +msgid "SGBackup: Add Steam Game" +msgstr "SGBackup: Füge Steam-Spiel hinzu" + +#: sgbackup/gui/_steam.py:386 +#, python-brace-format +msgid "" +"Do you want to put \"{steamapp}\" on the ignore " +"list?" +msgstr "" +"Wills du wirklich das Spiel {steamapp} auf die " +"Ignorieren-Liste setzen?" + +#: sgbackup/gui/_steam.py:390 +msgid "Please enter the reason for ignoring this app." +msgstr "Bitte gib eine Begründing für das Ignorieren dieser App ein." + +#: sgbackup/gui/_steam.py:413 +msgid "There were no new Steam-Apps found!" +msgstr "Es wurden keine Steam-Apps gefunden!" + +#: sgbackup/gui/_steam.py:426 +msgid "There are no Steam-Apps that are ignored!" +msgstr "Es gibt keine Steam-Apps die ignoriert werden!" + +#: sgbackup/gui/_steam.py:449 +msgid "SGBackup: manage ignored SteamApps" +msgstr "SGBackup: Verwalte ignorierte Steam-Apps" + +#: sgbackup/gui/_steam.py:480 +msgid "Close" +msgstr "Schießen" + +#: sgbackup/gui/_steam.py:489 +msgid "Reason:" +msgstr "Begründung:" + +#: sgbackup/gui/appmenu.ui:6 msgid "_Add Game" msgstr "Spiel _hinzufügen" -#: sgbackup/gui/appmenu.ui:14 +#: sgbackup/gui/appmenu.ui:12 +msgid "_Backup active & live games" +msgstr "Aktive- & Live-Spiele sichern" + +#: sgbackup/gui/appmenu.ui:16 +msgid "Backup _all games" +msgstr "_Alle Spiele sichern" + +#: sgbackup/gui/appmenu.ui:22 msgid "_Steam" msgstr "_Steam" -#: sgbackup/gui/appmenu.ui:17 +#: sgbackup/gui/appmenu.ui:25 msgid "New Steam Apps" msgstr "Neue Steam-App" -#: sgbackup/gui/appmenu.ui:21 +#: sgbackup/gui/appmenu.ui:29 msgid "Manage Steam Libraries" msgstr "Steam Bibliotheken verwalten" -#: sgbackup/gui/appmenu.ui:27 +#: sgbackup/gui/appmenu.ui:33 +msgid "Manage Ignored Apps" +msgstr "Ignorierte Steam-Apps verwalten" + +#: sgbackup/gui/appmenu.ui:39 msgid "_Epic" msgstr "_Epic" -#: sgbackup/gui/appmenu.ui:30 +#: sgbackup/gui/appmenu.ui:42 msgid "_GoG" msgstr "_GoG" -#: sgbackup/gui/appmenu.ui:35 +#: sgbackup/gui/appmenu.ui:47 msgid "_Settings" msgstr "_Einstellungen" -#: sgbackup/gui/appmenu.ui:42 +#: sgbackup/gui/appmenu.ui:54 msgid "_Help" msgstr "_Hilfe" -#: sgbackup/gui/appmenu.ui:47 +#: sgbackup/gui/appmenu.ui:59 msgid "_About SGBackup" msgstr "_Über SGBackup" -#: sgbackup/gui/appmenu.ui:56 +#: sgbackup/gui/appmenu.ui:68 msgid "_Quit" msgstr "_Beenden" -#: sgbackup/gui/appmenu.ui:65 +#: sgbackup/gui/appmenu.ui:77 msgid "Convert to _Windows" msgstr "Nach _Windows konvertieren" -#: sgbackup/gui/appmenu.ui:69 +#: sgbackup/gui/appmenu.ui:81 msgid "Convert to _Linux" msgstr "Nach _Linux konvertieren" -#: sgbackup/gui/appmenu.ui:73 +#: sgbackup/gui/appmenu.ui:85 msgid "Convert to _Mac OS" msgstr "Nach _MacOS konvertieren" -#: sgbackup/gui/appmenu.ui:77 +#: sgbackup/gui/appmenu.ui:89 msgid "Convert to _Steam Windows" msgstr "Nach _Steam-Windows konvertieren" -#: sgbackup/gui/appmenu.ui:81 +#: sgbackup/gui/appmenu.ui:93 msgid "Convert to Steam Linux" msgstr "Nach _Steam-Linux konvertieren" -#: sgbackup/gui/appmenu.ui:85 +#: sgbackup/gui/appmenu.ui:97 msgid "Convert to Steam Mac OS" msgstr "Nach _Steam-MacOS konvertieren" diff --git a/sgbackup/epic.py b/sgbackup/epic.py index e60bf44..49d419e 100644 --- a/sgbackup/epic.py +++ b/sgbackup/epic.py @@ -159,7 +159,7 @@ class Epic(GObject): with open(filename,'r',encoding="utf-8") as ifile: data = json.loads(ifile.read()) except Exception as ex: - self._logger.error(_("Unable ot load Epic manifest \"{manifest}\"! ({error})").format( + self._logger.error(_("Unable to load Epic manifest \"{manifest}\"! ({error})").format( manifest=filename, error=str(ex) )) diff --git a/sgbackup/game.py b/sgbackup/game.py index 0177e5a..87018d4 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -152,7 +152,7 @@ VALID_SAVEGAME_TYPES = [ SavegameType.STEAM_MACOS, SavegameType.STEAM_WINDOWS, #SavegameType.EPIC_LINUX, - #SavegameType.EPIC_WINDOWS, + SavegameType.EPIC_WINDOWS, #SavegameType.GOG_LINUX, #SavegameType.GOG_WINDOWS, ] @@ -160,8 +160,8 @@ VALID_SAVEGAME_TYPES = [ SAVEGAME_TYPE_ICONS = { SavegameType.UNSET : None, SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic', - SavegameType.LINUX: 'linux-svgrepo-com-symbolic.svg', - SavegameType.MACOS: 'apple-svgrepo-com-symbolic.svg', + SavegameType.LINUX: 'linux-svgrepo-com-symbolic', + SavegameType.MACOS: 'apple-svgrepo-com-symbolic', SavegameType.STEAM_LINUX: 'steam-svgrepo-com-symbolic', SavegameType.STEAM_MACOS: 'steam-svgrepo-com-symbolic', SavegameType.STEAM_WINDOWS: 'steam-svgrepo-com-symbolic', @@ -171,6 +171,23 @@ SAVEGAME_TYPE_ICONS = { SavegameType.GOG_WINDOWS: 'gog-com-svgrepo-com-symbolic', } +class GameProvider(StrEnum): + WINDOWS = "windows" + LINUX = "linux" + MACOS = "macos" + STEAM = "steam" + EPIC_GAMES = "epic-games" + GOG = "gog" + +GAME_PROVIDER_ICONS = { + GameProvider.WINDOWS: 'windows-svgrepo-com-symbolic', + GameProvider.LINUX: 'liux-svgrepo-com-symbolic', + GameProvider.MACOS: 'apple-svgrepo-com-symbolic', + GameProvider.STEAM: 'steam-svgrepo-com-symbolic', + GameProvider.EPIC_GAMES: 'epic-games-svgrepo-com-symbolic', + GameProvider.GOG: 'gog-com-svgrepo-com-symbolic', +} + class GameFileType(StrEnum): """ GameFileType The file matcher type for `GameFileMatcher`. @@ -1784,9 +1801,7 @@ class GameManager(GObject): self.__games = {} self.__steam_games = {} - self.__steam_linux_games = {} - self.__steam_windows_games = {} - self.__steam_macos_games = {} + self.__epic_games = {} self.load() @@ -1798,18 +1813,6 @@ class GameManager(GObject): def steam_games(self)->dict[int:Game]: return self.__steam_games - @Property(type=object) - def steam_windows_games(self)->dict[int:Game]: - return self.__steam_windows_games - - @Property(type=object) - def steam_linux_games(self)->dict[int:Game]: - return self.__steam_linux_games - - @Property(type=object) - def steam_macos_games(self)->dict[int:Game]: - return self.__steam_macos_games - def load(self): if self.__games: self.__games = {} @@ -1838,13 +1841,30 @@ class GameManager(GObject): def add_game(self,game:Game): self.__games[game.key] = game - if (game.steam_macos): - self.__steam_games[game.steam_macos.appid] = game - self.__steam_macos_games[game.steam_macos.appid] = game - if (game.steam_linux): - self.__steam_games[game.steam_linux.appid] = game - self.__steam_linux_games[game.steam_linux.appid] = game - if (game.steam_windows): - self.__steam_games[game.steam_windows.appid] = game - self.__steam_windows_games[game.steam_windows.appid] = game + if game.steam: + self.__steam_games[game.steam.appid] = game + if game.epic: + self.__epic_games[game.epic.appname] = game + def remove_game(self,game:Game|str): + if isinstance(game,str): + if key not in self.__games: + return + key = game + game = self.__games[key] + elif isinstance(game,Game): + if game.key not in self.__games: + return + key = game.key + + + for appid,steam_game in list(self.__steam_games.items()): + if steam_game.key == game.key: + del self.__steam_games[appid] + + for appname,epic_game in list(self.__epic_games.items()): + if epic_game.key == game.key: + del self.__epic_games[appname] + + del self.__games[key] + diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index a86473f..72850ed 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -31,7 +31,14 @@ from pathlib import Path from ..settings import settings from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog -from ..game import Game,GameManager,SAVEGAME_TYPE_ICONS,SavegameType +from ..game import ( + Game, + GameManager, + SAVEGAME_TYPE_ICONS, + SavegameType, + GAME_PROVIDER_ICONS, + GameProvider +) from ._steam import ( SteamLibrariesDialog, NewSteamAppsDialog, @@ -223,20 +230,28 @@ class GameView(Gtk.Box): icon = Gtk.Image.new_from_icon_name('list-add-symbolic') icon.set_pixel_size(16) - add_game_button=Gtk.Button() + add_game_button = Gtk.Button() add_game_button.set_child(icon) add_game_button.set_tooltip_text(_("Add a new game.")) add_game_button.connect('clicked',self._on_add_game_button_clicked) self.actionbar.pack_start(add_game_button) - icon = Gtk.Image.new_from_icon_name('steam-svgrepo-com-symbolic') + icon = Gtk.Image.new_from_icon_name(GAME_PROVIDER_ICONS[GameProvider.STEAM]) icon.set_pixel_size(16) - new_steam_games_button=Gtk.Button() + new_steam_games_button = Gtk.Button() new_steam_games_button.set_child(icon) new_steam_games_button.set_tooltip_text(_("Manage new Steam-Apps.")) new_steam_games_button.connect('clicked',self._on_new_steam_games_button_clicked) self.actionbar.pack_start(new_steam_games_button) + icon = Gtk.Image.new_from_icon_name(GAME_PROVIDER_ICONS[GameProvider.EPIC_GAMES]) + icon.set_pixel_size(16) + new_epic_games_button = Gtk.Button() + new_epic_games_button.set_child(icon) + new_epic_games_button.set_tooltip_text(_("Manage new Epic-Games apps")) + new_epic_games_button.connect('clicked',self._on_new_epic_games_button_clicked) + self.actionbar.pack_start(new_epic_games_button) + self.actionbar.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) icon = Gtk.Image.new_from_icon_name('document-save-symbolic') @@ -395,6 +410,10 @@ class GameView(Gtk.Box): dialog = SteamNoNewAppsDialog(parent=self.get_root()) dialog.present() + def _on_new_epic_games_button_clicked(self,button): + # TODO + pass + def _on_backup_active_live_button_clicked(self,button): backup_games = [] for i in range(self._liststore.get_n_items()): @@ -1279,11 +1298,12 @@ class AppWindow(Gtk.ApplicationWindow): def statusbar(self): return self.__statusbar - def refresh(self): + def refresh(self,reload_game_manager:bool=False): """ refresh Refresh the views of this window. """ - GameManager.get_global().load() + if reload_game_manager: + GameManager.get_global().load() self.gameview.refresh() #self.backupview.refresh() @@ -1510,7 +1530,7 @@ class Application(Gtk.Application): dialog.connect_after('response',on_dialog_response) dialog.present() else: - dialog = NoNewSteamAppsDialog(self.appwindow) + dialog = SteamNoNewAppsDialog(self.appwindow) dialog.present() diff --git a/sgbackup/gui/_dialogs.py b/sgbackup/gui/_dialogs.py index 95a7b2a..7666ed6 100644 --- a/sgbackup/gui/_dialogs.py +++ b/sgbackup/gui/_dialogs.py @@ -1,6 +1,6 @@ ############################################################################### # sgbackup - The SaveGame Backup tool # -# Copyright (C) 2024,2025 Christian Moser # +# Copyright (C) 2024,2025 Christian Moser # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # @@ -17,8 +17,13 @@ ############################################################################### from gi.repository import Gtk,GLib,Gio +from gi.repository.GObject import GObject,Property + from ..version import VERSION from ..i18n import gettext as _ + + + class AboutDialog(Gtk.AboutDialog): def __init__(self): Gtk.AboutDialog.__init__(self) @@ -60,4 +65,5 @@ class NoGamesToBackupFoundDialog(Gtk.MessageDialog): def do_response(self,response): self.hide() self.destroy() - \ No newline at end of file + + diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py index d8cf4b7..96e3d91 100644 --- a/sgbackup/gui/_gamedialog.py +++ b/sgbackup/gui/_gamedialog.py @@ -1,6 +1,6 @@ ############################################################################### # sgbackup - The SaveGame Backup tool # -# Copyright (C) 2024,2025 Christian Moser # +# Copyright (C) 2024,2025 Christian Moser # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # @@ -20,7 +20,7 @@ from .. import _import_gtk from gi.repository import Gio,GLib,Gtk,Pango from gi.repository.GObject import Property,Signal,GObject,BindingFlags,SignalFlags - +import rapidfuzz from ..i18n import gettext as _, gettext_noop as N_ from ..game import ( @@ -39,6 +39,8 @@ from ..game import ( EpicGameData, EpicWindowsData, GameManager, + GameProvider, + GAME_PROVIDER_ICONS, ) @@ -1080,6 +1082,9 @@ class GameDialog(Gtk.Dialog): savegame_name = self.__sgname_entry.get_text() savegame_type = self.__savegame_type_dropdown.get_selected_item().savegame_type if self.has_game: + if self.__game.key != key: + GameManager.get_global().remove_game(self.__game) + self.__game.key = key self.__game.name = name self.__game.savegame_type = savegame_type @@ -1494,3 +1499,274 @@ class GameDialog(Gtk.Dialog): @Signal(name='apply',flags=SignalFlags.RUN_FIRST,return_type=None,arg_types=()) def do_apply(self): pass + +### GameSearchDialog ########################################################## + +class GameSearchDialogData(GObject): + def __init__(self,game:Game,fuzzy_match:float=0.0): + GObject.__init__(self) + self.__game = game + self.__fuzzy_match = fuzzy_match + + @Property + def game(self)->Game: + return self.__game + + @Property(type=float) + def fuzzy_match(self)->float: + return self.__fuzzy_match + + @fuzzy_match.setter + def fuzzy_match(self,match:float): + self.__fuzzy_match = match + + +class GameSearchDialogDataSorter(Gtk.Sorter): + def do_compare(self,item1:GameSearchDialogData,item2:GameSearchDialogData): + if (item1.fuzzy_match > item2.fuzzy_match): + return Gtk.Ordering.SMALLER + elif (item1.fuzzy_match < item2.fuzzy_match): + return Gtk.Ordering.LARGER + + name1 = item1.game.name.lower() + name2 = item2.game.name.lower() + + if (name1 > name2): + return Gtk.Ordering.LARGER + elif (name1 < name2): + return Gtk.Ordering.SMALLER + return Gtk.Ordering.EQUAL + +class GameSearchDialogDataNameSorter(Gtk.Sorter): + def do_compare(self,item1:GameSearchDialogData,item2:GameSearchDialogData): + name1 = item1.game.name.lower() + name2 = item2.game.name.lower() + + if (name1 > name2): + return Gtk.Ordering.LARGER + elif (name1 < name2): + return Gtk.Ordering.SMALLER + return Gtk.Ordering.EQUAL + + +class GameSearchDialogDataFilter(Gtk.Filter): + def do_match(self,item:GameSearchDialogData): + return (item.fuzzy_match > 0.0) + + +class GameSearchDialog(Gtk.Dialog): + def __init__(self, + parent:Gtk.Window|None=None, + search_name:str|None=None, + title:str|None=None): + Gtk.Dialog.__init__(self) + self.set_title("SGBackup: {title}".format( + title=title if title else _("Search for games") + )) + + if parent: + self.set_transient_for(parent) + + self.search_name = search_name + + self.__actionbar = Gtk.ActionBar() + new_game_button = Gtk.Button() + icon = Gtk.Image.new_from_icon_name('list-add-symbolic') + icon.set_pixel_size(16) + new_game_button.set_child(icon) + new_game_button.connect('clicked',self._on_new_game_button_clicked) + self.__actionbar.pack_start(new_game_button) + + self.__action_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,2) + label = Gtk.Label.new(_("Enable Search?")) + self.__search_switch = Gtk.Switch() + self.__search_switch.set_active(bool(self.search_name)) + self.__search_switch.set_sensitive(bool(self.search_name)) + + self.__action_hbox.append(label) + self.__action_hbox.append(self.__search_switch) + self.__actionbar.pack_end(self.__action_hbox) + + self.get_content_area().append(self.__actionbar) + + scrolled = Gtk.ScrolledWindow() + self.__liststore = Gio.ListStore() + for i in self.__get_search_games(search_name): + self.__liststore.append(GameSearchDialogData(**i)) + self.__sort_model = Gtk.SortListModel(model=self.__liststore,sorter=GameSearchDialogDataSorter()) + self.__filter_model = Gtk.FilterListModel(model=self.__sort_model, + filter=GameSearchDialogDataFilter() if search_name else None) + selection = Gtk.SingleSelection(model=self.__filter_model,autoselect=False,can_unselect=True) + + factory = Gtk.SignalListItemFactory() + factory.connect('setup',self._on_listview_setup) + factory.connect('bind',self._on_listview_bind) + + self.__listview = Gtk.ListView(model=selection,factory=factory) + scrolled.set_child(self.__listview) + + self.get_content_area().append(scrolled) + + self.add_button("Close",Gtk.ResponseType.OK) + + def __get_search_games(self,search_name:str|None)->list: + games=[] + game_names=[] + + gm = GameManager.get_global() + for g in gm.games.values(): + games.append({'game':g,'fuzzy_match':0.0}) + game_names.append(g.name) + + if search_name: + result = rapidfuzz.process.extract( + query=search_name, + choices=game_names, + limit=20, + scorer=rapidfuzz.fuzz.WRatio) + + for choice,score,index in result: + games[index]['fuzzy_match']=score + + return games + + def _on_listview_setup(self,factory,item): + child = Gtk.Grid() + child.set_column_spacing(4) + child.set_row_spacing(2) + + child.name_label=Gtk.Label() + child.name_label.set_xalign(0.0) + child.name_label.set_hexpand(True) + child.attach(child.name_label,1,0,1,1) + + label = Gtk.Label.new(_("Key:")) + label.set_xalign(0.0) + child.key_label = Gtk.Label() + child.key_label.set_xalign(0.0) + child.key_label.set_hexpand(True) + child.attach(label,0,1,1,1) + child.attach(child.key_label,1,1,1,1) + + label = Gtk.Label.new(_("Platform:")) + label.set_xalign(0.0) + child.platform_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,4) + child.platform_hbox.set_hexpand(True) + child.platform_hbox._is_bound = False + child.attach(label,0,2,1,1) + child.attach(child.platform_hbox,1,2,1,1) + + action_grid = Gtk.Grid() + icon = Gtk.Image.new_from_icon_name('document-edit-symbolic') + icon.set_pixel_size(16) + child.edit_button = Gtk.Button() + child.edit_button.set_child(icon) + action_grid.attach(child.edit_button,0,1,1,1) + child.attach(action_grid,2,0,1,3) + + item.set_child(child) + + def _on_listview_bind(self,factory,item): + def add_platform_icon(child,provider:GameProvider): + icon = Gtk.Image.new_from_icon_name(GAME_PROVIDER_ICONS[provider]) + icon.set_pixel_size(16) + child.platform_hbox.append(icon) + + child = item.get_child() + data = item.get_item() + + child.name_label.set_markup("{}".format( + GLib.markup_escape_text(data.game.name))) + child.key_label.set_text(data.game.key) + + if not child.platform_hbox._is_bound: + child.platform_hbox._is_bound = True + if data.game.windows: + add_platform_icon(child,GameProvider.WINDOWS) + if data.game.linux: + add_platform_icon(child,GameProvider.LINUX) + if data.game.macos: + add_platform_icon(child,GameProvider.MACOS) + if data.game.steam: + add_platform_icon(child,GameProvider.STEAM) + if data.game.epic: + add_platform_icon(child,GameProvider.EPIC_GAMES) + #if data.game.gog: + # add_platform_icon(child,GameProvider.GOG) + + if hasattr(child.edit_button,'_signal_clicked_connector'): + child.edit_button.disconnect(child.edit_button._signal_clicked_connector) + child.edit_button._signal_clicked_connector = child.edit_button.connect('clicked', + self._on_edit_button_clicked, + data.game) + + + def do_prepare_game(self,game:Game|None): + if game is None: + return Game("",self.serch_name,"") + else: + return game + + def _on_new_game_button_clicked(self,button): + def on_response(dialog,response,parent): + if response == Gtk.ResponseType.APPLY: + parent.refresh() + + game = self.do_prepare_game(None) + parent = self.get_Transient_for() + dialog = GameDialog(parent=parent,game=game) + if hasattr(parent,"refresh"): + dialog.connect('response',on_response) + dialog.present() + self.response(Gtk.ResponseType.OK) + + def _on_edit_button_clicked(self,button:Gtk.Button,game:Game): + def on_response(dialog,response,parent): + if response == Gtk.ResponseType.APPLY: + parent.refresh() + + try: + game = self.do_prepare_game() + except Exception as ex: + dialog = Gtk.MessageDialog( + message=_("Unable to edit game {}!".format(GLib.markup_escape_text(game.name))), + use_markup=True, + secondary_message=str(ex), + secondary_use_markup=False, + transient_for=self.get_transient_for(), + parent=self.get_transient_for(), + buttons=Gtk.Buttons.OK + ) + dialog.connect('response',lambda dialog,response: dialog.destroy()) + dialog.present() + return + + parent = self.get_transient_for() + dialog = GameDialog(parent=parent,game=game) + if hasattr(parent,"refresh"): + dialog.connect('response',on_response,parent) + dialog.present() + self.response(Gtk.ResponseType.OK) + + def _on_search_switch_state_set(self,switch:Gtk.Switch,state:bool): + if state: + self.__sort_model.set_sorter(GameSearchDialogDataSorter()) + self.__filter_model.set_filter(GameSearchDialogDataFilter()) + else: + self.__sort_model.set_sorter(GameSearchDialogDataNameSorter()) + self.__filter_model.set_filter(None) + + @Property(type=str) + def search_name(self)->str: + return self.__search_name if self.__search_name else "" + + @search_name.setter + def search_name(self,name:str): + if not name: + self.__search_name = None + else: + self.__search_name = name + + def do_response(self,response): + self.hide() + self.destroy() \ No newline at end of file diff --git a/sgbackup/gui/_steam.py b/sgbackup/gui/_steam.py index 1143ea6..03ec92c 100644 --- a/sgbackup/gui/_steam.py +++ b/sgbackup/gui/_steam.py @@ -22,9 +22,28 @@ from gi.repository.GObject import GObject,Property,Signal,BindingFlags from ..i18n import gettext as _ import os -from ..steam import Steam,SteamLibrary,SteamApp,IgnoreSteamApp,PLATFORM_LINUX,PLATFORM_MACOS,PLATFORM_WINDOWS -from ..game import GameManager,Game,SteamGameData,SteamLinuxData,SteamMacOSData,SteamWindowsData,SavegameType -from ._gamedialog import GameDialog +from ..steam import ( + Steam, + SteamLibrary, + SteamApp, + IgnoreSteamApp, + PLATFORM_LINUX, + PLATFORM_MACOS, + PLATFORM_WINDOWS +) + +from ..game import ( + GameManager, + Game, + SteamGameData, + SteamLinuxData, + SteamMacOSData, + SteamWindowsData, + SavegameType, +) + + +from ._gamedialog import GameDialog,GameSearchDialog class SteamLibrariesDialog(Gtk.Dialog): def __init__(self,parent:Gtk.Window|None=None): @@ -207,6 +226,61 @@ class NewSteamAppSorter(Gtk.Sorter): else: return Gtk.Ordering.EQUAL + +class SteamGameLookupDialog(GameSearchDialog): + def __init__(self,parent,steam_app:SteamApp): + GameSearchDialog.__init__(self,parent,steam_app.name,_("Search Steam Apps")) + self.__steam_app = steam_app + @Property + def steam_app(self)->SteamApp: + return self.__steam_app + + def do_prepare_game(self, game): + game = super().do_prepare_game(game) + if game.steam: + if not game.steam.appid != self.steam_app.appid: + raise ValueError("Steam appid error") + + if PLATFORM_WINDOWS: + if not game.steam.windows: + game.steam.windows = SteamWindowsData("","",installdir=self.steam_app.installdir) + else: + game.steam.windows.installdir=self.steam_app.installdir + if PLATFORM_LINUX: + if not game.steam.linux: + game.steam.linux = SteamLinuxData("","",installdir=self.steam_app.installdir) + else: + game.steam.linux.installdir=self.steam_app.installdir + + if PLATFORM_MACOS: + if not game.steam.macos: + game.steam.macos = SteamWindowsData("","",installdir=self.steam_app.installdir) + else: + game.steam.macos.installdir=self.steam_app.installdir + else: + if PLATFORM_WINDOWS: + windows = SteamWindowsData("","",installdir=self.steam_app.installdir) + else: + windows = None + + if PLATFORM_LINUX: + linux = SteamLinuxData("","",installdir=self.steam_app.installdir) + else: + linux = None + + if PLATFORM_MACOS: + macos = SteamMacOSData("","",installdir=self.steam_app.installdir) + else: + macos = None + + game.steam = SteamGameData(appid=self.steam_app.appid, + windows=windows, + linux=linux, + macos=macos) + + return game + + class NewSteamAppsDialog(Gtk.Dialog): def __init__(self,parent:Gtk.Window|None=None): Gtk.Dialog.__init__(self) @@ -334,8 +408,7 @@ class NewSteamAppsDialog(Gtk.Dialog): if hasattr(child.lookup_button,'_signal_clicked_connector'): child.lookup_button.disconnect(child.lookup_button._signal_clicked_connector) - #child.lookup_button._signal_clicked_connector = child.lookup_button.connect('clicked',self._on_lookup_steamapp_button_clicked,data) - child.lookup_button.set_sensitive(False) + child.lookup_button._signal_clicked_connector = child.lookup_button.connect('clicked',self._on_lookup_steamapp_button_clicked,data) if hasattr(child.search_online_button,'_signal_clicked_connector'): child.search_online_button.disconnect(child.search_online_button._signal_clicked_connector) @@ -394,6 +467,10 @@ class NewSteamAppsDialog(Gtk.Dialog): dialog.connect('response',on_dialog_response,data) dialog.present() + + def _on_lookup_steamapp_button_clicked(self,dialog,data:SteamApp): + dialog = SteamGameLookupDialog(parent=self,steam_app=data) + dialog.present() def refresh(self): self.__listmodel.remove_all() @@ -530,7 +607,6 @@ class SteamIgnoreAppsDialog(Gtk.Dialog): child.remove_button._signal_clicked_connector = child.remove_button.connect('clicked',self._on_remove_button_clicked,data) def __remove_item(self,item:IgnoreSteamApp): - for i in range(self.__liststore.get_n_items()): ignoreapp = self.__liststore.get_item(i) if item.appid == ignoreapp.appid: