added EpicGames support

This commit is contained in:
Christian Moser 2025-03-13 00:15:58 +01:00
parent f3ef3df537
commit db8a01d43d
Failed to extract signature
8 changed files with 345 additions and 69 deletions

View File

@ -24,24 +24,24 @@ from .settings import settings
import json
import logging
from i18n import gettext as _
from .i18n import gettext as _
from .game import GameManager
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):
catalog_item_id:str,
main_catalog_item_id:str):
GObject.__init__(self)
self.__name = name
self.__installdir = installdir
self.__appname = appname
self.__main_appname = main_appname
self.__catalog_item_id = catalog_item_id
self.__main_catalog_item_id = main_catalog_item_id
@Property(type=str)
@ -53,27 +53,27 @@ class EpicGameInfo(GObject):
return self.__installdir
@Property(type=str)
def appname(self)->str:
return self.__appname
def catalog_item_id(self)->str:
return self.__catalog_item_id
@Property(type=str)
def main_appname(self)->str:
return self.__main_appname
def main_catalog_item_id(self)->str:
return self.__main_catalog_item_id
@Property(type=bool,default=False)
def is_main(self)->bool:
return (self.appname == self.main_appname)
return (self.catalog_item_id == self.main_catalog_item_id)
class EpicIgnoredApp(GObject):
def __init__(self,appname:str,name:str,reason:str):
def __init__(self,catalog_item_id:str,name:str,reason:str):
GObject.__init__(self)
self.__appname = appname
self.__catalog_item_id = catalog_item_id
self.__name = name
self.__reason = reason
@Property(type=str)
def appname(self)->str:
return self.__appname
def catalog_item_id(self)->str:
return self.__catalog_item_id
@Property(type=str)
def name(self)->str:
@ -85,7 +85,7 @@ class EpicIgnoredApp(GObject):
def serialize(self):
return {
'appname':self.appname,
'catalog_item_id':self.catalog_item_id,
'name':self.name,
'reason':self.reason,
}
@ -107,11 +107,11 @@ class Epic(GObject):
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:
with open(self.ignore_file,'r',encoding="utf-8") as ifile:
data = json.loads(ifile.read())
for i in data:
ret[i['appname']] = EpicIgnoredApp(i['appname'],i['name'],i['reason'])
ret[i['catalog_item_id']] = EpicIgnoredApp(i['catalog_item_id'],i['name'],i['reason'])
return ret
@ -126,24 +126,24 @@ class Epic(GObject):
if not isinstance(app,EpicIgnoredApp):
raise TypeError('app is not an EpicIgnoredApp instance!')
self.__ignored_apps[app.appname] = app
self.__ignored_apps[app.catalog_item_id] = app
self.__write_ignore_file()
def remove_ignored_apps(self,app:str|EpicIgnoredApp):
if isinstance(app,str):
appname = app
item_id = app
elif isinstance(app,EpicIgnoredApp):
appname = app.appname
item_id = app.catalog_item_id
else:
raise TypeError("app is not a string and not an EpicIgnoredApp instance!")
if appname in self.__ignored_apps:
del self.__ignored_apps[appname]
if item_id in self.__ignored_apps:
del self.__ignored_apps[item_id]
self.__write_ignore_file()
@Property
def ignored_apps(self)->dict[str:EpicIgnoredApp]:
return self.__ignored_apps
return dict(self.__ignored_apps)
@Property(type=str)
def datadir(self):
@ -169,8 +169,8 @@ class Epic(GObject):
return EpicGameInfo(
name=data['DisplayName'],
installdir=data['InstallLocation'],
appname=data['AppName'],
main_appname=data['MainGameAppName']
catalog_item_id=data['CatalogItemId'],
main_catalog_item_id=data['MainGameCatalogItemId']
)
return None
@ -181,13 +181,15 @@ class Epic(GObject):
manifest_file = os.path.join(manifest_dir,item)
info = self.parse_manifest(manifest_file)
if info is not None:
ret += info
ret.append(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 find_apps(self)->list[EpicGameInfo]:
return [i for i in self.parse_all_manifests() if i.is_main]
def get_new_apps(self)->list[EpicGameInfo]:
return []
def find_new_apps(self)->list[EpicGameInfo]:
gm = GameManager.get_global()
return [i for i in self.find_apps()
if not gm.has_epic_game(i.catalog_item_id) and not i.catalog_item_id in self.__ignored_apps]

View File

@ -1207,19 +1207,21 @@ class EpicWindowsData(EpicPlatformData):
class EpicGameData(GObject):
def __init__(self,appname:str,windows:EpicWindowsData|None):
def __init__(self,
catalog_item_id:str|None=None,
windows:EpicWindowsData|None=None):
GObject.__init__(self)
self.__appname = appname
self.__catalog_item_id = catalog_item_id
self.windows = windows
@Property(type=str)
def appname(self)->str:
return self.__appname
def catalog_item_id(self)->str:
return self.__catalog_item_id if self.__catalog_item_id else ""
@appname.setter
def appname(self,appname:str):
self.__appname = appname
@catalog_item_id.setter
def catalog_item_id(self,catalog_item_id:str):
self.__catalog_item_id = catalog_item_id
@Property
def windows(self)->EpicWindowsData|None:
@ -1236,7 +1238,7 @@ class EpicGameData(GObject):
def serialize(self):
ret = {
"appname": self.appname
"catalog_item_id": self.catalog_item_id
}
if self.windows and self.windows.is_valid:
ret["windows"] = self.windows.serialize()
@ -1299,7 +1301,7 @@ class Game(GObject):
librarydir=data['librarydir'] if ('librarydir' in data and data['librarydir']) else None
)
if ('steam' not in conf or not 'appid' in conf['steam']):
if ('steam' not in conf):
return None
steam=conf['steam']
@ -1322,7 +1324,7 @@ class Game(GObject):
if windows is None and linux is None and macos is None:
return None
return SteamGameData(steam['appid'],
return SteamGameData(steam['appid'] if 'appid' in steam else -1,
windows=windows,
linux=linux,
macos=macos)
@ -1344,7 +1346,7 @@ class Game(GObject):
librarydir=data['librarydir'] if ('librarydir' in data and data['librarydir']) else None
)
if not "epic" in conf or not "appname" in conf["epic"]:
if not "epic" in conf:
return None
if ("windows" in conf['epic']):
@ -1352,7 +1354,7 @@ class Game(GObject):
else:
windows = None
return EpicGameData(conf['epic']['appname'],
return EpicGameData(conf['epic']['catalog_item_id'] if 'catalog_item_id' in conf['epic'] else "",
windows=windows)
# new_epic_data()
@ -1811,8 +1813,17 @@ class GameManager(GObject):
@Property
def steam_games(self)->dict[int:Game]:
return self.__steam_games
return dict(self.__steam_games)
def has_steam_game(self,appid:int)->bool:
return (appid in self.__steam_games)
@Property
def epic_games(self)->dict[str:Game]:
return dict(self.__epic_games)
def has_epic_game(self,catalog_item_id:str)->bool:
return (catalog_item_id in self.__epic_games)
def load(self):
if self.__games:
self.__games = {}
@ -1844,7 +1855,7 @@ class GameManager(GObject):
if game.steam:
self.__steam_games[game.steam.appid] = game
if game.epic:
self.__epic_games[game.epic.appname] = game
self.__epic_games[game.epic.catalog_item_id] = game
def remove_game(self,game:Game|str):
if isinstance(game,str):

View File

@ -46,8 +46,9 @@ from ._steam import (
SteamNoIgnoredAppsDialog,
SteamIgnoreAppsDialog,
)
from ..steam import Steam
from ..epic import Epic
from ._epic import EpicNewAppsDialog
from ._backupdialog import BackupSingleDialog,BackupManyDialog
from ..archiver import ArchiverManager
from ._dialogs import (
@ -392,7 +393,7 @@ class GameView(Gtk.Box):
def do_game_live_changed(self,game:Game):
pass
def _on_new_steamapps_dialog_response(self,dialog,response):
def _on_new_apps_dialog_response(self,dialog,response):
self.refresh()
def _on_add_game_button_clicked(self,button):
@ -404,15 +405,28 @@ class GameView(Gtk.Box):
steam = Steam()
if steam.find_new_steamapps():
dialog = NewSteamAppsDialog(parent=self.get_root())
dialog.connect_after('response',self._on_new_steamapps_dialog_response)
dialog.connect_after('response',self._on_new_apps_dialog_response)
dialog.present()
else:
dialog = SteamNoNewAppsDialog(parent=self.get_root())
dialog.present()
def _on_new_epic_games_button_clicked(self,button):
# TODO
pass
epic = Epic()
if not epic.find_new_apps():
dialog = Gtk.MessageDialog(
transient_for=self.get_root(),
message="No new Epic-Games applications found!",
buttons=Gtk.ButtonsType.OK,
modal=False
)
dialog.connect('response',lambda d,r: d.destroy())
dialog.present()
return
dialog = EpicNewAppsDialog(self.get_root())
dialog.connect_after('response',self._on_new_apps_dialog_response)
dialog.present()
def _on_backup_active_live_button_clicked(self,button):
backup_games = []

229
sgbackup/gui/_epic.py Normal file
View File

@ -0,0 +1,229 @@
###############################################################################
# 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 <https://www.gnu.org/licenses/>. #
###############################################################################
from gi.repository import Gtk,GLib,Gio
from gi.repository.GObject import GObject,Property,Signal,SignalFlags
from ..i18n import gettext as _,gettext_noop as N_
from ..game import GameManager,Game,EpicGameData,EpicWindowsData
from ..epic import Epic,EpicGameInfo,EpicIgnoredApp
from ..utility import PLATFORM_WINDOWS
from ._gamedialog import GameDialog
class EpicNewAppsDialogSorter(Gtk.Sorter):
def do_compare(self,item1:EpicGameInfo,item2:EpicGameInfo):
name1 = item1.name.lower()
name2 = item2.name.lower()
if name1 > name2:
return Gtk.Ordering.LARGER
elif name1 < name2:
return Gtk.Ordering.SMALLER
return Gtk.Ordering.EQUAL
class EpicNewAppsDialog(Gtk.Dialog):
def __init__(self,parent:Gtk.Window|None=None):
Gtk.Dialog.__init__(self,
transient_for=parent,
title=_("SGBackup: Manage new Epic-Games apps"),
modal=False)
self.set_default_size(800,600)
scrolled = Gtk.ScrolledWindow()
epic = Epic()
self.__liststore = Gio.ListStore.new(EpicGameInfo)
for info in epic.find_new_apps():
self.__liststore.append(info)
sort_model = Gtk.SortListModel(model=self.__liststore,
sorter=EpicNewAppsDialogSorter())
selection = Gtk.SingleSelection(model=sort_model,
autoselect=False,
can_unselect=True)
factory = Gtk.SignalListItemFactory()
factory.connect('setup',self._on_listview_setup)
factory.connect('bind',self._on_listview_bind)
self.__listview = Gtk.ListView(model=selection,factory=factory,hexpand=True,vexpand=True)
scrolled.set_child(self.__listview)
self.get_content_area().append(scrolled)
self.__close_button = self.add_button(_("Close"),Gtk.ResponseType.OK)
def _on_listview_setup(self,factory,item):
child = Gtk.Grid(column_spacing=4,row_spacing=2)
child.name_label = Gtk.Label(xalign=0.0,hexpand=True)
child.attach(child.name_label,1,0,1,1)
label = Gtk.Label(label=_("CatalogItemId:"),
use_markup=False,
xalign=0.0)
child.catalogitemid_label = Gtk.Label(xalign=0.0,
use_markup=False,
hexpand=True)
child.attach(label,0,1,1,1)
child.attach(child.catalogitemid_label,1,1,1,1)
label = Gtk.Label(label=_("Installation Directory:"),
use_markup=False,
xalign=0.0)
child.installdir_label = Gtk.Label(xalign=0.0,
use_markup=False,
hexpand=True)
child.attach(label,0,2,1,1)
child.attach(child.installdir_label,1,2,1,1)
actiongrid = Gtk.Grid(row_spacing=2,column_spacing=2)
icon = Gtk.Image.new_from_icon_name('list-add-symbolic')
icon.set_pixel_size(16)
child.new_button = Gtk.Button()
child.new_button.set_child(icon)
child.new_button.set_tooltip_text(_("Add Epic-Games-app as a new game."))
actiongrid.attach(child.new_button,0,0,1,1)
icon = Gtk.Image.new_from_icon_name('edit-delete-symbolic')
icon.set_pixel_size(16)
child.ignore_button = Gtk.Button()
child.ignore_button.set_child(icon)
child.ignore_button.set_tooltip_text(_("Add Epic-Games-app to the ignore list."))
actiongrid.attach(child.ignore_button,1,0,1,1)
icon = Gtk.Image.new_from_icon_name('edit-find-symbolic')
icon.set_pixel_size(16)
child.lookup_button = Gtk.Button()
child.lookup_button.set_child(icon)
child.lookup_button.set_tooltip_text(_("Lookup Epic-Games-app for already registered game."))
actiongrid.attach(child.lookup_button,0,1,1,1)
icon = Gtk.Image.new_from_icon_name('folder-download-symbolic')
icon.set_pixel_size(16)
child.online_button = Gtk.Button()
child.online_button.set_child(icon)
child.online_button.set_tooltip_text(_("Lookup Epic-Games-app online."))
actiongrid.attach(child.online_button,1,1,1,1)
child.attach(actiongrid,2,0,1,3)
item.set_child(child)
def _on_listview_bind(self,factory,item):
child = item.get_child()
data = item.get_item()
child.name_label.set_markup("<span size='large' weight='bold'>{}</span>".format(
GLib.markup_escape_text(data.name)))
child.catalogitemid_label.set_text(data.catalog_item_id)
child.installdir_label.set_text(data.installdir)
if hasattr(child.new_button,'_signal_clicked_connector'):
child.new_button.disconnect(child.new_button._signal_clicked_connector)
child.new_button._signal_clicked_connector = child.new_button.connect('clicked',
self._on_listview_new_button_clicked,
data)
if hasattr(child.ignore_button,'_signal_clicked_connector'):
child.ignore_button.disconnect(child.ignore_button._signal_clicked_connector)
child.ignore_button._signal_clicked_connector = child.ignore_button.connect('clicked',
self._on_listview_ignore_button_clicked,
data)
if hasattr(child.lookup_button,'_signal_clicked_connector'):
child.lookup_button.disconnect(child.lookup_button._signal_clicked_connector)
child.lookup_button._signal_clicked_connector = child.lookup_button.connect('clicked',
self._on_listview_lookup_button_clicked,
data)
child.lookup_button.set_sensitive(False)
if hasattr(child.online_button,'_signal_clicked_connector'):
child.online_button.disconnect(child.online_button._signal_clicked_connector)
child.online_button._signal_clicked_connector = child.online_button.connect('clicked',
self._on_listview_online_button_clicked,
data)
child.online_button.set_sensitive(False)
def _on_game_dialog_response(self,dialog,response,info:EpicGameInfo):
if response == Gtk.ResponseType.APPLY:
for i in range(self.__liststore.get_n_items()):
item = self.__liststore.get_item(i)
if item.catalog_item_id == info.catalog_item_id:
self.__liststore.remove(i)
break
def _on_listview_new_button_clicked(self,button:Gtk.Button,info:EpicGameInfo):
game = Game("",info.name,"")
if PLATFORM_WINDOWS:
windows = EpicWindowsData("","",installdir=info.installdir)
else:
windows = None
game.epic = EpicGameData(catalog_item_id=info.catalog_item_id,
windows=windows)
dialog = GameDialog(parent=self,game=game)
dialog.connect_after('response',self._on_dialog_reponse,info)
dialog.present()
def _on_ignore_dialog_response(self,dialog,response,info:EpicGameInfo):
if response == Gtk.ResponseType.YES:
epic = Epic()
epic.add_ignored_app(EpicIgnoredApp(info.catalog_item_id,
info.name,
dialog.reason_entry.get_text()))
for i in range(self.__liststore.get_n_items()):
item = self.__liststore.get_item()
if item.catalog_item_id == info.catalog_item_id:
self.__liststore.remove(i)
break
dialog.hide()
dialog.destroy()
def _on_listview_ignore_button_clicked(self,button:Gtk.Button,info:EpicGameInfo):
dialog = Gtk.MessageDialog(transient_for=self,
text=_("Do you really won to add the game <i>{game}</i> to the ignore list?").format(
game=GLib.markup_escape_text(info.name)),
use_markup=True,
secondary_text=_("Please enter a reason below."),
secondary_use_markup=True,
buttons=Gtk.ButtonsType.YES_NO,
modal=False)
dialog.reason_entry = Gtk.Entry()
dialog.reason_entry.set_hexpand(True)
dialog.get_content_area().append(dialog.reason_entry)
dialog.connect('response',self._on_ignore_dialog_response,info)
dialog.present()
def _on_listview_lookup_button_clicked(self,button:Gtk.Button,info:EpicGameInfo):
pass
def _on_listview_online_button_clicked(self,button:Gtk.Button,info:EpicGameInfo):
pass
def do_response(self,response):
self.hide()
self.destroy()

View File

@ -755,9 +755,9 @@ class GameDialog(Gtk.Dialog):
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)
label = self.__create_label(_("CatalogIemID:"))
page.catalogitemid_entry = Gtk.Entry()
page.catalogitemid_entry.set_hexpand(True)
grid.attach(label,0,0,1,1)
grid.attach(page.appname_entry,1,0,1,1)
page.append(grid)
@ -1015,9 +1015,11 @@ class GameDialog(Gtk.Dialog):
else "")
# Epic Games
set_game_widget_data(self.__epic.windows,self.__game.epic.windows if self.has_game and self.__game.epic else None)
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.catalogitemid_entry.set_text(self.__game.epic.catalog_item_id
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
@ -1254,9 +1256,9 @@ class GameDialog(Gtk.Dialog):
data = get_epic_data(self.__epic.windows)
if self.__game.epic:
self.__game.epic.appname = self.__epic.appname_entry.get_text()
self.__game.epic.catalog_item_id = self.__epic.catalogitemid_entry.get_text()
else:
self.__game.epic = EpicGameData(appname=self.__epic.appname_entry.get_text())
self.__game.epic = EpicGameData(appname=self.__epic.catalogitemid_entry.get_text())
if self.__game.epic.windows:
self.__game.epic.windows.savegame_root = data['sgroot']

View File

@ -238,7 +238,7 @@ class SteamGameLookupDialog(GameSearchDialog):
def do_prepare_game(self, game):
game = super().do_prepare_game(game)
if game.steam:
if not game.steam.appid != self.steam_app.appid:
if not game.steam.appid != self.steam_app.appid and game.steam_appid >= 0:
raise ValueError("Steam appid error")
if PLATFORM_WINDOWS:
@ -494,6 +494,9 @@ class SteamNoNewAppsDialog(Gtk.MessageDialog):
self.hide()
self.destroy()
### SteamIgnoreApps ###########################################################
class SteamNoIgnoredAppsDialog(Gtk.MessageDialog):
def __init__(self,parent:Gtk.Window|None=None):
Gtk.MessageDialog.__init__(self,buttons=Gtk.ButtonsType.OK)
@ -506,7 +509,8 @@ class SteamNoIgnoredAppsDialog(Gtk.MessageDialog):
def do_response(self,response):
self.hide()
self.destroy()
class SteamIgnoreAppsSorter(Gtk.Sorter):
def do_compare(self,item1:IgnoreSteamApp,item2:IgnoreSteamApp):
s1 = item1.name.lower()

View File

@ -350,7 +350,7 @@ class Steam(GObject):
new_apps = []
for lib in self.libraries:
for app in lib.steam_apps:
if not app.appid in GameManager.get_global().steam_games and not app.appid in self.ignore_apps:
if not GameManager.get_global().has_steam_game(app.appid) and not app.appid in self.ignore_apps:
new_apps.append(app)
return sorted(new_apps)

View File

@ -17,15 +17,29 @@
###############################################################################
import sys
if sys.platform.lower() == 'win32':
PLATFORM_WINDOWS=True
else:
PLATFORM_WINDOWS=False
if sys.platform.lower() in ['linux','freebsd','netbsd','openbsd','dragonfly','macos','cygwin']:
PLATFORM_UNIX = True
else:
PLATFORM_UNIX = False
def _platform_is_linux():
if sys.platform == 'linux':
return True
for i in ('freebsd','netbsd','openbsd','dragonfly'):
if sys.platform.startswith(i):
return True
return False
def _platform_is_unix():
if sys.platform in ('linux','darwin','aix'):
return True
for i in ('freebsd','netbsd','openbsd','dragonfly'):
if sys.platform.startswith(i):
return True
return False
PLATFORM_WINDOWS = (sys.platform == 'win32')
PLATFORM_LINUX = _platform_is_linux()
PLATFORM_MACOS = (sys.platform == 'darwin')
PLATFORM_UNIX = _platform_is_unix()
del _platform_is_unix
del _platform_is_linux
def sanitize_windows_path(path:str)->str:
return path.replace('/','\\')