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: