diff --git a/sgbackup/archiver.py b/sgbackup/archiver.py index b44bf74..fc63281 100644 --- a/sgbackup/archiver.py +++ b/sgbackup/archiver.py @@ -16,5 +16,51 @@ # along with this program. If not, see . # ############################################################################### +from gi.repository.GObject import ( + GObject, + Property, + Signal, + SignalFlags, + signal_accumulator_true_handled, +) + +from .game import Game + class Archiver: - pass \ No newline at end of file + def __init__(self,key:str,name:str,extensions:list[str],decription:str|None=None): + self.__key = key + self.__name = name + if decription: + self.__description = decription + else: + self.__description = "" + + @Property(type=str) + def name(self)->str: + return self.__name + + @Property + def key(self)->str: + return self.__key + + @Property + def description(self)->str: + return self.__description + + def backup(self,game)->bool: + pass + + def restore(self,game,file)->bool: + pass + + @Signal(name="backup",flags=SignalFlags.RUN_FIRST, + return_type=bool, arg_types=(GObject,str), + accumulator=signal_accumulator_true_handled) + def do_backup(self,game:Game,filename:str): + pass + + @Signal(name="restore",flags=SignalFlags.RUN_FIRST, + return_type=bool,arg_types=(GObject,str), + accumulator=signal_accumulator_true_handled) + def do_backup(self,game:Game,filanme:str): + pass \ No newline at end of file diff --git a/sgbackup/game.py b/sgbackup/game.py index 864e2e5..b554ea4 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -46,21 +46,71 @@ else: class SavegameType(StrEnum): + """ + SavegameType The savegame type for `Game` instance. + + The SavegameType selects the `GameData` provider for the + `Game` instance. + """ + + #: UNSET The SavegameType is unset UNSET = "unset" + + #: OTHER Not listed game-provider. + #: + #: **Currently not supported!** OTHER = "other" + + #: WINDOWS Native Windows game WINDOWS = "windows" + + #: LINUX Native Linux game LINUX = "linux" + + #: MACOS Native MacOS game MACOS = "macos" + + #: STEAM_WINDOWS *Steam* for Windows STEAM_WINDOWS = "steam_windows" + + #: STEAM_LINUX *Steam* for Linux STEAM_LINUX = "steam_linux" + + #: STEAM_MACOS *Steam* for MacOS STEAM_MACOS = "steam_macos" + + #: GOG WINDOWS *Good old Games* for Windows + #: + #: **Currently not supported!** GOG_WINDOWS = "gog_windows" + + #: GOG_LINUX *Good old Games* for Linux + #: + #: **Currently not supported!** GOG_LINUX = "gog_linux" + + #: EPIC_WINDOWS *Epic Games* for Windows + #: + #: **Currently not supported!** EPIC_WINDOWS = "epic_windows" + + #: EPIC_LINUX *Epic Games* for Linux + #: + #: **Currently not supported!** EPIC_LINUX = "epic_linux" @staticmethod def from_string(typestring:str): + """ + from_string Get SavegameType from string. + + :param typestring: The string to parse + :type typestring: str + :return: The SavegameType if any. If no matching SavegameType is found, + SavegameType.UNSET is returned. + :rtype: SavegameType + """ + st=SavegameType s=typestring.lower() if (s == 'other'): @@ -71,31 +121,53 @@ class SavegameType(StrEnum): return st.LINUX elif (s == 'macos'): return st.MACOS - elif (s == 'steam_windows' or s == 'steamwindows' or s == 'steam.windows'): + elif (s == 'steam_windows' or s == 'steam-windows' or s == 'steamwindows' or s == 'steam.windows'): return st.STEAM_WINDOWS - elif (s == 'steam_linux' or s == 'steamlinux' or s == 'steam.linux'): + elif (s == 'steam_linux' or s == 'steam-linux' or s == 'steamlinux' or s == 'steam.linux'): return st.STEAM_LINUX - elif (s == 'steam_macos' or s == 'steammacos' or s == 'steam.macos'): + elif (s == 'steam_macos' or s == 'steam-macos' or s == 'steammacos' or s == 'steam.macos'): return st.STEAM_MACOS - elif (s == 'gog_winows' or s == 'gogwindows' or s == 'gog.windows'): + elif (s == 'gog_winows' or s == 'gog-windows' or s == 'gogwindows' or s == 'gog.windows'): return st.GOG_WINDOWS - elif (s == 'gog_linux' or s == 'goglinux' or s == 'gog.linux'): + elif (s == 'gog_linux' or s == 'gog-linux' or s == 'goglinux' or s == 'gog.linux'): return st.GOG_LINUX - elif (s == 'epic_windows' or s == 'epicwindows' or s == 'epic.windows'): + elif (s == 'epic_windows' or s == 'epic-windows' or s == 'epicwindows' or s == 'epic.windows'): return st.EPIC_WINDOWS - elif (s == 'epic_linux' or s == 'epiclinux' or s == 'epic.linux'): + elif (s == 'epic_linux' or s == 'epic-linux' or s == 'epiclinux' or s == 'epic.linux'): return st.EPIC_LINUX return st.UNSET class GameFileType(StrEnum): + """ + GameFileType The file matcher type for `GameFileMatcher`. + + The path to be matched is originating from *${SAVEGAME_ROOT}/${SAVEGAME_DIR}*. + """ + + #: GLOB Glob matching. GLOB = "glob" + + #: REGEX Regex file matching REGEX = "regex" + + #: FILENAME Filename matching. FILENAME = "filename" @staticmethod def from_string(typestring:str): + """ + from_string Get the `GameFileType` from a string. + + If an illegal string-value is given this method raises a `ValueError`. + + :param typestring: The string to be used. + :type typestring: str + :raises ValueError: If an illegal string is given. + :return: The corresponding Enum-value + :rtype: GameFileType + """ s = typestring.lower() if (s == 'glob'): return GameFileType.GLOB @@ -104,9 +176,12 @@ class GameFileType(StrEnum): elif (s == 'filename'): return GameFileType.FILENAME - raise ValueError("Unknown GameFileType \"{}\"!".fomrat(typestring)) + raise ValueError("Unknown GameFileType \"{}\"!".format(typestring)) class GameFileMatcher(GObject): + """ + GameFileMatcher Match savegame files if they are to be included in the backup. + """ __gtype_name__ = "GameFileMatcher" def __init__(self,match_type:GameFileType,match_file:str): @@ -116,7 +191,14 @@ class GameFileMatcher(GObject): @Property def match_type(self)->GameFileType: + """ + match_type The type of the matcher. + + :type: GameFileType + """ + return self.__match_type + @match_type.setter def match_type(self,type:GameFileType): if not isinstance(type,GameFileType): @@ -125,15 +207,27 @@ class GameFileMatcher(GObject): @Property(type=str) def match_file(self)->str: + """ + match_file The matcher value. + + :type: str + """ return self.__match_file @match_file.setter def match_file(self,file:str): - self.__match_file = file + self.__match_file = file - ## @} - - def match(self,rel_filename:str): - def match_glob(filename): + def match(self,rel_filename:str)->bool: + """ + match Match the file. + + :param rel_filename: The relative filename originating from + *${SAVEGAME_ROOT}/${SAVEGAME_DIR}*. + :type rel_filename: str + :rtype: bool + :returns: True if file matches + """ + def match_glob(filename)->bool: return fnmatch.fnmatch(filename,self.match_file) # match_glob() @@ -170,8 +264,7 @@ class GameData(GObject): __gtype_name__ = 'GameData' """ - :class: GameData - :brief: Base class for platform specific data. + :class: GameData Base class for savegame specific data data. """ def __init__(self, savegame_type:SavegameType, @@ -185,8 +278,8 @@ class GameData(GObject): self.__savegame_root = savegame_root self.__savegame_dir = savegame_dir self.__variables = {} - self.__filematch = [] - self.__ignorematch = [] + self.__filematchers = [] + self.__ignorematchers = [] if variables is not None: variables.update(variables) @@ -202,15 +295,14 @@ class GameData(GObject): @Property def savegame_type(self)->SavegameType: """ - :attr: savegame_type - :brief: Type of the class. + :type: SavegameType """ return self.__savegame_type @Property(type=str) def savegame_root(self)->str: """ - :attr: savegame_root + :type: str """ return self.__savegame_root @@ -221,7 +313,7 @@ class GameData(GObject): @Property def savegame_dir(self)->str: """ - :attr: savegame_dir + :type: str """ return self.__savegame_dir @@ -230,7 +322,10 @@ class GameData(GObject): self.__savegame_dir = sgdir @Property - def variables(self)->dict: + def variables(self)->dict[str:str]: + """ + :type: dict[str:str] + """ return self.__variables @variables.setter def variables(self,vars:dict|None): @@ -240,109 +335,225 @@ class GameData(GObject): self.__variables = dict(vars) @Property - def file_match(self): - return self.__filematch - @file_match.setter - def file_match(self,fm:list[GameFileMatcher]|None): + def file_matchers(self)->list[GameFileMatcher]: + """ + :type: list[GameFileMatcher] + """ + return self.__filematchers + + @file_matchers.setter + def file_matchers(self,fm:list[GameFileMatcher]|None): if not fm: - self.__filematch = [] + self.__filematchers = [] else: for matcher in fm: if not isinstance(matcher,GameFileMatcher): raise TypeError("\"file_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!") - self.__filematch = list(fm) + self.__filematchers = list(fm) + @Property - def ignore_match(self): - return self.__ignorematch - @file_match.setter - def file_match(self,im:list[GameFileMatcher]|None): + def ignore_matchers(self)->list[GameFileMatcher]: + """ + :type: list[GameFileMatcher] + """ + return self.__ignorematchers + @ignore_matchers.setter + def ignore_matchers(self,im:list[GameFileMatcher]|None): if not im: - self.__ignorematch = [] + self.__ignorematchers = [] else: for matcher in im: if not isinstance(matcher,GameFileMatcher): raise TypeError("\"ignore_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!") - self.__ignorematch = list(im) + self.__ignorematchers = list(im) def has_variable(self,name:str)->bool: + """ + has_variable Check if variable exists. + + :param name: The variable name. + :type name: str + :return: `True` if the variable exists. + :rtype: bool + """ + return (name in self.__variables) def get_variable(self,name:str)->str: + """ + get_variable Get a variable value byy variable name. + + :param name: The variable name + :type name: str + :return: The vairable value if the variable exists or an empty string. + :rtype: str + """ if name not in self.__variables: return "" return self.__variables[name] def set_variable(self,name:str,value:str): + """ + set_variable Set a variable. + + If the variable exists, it is replaced by the new variable. + + :param name: The variable name. + :type name: str + :param value: The variable value. + :type value: str + """ self.__variables[name] = value def delete_variable(self,name:str): + """ + delete_variable Deletes as variable if the variable exists + + :param name: The vairable name to delete. + :type name: str + """ if name in self.__variables: del self.__variables[name] - def get_variables(self): + def get_variables(self)->dict[str:str]: + """ + get_variables Get the variables set by this instance. + + :return: The variables as a dict. + :rtype: dict[str:str] + """ return self.variables - def match_file(self,rel_filename:str): - if not self.__filematch: + def match_file(self,rel_filename:str)->bool: + """ + match_file Matches a file with the `GameFileMatcher`s for this class. + + This method returns `True` if there is no `GameFileMatcher` set for + `GameData.file_match`. + + :param rel_filename: The relative filename originating from *$SAVEGAME_DIR* + :type rel_filename: str + :return: `True` if the file matches. + :rtype: bool + """ + if not self.file_matchers: return True - for fm in self.__filematch: + for fm in self.file_matchers: if fm.match(rel_filename): return True return False - def match_ignore(self,rel_filename:str): - if not self.__ignorematch: + def match_ignore(self,rel_filename:str)->bool: + """ + match_ignore Matches file agains the ignore_match `GameFileMatcher`s. + + This method returns `False` if there is no `GameFileMatcher` set in + `GameData.ignore_match`. + + :param rel_filename: The relative filename originating from *$SAVEGAME_DIR* + :type rel_filename: str + :return: `True` if the file matches. + :rtype: bool + """ + if not self.ignore_matchers: return False - for fm in self.__ignorematch: + for fm in self.ignore_matchers: if fm.match(rel_filename): return True return False - def match(self,rel_filename:str): + def match(self,rel_filename:str)->bool: + """ + match Match files against `file_match` and `ignore_match`. + + If this method returns `True` the file should be included in the + savegame backup. + + :param rel_filename: The relative filename originating from *$SAVEGAME_DIR* + :type rel_filename: str + :return: True if the file should be included in the savegame backup. + :rtype: bool + """ if self.match_file(rel_filename) and not self.match_ignore(rel_filename): return True return False def add_file_match(self,matcher:GameFileMatcher): + """ + add_file_match Add a `GameFileMatcher` to `file_match`. + + :param matcher: The `GameFileMatcher` to add. + :type matcher: GameFileMatcher + :raises TypeError: If the `matcher` is not a `GameFileMatcher` instance. + """ if not isinstance(matcher,GameFileMatcher): raise TypeError("matcher is not a \"GameFileMatcher\" instance!") - self.__filematch.append(matcher) + self.__filematchers.append(matcher) def remove_file_match(self,matcher:GameFileMatcher): - for i in reversed(range(len(self.__filematch))): - if (matcher == self.__filematch[i]): - del self.__filematch[i] + """ + remove_file_match Remove a file_matcher. + + :param matcher: The `GameFileMatcher` to remove. + :type matcher: GameFileMatcher + """ + for i in reversed(range(len(self.__filematchers))): + if (matcher == self.__filematchers[i]): + del self.__filematchers[i] def add_ignore_match(self,matcher:GameFileMatcher): + """ + add_file_match Add a `GameFileMatcher` to `ignore_match`. + + :param matcher: The `GameFileMatcher` to add. + :type matcher: GameFileMatcher + :raises TypeError: If the `matcher` is not a `GameFileMatcher` instance. + """ if not isinstance(matcher,GameFileMatcher): raise TypeError("matcher is not a \"GameFileMatcher\" instance!") - self.__ignorematch.append(matcher) + self.__ignorematchers.append(matcher) def remove_ignore_match(self,matcher:GameFileMatcher): - for i in reversed(range(len(self.__ignorematch))): - if (matcher == self.__ignorematch[i]): - del self.__ignorematch[i] + """ + remove_file_match Remove a ignore_match. + + :param matcher: The `GameFileMatcher` to remove. + :type matcher: GameFileMatcher + """ + + for i in reversed(range(len(self.__ignorematchers))): + if (matcher == self.__ignorematchers[i]): + del self.__ignorematchers[i] def serialize(self)->dict: + """ + serialize Serialize the instance to a dict. + + This method should be overloaded by child-classes, so that their data + is exported too. + + :return: The dict holding the data for recreating an instance of this class. + :rtype: dict + """ ret = { 'savegame_root': self.savegame_root, 'savegame_dir': self.savegame_dir, } if (self.__variables): ret['variables'] = self.variables - if (self.file_match): + if (self.file_matchers): fm = [] - for matcher in self.file_match: + for matcher in self.file_matchers: fm.append({'type':matcher.match_type.value,'match':matcher.match_file}) ret['file_match'] = fm - if (self.add_ignore_match): + if (self.ignore_matchers): im = [] - for matcher in self.ignore_match: + for matcher in self.ignore_matchers: im.append({'type':matcher.match_type.value,'match':matcher.match_file}) ret['ignore_match'] = im @@ -978,7 +1189,19 @@ class Game(GObject): if not isinstance(data,SteamMacOSGame): raise TypeError("SteamWindowsGame") self.__steam_macos = data - + + @Property + def savegame_root(self)->str|None: + if not self.game_data: + return None + return self.game_data.savegame_root + + @Property + def savegame_dir(self)->str|None: + if not self.game_data: + return None + return self.game_data.savegame_dir + def add_variable(self,name:str,value:str): self.__variables[str(name)] = str(value) @@ -1038,58 +1261,121 @@ class Game(GObject): with open(new_path,'wt',encoding='utf-8') as ofile: ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4)) - + def __bool__(self): + return (bool(self.game_data) and bool(self.savegame_root) and bool(self.savegame_dir)) + + def is_backup_file(self,filename:str): + pass + + def get_backup_files(self)->dict[str:str]|None: + def get_backup_files_recursive(sgroot:pathlib.Path,sgdir:str,subdir:str|None=None): + ret = {} + if subdir: + path = sgroot / sgdir / subdir + else: + path = sgroot / sgdir + + ret = {} + for dirent in os.listdir(path): + file_path = path / dirent + if file_path.is_file(): + if subdir: + fname = (os.path.join(subdir,dirent)) + else: + fname = dirent + + if self.game_data.match(fname): + ret[str(path)] = os.path.join(sgdir,fname) + elif file_path.is_dir(): + ret.update(get_backup_files_recursive(sgroot,sgdir,os.path.join(subdir,dirent))) + + return ret + + if not bool(self): + return None + + sgroot = pathlib.Path(self.savegame_root).resolve() + sgdir = self.savegame_dir + sgpath = sgroot / sgdir + if not os.path.exists(sgpath): + return None + + backup_files = get_backup_files_recursive(sgroot,sgdir) -GAMES={} -STEAM_GAMES={} -STEAM_LINUX_GAMES={} -STEAM_WINDOWS_GAMES={} -STEAM_MACOS_GAMES={} - -def __init_games(): - gameconf_dir = settings.gameconf_dir - if not os.path.isdir(gameconf_dir): - return - for gcf in (os.path.join(gameconf_dir,i) for i in os.listdir(gameconf_dir)): - if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'): - continue +class GameManager(GObject): + __global_gamemanager = None + + @staticmethod + def get_global(): + if GameManager.__global_gamemanager is None: + GameManager.__global_gamemanager = GameManager() + return GameManager.__global_gamemanager + + def __init__(self): + GObject.__init__(self) + + self.__games = {} + self.__steam_games = {} + self.__steam_linux_games = {} + self.__steam_windows_games = {} + self.__steam_macos_games = {} + + self.load() + + @Property(type=object) + def games(self)->dict[str:Game]: + return self.__games + + @Property(type=object) + def stam_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 = {} - try: - game = Game.new_from_json_file(gcf) - if not game: + gameconf_dir = settings.gameconf_dir + if not os.path.isdir(gameconf_dir): + return + + for gcf in (os.path.join(gameconf_dir,i) for i in os.listdir(gameconf_dir)): + if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'): continue - except: - continue + + try: + game = Game.new_from_json_file(gcf) + if not game: + continue + except Exception as ex: + logger.error("Unable to load gameconf {gameconf}! ({what})".format( + gameconf = os.path.basename(gcf), + what = str(ex))) + continue + + self.add_game(game) - GAMES[game.key] = game - if (game.steam_windows): - if not game.steam_windows.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_windows.appid] = game - STEAM_WINDOWS_GAMES[game.steam_windows.appid] = game - if (game.steam_linux): - if not game.steam_linux.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_linux.appid] = game - STEAM_LINUX_GAMES[game.steam_linux.appid] = game + def add_game(self,game:Game): + self.__[game.key] = game if (game.steam_macos): - if not game.steam_macos.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_macos.appid] = game - STEAM_MACOS_GAMES[game.steam_macos.appid] = game -__init_games() - -def add_game(game:Game): - GAMES[game.key] = game - if game.steam_windows: - if not game.steam_windows.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_windows.appid] = game - STEAM_WINDOWS_GAMES[game.steam_windows.appid] = game - if (game.steam_linux): - if not game.steam_linux.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_linux.appid] = game - STEAM_LINUX_GAMES[game.steam_linux.appid] = game - if (game.steam_macos): - if not game.steam_macos.appid in STEAM_GAMES: - STEAM_GAMES[game.steam_macos.appid] = game - STEAM_MACOS_GAMES[game.steam_macos.appid] = game - \ No newline at end of file + 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 diff --git a/sgbackup/gui/__init__.py b/sgbackup/gui/__init__.py index ee4060d..3769b57 100644 --- a/sgbackup/gui/__init__.py +++ b/sgbackup/gui/__init__.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # ############################################################################### -from ._app import Application,AppWindow +from ._app import Application,AppWindow,GameView,BackupView from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog diff --git a/sgbackup/gui/_app.py b/sgbackup/gui/_app.py index 71d2405..c7036d8 100644 --- a/sgbackup/gui/_app.py +++ b/sgbackup/gui/_app.py @@ -28,11 +28,19 @@ from .. import game from ..settings import settings from ._settingsdialog import SettingsDialog from ._gamedialog import GameDialog +from ..game import Game + +__gtype_name__ = __name__ class GameView(Gtk.ScrolledWindow): - __gtype_name__ = "sgbackup-gui-GameView" + __gtype_name__ = "GameView" def __init__(self): + """ + GameView The View for games. + + This is widget presents a clumnview for the installed games. + """ Gtk.ScrolledWindow.__init__(self) self.__liststore = Gio.ListStore.new(game.Game) @@ -78,7 +86,7 @@ class GameView(Gtk.ScrolledWindow): return self.__liststore @property - def _columnview(self): + def _columnview(self)->Gtk.ColumnView: return self.__columnview def _on_key_column_setup(self,factory,item): @@ -149,6 +157,15 @@ class GameView(Gtk.ScrolledWindow): dialog.props.secondary_use_markup = False dialog.connect('response',on_dialog_response) dialog.present() + + @property + def current_game(self)->Game|None: + selection = self._columnview.get_model() + pos = selection.get_selected() + if pos == Gtk.INVALID_LIST_POSITION: + return None + return selection.get_model().get_item(pos) + # GameView class class BackupViewData(GObject.GObject): @@ -188,6 +205,9 @@ class BackupViewData(GObject.GObject): @GObject.Property def timestamp(self): return self.__timestamp + + def _on_selection_changed(self,selection): + pass class BackupView(Gtk.ScrolledWindow): __gtype_name__ = "BackupView" @@ -443,6 +463,6 @@ class Application(Gtk.Application): flags=GObject.SignalFlags.RUN_LAST, return_type=None, arg_types=(SettingsDialog,)) - def settings_dialog_init(self,dialog): + def do_settings_dialog_init(self,dialog): pass diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py index a497f01..7d51d95 100644 --- a/sgbackup/gui/_gamedialog.py +++ b/sgbackup/gui/_gamedialog.py @@ -28,11 +28,32 @@ from ..game import ( SteamLinuxGame, SteamWindowsGame, SteamMacOSGame, + GameManager, ) class GameVariableData(GObject.GObject): + """ + GameVariableData The Gio.ListStore data for Variables. + """ + def __init__(self,name:str,value:str): + """ + GameVariableData + + :param name: The variable name + :type name: str + :param value: The variable value + :type value: str + + Properties + __________ + .. py:property:: name + :type: str + + .. py:property:: value + :type: str + """ GObject.GObject.__init__(self) self.name = name self.value = value @@ -52,13 +73,27 @@ class GameVariableData(GObject.GObject): self.__value = value class RegistryKeyData(GObject.GObject): - def __init__(self,regkey=None): + """ + RegistyKeyData The data for Windows registry keys. + """ + def __init__(self,regkey:str|None=None): + """ + RegistryKeyData + + :param regkey: The registry key ot set, defaults to None + :type regkey: str | None, optional + + Properties + __________ + .. py:property:: regkey + :type: str + """ GObject.GObject.__init__(self) if not regkey: self.__regkey = "" @GObject.Property(type=str) - def regkey(self): + def regkey(self)->str: return self.__regkey @regkey.setter def regkey(self,key:str): @@ -67,14 +102,35 @@ class RegistryKeyData(GObject.GObject): def __bool__(self): return bool(self.__regkey) + class GameFileMatcherData(GObject.GObject): + """ + GameFileMatcherData The data for the file matcher. + """ def __init__(self,match_type:GameFileType,match_value:str): + """ + GameFileMatcherData + + :param match_type: The type of the game file matcher. + :type match_type: GameFileType + :param match_value: The value to match the file. + :type match_value: str + + Properties + __________ + .. py:property:: match_type + :type: GameFileType + + .. py:property:: match_value + :type: str + """ GObject.GObject.__init__(self) self.match_type = match_type self.match_value = match_value @GObject.Property def match_type(self)->GameFileType: + return self.__match_type @match_type.setter def match_type(self,type:GameFileType): @@ -83,12 +139,30 @@ class GameFileMatcherData(GObject.GObject): @GObject.Property(type=str) def match_value(self)->str: return self.__match_value + @match_value.setter def match_value(self,value:str): self.__match_value = value class GameFileTypeData(GObject.GObject): + """ GameFileTypeData The *Gio.Liststore* data for GameFileType *Gtk.DropDown* widgets.""" def __init__(self,match_type:GameFileType,name:str): + """ + GameFileTypeData + + :param match_type: The matcher type + :type match_type: GameFileType + :param name: The name of the matcher type + :type name: str + + Properties: + ___________ + .. py:property:: match_type + :type: GameFileType + + .. py:property:: name + :type: str + """ GObject.GObject.__init__(self) self.__match_type = match_type self.__name = name @@ -102,7 +176,34 @@ class GameFileTypeData(GObject.GObject): return self.__name class SavegameTypeData(GObject.GObject): + """ + SavegameTypeData Holds the data for the SavegameType *Gtk.DropDown*. + """ + def __init__(self,type:SavegameType,name:str,icon_name:str): + """ + SavegameTypeData + + :param type: The SavegameType to select. + :type type: SavegameType + :param name: The name of the SavegameType. + :type name: str + :param icon_name: The Icon name to display for the SavegameType + :type icon_name: str + + Properties + __________ + + .. py:property:: savegame_type + :type: SavegameType + + .. py:property:: name + :type: str + + .. py:property:: icon_name + :type: str + + """ GObject.GObject.__init__(self) self.__sgtype = type self.__name = name @@ -122,7 +223,27 @@ class SavegameTypeData(GObject.GObject): class GameVariableDialog(Gtk.Dialog): + """ + GameVariableDialog The dialog for setting game variables. + + It is bound to on the GameDialog variable columnviews. This dialog + will update the given columnview automatically if the response is + *Gtk.Response.APPLY* and destroy itself on any response. + + If not variable is given, this dialog will create a new one + + """ def __init__(self,parent:Gtk.Window,columnview:Gtk.ColumnView,variable:GameVariableData|None=None): + """ + GameVariableDialog + + :param parent: The parent window (should be a GameDialog instance). + :type parent: Gtk.Window + :param columnview: The Columnview to operate on. + :type columnview: Gtk.ColumnView + :param variable: The variable to edit, defaults to None + :type variable: GameVariableData | None, optional + """ Gtk.Dialog.__init__(self) self.set_transient_for(parent) self.set_default_size(600,-1) @@ -186,14 +307,24 @@ class GameVariableDialog(Gtk.Dialog): model = self.__columnview.get_model().get_model() model.append(GameVariableData(self.__name_entry.get_text(),self.__value_entry.get_text())) self.hide() - self.destroy() + self.destroy() class GameDialog(Gtk.Dialog): def __init__(self, parent:Gtk.Window|None=None, - game:Game|None=Game): + game:Game|None=None): + """ + GameDialog This dialog is for setting game config. + + The dialog automatically saves the game. + + :param parent: The parent Window, defaults to None + :type parent: Gtk.Window | None, optional + :param game: The game to configure, defaults to None + :type game: Game | None, optional + """ Gtk.Dialog.__init__(self) if (parent): @@ -685,6 +816,9 @@ class GameDialog(Gtk.Dialog): return widget def reset(self): + """ + reset Resets the dialog to the Game set on init or clears the dialog if no Game was set. + """ self.__active_switch.set_active(True) self.__live_switch.set_active(True) self.__name_entry.set_text("") @@ -758,11 +892,11 @@ class GameDialog(Gtk.Dialog): #filematch fm_model = self.__windows.filematch.columnview.get_model().get_model() - for fm in self.__game.windows.filematch: + for fm in self.__game.windows.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__windows.ignorematch.columnview.get_model().get_model() - for im in self.__game.windows.ignorematch: + for im in self.__game.windows.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) # set lookup regkeys @@ -787,11 +921,11 @@ class GameDialog(Gtk.Dialog): #filematch fm_model = self.__linux.filematch.columnview.get_model().get_model() - for fm in self.__game.linux.filematch: + for fm in self.__game.linux.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__linux.ignorematch.columnview.get_model().get_model() - for im in self.__game.linux.ignorematch: + for im in self.__game.linux.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) var_model = self.__linux.variables.columnview.get_model().get_model() @@ -805,11 +939,11 @@ class GameDialog(Gtk.Dialog): #filematch fm_model = self.__macos.filematch.columnview.get_model().get_model() - for fm in self.__game.macos.filematch: + for fm in self.__game.macos.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__macos.ignorematch.columnview.get_model().get_model() - for im in self.__game.macos.ignorematch: + for im in self.__game.macos.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) var_model = self.__macos.variables.columnview.get_model().get_model() @@ -824,11 +958,11 @@ class GameDialog(Gtk.Dialog): #filematch fm_model = self.__steam_windows.filematch.columnview.get_model().get_model() - for fm in self.__game.steam_windows.filematch: + for fm in self.__game.steam_windows.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__steam_windows.ignorematch.columnview.get_model().get_model() - for im in self.__game.steam_windows.ignorematch: + for im in self.__game.steam_windows.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) var_model = self.__steam_windows.variables.columnview.get_model().get_model() @@ -842,11 +976,11 @@ class GameDialog(Gtk.Dialog): self.__steam_linux.installdir_entry.set_text(self.__game.steam_linux.installdir) fm_model = self.__steam_linux.filematch.columnview.get_model().get_model() - for fm in self.__game.steam_linux.filematch: + for fm in self.__game.steam_linux.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__steam_linux.ignorematch.columnview.get_model().get_model() - for im in self.__game.steam_linux.ignorematch: + for im in self.__game.steam_linux.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) var_model = self.__steam_linux.variables.columnview.get_model().get_model() @@ -860,11 +994,11 @@ class GameDialog(Gtk.Dialog): self.__steam_macos.installdir_entry.set_text(self.__game.steam_macos.installdir) fm_model = self.__steam_macos.filematch.columnview.get_model().get_model() - for fm in self.__game.steam_macos.filematch: + for fm in self.__game.steam_macos.file_matchers: fm_model.append(GameFileMatcherData(fm.match_type,fm.match_file)) im_model = self.__steam_macos.ignorematch.columnview.get_model().get_model() - for im in self.__game.steam_macos.ignorematch: + for im in self.__game.steam_macos.ignore_matchers: im_model.append(GameFileMatcherData(im.match_type,im.match_file)) var_model = self.__steam_macos.variables.columnview.get_model().get_model() @@ -873,6 +1007,9 @@ class GameDialog(Gtk.Dialog): # reset() def save(self): + """ + save Saves the game configuration to file. + """ def get_game_data(widget): fm_model = widget.filematch.columnview.get_model().get_model() im_model = widget.ignorematch.columnview.get_model().get_model() @@ -1078,14 +1215,28 @@ class GameDialog(Gtk.Dialog): self.__steam_macos = None self.__game.save() + GameManager.get_global().add_game(self.__game) + - def get_is_valid(self): + def get_is_valid(self)->bool: + """ + get_is_valid Check if the configuration is valid for saving. + + :returns: bool + """ if (self.__key_entry.get_text() and self.__name_entry.get_text() and self.__sgname_entry.get_text()): sgtype_data = self.__savegame_type_dropdown.get_selected_item() return self.get_is_valid_savegame_type(sgtype_data.savegame_type) return False def get_is_valid_savegame_type(self,sgtype:SavegameType)->bool: + """ + get_is_valid_savegame_type Check if the data for a SavegameType savegame is valid. + + :param sgtype: The type of the Savegame provider + :type: sgbackup.game.SavegameType + :returns: bool + """ def check_is_valid(widget): return (bool(widget.sgroot_entry.get_text()) and bool(widget.sgdir_entry.get_text())) diff --git a/sgbackup/icons/hicolor/org.sgabackup.sgbackup.gresource.xml b/sgbackup/icons/hicolor/org.sgabackup.sgbackup.gresource.xml deleted file mode 100644 index 9f01832..0000000 --- a/sgbackup/icons/hicolor/org.sgabackup.sgbackup.gresource.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - sgbackup.png - - - sgbackup.png - - - sgbackup.png - - - sgbackup.png - - - sgbackup.png - - - icons8-windows-10.svg - - \ No newline at end of file diff --git a/sgbackup/icons/hicolor/symbolic/apps/epic-games-svgrepo-com-symbolic.svg b/sgbackup/icons/hicolor/symbolic/apps/epic-games-svgrepo-com-symbolic.svg new file mode 100644 index 0000000..dbffdbc --- /dev/null +++ b/sgbackup/icons/hicolor/symbolic/apps/epic-games-svgrepo-com-symbolic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sgbackup/icons/hicolor/symbolic/apps/gog-com-svgrepo-com-symbolic.svg b/sgbackup/icons/hicolor/symbolic/apps/gog-com-svgrepo-com-symbolic.svg new file mode 100644 index 0000000..2bd5192 --- /dev/null +++ b/sgbackup/icons/hicolor/symbolic/apps/gog-com-svgrepo-com-symbolic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sgbackup/logger.conf b/sgbackup/logger.conf index 6c7d75f..6889fbb 100644 --- a/sgbackup/logger.conf +++ b/sgbackup/logger.conf @@ -8,7 +8,7 @@ keys=consoleHandler,fileHandler keys=consoleFormatter,fileFormatter [logger_root] -level=DEBUG +level=INFO handlers=consoleHandler,fileHandler [logger_console] diff --git a/sgbackup/steam.py b/sgbackup/steam.py index 11b9104..3dff60f 100644 --- a/sgbackup/steam.py +++ b/sgbackup/steam.py @@ -22,7 +22,7 @@ from pathlib import Path import sys import json from .settings import settings -from .game import STEAM_GAMES,STEAM_WINDOWS_GAMES,STEAM_LINUX_GAMES,STEAM_MACOS_GAMES +from .game import GameManager __gtype_name__ = __name__ @@ -102,7 +102,7 @@ class IgnoreSteamApp(GObject): appid = conf['appid'] name = conf['name'] reason = conf['reason'] if 'reason' in conf else "" - return SteamIgnoreApp(appid,name,reason) + return IgnoreSteamApp(appid,name,reason) return None @@ -331,23 +331,25 @@ class Steam(GObject): return sorted(new_apps) def update_steam_apps(self): + gm = GameManager.get_global() + for lib in self.libraries(): for app in lib.steam_apps: if PLATFORM_WINDOWS: - if ((app.appid in STEAM_WINDOWS_GAMES) - and (STEAM_WINDOWS_GAMES[app.appid].installdir != app.installdir)): - game = STEAM_WINDOWS_GAMES[app.appid] + if ((app.appid in gm.steam_windows_games) + and (gm.steam_windows_games[app.appid].installdir != app.installdir)): + game = gm.steam_windows_games[app.appid] game.installdir = app.installdir game.save() elif PLATFORM_LINUX: - if ((app.appid in STEAM_LINUX_GAMES) - and (STEAM_LINUX_GAMES[app.appid].installdir != app.installdir)): - game = STEAM_LINUX_GAMES[app.appid] + if ((app.appid in gm.steam_linux_games) + and (gm.steam_linux_games[app.appid].installdir != app.installdir)): + game = gm.steam_linux_games[app.appid] game.installdir = app.installdir game.save() elif PLATFORM_MACOS: - if ((app.appid in STEAM_MACOS_GAMES) - and (STEAM_MACOS_GAMES[app.appid].installdir != app.installdir)): - game = STEAM_MACOS_GAMES[app.appid] + if ((app.appid in gm.steam_macos_games) + and (gm.steam_macos_games[app.appid].installdir != app.installdir)): + game = gm.steam_macos_games[app.appid] game.installdir = app.installdir game.save() diff --git a/sphinx/conf.py b/sphinx/conf.py index a69df6a..9e8d1b6 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -27,7 +27,12 @@ extensions = [ ] language = 'en' master_doc = 'index' -source_suffix = '.rst' +source_suffix = { + '.rst': "restructuredtext", + '.txt': "restructuredtext", + '.md': 'markdown', + '.markdown': 'mardown', +} templates_path = ['templates'] html_theme = 'sphinx_rtd_theme' diff --git a/sphinx/index.rst b/sphinx/index.rst index b06ebab..80f3e01 100644 --- a/sphinx/index.rst +++ b/sphinx/index.rst @@ -26,4 +26,6 @@ Table of Contents modules/sgbackup.settings.rst modules/sgbackup.game.rst modules/sgbackup.archiver.rst + modules/sgbackup.gui.rst + diff --git a/sphinx/modules/sgbackup.game.rst b/sphinx/modules/sgbackup.game.rst new file mode 100644 index 0000000..4ae3038 --- /dev/null +++ b/sphinx/modules/sgbackup.game.rst @@ -0,0 +1,76 @@ +===================== +Module: sgbackup.game +===================== + +.. title:: sgbackup API documentation + +Game classes +------------ + +.. autoclass:: sgbackup.game.Game + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.GameManager + :members: + :undoc-members: + :show-inheritance: + + +GameData classes +---------------- + +.. autoclass:: sgbackup.game.GameData + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.WindowsGame + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.LinuxGame + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.MacOSGame + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.SteamWindowsGame + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.SteamLinuxGame + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: sgbackup.game.SteamMacOSGame + :members: + :undoc-members: + :show-inheritance: + + +Helper classes +-------------- + +.. autoclass:: sgbackup.game.GameFileMatcher + :members: + :show-inheritance: + +Enums +----- + +.. autoclass:: sgbackup.game.SavegameType + :members: + :show-inheritance: + +.. autoclass:: sgbackup.game.GameFileType + :members: + :show-inheritance: diff --git a/sphinx/modules/sgbackup.gui-app.rst b/sphinx/modules/sgbackup.gui-app.rst new file mode 100644 index 0000000..8e17314 --- /dev/null +++ b/sphinx/modules/sgbackup.gui-app.rst @@ -0,0 +1,18 @@ +============ +Applicaction +============ + +Applicaction +------------ + +.. autoclass:: sgbackup.gui.Application + :members: + :undoc-members: + +Application Window +------------------ + +.. autoclass:: sgbackup.gui.AppWindow + :members: + :undoc-members: + \ No newline at end of file diff --git a/sphinx/modules/sgbackup.gui-data.rst b/sphinx/modules/sgbackup.gui-data.rst new file mode 100644 index 0000000..9d905e9 --- /dev/null +++ b/sphinx/modules/sgbackup.gui-data.rst @@ -0,0 +1,22 @@ +========================== +Module: sgbackup.gui._game +========================== + +Game data classes +----------------- + +.. autoclass:: sgbackup.gui._gamedialog.GameFileMatcherData + :members: + +.. autoclass:: sgbackup.gui._gamedialog.GameFileTypeData + :members: + +.. autoclass:: sgbackup.gui._gamedialog.GameVariableData + :members: + +.. autoclass:: sgbackup.gui._gamedialog.RegistryKeyData + :members: + +.. autoclass:: sgbackup.gui._gamedialog.SavegameTypeData + :members: + diff --git a/sphinx/modules/sgbackup.gui-dialogs.rst b/sphinx/modules/sgbackup.gui-dialogs.rst new file mode 100644 index 0000000..bf9a22d --- /dev/null +++ b/sphinx/modules/sgbackup.gui-dialogs.rst @@ -0,0 +1,23 @@ +======= +Dialogs +======= + +.. title:: sgbackup API documentation + +SettingsDialog +--------------- + +.. autoclass:: sgbackup.gui.SettingsDialog + :members: + :undoc-members: + + +GameDialog +---------- + +.. autoclass:: sgbackup.gui.GameDialog + :members: + +.. autoclass:: sgbackup.gui._gamedialog.GameVariableDialog + :members: + diff --git a/sphinx/modules/sgbackup.gui-widgets.rst b/sphinx/modules/sgbackup.gui-widgets.rst new file mode 100644 index 0000000..89892f8 --- /dev/null +++ b/sphinx/modules/sgbackup.gui-widgets.rst @@ -0,0 +1,11 @@ +======= +Widgets +======= + +.. autoclass:: sgbackup.gui.GameView + :members: + :undoc-members: + +.. autoclass:: sgbackup.gui.BackupView + :members: + :undoc-members: diff --git a/sphinx/modules/sgbackup.gui.rst b/sphinx/modules/sgbackup.gui.rst new file mode 100644 index 0000000..f1487f4 --- /dev/null +++ b/sphinx/modules/sgbackup.gui.rst @@ -0,0 +1,11 @@ +===================== +Package: sgbackup.gui +===================== + +.. title:: sgbackup API documentation + +.. toctree:: 1 + sgbackup.gui-app.rst + sgbackup.gui-widgets.rst + sgbackup.gui-dialogs.rst + sgbackup.gui-data.rst diff --git a/sphinx/modules/sgbackup.rst b/sphinx/modules/sgbackup.rst index aa668e1..68281fb 100644 --- a/sphinx/modules/sgbackup.rst +++ b/sphinx/modules/sgbackup.rst @@ -4,7 +4,7 @@ sgbackup API .. title:: sgbackup API +.. toctree:: 1 + sgbackup.game.rst + sgbackup.gui.rst -.. automodule:: sgbackup - :imported-mebers: - :undoc-members: