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)