diff --git a/msys-install.sh b/msys-install.sh index abcdbdc..fb551b0 100755 --- a/msys-install.sh +++ b/msys-install.sh @@ -85,8 +85,7 @@ EOF cat > "${venv_bin}/gsgbackup.cmd" << EOF @ECHO OFF -cd $Env:userprofile -"$wvenv_bin\\gsgbackup.ps1" %* +powershell -File "$wvenv_bin\\gsgbackup.ps1" %* EOF unix2dos "${bindir}/gsgbackup.cmd" diff --git a/sgbackup/epic.py b/sgbackup/epic.py new file mode 100644 index 0000000..e60bf44 --- /dev/null +++ b/sgbackup/epic.py @@ -0,0 +1,193 @@ +############################################################################### +# sgbackup - The SaveGame Backup tool # +# Copyright (C) 2024,2025 Christian Moser # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +############################################################################### + +import sys,os +from gi.repository import GLib +from gi.repository.GObject import GObject,Signal,SignalFlags,Property + +from .settings import settings +import json + +import logging +from i18n import gettext as _ + +logger = logging.getLogger(__name__) + +PLATFORM_WINDOWS = sys.platform.lower().startswith('win') + +class EpicGameInfo(GObject): + def __init__(self, + name:str, + installdir:str, + appname:str, + main_appname:str): + GObject.__init__(self) + + self.__name = name + self.__installdir = installdir + self.__appname = appname + self.__main_appname = main_appname + + + @Property(type=str) + def name(self)->str: + return self.__name + + @Property(type=str) + def installdir(self)->str: + return self.__installdir + + @Property(type=str) + def appname(self)->str: + return self.__appname + + @Property(type=str) + def main_appname(self)->str: + return self.__main_appname + + @Property(type=bool,default=False) + def is_main(self)->bool: + return (self.appname == self.main_appname) + +class EpicIgnoredApp(GObject): + def __init__(self,appname:str,name:str,reason:str): + GObject.__init__(self) + self.__appname = appname + self.__name = name + self.__reason = reason + + @Property(type=str) + def appname(self)->str: + return self.__appname + + @Property(type=str) + def name(self)->str: + return self.__name + + @Property(type=str) + def reason(self)->str: + return self.__reason + + def serialize(self): + return { + 'appname':self.appname, + 'name':self.name, + 'reason':self.reason, + } + + +class Epic(GObject): + _logger = logger.getChild('Epic') + + + def __init__(self): + GObject.__init__(self) + self.__ignored_apps=self.__parse_ignore_file() + + + @Property(type=str) + def ignore_file(self)->str: + return os.path.join(settings.config_dir,'epic.ignore') + + def __parse_ignore_file(self)->dict[str:EpicIgnoredApp]: + ret = {} + if os.path.isfile(self.ignore_file): + with open(self.ignore_file,'r',encoding="urf-8") as ifile: + data = json.loads(ifile.read()) + + for i in data: + ret[i['appname']] = EpicIgnoredApp(i['appname'],i['name'],i['reason']) + + return ret + + def __write_ignore_file(self): + with open(self.ignore_file,'w',encoding="utf-8") as ofile: + ofile.write(json.dumps( + [v.serialize() for v in self.__ignored_apps.values()], + ensure_ascii=False, + indent=4)) + + def add_ignored_app(self,app:EpicIgnoredApp): + if not isinstance(app,EpicIgnoredApp): + raise TypeError('app is not an EpicIgnoredApp instance!') + + self.__ignored_apps[app.appname] = app + self.__write_ignore_file() + + def remove_ignored_apps(self,app:str|EpicIgnoredApp): + if isinstance(app,str): + appname = app + elif isinstance(app,EpicIgnoredApp): + appname = app.appname + else: + raise TypeError("app is not a string and not an EpicIgnoredApp instance!") + + if appname in self.__ignored_apps: + del self.__ignored_apps[appname] + self.__write_ignore_file() + + @Property + def ignored_apps(self)->dict[str:EpicIgnoredApp]: + return self.__ignored_apps + + @Property(type=str) + def datadir(self): + return settings.epic_datadir if settings.epic_datadir is not None else "" + + def parse_manifest(self,filename)->EpicGameInfo|None: + if not os.path.exists(filename): + return None + if not filename.endswith('.item'): + return None + + try: + with open(filename,'r',encoding="utf-8") as ifile: + data = json.loads(ifile.read()) + except Exception as ex: + self._logger.error(_("Unable ot load Epic manifest \"{manifest}\"! ({error})").format( + manifest=filename, + error=str(ex) + )) + return None + + if data['FormatVersion'] == 0: + return EpicGameInfo( + name=data['DisplayName'], + installdir=data['InstallLocation'], + appname=data['AppName'], + main_appname=data['MainGameAppName'] + ) + return None + + def parse_all_manifests(self)->list[EpicGameInfo]: + manifest_dir=os.path.join(settings.epic_datadir,'Manifests') + ret = [] + for item in [ i for i in os.listdir(manifest_dir) if i.endswith('.item') ]: + manifest_file = os.path.join(manifest_dir,item) + info = self.parse_manifest(manifest_file) + if info is not None: + ret += info + + return ret + + def get_apps(self)->list[EpicGameInfo]: + return [i for i in self.parse_all_manifests() if i.appname == i.main_appname] + + def get_new_apps(self)->list[EpicGameInfo]: + return [] + \ No newline at end of file diff --git a/sgbackup/game.py b/sgbackup/game.py index 6173053..0177e5a 100644 --- a/sgbackup/game.py +++ b/sgbackup/game.py @@ -385,6 +385,11 @@ class GameData(GObject): raise TypeError("\"ignore_match\" needs to be \"None\" or a list of \"GameFileMatcher\" instances!") self.__ignorematchers = list(im) + @Property(type=bool,default=False) + def is_valid(self)->bool: + return (bool(self.__savegame_root) and bool(self.__savegame_dir) and (self.__savegame_type != SavegameType.UNSET)) + + def has_variable(self,name:str)->bool: """ has_variable Check if variable exists. @@ -1036,6 +1041,7 @@ class SteamMacOSData(SteamPlatformData): installdir=installdir, librarydir=librarydir) + class SteamGameData(GObject): def __init__(self,appid:int, windows:SteamWindowsData|None=None, @@ -1095,24 +1101,24 @@ class SteamGameData(GObject): def linux_game(self)->SteamLinuxGame|None: if self.linux: return SteamLinuxGame(appid=self.appid, - savegame_root=self.windows.savegame_root, - savegame_dir=self.windows.savegame_dir, - variables=self.windows.variables, - installdir=self.windows.installdir, - file_match=self.windows.file_matchers, - ignore_match=self.windows.ignore_matchers) + savegame_root=self.linux.savegame_root, + savegame_dir=self.linux.savegame_dir, + variables=self.linux.variables, + installdir=self.linux.installdir, + file_match=self.linux.file_matchers, + ignore_match=self.linux.ignore_matchers) return None @property def macos_game(self)->SteamLinuxGame|None: if self.macos: return SteamMacOSGame(appid=self.appid, - savegame_root=self.windows.savegame_root, - savegame_dir=self.windows.savegame_dir, - variables=self.windows.variables, - installdir=self.windows.installdir, - file_match=self.windows.file_matchers, - ignore_match=self.windows.ignore_matchers) + savegame_root=self.macos.savegame_root, + savegame_dir=self.macos.savegame_dir, + variables=self.macos.variables, + installdir=self.macos.installdir, + file_match=self.macos.file_matchers, + ignore_match=self.macos.ignore_matchers) return None def serialize(self)->dict: @@ -1127,7 +1133,99 @@ class SteamGameData(GObject): data['macos'] = self.macos.serialize() return data + + +class EpicPlatformData(GameData): + def __init__(self, + savegame_type:SavegameType, + savegame_root:str, + savegame_dir:str, + variables:dict[str:str], + file_match:list[GameFileMatcher], + ignore_match:list[GameFileMatcher], + installdir:str|None): + if savegame_type not in (SavegameType.EPIC_WINDOWS,SavegameType.EPIC_LINUX): + raise ValueError("Savegame type needs to be EPIC_WINDOWS or EPIC_LINUX!") + GameData.__init__(self, + savegame_type=savegame_type, + savegame_root=savegame_root, + savegame_dir=savegame_dir, + variables=variables, + file_match=file_match, + ignore_match=ignore_match) + + self.__installdir=installdir + + @Property + def installdir(self)->str: + return self.__installdir if self.__installdir else "" + + @installdir.setter + def installdir(self,directory:str|None): + self.__installdir = directory + + def serialize(self): + data = super().serialize() + if self.__installdir: + data['installdir'] = self.__installdir + return data + +class EpicWindowsData(EpicPlatformData): + def __init__(self, + savegame_root:str, + savegame_dir:str, + variables:dict[str:str], + file_match:list[GameFileMatcher], + ignore_match:list[GameFileMatcher], + installdir:str|None): + GameData.__init__(self, + savegame_type=SavegameType.EPIC_WINDOWS, + savegame_root=savegame_root, + savegame_dir=savegame_dir, + variables=variables, + file_match=file_match, + ignore_match=ignore_match, + installdir=installdir) + + +class EpicGameData(GObject): + def __init__(self,appname:str,windows:EpicWindowsData|None): + GObject.__init__(self) + self.__appname = appname + self.windows = windows + + + @Property(type=str) + def appname(self)->str: + return self.__appname + + @appname.setter + def appname(self,appname:str): + self.__appname = appname + + @Property + def windows(self)->EpicWindowsData|None: + return self.__windows + + @windows.setter + def windows(self,data:EpicWindowsData|None): + self.__windows = data + + @Property(type=bool,default=False) + def is_valid(self)->bool: + if (self.windows and self.windows.is_valid): + return True + + def serialize(self): + ret = { + "appname": self.appname + } + if self.windows and self.windows.is_valid: + ret["windows"] = self.windows.serialize() + + return ret + class Game(GObject): __gtype_name__ = "Game" @@ -1212,7 +1310,36 @@ class Game(GObject): linux=linux, macos=macos) # new_steamdata() - + + def new_epic_data(conf:dict): + def new_epic_platform_data(data,cls): + if not 'savegame_root' in data or not 'savegame_dir' in data: + return None + + file_match,ignore_match = get_file_match(data) + return cls( + savegame_root=data['savegame_root'], + savegame_dir=data['savegame_dir'], + variables=dict(((v['name'],v['value']) for v in data['variables'])) if ('variables' in data and config['variables']) else None, + file_match=file_match, + ignore_match=ignore_match, + installdir=data['installdir'] if ('installdir' in data and data['installdir']) else None, + librarydir=data['librarydir'] if ('librarydir' in data and data['librarydir']) else None + ) + + if not "epic" in conf or not "appname" in conf["epic"]: + return None + + if ("windows" in conf['epic']): + windows = new_epic_platform_data(conf['windows'],EpicWindowsData) + else: + windows = None + + return EpicGameData(conf['epic']['appname'], + windows=windows) + + # new_epic_data() + if not 'key' in config or not 'name' in config: return None @@ -1267,6 +1394,7 @@ class Game(GObject): game.macos = MacOSGame(sgroot,sgdir,vars,binary,file_match,ignore_match) game.steam = new_steamdata(config) + game.epic = new_epic_data(config) return game @@ -1300,13 +1428,7 @@ class Game(GObject): self.__steam = None self.__epic = None self.__gog = None - self.__steam_windows = None - self.__steam_linux = None - self.__steam_macos = None - self.__gog_windows = None - self.__gog_linux = None - self.__epic_windows = None - self.__epic_linux = None + self.__epic = None @Property(type=str) def dbid(self)->str: @@ -1405,19 +1527,23 @@ class Game(GObject): elif (sgtype == SavegameType.MACOS): return self.macos elif (sgtype == SavegameType.STEAM_WINDOWS): - return self.steam.windows_game + if self.steam: + return self.steam.windows_game elif (sgtype == SavegameType.STEAM_LINUX): - return self.steam.linux_game + if self.steam: + return self.steam.linux_game elif (sgtype == SavegameType.STEAM_MACOS): - return self.steam.macos_game - elif (sgtype == SavegameType.GOG_WINDOWS): - return self.__gog_windows - elif (sgtype == SavegameType.GOG_LINUX): - return self.__gog_linux + if self.steam: + return self.steam.macos_game + #elif (sgtype == SavegameType.GOG_WINDOWS): + # return self.__gog_windows + #elif (sgtype == SavegameType.GOG_LINUX): + # return self.__gog_linux elif (sgtype == SavegameType.EPIC_WINDOWS): - return self.__epic_windows + if self.epic: + return self.__epic.windows elif (sgtype == SavegameType.EPIC_LINUX): - return self.__epic_linux + return None return None @Property @@ -1504,6 +1630,14 @@ class Game(GObject): t = Template(self.game_data.savegame_dir) return t.safe_substitute(self.get_variables()) + @Property + def epic(self)->EpicGameData|None: + return self.__epic + + @epic.setter + def epic(self,epic:EpicGameData|None): + self.__epic = epic + def add_variable(self,name:str,value:str): self.__variables[str(name)] = str(value) @@ -1537,23 +1671,20 @@ class Game(GObject): if self.dbid: ret['dbid'] = self.dbid - if (self.windows): + if (self.windows and self.windows.is_valid): ret['windows'] = self.windows.serialize() - if (self.linux): + if (self.linux and self.linux.is_valid): ret['linux'] = self.linux.serialize() - if (self.macos): + if (self.macos and self.macos.is_valid): ret['macos'] = self.macos.serialize() if (self.steam): ret['steam'] = self.steam.serialize() - + if (self.epic and self.epic.is_valid): + ret['epic'] = self.epic.serialize() #if self.gog_windows: # ret['gog_windows'] = self.gog_windows.serialize() #if self.gog_linux: # ret['gog_linux'] = self.gog_linux.serialize() - #if self.epic_windows: - # ret['epic_windows'] = self.epic_windows.serialize() - #if self.epic_linux: - # ret['epic_linux'] = self.epic_linux.serialize() return ret diff --git a/sgbackup/gog.py b/sgbackup/gog.py new file mode 100644 index 0000000..d24c211 --- /dev/null +++ b/sgbackup/gog.py @@ -0,0 +1,17 @@ +############################################################################### +# sgbackup - The SaveGame Backup tool # +# Copyright (C) 2024,2025 Christian Moser # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +############################################################################### diff --git a/sgbackup/gui/_gamedialog.py b/sgbackup/gui/_gamedialog.py index 29d9c35..d8cf4b7 100644 --- a/sgbackup/gui/_gamedialog.py +++ b/sgbackup/gui/_gamedialog.py @@ -21,6 +21,8 @@ from .. import _import_gtk from gi.repository import Gio,GLib,Gtk,Pango from gi.repository.GObject import Property,Signal,GObject,BindingFlags,SignalFlags +from ..i18n import gettext as _, gettext_noop as N_ + from ..game import ( Game, GameData, @@ -30,13 +32,12 @@ from ..game import ( WindowsGame, LinuxGame, MacOSGame, - SteamLinuxGame, - SteamWindowsGame, - SteamMacOSGame, SteamGameData, SteamLinuxData, SteamMacOSData, SteamWindowsData, + EpicGameData, + EpicWindowsData, GameManager, ) @@ -244,14 +245,16 @@ class GameVariableDialog(Gtk.Dialog): self.__variable = None grid = Gtk.Grid() - label = Gtk.Label.new("Name:") + label = Gtk.Label.new(_("Name:")) + label.set_xalign(0.0) self.__name_entry = Gtk.Entry() self.__name_entry.set_hexpand(True) self.__name_entry.connect("changed",self._on_name_entry_changed) grid.attach(label,0,0,1,1) grid.attach(self.__name_entry,1,0,1,1) - label = Gtk.Label.new("Value") + label = Gtk.Label.new(_("Value:")) + label.set_xalign(0.0) self.__value_entry = Gtk.Entry() self.__value_entry.set_hexpand(True) @@ -326,9 +329,9 @@ class GameDialog(Gtk.Dialog): self.set_default_size(800,600) self.__filematch_dropdown_model = Gio.ListStore.new(GameFileTypeData) - self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.FILENAME,"Filename")) - self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.GLOB,"Glob")) - self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.REGEX,"Regular expression")) + self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.FILENAME,_("Filename"))) + self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.GLOB,_("Glob"))) + self.__filematch_dropdown_model.append(GameFileTypeData(GameFileType.REGEX,_("Regular expression"))) paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL) paned.set_position(200) @@ -360,6 +363,7 @@ class GameDialog(Gtk.Dialog): self.__linux = self.__create_linux_page() self.__macos = self.__create_macos_page() self.__steam = self.__create_steam_page() + self.__epic = self.__create_epic_page() for stack_page in self.__stack.get_pages(): @@ -396,18 +400,18 @@ class GameDialog(Gtk.Dialog): sgtype_data = ( (SavegameType.UNSET,"Not set","process-stop-symbolic"), - (SavegameType.WINDOWS,"Windows native","windows-svgrepo-com-symbolic"), - (SavegameType.STEAM_WINDOWS,"Steam Windows","steam-svgrepo-com-symbolic"), - #(SavegameType.EPIC_WINDOWS,"Epic Windows","object-select-symbolic"), + (SavegameType.WINDOWS,N_("Windows"),"windows-svgrepo-com-symbolic"), + (SavegameType.STEAM_WINDOWS,N_("Steam Windows"),"steam-svgrepo-com-symbolic"), + (SavegameType.EPIC_WINDOWS,N_("Epic Windows"),"epic-games-svgrepo-com-symbolic"), #(SavegameType.GOG_WINDOWS,"GoG Windows","object-select-symbolic"), - (SavegameType.LINUX,"Linux native","linux-svgrepo-com-symbolic"), - (SavegameType.STEAM_LINUX,"Steam Linux","steam-svgrepo-com-symbolic"), - #(SavegameType.EPIC_LINUX,"Epic Linux","object-select-symbolic"), + (SavegameType.LINUX,N_("Linux native"),"linux-svgrepo-com-symbolic"), + (SavegameType.STEAM_LINUX,_("Steam Linux"),"steam-svgrepo-com-symbolic"), + #(SavegameType.EPIC_LINUX,_("Epic Linux"),"epic-games-svgrepo-com-symbolic"), #(SavegameType.GOG_LINUX,"GoG Linux","object-select-symbolic"), - (SavegameType.MACOS,"MacOS native","apple-svgrepo-com-symbolic"), - (SavegameType.STEAM_MACOS,"Steam MacOS","steam-svgrepo-com-symbolic"), + (SavegameType.MACOS,_("MacOS"),"apple-svgrepo-com-symbolic"), + (SavegameType.STEAM_MACOS,_("Steam MacOS"),"steam-svgrepo-com-symbolic"), ) sgtype_model = Gio.ListStore.new(SavegameTypeData) for data in sgtype_data: @@ -415,14 +419,14 @@ class GameDialog(Gtk.Dialog): grid = Gtk.Grid.new() - label = Gtk.Label.new("Is active?") + label = self.__create_label(_("Is active?")) self.__active_switch = Gtk.Switch() entry_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,5) entry_hbox.append(self.__active_switch) entry_hbox.append(label) vbox.append(entry_hbox) - label = Gtk.Label.new("Is live?") + label = self.__create_label(_("Is live?")) self.__live_switch = Gtk.Switch() entry_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,5) @@ -430,13 +434,13 @@ class GameDialog(Gtk.Dialog): entry_hbox.append(label) vbox.append(entry_hbox) - label = Gtk.Label.new("ID:") + label = self.__create_label(_("ID:")) self.__id_label = Gtk.Label() self.__id_label.set_hexpand(True) grid.attach(label,0,0,1,1) grid.attach(self.__id_label,1,0,1,1) - label = Gtk.Label.new("Key:") + label = self.__create_label(_("Key:")) self.__key_entry = Gtk.Entry() self.__key_entry.set_hexpand(True) self.__key_entry.connect('changed',lambda w: self._on_savegame_type_changed()) @@ -451,18 +455,18 @@ class GameDialog(Gtk.Dialog): self.__savegame_type_dropdown.set_selected(0) self.__savegame_type_dropdown.set_hexpand(True) self.__savegame_type_dropdown.connect('notify::selected-item',lambda w,d: self._on_savegame_type_changed()) - label = Gtk.Label.new("Savegame Type:") + label = self.__create_label(_("Savegame Type:")) grid.attach(label,0,2,1,1) grid.attach(self.__savegame_type_dropdown,1,2,1,1) - label = Gtk.Label.new("Game name:") + label = self.__create_label(_("Game name:")) self.__name_entry = Gtk.Entry() self.__name_entry.set_hexpand(True) self.__name_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,3,1,1) grid.attach(self.__name_entry,1,3,1,1) - label = Gtk.Label.new("Savegame name:") + label = self.__create_label(_("Savegame name:")) self.__sgname_entry = Gtk.Entry() self.__sgname_entry.set_hexpand(True) self.__sgname_entry.connect('changed',lambda w: self._on_savegame_type_changed()) @@ -475,7 +479,7 @@ class GameDialog(Gtk.Dialog): vbox.append(self.__game_variables) page.set_child(vbox) - self.__stack.add_titled(page,"main","Game") + self.__stack.add_titled(page,"main",_("Game")) stack_page = self.__stack.get_page(page) stack_page.set_icon_name('org.sgbackup.sgbackup-symbolic') @@ -487,44 +491,44 @@ class GameDialog(Gtk.Dialog): grid = Gtk.Grid() - label = Gtk.Label.new("Root directory:") + label = self.__create_label(_("Root directory:")) page.sgroot_entry = Gtk.Entry() page.sgroot_entry.set_hexpand(True) page.sgroot_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,0,1,1) grid.attach(page.sgroot_entry,1,0,1,1) - label = Gtk.Label.new("Backup directory:") + label = self.__create_label(_("Backup directory:")) page.sgdir_entry = Gtk.Entry() page.sgdir_entry.set_hexpand(True) page.sgdir_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,1,1,1) grid.attach(page.sgdir_entry,1,1,1,1) - label = Gtk.Label.new("Installation directory:") + label = self.__create_label(("Installation directory:")) page.installdir_entry = Gtk.Entry() page.installdir_entry.set_hexpand(True) grid.attach(label,0,2,1,1) grid.attach(page.installdir_entry,1,2,1,1) vbox.append(grid) - page.filematch = self.__create_filematch_widget('Match Files') + page.filematch = self.__create_filematch_widget(_('Match Files')) vbox.append(page.filematch) - page.ignorematch = self.__create_filematch_widget('Ignore Files') + page.ignorematch = self.__create_filematch_widget(_('Ignore Files')) vbox.append(page.ignorematch) - page.lookup_regkeys = self.__create_registry_key_widget("Lookup Registry keys") + page.lookup_regkeys = self.__create_registry_key_widget(_("Lookup Registry keys")) vbox.append(page.lookup_regkeys) - page.installdir_regkeys = self.__create_registry_key_widget("Installations directory Registry keys") + page.installdir_regkeys = self.__create_registry_key_widget(_("Installations directory Registry keys")) vbox.append(page.installdir_regkeys) page.variables = self.__create_variables_widget() vbox.append(page.variables) page.set_child(vbox) - self.__stack.add_titled(page,"windows","Windows") + self.__stack.add_titled(page,"windows",_("Windows")) stack_page = self.__stack.get_page(page) stack_page.set_icon_name("windows-svgrepo-com-symbolic") @@ -537,21 +541,21 @@ class GameDialog(Gtk.Dialog): self.__set_widget_margin(vbox,5) grid = Gtk.Grid() - label = Gtk.Label.new("Root directory:") + label = self.__create_label(_("Root directory:")) page.sgroot_entry = Gtk.Entry() page.sgroot_entry.set_hexpand(True) page.sgroot_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,0,1,1) grid.attach(page.sgroot_entry,1,0,1,1) - label = Gtk.Label.new("Backup directory:") + label = self.__create_label(_("Backup directory:")) page.sgdir_entry = Gtk.Entry() page.sgdir_entry.set_hexpand(True) page.sgdir_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,1,1,1) grid.attach(page.sgdir_entry,1,1,1,1) - label = Gtk.Label.new("Executable") + label = self.__create_label(_("Executable:")) page.binary_entry = Gtk.Entry() page.binary_entry.set_hexpand(True) grid.attach(label,0,2,1,1) @@ -568,7 +572,7 @@ class GameDialog(Gtk.Dialog): vbox.append(page.variables) page.set_child(vbox) - self.__stack.add_titled(page,"linux","Linux") + self.__stack.add_titled(page,"linux",_("Linux")) stack_page = self.__stack.get_page(page) stack_page.set_icon_name("linux-svgrepo-com-symbolic") @@ -580,21 +584,21 @@ class GameDialog(Gtk.Dialog): self.__set_widget_margin(vbox,5) grid = Gtk.Grid() - label = Gtk.Label.new("Root directory:") + label = self.__create_label(_("Root directory:")) page.sgroot_entry = Gtk.Entry() page.sgroot_entry.set_hexpand(True) page.sgroot_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,0,1,1) grid.attach(page.sgroot_entry,1,0,1,1) - label = Gtk.Label.new("Backup directory:") + label = self.__create_label(_("Backup directory:")) page.sgdir_entry = Gtk.Entry() page.sgdir_entry.set_hexpand(True) page.sgdir_entry.connect('changed',lambda w: self._on_savegame_type_changed()) grid.attach(label,0,1,1,1) grid.attach(page.sgdir_entry,1,1,1,1) - label = Gtk.Label.new("Executable") + label = self.__create_label(_("Executable")) page.binary_entry = Gtk.Entry() page.binary_entry.set_hexpand(True) grid.attach(label,0,2,1,1) @@ -611,7 +615,7 @@ class GameDialog(Gtk.Dialog): vbox.append(page.variables) page.set_child(vbox) - self.__stack.add_titled(page,"macos","MacOS") + self.__stack.add_titled(page,"macos",_("MacOS")) stack_page = self.__stack.get_page(page) stack_page.set_icon_name("apple-svgrepo-com-symbolic") @@ -631,21 +635,21 @@ class GameDialog(Gtk.Dialog): nbgrid = Gtk.Grid() self.__set_widget_margin(nbgrid,5) - label = Gtk.Label.new("Root directory:") + label = self.__create_label(_("Root directory:")) nbpage.sgroot_entry = Gtk.Entry() nbpage.sgroot_entry.set_hexpand(True) nbpage.sgroot_entry.connect('changed',lambda w: self._on_savegame_type_changed()) nbgrid.attach(label,0,0,1,1) nbgrid.attach(nbpage.sgroot_entry,1,0,1,1) - label = Gtk.Label.new("Backup directory:") + label = self.__create_label(_("Backup directory:")) nbpage.sgdir_entry = Gtk.Entry() nbpage.sgdir_entry.set_hexpand(True) nbpage.sgdir_entry.connect('changed',lambda w: self._on_savegame_type_changed()) nbgrid.attach(label,0,1,1,1) nbgrid.attach(nbpage.sgdir_entry,1,1,1,1) - label = Gtk.Label.new("Installation directory:") + label = self.__create_label(_("Installation directory:")) nbpage.installdir_entry = Gtk.Entry() nbpage.installdir_entry.set_hexpand(True) nbgrid.attach(label,0,3,1,1) @@ -681,13 +685,13 @@ class GameDialog(Gtk.Dialog): page.notebook = Gtk.Notebook() page.notebook.set_hexpand(True) page.notebook.set_vexpand(True) - page.windows,nb_label = create_notebook_page('Windows','windows-svgrepo-com-symbolic') + page.windows,nb_label = create_notebook_page(_('Windows'),'windows-svgrepo-com-symbolic') page.notebook.append_page(page.windows,nb_label) - page.linux,nb_label = create_notebook_page("Linux",'linux-svgrepo-com-symbolic') + page.linux,nb_label = create_notebook_page(_("Linux"),'linux-svgrepo-com-symbolic') page.notebook.append_page(page.linux,nb_label) - page.macos,nb_label = create_notebook_page("Mac OS",'apple-svgrepo-com-symbolic') + page.macos,nb_label = create_notebook_page(_("Mac OS"),'apple-svgrepo-com-symbolic') page.notebook.append_page(page.macos,nb_label) page.append(page.notebook) @@ -697,6 +701,80 @@ class GameDialog(Gtk.Dialog): return page + def __create_epic_page(self): + def create_notebook_page(title,icon_name): + label_widget = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,2) + label_icon = Gtk.Image.new_from_icon_name(icon_name) + label_icon.set_pixel_size(12) + label=Gtk.Label.new(title) + label_widget.append(label_icon) + label_widget.append(label) + + nbpage = Gtk.ScrolledWindow() + nbvbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,5) + nbgrid = Gtk.Grid() + self.__set_widget_margin(nbgrid,5) + + label = self.__create_label(_("Root directory:")) + nbpage.sgroot_entry = Gtk.Entry() + nbpage.sgroot_entry.set_hexpand(True) + nbpage.sgroot_entry.connect('changed',lambda w: self._on_savegame_type_changed()) + nbgrid.attach(label,0,0,1,1) + nbgrid.attach(nbpage.sgroot_entry,1,0,1,1) + + label = self.__create_label(_("Backup directory:")) + nbpage.sgdir_entry = Gtk.Entry() + nbpage.sgdir_entry.set_hexpand(True) + nbpage.sgdir_entry.connect('changed',lambda w: self._on_savegame_type_changed()) + nbgrid.attach(label,0,1,1,1) + nbgrid.attach(nbpage.sgdir_entry,1,1,1,1) + + label = self.__create_label(_("Installation directory:")) + nbpage.installdir_entry = Gtk.Entry() + nbpage.installdir_entry.set_hexpand(True) + nbgrid.attach(label,0,3,1,1) + nbgrid.attach(nbpage.installdir_entry,1,3,1,1) + + nbvbox.append(nbgrid) + + nbpage.filematch = self.__create_filematch_widget('Match Files') + nbvbox.append(nbpage.filematch) + + nbpage.ignorematch = self.__create_filematch_widget('Ignore Files') + nbvbox.append(nbpage.ignorematch) + + nbpage.variables = self.__create_variables_widget() + nbvbox.append(nbpage.variables) + + nbpage.set_child(nbvbox) + return nbpage,label_widget + # create_notebook_page() + + page = Gtk.Box.new(Gtk.Orientation.VERTICAL,2) + grid = Gtk.Grid() + + label = self.__create_label(_("AppName:")) + page.appname_entry = Gtk.Entry() + page.appname_entry.set_hexpand(True) + grid.attach(label,0,0,1,1) + grid.attach(page.appname_entry,1,0,1,1) + page.append(grid) + + page.notebook = Gtk.Notebook() + page.notebook.set_hexpand(True) + page.notebook.set_vexpand(True) + + page.windows,nb_label = create_notebook_page(_("Windows"),'windows-svgrepo-com-symbolic') + page.notebook.append_page(page.windows,nb_label) + + page.append(page.notebook) + + self.__stack.add_titled(page,'epic','Epic Games') + stack_page = self.__stack.get_page(page) + stack_page.set_icon_name("epic-games-svgrepo-com-symbolic") + + return page + def __set_widget_margin(self,widget,margin): widget.set_margin_start(margin) widget.set_margin_end(margin) @@ -758,6 +836,11 @@ class GameDialog(Gtk.Dialog): widget.set_child(vbox) return widget + def __create_label(self,text:str): + label = Gtk.Label.new(text) + label.set_xalign(0.0) + return label + def __create_filematch_dropdown(self,item,widget): factory = Gtk.SignalListItemFactory() factory.connect('setup',self._on_filematch_type_dropdown_setup) @@ -928,6 +1011,17 @@ class GameDialog(Gtk.Dialog): and self.__game.steam.macos and self.__game.steam.macos.installdir else "") + + # Epic Games + set_game_widget_data(self.__epic.windows,self.__game.epic.windows if self.has_game and self.__game.epic else None) + + self.__epic.appname_entry.set_text(self.__game.epic.appname if self.has_game and self.__game.epic else "") + self.__epic.windows.installdir_entry.set_text(self.__game.epic.windows.installdir + if self.has_game + and self.__game.epic + and self.__game.epic.windows + and self.__game.epic.windows.installdir + else "") # reset() def save(self): @@ -970,6 +1064,9 @@ class GameDialog(Gtk.Dialog): }) return conf + def get_epic_data(widget): + conf = get_game_data(widget) + conf.update({'installdir':widget.installdir_entry.get_text()}) if not self.get_is_valid(): return @@ -1145,7 +1242,32 @@ class GameDialog(Gtk.Dialog): ignore_match=data['ignorematch']) elif self.__game.steam.macos: self.__game.steam.macos = None - # bEND: steam + # END: steam + + # BEGIN: epic + if self.get_is_valid_savegame_type(SavegameType.EPIC_WINDOWS): + data = get_epic_data(self.__epic.windows) + + if self.__game.epic: + self.__game.epic.appname = self.__epic.appname_entry.get_text() + else: + self.__game.epic = EpicGameData(appname=self.__epic.appname_entry.get_text()) + + if self.__game.epic.windows: + self.__game.epic.windows.savegame_root = data['sgroot'] + self.__game.epic.windows.savegame_dir = data['sgdir'] + self.__game.epic.windows.variables = data['variables'] + self.__game.epic.windows.file_matchers = data['filematch'] + self.__game.epic.windows.ignore_matchers = data['ignorematch'] + self.__game.epic.windows.installdir = data['installdir'] + else: + self.__game.epic.windows = EpicWindowsData(savegame_root=data['sgroot'], + savegame_dir=data['sgdir'], + variables=data['variables'], + file_match=data['filematch'], + ignore_match=data['ignorematch'], + installdir=data['installdir']) + # END: epic self.__game.save() GameManager.get_global().add_game(self.__game) @@ -1189,8 +1311,8 @@ class GameDialog(Gtk.Dialog): return check_is_valid(self.__steam.linux) elif sgtype == SavegameType.STEAM_MACOS: return check_is_valid(self.__steam.macos) - #elif sgtype == SavegameType.EPIC_WINDOWS: - # return check_is_valid(self.__epic_windows) + elif sgtype == SavegameType.EPIC_WINDOWS: + return check_is_valid(self.__epic.windows) #elif sgtype == SavegameType.EPIC_LINUX: # return check_is_valid(self.__epic_linux) #elif sgtype == SavegameType.GOG_WINDOWS: diff --git a/sgbackup/i18n.py b/sgbackup/i18n.py index 2ddb9eb..e1ef3e6 100644 --- a/sgbackup/i18n.py +++ b/sgbackup/i18n.py @@ -27,4 +27,5 @@ gettext = lambda s: _gettext.dgettext(TEXTDOMAIN,s) pgettext = lambda context,s: _gettext.dpgettext(TEXTDOMAIN,context,s) ngettext = lambda singular,plural,n: _gettext.dngettext(TEXTDOMAIN,singular,plural,n) npgettext = lambda context,singular,plural,n: _gettext.dnpgettext(TEXTDOMAIN,context,singular,plural,n) -noop = lambda s: s +def gettext_noop(msgid:str): return msgid +def noop(msgid:str): return msgid diff --git a/sgbackup/settings.py b/sgbackup/settings.py index 5db9694..29596f5 100644 --- a/sgbackup/settings.py +++ b/sgbackup/settings.py @@ -405,7 +405,6 @@ class Settings(GObject.GObject): skey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,i) svalue = winreg.QueryValueEx(skey,'InstallPath')[0] if svalue: - self.set_string('steam','installpath',svalue) return svalue except: continue @@ -418,6 +417,36 @@ class Settings(GObject.GObject): def steam_installpath(self,path:str): self.set_string('steam','installpath',path) + @GObject.Property + def epic_datadir(self)->str|None: + datadir = self.get_string('epic',"dataDir",None) + if datadir is None and PLATFORM_WINDOWS: + for i in ("SOFTWARE\\WOW6432Node\\Epic Games\\EpicGamesLauncher","SOFTWARE\\Epic Games\\EpicGamesLauncher"): + try: + skey = None + skey = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,i) + svalue = winreg.QueryValueEx(skey,'AppDataPath')[0] + if svalue: + datadir = svalue + break + except: + continue + finally: + if skey: + skey.Close() + return datadir + + @epic_datadir.setter + def epic_datadir(self,datadir:str|None): + if not datadir: + if self.has_key('epic',"dataDir"): + self.remove_key('epic','dataDir') + return + + if not os.path.isabs(datadir): + raise ValueError("\"epic_datadir\" is not an absolute path!") + self.set_string('epic','dataDir',datadir) + @GObject.Property def cli_pager(self)->str: pager = self.get_string('cli','pager',None)