2025.01.07 09:40:43

This commit is contained in:
Christian Moser 2025-01-07 09:40:43 +01:00
parent ab4b11f118
commit ec55f3b8eb
19 changed files with 822 additions and 162 deletions

View File

@ -16,5 +16,51 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository.GObject import (
GObject,
Property,
Signal,
SignalFlags,
signal_accumulator_true_handled,
)
from .game import Game
class Archiver:
pass
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

View File

@ -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
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

View File

@ -16,7 +16,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
###############################################################################
from ._app import Application,AppWindow
from ._app import Application,AppWindow,GameView,BackupView
from ._settingsdialog import SettingsDialog
from ._gamedialog import GameDialog

View File

@ -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

View File

@ -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()))

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/sgbackup/sgbackup/icons/32x32/apps">
<file>sgbackup.png</file>
</gresource>
<gresource prefix="/org/sgbackup/sgbacukp/icons/64x64/apps">
<file>sgbackup.png</file>
</gresource>
<gresource prefix="/org/sgbackup/sgbackup/icons/128x128/apps">
<file>sgbackup.png</file>
</gresource>
<gresource prefix="/org/sgbackup/sgbackup/icons/256x256/apps">
<file>sgbackup.png</file>
</gresource>
<gresource prefix="/org/sgbackup/sgbackup/icons/512x512/apps">
<file>sgbackup.png</file>
</gresource>
<gresource prefix="/org/sgbackup/sgbackup/icons/scalable/apps">
<file>icons8-windows-10.svg</file>
</gresource>
</gresources>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M9.531 20.317h-3.719c-0.291 0-0.531 0.24-0.531 0.537v2.667c0 0.281 0.24 0.531 0.531 0.531h3.735v1.76h-4.667c-0.744 0-1.359-0.615-1.359-1.375v-4.516c0-0.749 0.615-1.359 1.375-1.359h4.635zM10.88 15.385c0 0.776-0.625 1.401-1.401 1.401h-5.973v-1.803h5.041c0.297 0 0.532-0.235 0.532-0.531v-5.932c0-0.297-0.235-0.537-0.532-0.537h-2.692c-0.303-0.005-0.548 0.235-0.548 0.537v2.692c0 0.308 0.24 0.532 0.532 0.532h2.161v1.801h-3.093c-0.771 0-1.401-0.615-1.401-1.385v-4.588c0-0.761 0.631-1.385 1.401-1.385h4.563c0.771 0 1.395 0.624 1.395 1.385v7.812zM28.479 25.812h-1.76v-5.495h-1.24c-0.291 0-0.531 0.24-0.531 0.537v4.957h-1.776v-5.495h-1.24c-0.292 0-0.531 0.24-0.531 0.537v4.957h-1.776v-5.891c0-0.749 0.615-1.359 1.375-1.359h7.479zM28.495 15.385c0 0.776-0.631 1.401-1.401 1.401h-5.973v-1.803h5.041c0.292 0 0.532-0.235 0.532-0.531v-5.932c0-0.297-0.24-0.537-0.532-0.537h-2.708c-0.297 0-0.532 0.24-0.532 0.537v2.692c0 0.308 0.24 0.532 0.532 0.532h2.161v1.801h-3.084c-0.771 0-1.395-0.615-1.395-1.385v-4.588c0-0.761 0.624-1.385 1.395-1.385h4.573c0.776 0 1.401 0.624 1.401 1.385v7.812zM18.292 6.188h-4.584c-0.776 0-1.391 0.624-1.391 1.385v4.588c0 0.771 0.615 1.385 1.391 1.385h4.584c0.76 0 1.391-0.615 1.391-1.385v-4.588c0-0.761-0.631-1.385-1.391-1.385zM17.896 8.521v2.692c0 0.297-0.24 0.532-0.536 0.532h-2.709c-0.291 0-0.531-0.235-0.531-0.532v-2.683c0-0.291 0.229-0.531 0.531-0.531h2.683c0.307 0 0.531 0.24 0.531 0.531zM16.839 18.563h-4.521c-0.755 0-1.369 0.609-1.369 1.359v4.516c0 0.76 0.615 1.375 1.369 1.375h4.521c0.76 0 1.375-0.615 1.375-1.375v-4.516c0-0.749-0.615-1.359-1.375-1.359zM16.437 20.855v2.667c0 0.291-0.235 0.531-0.531 0.531v-0.011h-2.652c-0.296 0-0.536-0.239-0.536-0.536v-2.651c0-0.292 0.24-0.537 0.536-0.537h2.667c0.292 0 0.532 0.245 0.532 0.537zM31.317 1.469c-0.432-0.448-1.031-0.693-1.651-0.699h-27.333c-1.292-0.005-2.339 1.041-2.333 2.333v25.792c-0.005 1.292 1.041 2.339 2.333 2.333h27.333c1.292 0.005 2.339-1.041 2.333-2.333v-25.792c0-0.635-0.265-1.224-0.683-1.651zM31.317 28.896c0.011 0.911-0.733 1.656-1.651 1.651h-27.333c-0.921 0.016-1.672-0.735-1.667-1.651v-25.792c-0.005-0.911 0.74-1.656 1.651-1.651h27.333c0.917 0 1.656 0.74 1.656 1.651v25.792z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -8,7 +8,7 @@ keys=consoleHandler,fileHandler
keys=consoleFormatter,fileFormatter
[logger_root]
level=DEBUG
level=INFO
handlers=consoleHandler,fileHandler
[logger_console]

View File

@ -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()

View File

@ -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'

View File

@ -26,4 +26,6 @@ Table of Contents
modules/sgbackup.settings.rst
modules/sgbackup.game.rst
modules/sgbackup.archiver.rst
modules/sgbackup.gui.rst

View File

@ -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:

View File

@ -0,0 +1,18 @@
============
Applicaction
============
Applicaction
------------
.. autoclass:: sgbackup.gui.Application
:members:
:undoc-members:
Application Window
------------------
.. autoclass:: sgbackup.gui.AppWindow
:members:
:undoc-members:

View File

@ -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:

View File

@ -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:

View File

@ -0,0 +1,11 @@
=======
Widgets
=======
.. autoclass:: sgbackup.gui.GameView
:members:
:undoc-members:
.. autoclass:: sgbackup.gui.BackupView
:members:
:undoc-members:

View File

@ -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

View File

@ -4,7 +4,7 @@ sgbackup API
.. title:: sgbackup API
.. toctree:: 1
sgbackup.game.rst
sgbackup.gui.rst
.. automodule:: sgbackup
:imported-mebers:
:undoc-members: