2025.03.11 23:40:14 (desktop)

This commit is contained in:
Christian Moser 2025-03-11 23:40:14 +01:00
parent 5a36b4f358
commit f3ef3df537
Failed to extract signature
8 changed files with 726 additions and 92 deletions

View File

@ -7,7 +7,9 @@ sgbackup/commands/help.py
sgbackup/commands/__init__.py sgbackup/commands/__init__.py
sgbackup/curses/__init__.py sgbackup/curses/__init__.py
sgbackup/curses/__main__.py sgbackup/curses/__main__.py
sgbackup/epic.py
sgbackup/game.py sgbackup/game.py
sgbackup/gog.py
sgbackup/gui/_app.py sgbackup/gui/_app.py
sgbackup/gui/_backupdialog.py sgbackup/gui/_backupdialog.py
sgbackup/gui/_dialogs.py sgbackup/gui/_dialogs.py

328
PO/de.po
View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-05 10:15+0100\n" "POT-Creation-Date: 2025-03-11 02:01+0100\n"
"PO-Revision-Date: 2025-03-05 10:19+0100\n" "PO-Revision-Date: 2025-03-11 15:31+0100\n"
"Last-Translator: Christian Moser <christian@cmoser.eu>\n" "Last-Translator: Christian Moser <christian@cmoser.eu>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: de\n" "Language: de\n"
@ -18,54 +18,59 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.5\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." msgid "Add a new game."
msgstr "Neues Spiel hinzufügen." msgstr "Neues Spiel hinzufügen."
#: sgbackup/gui/_app.py:232 #: sgbackup/gui/_app.py:236
msgid "Manage new Steam-Apps." msgid "Manage new Steam-Apps."
msgstr "Neue Steam-Apps verwalten." msgstr "Neue Steam-Apps verwalten."
#: sgbackup/gui/_app.py:242 #: sgbackup/gui/_app.py:246
msgid "Backup all <i>active</i> and <i>live</i> Games." msgid "Backup all <i>active</i> and <i>live</i> Games."
msgstr "Alle <i>aktiven</i> und <i>live</i> Spiele sichern." msgstr "Alle <i>aktiven</i> und <i>live</i> Spiele sichern."
#: sgbackup/gui/_app.py:276 #: sgbackup/gui/_app.py:280
msgid "Key" msgid "Key"
msgstr "Schlüssel" msgstr "Schlüssel"
#: sgbackup/gui/_app.py:282 #: sgbackup/gui/_app.py:286
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
#: sgbackup/gui/_app.py:290 #: sgbackup/gui/_app.py:294
msgid "Active" msgid "Active"
msgstr "Aktiv" msgstr "Aktiv"
#: sgbackup/gui/_app.py:296 sgbackup/gui/_app.py:864 #: sgbackup/gui/_app.py:300 sgbackup/gui/_app.py:868
msgid "Live" msgid "Live"
msgstr "Live" msgstr "Live"
#: sgbackup/gui/_app.py:455 #: sgbackup/gui/_app.py:459
msgid "No icon-name for sgtype {}" msgid "No icon-name for sgtype {}"
msgstr "Kein Icon-Name für sgtype {}" msgstr "Kein Icon-Name für sgtype {}"
#: sgbackup/gui/_app.py:543 #: sgbackup/gui/_app.py:547
#, python-brace-format #, python-brace-format
msgid "Do you want to create a new savegame for <i>{game}</i>?" msgid "Do you want to create a new savegame for <i>{game}</i>?"
msgstr "Willst du ein neues SaveGame für <i>{game}</i> erstellen?" msgstr "Willst du ein neues SaveGame für <i>{game}</i> erstellen?"
#: sgbackup/gui/_app.py:545 #: sgbackup/gui/_app.py:549
msgid "The new savegame is added to the finsihed savegames for the game." msgid "The new savegame is added to the finsihed savegames for the game."
msgstr "" msgstr ""
"Das Neue SaveGame wird zu den beendeten Speicherständen für dieses Spiel " "Das Neue SaveGame wird zu den beendeten Speicherständen für dieses Spiel "
"hinzugefügt." "hinzugefügt."
#: sgbackup/gui/_app.py:557 #: sgbackup/gui/_app.py:561
msgid "Backup the SaveGames for this game." msgid "Backup the SaveGames for this game."
msgstr "Sichere das SaveGame für dieses Spiel." msgstr "Sichere das SaveGame für dieses Spiel."
#: sgbackup/gui/_app.py:569 #: sgbackup/gui/_app.py:573
msgid "" msgid ""
"Remove this game.\n" "Remove this game.\n"
"<span weight='ultrabold'>This also deletes the game configuration file!!!</" "<span weight='ultrabold'>This also deletes the game configuration file!!!</"
@ -75,125 +80,354 @@ msgstr ""
"<span weight='ultrabold'>Dies wird auch die Konfigurationsdatei des Spiels " "<span weight='ultrabold'>Dies wird auch die Konfigurationsdatei des Spiels "
"löschen!!!</span>" "löschen!!!</span>"
#: sgbackup/gui/_app.py:663 #: sgbackup/gui/_app.py:667
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Do you really want to remove the game <span weight='bold'>{game}</span>?" "Do you really want to remove the game <span weight='bold'>{game}</span>?"
msgstr "Wills du wirklich das Spiel <span weight='bold'>{game}</span> löschen?" msgstr "Wills du wirklich das Spiel <span weight='bold'>{game}</span> löschen?"
#: sgbackup/gui/_app.py:666 #: sgbackup/gui/_app.py:670
msgid "Removing games cannot be undone!!!" msgid "Removing games cannot be undone!!!"
msgstr "Das Löschen von Spielen kann nicht Rückgängig gemacht werden!!!" 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" msgid "OS"
msgstr "OS" msgstr "OS"
#: sgbackup/gui/_app.py:869 #: sgbackup/gui/_app.py:873
msgid "Savegame name" msgid "Savegame name"
msgstr "Speicherstandname" msgstr "Speicherstandname"
#: sgbackup/gui/_app.py:875 #: sgbackup/gui/_app.py:879
msgid "Timestamp" msgid "Timestamp"
msgstr "Zeitstempel" msgstr "Zeitstempel"
#: sgbackup/gui/_app.py:880 #: sgbackup/gui/_app.py:884
msgid "Size" msgid "Size"
msgstr "Größe" msgstr "Größe"
#: sgbackup/gui/_app.py:1066 #: sgbackup/gui/_app.py:1072
msgid "%m.%d.%Y %H:%M:%S" msgid "%m.%d.%Y %H:%M:%S"
msgstr "%d.%m.%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." msgid "Restore the SaveGameBackup."
msgstr "Stelle das Speicherstandbackup wieder her." msgstr "Stelle das Speicherstandbackup wieder her."
#: sgbackup/gui/_app.py:1104 #: sgbackup/gui/_app.py:1110
msgid "Convert to another SaveGameBackup." 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." msgid "Delete this SaveGameBackup."
msgstr "Lösche dieses Speicherstandbackup." msgstr "Lösche dieses Speicherstandbackup."
#: sgbackup/gui/_app.py:1170 #: sgbackup/gui/_app.py:1176
msgid "SGBackup" msgid "SGBackup"
msgstr "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 #, python-brace-format
msgid "" msgid ""
"{games} Games -- {active} Games active -- {live} Games live -- {finished} " "{games} Games -- {active} Games active -- {live} Games live -- {finished} "
"Games finished" "Games finished"
msgstr "" msgstr ""
"{games} Spile -- {active} Spiele aktiv -- {live} Spiele live -- {finished} " "{games} Spiele -- {active} Spiele aktiv -- {live} Spiele live -- {finished} "
"Spiele beendet" "Spiele beendet"
#: sgbackup/gui/appmenu.ui:6 #: sgbackup/gui/_dialogs.py:38
msgid "_Game" msgid "There are no games to backup!"
msgstr "_Spiel" 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 <span weight=\"bold\">\"{steamapp}\"</span> on the ignore "
"list?"
msgstr ""
"Wills du wirklich das Spiel <span weight='bold'>{steamapp}</span> 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" msgid "_Add Game"
msgstr "Spiel _hinzufügen" 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" msgid "_Steam"
msgstr "_Steam" msgstr "_Steam"
#: sgbackup/gui/appmenu.ui:17 #: sgbackup/gui/appmenu.ui:25
msgid "New Steam Apps" msgid "New Steam Apps"
msgstr "Neue Steam-App" msgstr "Neue Steam-App"
#: sgbackup/gui/appmenu.ui:21 #: sgbackup/gui/appmenu.ui:29
msgid "Manage Steam Libraries" msgid "Manage Steam Libraries"
msgstr "Steam Bibliotheken verwalten" 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" msgid "_Epic"
msgstr "_Epic" msgstr "_Epic"
#: sgbackup/gui/appmenu.ui:30 #: sgbackup/gui/appmenu.ui:42
msgid "_GoG" msgid "_GoG"
msgstr "_GoG" msgstr "_GoG"
#: sgbackup/gui/appmenu.ui:35 #: sgbackup/gui/appmenu.ui:47
msgid "_Settings" msgid "_Settings"
msgstr "_Einstellungen" msgstr "_Einstellungen"
#: sgbackup/gui/appmenu.ui:42 #: sgbackup/gui/appmenu.ui:54
msgid "_Help" msgid "_Help"
msgstr "_Hilfe" msgstr "_Hilfe"
#: sgbackup/gui/appmenu.ui:47 #: sgbackup/gui/appmenu.ui:59
msgid "_About SGBackup" msgid "_About SGBackup"
msgstr "_Über SGBackup" msgstr "_Über SGBackup"
#: sgbackup/gui/appmenu.ui:56 #: sgbackup/gui/appmenu.ui:68
msgid "_Quit" msgid "_Quit"
msgstr "_Beenden" msgstr "_Beenden"
#: sgbackup/gui/appmenu.ui:65 #: sgbackup/gui/appmenu.ui:77
msgid "Convert to _Windows" msgid "Convert to _Windows"
msgstr "Nach _Windows konvertieren" msgstr "Nach _Windows konvertieren"
#: sgbackup/gui/appmenu.ui:69 #: sgbackup/gui/appmenu.ui:81
msgid "Convert to _Linux" msgid "Convert to _Linux"
msgstr "Nach _Linux konvertieren" msgstr "Nach _Linux konvertieren"
#: sgbackup/gui/appmenu.ui:73 #: sgbackup/gui/appmenu.ui:85
msgid "Convert to _Mac OS" msgid "Convert to _Mac OS"
msgstr "Nach _MacOS konvertieren" msgstr "Nach _MacOS konvertieren"
#: sgbackup/gui/appmenu.ui:77 #: sgbackup/gui/appmenu.ui:89
msgid "Convert to _Steam Windows" msgid "Convert to _Steam Windows"
msgstr "Nach _Steam-Windows konvertieren" msgstr "Nach _Steam-Windows konvertieren"
#: sgbackup/gui/appmenu.ui:81 #: sgbackup/gui/appmenu.ui:93
msgid "Convert to Steam Linux" msgid "Convert to Steam Linux"
msgstr "Nach _Steam-Linux konvertieren" msgstr "Nach _Steam-Linux konvertieren"
#: sgbackup/gui/appmenu.ui:85 #: sgbackup/gui/appmenu.ui:97
msgid "Convert to Steam Mac OS" msgid "Convert to Steam Mac OS"
msgstr "Nach _Steam-MacOS konvertieren" msgstr "Nach _Steam-MacOS konvertieren"

View File

@ -159,7 +159,7 @@ class Epic(GObject):
with open(filename,'r',encoding="utf-8") as ifile: with open(filename,'r',encoding="utf-8") as ifile:
data = json.loads(ifile.read()) data = json.loads(ifile.read())
except Exception as ex: 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, manifest=filename,
error=str(ex) error=str(ex)
)) ))

View File

@ -152,7 +152,7 @@ VALID_SAVEGAME_TYPES = [
SavegameType.STEAM_MACOS, SavegameType.STEAM_MACOS,
SavegameType.STEAM_WINDOWS, SavegameType.STEAM_WINDOWS,
#SavegameType.EPIC_LINUX, #SavegameType.EPIC_LINUX,
#SavegameType.EPIC_WINDOWS, SavegameType.EPIC_WINDOWS,
#SavegameType.GOG_LINUX, #SavegameType.GOG_LINUX,
#SavegameType.GOG_WINDOWS, #SavegameType.GOG_WINDOWS,
] ]
@ -160,8 +160,8 @@ VALID_SAVEGAME_TYPES = [
SAVEGAME_TYPE_ICONS = { SAVEGAME_TYPE_ICONS = {
SavegameType.UNSET : None, SavegameType.UNSET : None,
SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic', SavegameType.WINDOWS: 'windows-svgrepo-com-symbolic',
SavegameType.LINUX: 'linux-svgrepo-com-symbolic.svg', SavegameType.LINUX: 'linux-svgrepo-com-symbolic',
SavegameType.MACOS: 'apple-svgrepo-com-symbolic.svg', SavegameType.MACOS: 'apple-svgrepo-com-symbolic',
SavegameType.STEAM_LINUX: 'steam-svgrepo-com-symbolic', SavegameType.STEAM_LINUX: 'steam-svgrepo-com-symbolic',
SavegameType.STEAM_MACOS: 'steam-svgrepo-com-symbolic', SavegameType.STEAM_MACOS: 'steam-svgrepo-com-symbolic',
SavegameType.STEAM_WINDOWS: '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', 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): class GameFileType(StrEnum):
""" """
GameFileType The file matcher type for `GameFileMatcher`. GameFileType The file matcher type for `GameFileMatcher`.
@ -1784,9 +1801,7 @@ class GameManager(GObject):
self.__games = {} self.__games = {}
self.__steam_games = {} self.__steam_games = {}
self.__steam_linux_games = {} self.__epic_games = {}
self.__steam_windows_games = {}
self.__steam_macos_games = {}
self.load() self.load()
@ -1798,18 +1813,6 @@ class GameManager(GObject):
def steam_games(self)->dict[int:Game]: def steam_games(self)->dict[int:Game]:
return self.__steam_games 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): def load(self):
if self.__games: if self.__games:
self.__games = {} self.__games = {}
@ -1838,13 +1841,30 @@ class GameManager(GObject):
def add_game(self,game:Game): def add_game(self,game:Game):
self.__games[game.key] = game self.__games[game.key] = game
if (game.steam_macos): if game.steam:
self.__steam_games[game.steam_macos.appid] = game self.__steam_games[game.steam.appid] = game
self.__steam_macos_games[game.steam_macos.appid] = game if game.epic:
if (game.steam_linux): self.__epic_games[game.epic.appname] = game
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
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]

View File

@ -31,7 +31,14 @@ from pathlib import Path
from ..settings import settings from ..settings import settings
from ._settingsdialog import SettingsDialog from ._settingsdialog import SettingsDialog
from ._gamedialog import GameDialog 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 ( from ._steam import (
SteamLibrariesDialog, SteamLibrariesDialog,
NewSteamAppsDialog, NewSteamAppsDialog,
@ -223,20 +230,28 @@ class GameView(Gtk.Box):
icon = Gtk.Image.new_from_icon_name('list-add-symbolic') icon = Gtk.Image.new_from_icon_name('list-add-symbolic')
icon.set_pixel_size(16) 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_child(icon)
add_game_button.set_tooltip_text(_("Add a new game.")) add_game_button.set_tooltip_text(_("Add a new game."))
add_game_button.connect('clicked',self._on_add_game_button_clicked) add_game_button.connect('clicked',self._on_add_game_button_clicked)
self.actionbar.pack_start(add_game_button) 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) 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_child(icon)
new_steam_games_button.set_tooltip_text(_("Manage new Steam-Apps.")) new_steam_games_button.set_tooltip_text(_("Manage new Steam-Apps."))
new_steam_games_button.connect('clicked',self._on_new_steam_games_button_clicked) new_steam_games_button.connect('clicked',self._on_new_steam_games_button_clicked)
self.actionbar.pack_start(new_steam_games_button) 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)) self.actionbar.pack_start(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
icon = Gtk.Image.new_from_icon_name('document-save-symbolic') 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 = SteamNoNewAppsDialog(parent=self.get_root())
dialog.present() dialog.present()
def _on_new_epic_games_button_clicked(self,button):
# TODO
pass
def _on_backup_active_live_button_clicked(self,button): def _on_backup_active_live_button_clicked(self,button):
backup_games = [] backup_games = []
for i in range(self._liststore.get_n_items()): for i in range(self._liststore.get_n_items()):
@ -1279,11 +1298,12 @@ class AppWindow(Gtk.ApplicationWindow):
def statusbar(self): def statusbar(self):
return self.__statusbar return self.__statusbar
def refresh(self): def refresh(self,reload_game_manager:bool=False):
""" """
refresh Refresh the views of this window. refresh Refresh the views of this window.
""" """
GameManager.get_global().load() if reload_game_manager:
GameManager.get_global().load()
self.gameview.refresh() self.gameview.refresh()
#self.backupview.refresh() #self.backupview.refresh()
@ -1510,7 +1530,7 @@ class Application(Gtk.Application):
dialog.connect_after('response',on_dialog_response) dialog.connect_after('response',on_dialog_response)
dialog.present() dialog.present()
else: else:
dialog = NoNewSteamAppsDialog(self.appwindow) dialog = SteamNoNewAppsDialog(self.appwindow)
dialog.present() dialog.present()

View File

@ -1,6 +1,6 @@
############################################################################### ###############################################################################
# sgbackup - The SaveGame Backup tool # # 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 # # 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 # # 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 import Gtk,GLib,Gio
from gi.repository.GObject import GObject,Property
from ..version import VERSION from ..version import VERSION
from ..i18n import gettext as _ from ..i18n import gettext as _
class AboutDialog(Gtk.AboutDialog): class AboutDialog(Gtk.AboutDialog):
def __init__(self): def __init__(self):
Gtk.AboutDialog.__init__(self) Gtk.AboutDialog.__init__(self)
@ -60,4 +65,5 @@ class NoGamesToBackupFoundDialog(Gtk.MessageDialog):
def do_response(self,response): def do_response(self,response):
self.hide() self.hide()
self.destroy() self.destroy()

View File

@ -1,6 +1,6 @@
############################################################################### ###############################################################################
# sgbackup - The SaveGame Backup tool # # 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 # # 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 # # 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 import Gio,GLib,Gtk,Pango
from gi.repository.GObject import Property,Signal,GObject,BindingFlags,SignalFlags from gi.repository.GObject import Property,Signal,GObject,BindingFlags,SignalFlags
import rapidfuzz
from ..i18n import gettext as _, gettext_noop as N_ from ..i18n import gettext as _, gettext_noop as N_
from ..game import ( from ..game import (
@ -39,6 +39,8 @@ from ..game import (
EpicGameData, EpicGameData,
EpicWindowsData, EpicWindowsData,
GameManager, GameManager,
GameProvider,
GAME_PROVIDER_ICONS,
) )
@ -1080,6 +1082,9 @@ class GameDialog(Gtk.Dialog):
savegame_name = self.__sgname_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.has_game: if self.has_game:
if self.__game.key != key:
GameManager.get_global().remove_game(self.__game)
self.__game.key = key self.__game.key = key
self.__game.name = name self.__game.name = name
self.__game.savegame_type = savegame_type 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=()) @Signal(name='apply',flags=SignalFlags.RUN_FIRST,return_type=None,arg_types=())
def do_apply(self): def do_apply(self):
pass 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("<span weight='bold' size='large'>{}</span>".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 <i>{}</i>!".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()

View File

@ -22,9 +22,28 @@ from gi.repository.GObject import GObject,Property,Signal,BindingFlags
from ..i18n import gettext as _ from ..i18n import gettext as _
import os import os
from ..steam import Steam,SteamLibrary,SteamApp,IgnoreSteamApp,PLATFORM_LINUX,PLATFORM_MACOS,PLATFORM_WINDOWS from ..steam import (
from ..game import GameManager,Game,SteamGameData,SteamLinuxData,SteamMacOSData,SteamWindowsData,SavegameType Steam,
from ._gamedialog import GameDialog 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): class SteamLibrariesDialog(Gtk.Dialog):
def __init__(self,parent:Gtk.Window|None=None): def __init__(self,parent:Gtk.Window|None=None):
@ -207,6 +226,61 @@ class NewSteamAppSorter(Gtk.Sorter):
else: else:
return Gtk.Ordering.EQUAL 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): class NewSteamAppsDialog(Gtk.Dialog):
def __init__(self,parent:Gtk.Window|None=None): def __init__(self,parent:Gtk.Window|None=None):
Gtk.Dialog.__init__(self) Gtk.Dialog.__init__(self)
@ -334,8 +408,7 @@ class NewSteamAppsDialog(Gtk.Dialog):
if hasattr(child.lookup_button,'_signal_clicked_connector'): if hasattr(child.lookup_button,'_signal_clicked_connector'):
child.lookup_button.disconnect(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._signal_clicked_connector = child.lookup_button.connect('clicked',self._on_lookup_steamapp_button_clicked,data)
child.lookup_button.set_sensitive(False)
if hasattr(child.search_online_button,'_signal_clicked_connector'): if hasattr(child.search_online_button,'_signal_clicked_connector'):
child.search_online_button.disconnect(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.connect('response',on_dialog_response,data)
dialog.present() dialog.present()
def _on_lookup_steamapp_button_clicked(self,dialog,data:SteamApp):
dialog = SteamGameLookupDialog(parent=self,steam_app=data)
dialog.present()
def refresh(self): def refresh(self):
self.__listmodel.remove_all() 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) child.remove_button._signal_clicked_connector = child.remove_button.connect('clicked',self._on_remove_button_clicked,data)
def __remove_item(self,item:IgnoreSteamApp): def __remove_item(self,item:IgnoreSteamApp):
for i in range(self.__liststore.get_n_items()): for i in range(self.__liststore.get_n_items()):
ignoreapp = self.__liststore.get_item(i) ignoreapp = self.__liststore.get_item(i)
if item.appid == ignoreapp.appid: if item.appid == ignoreapp.appid: