2025.01.02 10:13:39

This commit is contained in:
Christian Moser 2025-01-02 10:13:39 +01:00
parent 7f96010822
commit 39bb1ebdac
27 changed files with 1562 additions and 260 deletions

15
.gitattributes vendored Normal file
View File

@ -0,0 +1,15 @@
* text=auto
.githooks/pre-commit text eol=lf
*.yml text eol=lf
*.sh text eol=lf
*.py text
*.txt text
*.ps1 text eol=crlf
*.ps1.in text eol=crlf
*.bat text eol=crlf
*.bat.in text eol=crlf
*.cmd text eol=crlf
*.cmd.in text eol=crlf

View File

@ -7,6 +7,7 @@ PROJECT_ROOT="$(dirname "$(dirname "$SELF")")" ; export PROJECT_ROOT
pre_commit_d="${GITHOOKS_DIR}/pre-commit-d" pre_commit_d="${GITHOOKS_DIR}/pre-commit-d"
# run scripts from the pre-commit.d directory
for i in $(ls "$pre_commit_d"); do for i in $(ls "$pre_commit_d"); do
script="${pre_commit_d}/$i" script="${pre_commit_d}/$i"
if [ -x "$script" ]; then if [ -x "$script" ]; then

View File

@ -17,6 +17,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # # along with this program. If not, see <https://www.gnu.org/licenses/>. #
############################################################################### ###############################################################################
class Command: class Command:
def __init__(self,id:str,name:str,description:str): def __init__(self,id:str,name:str,description:str):
self.__id = id self.__id = id

View File

@ -107,6 +107,8 @@ class GameFileType(StrEnum):
raise ValueError("Unknown GameFileType \"{}\"!".fomrat(typestring)) raise ValueError("Unknown GameFileType \"{}\"!".fomrat(typestring))
class GameFileMatcher(GObject): class GameFileMatcher(GObject):
__gtype_name__ = "GameFileMatcher"
def __init__(self,match_type:GameFileType,match_file:str): def __init__(self,match_type:GameFileType,match_file:str):
GObject.__init__(self) GObject.__init__(self)
self.match_type = type self.match_type = type
@ -165,6 +167,8 @@ class GameFileMatcher(GObject):
return False return False
class GameData(GObject): class GameData(GObject):
__gtype_name__ = 'GameData'
""" """
:class: GameData :class: GameData
:brief: Base class for platform specific data. :brief: Base class for platform specific data.
@ -226,17 +230,41 @@ class GameData(GObject):
self.__savegame_dir = sgdir self.__savegame_dir = sgdir
@Property @Property
def variables(self): def variables(self)->dict:
return self.__variables return self.__variables
@variables.setter
def variables(self,vars:dict|None):
if not vars:
self.__variables = {}
else:
self.__variables = dict(vars)
@Property @Property
def file_match(self): def file_match(self):
return self.__filematch return self.__filematch
@file_match.setter
def file_match(self,fm:list[GameFileMatcher]|None):
if not fm:
self.__filematch = []
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)
@Property @Property
def ignore_match(self): def ignore_match(self):
return self.__ignorematch return self.__ignorematch
@file_match.setter
def file_match(self,im:list[GameFileMatcher]|None):
if not im:
self.__ignorematch = []
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)
def has_variable(self,name:str)->bool: def has_variable(self,name:str)->bool:
return (name in self.__variables) return (name in self.__variables)
@ -535,18 +563,25 @@ class SteamGame(GameData):
GameData.__init__(self, GameData.__init__(self,
sgtype, sgtype,
appid,
savegame_root, savegame_root,
savegame_dir, savegame_dir,
variables, variables,
file_match, file_match,
ignore_match) ignore_match)
self.__installdir = installdir self.appid = int(appid)
self.installdir = installdir
def get_variables(self): def get_variables(self):
vars = super().get_variables() vars = super().get_variables()
vars["INSTALLDIR"] = self.installdir if self.installdir else "" vars["INSTALLDIR"] = self.installdir if self.installdir else ""
@Property(type=int)
def appid(self):
return self.__appid
@appid.setter
def appid(self,appid):
self.__appid = appid
@Property @Property
def installdir(self): def installdir(self):
return self.__installdir return self.__installdir
@ -622,6 +657,8 @@ class SteamMacOSGame(SteamGame):
class Game(GObject): class Game(GObject):
__gtype_name__ = "Game"
@staticmethod @staticmethod
def new_from_dict(config:str): def new_from_dict(config:str):
logger = logger.getChild("Game.new_from_dict()") logger = logger.getChild("Game.new_from_dict()")
@ -740,15 +777,17 @@ class Game(GObject):
with open(filename,'rt',encoding="UTF-8") as ifile: with open(filename,'rt',encoding="UTF-8") as ifile:
return Game.new_from_dict(json.loads(ifile.read())) return Game.new_from_dict(json.loads(ifile.read()))
def __init__(self,id:str,name:str,savegame_name:str): def __init__(self,key:str,name:str,savegame_name:str):
GObject.__init__(self) GObject.__init__(self)
self.__id = id self.__dbid = None
self.__key = key
self.__name = name self.__name = name
self.__filename = None self.__filename = None
self.__savegame_name = savegame_name self.__savegame_name = savegame_name
self.__savegame_type = SavegameType.UNSET self.__savegame_type = SavegameType.UNSET
self.__active = False self.__active = False
self.__live = True self.__live = True
self.__variables = dict()
self.__windows = None self.__windows = None
self.__linux = None self.__linux = None
@ -762,12 +801,25 @@ class Game(GObject):
self.__epic_linux = None self.__epic_linux = None
@Property(type=str) @Property(type=str)
def id(self)->str: def dbid(self)->str:
return self.__id return self.__id
@id.setter @dbid.setter
def id(self,id:str): def id(self,id:str):
self.__id = id self.__id = id
@Property(type=str)
def key(self)->str:
return self.__key
@key.setter
def key(self,key:str):
set_game = False
if self.__key in GAMES:
del GAMES[self.__key]
set_game = True
self.__key = key
if set_game:
GAMES[self.__key] = self
@Property(type=str) @Property(type=str)
def name(self)->str: def name(self)->str:
@ -820,6 +872,16 @@ class Game(GObject):
else: else:
self.__filename = fn self.__filename = fn
@Property
def variables(self):
return self.__variables
@variables.setter
def variables(self,vars:dict|None):
if not vars:
self.__variables = {}
else:
self.__variables = dict(vars)
@Property @Property
def game_data(self): def game_data(self):
sgtype = self.savegame_type sgtype = self.savegame_type
@ -917,6 +979,22 @@ class Game(GObject):
raise TypeError("SteamWindowsGame") raise TypeError("SteamWindowsGame")
self.__steam_macos = data self.__steam_macos = data
def add_variable(self,name:str,value:str):
self.__variables[str(name)] = str(value)
def delete_variable(self,name):
if name in self.__variables:
del self.__variables[name]
def get_variable(self,name):
vars = dict(os.environ)
#vars.update(settings.variables)
vars.update(self.__variables)
game_data = self.game_data
if (game_data is not None):
vars.update(game_data.variables)
def serialize(self)->dict: def serialize(self)->dict:
ret = { ret = {
'id': self.id, 'id': self.id,
@ -949,9 +1027,8 @@ class Game(GObject):
# ret['epic_linux'] = self.epic_linux.serialize() # ret['epic_linux'] = self.epic_linux.serialize()
return ret return ret
def save(self): def save(self):
data = self.serialize()
old_path = pathlib.Path(self.filename).resolve() old_path = pathlib.Path(self.filename).resolve()
new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve() new_path = pathlib.Path(settings.gameconf_dir / '.'.join(self.id,'gameconf')).resolve()
if (str(old_path) != str(new_path)) and old_path.is_file(): if (str(old_path) != str(new_path)) and old_path.is_file():
@ -959,8 +1036,10 @@ class Game(GObject):
if not new_path.parent.is_dir(): if not new_path.parent.is_dir():
os.makedirs(new_path.parent) os.makedirs(new_path.parent)
with open(new_path,'wt',encoding='UTF-8') as ofile: with open(new_path,'wt',encoding='utf-8') as ofile:
ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4)) ofile.write(json.dumps(self.serialize(),ensure_ascii=False,indent=4))
GAMES={} GAMES={}
STEAM_GAMES={} STEAM_GAMES={}
@ -973,7 +1052,7 @@ def __init_games():
if not os.path.isdir(gameconf_dir): if not os.path.isdir(gameconf_dir):
return return
for gcf in (os.path.join(gameconf_dir,i) for i in os.path.listdir(gameconf_dir)): 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'): if not os.path.isfile(gcf) or not gcf.endswith('.gameconf'):
continue continue
@ -984,7 +1063,7 @@ def __init_games():
except: except:
continue continue
GAMES[game.id] = game GAMES[game.key] = game
if (game.steam_windows): if (game.steam_windows):
if not game.steam_windows.appid in STEAM_GAMES: if not game.steam_windows.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_windows.appid] = game STEAM_GAMES[game.steam_windows.appid] = game
@ -1000,7 +1079,7 @@ def __init_games():
__init_games() __init_games()
def add_game(game:Game): def add_game(game:Game):
GAMES[game.id] = game GAMES[game.key] = game
if game.steam_windows: if game.steam_windows:
if not game.steam_windows.appid in STEAM_GAMES: if not game.steam_windows.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_windows.appid] = game STEAM_GAMES[game.steam_windows.appid] = game
@ -1012,4 +1091,5 @@ def add_game(game:Game):
if (game.steam_macos): if (game.steam_macos):
if not game.steam_macos.appid in STEAM_GAMES: if not game.steam_macos.appid in STEAM_GAMES:
STEAM_GAMES[game.steam_macos.appid] = game STEAM_GAMES[game.steam_macos.appid] = game
STEAM_MACOS_GAMES[game.steam_macos.appid] = game STEAM_MACOS_GAMES[game.steam_macos.appid] = game

View File

@ -15,4 +15,9 @@
# You should have received a copy of the GNU General Public License # # You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. # # along with this program. If not, see <https://www.gnu.org/licenses/>. #
############################################################################### ###############################################################################
from .application import Application
from ._app import Application,AppWindow
from ._settingsdialog import SettingsDialog
from ._gamedialog import GameDialog
app = None

448
sgbackup/gui/_app.py Normal file
View File

@ -0,0 +1,448 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,GObject,Gio,Gdk
import logging; logger=logging.getLogger(__name__)
import os
from datetime import datetime as DateTime
from pathlib import Path
from .. import game
from ..settings import settings
from ._settingsdialog import SettingsDialog
from ._gamedialog import GameDialog
class GameView(Gtk.ScrolledWindow):
__gtype_name__ = "sgbackup-gui-GameView"
def __init__(self):
Gtk.ScrolledWindow.__init__(self)
self.__liststore = Gio.ListStore.new(game.Game)
for g in game.GAMES.values():
pass
self.__liststore.append(g)
factory_key = Gtk.SignalListItemFactory.new()
factory_key.connect('setup',self._on_key_column_setup)
factory_key.connect('bind',self._on_key_column_bind)
column_key = Gtk.ColumnViewColumn.new("Key",factory_key)
factory_name = Gtk.SignalListItemFactory.new()
factory_name.connect('setup',self._on_name_column_setup)
factory_name.connect('bind',self._on_name_column_bind)
column_name = Gtk.ColumnViewColumn.new("Name",factory_name)
column_name.set_expand(True)
factory_active = Gtk.SignalListItemFactory.new()
factory_active.connect('setup',self._on_active_column_setup)
factory_active.connect('bind',self._on_active_column_bind)
factory_active.connect('unbind',self._on_active_column_unbind)
column_active = Gtk.ColumnViewColumn.new("Active",factory_active)
factory_live = Gtk.SignalListItemFactory.new()
factory_live.connect('setup',self._on_live_column_setup)
factory_live.connect('bind',self._on_live_column_bind)
factory_live.connect('unbind',self._on_live_column_unbind)
column_live = Gtk.ColumnViewColumn.new("Live",factory_live)
selection = Gtk.SingleSelection.new(self._liststore)
self.__columnview = Gtk.ColumnView.new(selection)
self._columnview.append_column(column_key)
self._columnview.append_column(column_name)
self._columnview.append_column(column_active)
self._columnview.append_column(column_live)
self._columnview.set_single_click_activate(True)
self.set_child(self._columnview)
@property
def _liststore(self):
return self.__liststore
@property
def _columnview(self):
return self.__columnview
def _on_key_column_setup(self,factory,item):
item.set_child(Gtk.Label())
def _on_key_column_bind(self,factory,item):
label = item.get_child()
game = item.get_item()
label.bind_property(game,'key','label',GObject.BindingFlags.DEFAULT)
def _on_name_column_setup(self,factory,item):
item.set_child(Gtk.Label())
def _on_name_column_bind(self,factory,item):
label = item.get_child()
game = item.get_item()
label.bind_proprety(game,'name','label',GObject.BindingFlags.DEFAULT)
def _on_active_column_setup(self,factory,item):
item.set_child(Gtk.Switch())
def _on_active_column_bind(self,factory,item):
switch = item.get_child()
game = item.get_data()
switch.set_active(game.is_active)
item._signal_active_state_set = switch.connect('state-set',self._on_active_state_set,game)
def _on_active_column_unbind(self,factory,item):
if hasattr(item,'_signal_active_state_set'):
item.get_child().disconnect(item._signal_active_state_set)
del item._signal_active_state_set
def _on_active_state_set(self,switch,state,game):
game.is_active = state
game.save()
def _on_live_column_setup(self,factory,item):
item.set_child(Gtk.Switch())
def _on_live_column_bind(self,factory,item):
switch = item.get_child()
game = item.get_data()
switch.set_active(game.is_active)
item._signal_live_state_set = switch.connect('state-set',self._on_live_state_set,game)
def _on_live_column_unbind(self,factory,item):
if hasattr(item,'_signal_live_state_set'):
item.get_child().disconnect(item._signal_live_state_set)
del item._signal_live_state_set
def _on_live_state_set(self,switch,state,game):
def on_dialog_response(dialog,response):
if response == Gtk.ResponseType.YES:
pass
#archiver.backup(game)
dialog.hide()
dialog.destroy()
game.is_live = state
game.save()
if not state:
dialog = Gtk.MessageDialog()
dialog.set_transient_for(self.get_toplevel())
dialog.props.buttons = Gtk.ButtonsType.YES_NO
dialog.props.text = "Do you want to create a new savegame for <i>{game}</i>?".format(game=game.name)
dialog.props.use_markup = True
dialog.props.secondary_text = "The new savegame is added to the finsihed savegames for the game."
dialog.props.secondary_use_markup = False
dialog.connect('response',on_dialog_response)
dialog.present()
# GameView class
class BackupViewData(GObject.GObject):
def __init__(self,_game:game.Game,filename:str):
GObject.GObject.__init__(self)
self.__game = _game
self.__filename = filename
basename = os.path.basename(filename)
self.__is_live = (os.path.basename(os.path.dirname(filename)) == 'live')
parts = filename.split('.')
self.__savegame_name = parts[0]
self.__timestamp = DateTime.strptime(parts[1],"%Y%m%d-%H%M%S")
self.__extension = '.' + parts[3:]
@property
def game(self)->game.Game:
return self.__game
@GObject.Property
def savegame_name(self):
return self.__savegame_name
@GObject.Property(type=str)
def filename(self)->str:
return self.__filename
@GObject.Property(type=bool,default=False)
def is_live(self)->bool:
pass
@GObject.Property
def extension(self):
return self.__extension
@GObject.Property
def timestamp(self):
return self.__timestamp
class BackupView(Gtk.ScrolledWindow):
__gtype_name__ = "BackupView"
def __init__(self,gameview:GameView):
Gtk.ScrolledWindow.__init__(self)
self.__gameview = gameview
self.__liststore = Gio.ListStore()
selection = Gtk.SingleSelection.new(self.__liststore)
live_factory = Gtk.SignalListItemFactory()
live_factory.connect('setup',self._on_live_column_setup)
live_factory.connect('bind',self._on_live_column_bind)
live_column = Gtk.ColumnViewColumn.new("Live",live_factory)
sgname_factory = Gtk.SignalListItemFactory()
sgname_factory.connect('setup',self._on_savegamename_column_setup)
sgname_factory.connect('bind',self._on_savegamename_column_bind)
sgname_column = Gtk.ColumnViewColumn.new("Savegame name",sgname_factory)
sgname_column.set_expand(True)
timestamp_factory = Gtk.SignalListItemFactory()
timestamp_factory.connect('setup',self._on_timestamp_column_setup)
timestamp_factory.connect('bind',self._on_timestamp_column_bind)
timestamp_column = Gtk.ColumnViewColumn.new("Timestamp",timestamp_factory)
self.__columnview = Gtk.ColumnView.new(selection)
self.__columnview.append_column(live_column)
self.__columnview.append_column(sgname_column)
self.__columnview.append_column(timestamp_column)
self._on_gameview_selection_changed(selection)
self.gameview._columnview.get_model().connect('selection-changed',self._on_gameview_selection_changed)
self.set_child(self.__columnview)
@property
def gameview(self)->GameView:
return self.__gameview
def _on_live_column_setup(self,factory,item):
checkbutton = Gtk.CheckButton()
checkbutton.set_sensitive(False)
def _on_live_column_bind(self,factory,item):
checkbutton = item.get_child()
data = item.get_item()
checkbutton.set_active(data.is_live)
def _on_savegamename_column_setup(self,factory,item):
label = Gtk.Label()
self.set_child(label)
def _on_savegamename_column_bind(self,factory,item):
label = item.get_child()
data = item.get_item()
label.set_text(data.savegame_name)
def _on_timestamp_column_setup(self,factory,item):
label = Gtk.Label()
item.set_child(label)
def _on_timestamp_column_bind(self,factory,item):
label = item.get_child()
data = item.get_item()
label.set_text(data.timestamp.strftime("%d.%m.%Y %H:%M:%S"))
def _on_size_column_setup(self,factory,item):
label = Gtk.Label()
item.set_child(label)
def _on_size_column_bind(self,factory,item):
label = item.get_child()
data = item.get_item()
file = Path(data.filename).resolve()
if not file.is_file():
label.set_text("0 B")
return
size = file.stat().st_size
if (size > 1073741824):
display_size = ".".join((str(int(size / 1073741824)),str(int(((size * 10) / 1073741824) % 10)))) + " GiB"
elif (size > 1048576):
display_size = ".".join((str(int(size / 1048576)), str(int(((size * 10) / 1048576) % 10)))) + " MiB"
elif (size > 1024):
display_size = ".".join((str(int(size / 1024)), str(int(((size * 10) / 1024) % 10)))) + " KiB"
else:
display_size = str(size) + " B"
label.set_text(display_size)
def _on_gameview_selection_changed(self,model):
game = model.get_selected_item()
if game is None:
return
class AppWindow(Gtk.ApplicationWindow):
__gtype_name__ = "AppWindow"
def __init__(self,application=None,**kwargs):
kwargs['title'] = "SGBackup"
if (application is not None):
kwargs['application']=application
if (hasattr(application,'builder')):
builder = application.builder
else:
builder = Gtk.Builder.new()
Gtk.ApplicationWindow.__init__(self,**kwargs)
self.set_default_size(800,600)
self.set_icon_name('sgbackup')
self.__builder = builder
self.builder.add_from_file(os.path.join(os.path.dirname(__file__),'appmenu.ui'))
gmenu = self.builder.get_object('appmenu')
appmenu_popover = Gtk.PopoverMenu.new_from_model(gmenu)
image = Gtk.Image.new_from_icon_name('open-menu-symbolic')
menubutton = Gtk.MenuButton.new()
menubutton.set_popover(appmenu_popover)
menubutton.set_child(image)
headerbar = Gtk.HeaderBar.new()
headerbar.pack_start(menubutton)
self.set_titlebar(headerbar)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned.set_hexpand(True)
self.__vpaned.set_vexpand(True)
self.__vpaned.set_wide_handle(True)
self.__gameview = GameView()
self.__vpaned.set_start_child(self.gameview)
self.__backupview = BackupView(self.gameview)
self.__vpaned.set_end_child(self.backupview)
self.__vpaned.set_resize_start_child(True)
self.__vpaned.set_resize_end_child(True)
vbox.append(self.__vpaned)
statusbar = Gtk.Statusbar()
statusbar.set_hexpand(True)
statusbar.set_vexpand(False)
statusbar.push(0,'Running ...')
vbox.append(statusbar)
self.set_child(vbox)
@GObject.Property
def builder(self):
return self.__builder
@GObject.Property
def backupview(self):
return self.__backupview
@GObject.Property
def gameview(self):
return self.__gameview
class Application(Gtk.Application):
__gtype_name__ = "Application"
def __init__(self,*args,**kwargs):
AppFlags = Gio.ApplicationFlags
kwargs['application_id'] = 'org.sgbackup.sgbackup'
kwargs['flags'] = AppFlags.FLAGS_NONE
Gtk.Application.__init__(self,*args,**kwargs)
self.__logger = logger.getChild('Application')
self.__builder = None
self.__appwindow = None
@property
def _logger(self):
return self.__logger
@GObject.Property
def appwindow(self):
return self.__appwindow
def do_startup(self):
self._logger.debug('do_startup()')
if not self.__builder:
self.__builder = Gtk.Builder.new()
Gtk.Application.do_startup(self)
pkg_path = Path(__file__).resolve()
pkg_path = pkg_path.parent.parent
icons_path = pkg_path / "icons"
theme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default())
theme.add_resource_path("/org/sgbackup/sgbackup/icons")
theme.add_search_path(str(icons_path))
action_about = Gio.SimpleAction.new('about',None)
action_about.connect('activate',self.on_action_about)
self.add_action(action_about)
action_new_game = Gio.SimpleAction.new('new-game',None)
action_new_game.connect('activate',self.on_action_new_game)
self.add_action(action_new_game)
action_quit = Gio.SimpleAction.new('quit',None)
action_quit.connect('activate',self.on_action_quit)
self.add_action(action_quit)
action_settings = Gio.SimpleAction.new('settings',None)
action_settings.connect('activate',self.on_action_settings)
self.add_action(action_settings)
# add accels
self.set_accels_for_action('app.quit',["<Primary>q"])
@GObject.Property
def builder(self):
return self.__builder
def do_activate(self):
self._logger.debug('do_activate()')
if not (self.__appwindow):
self.__appwindow = AppWindow(application=self)
self.appwindow.present()
def on_action_about(self,action,param):
pass
def on_action_settings(self,action,param):
dialog = self.new_settings_dialog()
dialog.present()
def on_action_quit(self,action,param):
self.quit()
def on_action_new_game(self,action,param):
def on_reponse(dialog,response):
dialog.destroy()
dialog = GameDialog(self.appwindow)
dialog.connect('response',on_reponse)
dialog.present()
def new_settings_dialog(self):
dialog = SettingsDialog(self.appwindow)
self.emit('settings-dialog-init',dialog)
return dialog
@GObject.Signal(name='settings-dialog-init',
flags=GObject.SignalFlags.RUN_LAST,
return_type=None,
arg_types=(SettingsDialog,))
def settings_dialog_init(self,dialog):
pass

675
sgbackup/gui/_gamedialog.py Normal file
View File

@ -0,0 +1,675 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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 GObject,Gio,GLib,Gtk,Pango
from ..game import Game,GameFileMatcher,GameFileType
class GameVariableData(GObject.GObject):
def __init__(self,name:str,value:str):
GObject.GObject.__init__(self)
self.name = name
self.value = value
@GObject.Property(type=str)
def name(self)->str:
return self.__name
@name.setter
def name(self,name):
self.__name = name
@GObject.Property(type=str)
def value(self)->str:
return self.__value
@value.setter
def value(self,value:str):
self.__value = value
class RegistryKeyData(GObject.GObject):
def __init__(self,regkey=None):
GObject.GObject.__init__(self)
if not regkey:
self.__regkey = ""
@GObject.Property(type=str)
def regkey(self):
return self.__regkey
@regkey.setter
def regkey(self,key:str):
self.__regkey = key
def __bool__(self):
return bool(self.__regkey)
class GameVariableDialog(Gtk.Dialog):
def __init__(self,parent:Gtk.Window,columnview:Gtk.ColumnView,variable:GameVariableData|None=None):
Gtk.Dialog.__init__(self)
self.set_transient_for(parent)
self.set_default_size(600,-1)
self.__columnview = columnview
if variable:
self.__variable = variable
else:
self.__variable = None
grid = Gtk.Grid()
label = Gtk.Label.new("Name:")
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")
self.__value_entry = Gtk.Entry()
self.__value_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(self.__value_entry,1,1,1,1)
self.get_content_area().append(grid)
if self.__variable:
self.__name_entry.set_text(self.__variable.name)
self.__value_entry.set_text(self.__variable.value)
self.__apply_button = self.add_button("Apply",Gtk.ResponseType.APPLY)
self.__apply_button.set_sensitive(bool(self))
self.add_button("Cancel",Gtk.ResponseType.CANCEL)
def __bool__(self):
name = self.__name_entry.get_text()
if name:
if self.__variable and self.__variable.name == name:
return True
model = self.__columnview.get_model().get_model()
for i in range(model.get_n_items()):
if name == model.get_item(i).name:
return False
return True
return False
def _on_name_entry_changed(self,entry):
self.__apply_button.set_sensitive(bool(self))
def do_response(self,response):
if response == Gtk.ResponseType.APPLY:
if not bool(self):
return
if self.__variable:
self.__variable.name = self.__name_entry.get_text()
self.__variable.value = self.__value_entry.get_text()
else:
model = self.__columnview.get_model().get_model()
model.append(GameVariableData(self.__name_entry.get_text(),self.__value_entry.get_text()))
self.hide()
self.destroy()
class GameDialog(Gtk.Dialog):
def __init__(self,
parent:Gtk.Window|None=None,
game:Game|None=Game):
Gtk.Dialog.__init__(self)
if (parent):
self.set_transient_for(parent)
if isinstance(game,Game):
self.__game = game
else:
self.__game = None
self.set_default_size(800,600)
paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
paned.set_position(200)
self.__stack = Gtk.Stack.new()
paned.set_end_child(self.__stack)
paned.set_hexpand(True)
paned.set_vexpand(True)
self.__stack_sidebar = Gtk.ListBox()
self.__stack_sidebar.set_activate_on_single_click(False)
self.__stack_sidebar.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.__stack_sidebar.connect('selected_rows_changed',self._on_stack_sidebar_selected_rows_changed)
sidebar_scrolled = Gtk.ScrolledWindow()
sidebar_scrolled.set_child(self.__stack_sidebar)
paned.set_start_child(sidebar_scrolled)
self.__variable_name_factory = Gtk.SignalListItemFactory()
self.__variable_name_factory.connect('setup',self._on_variable_name_setup)
self.__variable_name_factory.connect('bind',self._on_variable_name_bind)
self.__variable_value_factory = Gtk.SignalListItemFactory()
self.__variable_value_factory.connect('setup',self._on_variable_value_setup)
self.__variable_value_factory.connect('bind',self._on_variable_value_bind)
self.__add_game_page()
self.__windows = self.__create_windows_page()
self.__linux = self.__create_linux_page()
self.__macos = self.__create_macos_page()
self.__steam_windows = self.__create_steam_page('steam-windows','Steam Windows')
self.__steam_linux = self.__create_steam_page('steam-linux','Steam Linux')
self.__steam_macos = self.__create_steam_page('steam-macos','Steam MacOS')
for stack_page in self.__stack.get_pages():
hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,4)
label = Gtk.Label.new(stack_page.props.title)
attrs = Pango.AttrList.new()
size_attr = Pango.AttrSize.new(14 * Pango.SCALE)
attrs.insert(size_attr)
label.set_attributes(attrs)
icon = Gtk.Image.new_from_icon_name(stack_page.props.icon_name)
icon.set_pixel_size(20)
hbox.append(icon)
hbox.append(label)
hbox.page_name = stack_page.props.name
self.__stack_sidebar.append(hbox)
self.reset()
self.__stack_sidebar.select_row(self.__stack_sidebar.get_row_at_index(0))
self.get_content_area().append(paned)
self.add_button("Apply",Gtk.ResponseType.APPLY)
self.add_button("Cancel",Gtk.ResponseType.CANCEL)
def __add_game_page(self):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
self.__set_widget_margin(vbox,5)
grid = Gtk.Grid.new()
label = Gtk.Label.new("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?")
self.__live_switch = Gtk.Switch()
entry_hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL,5)
entry_hbox.append(self.__live_switch)
entry_hbox.append(label)
vbox.append(entry_hbox)
label = Gtk.Label.new("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:")
self.__key_entry = Gtk.Entry()
self.__key_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(self.__key_entry,1,1,1,1)
label = Gtk.Label.new("Game name:")
self.__name_entry = Gtk.Entry()
self.__name_entry.set_hexpand(True)
grid.attach(label,0,2,1,1)
grid.attach(self.__name_entry,1,2,1,1)
label = Gtk.Label.new("Savegame name:")
self.__sgname_entry = Gtk.Entry()
self.__sgname_entry.set_hexpand(True)
grid.attach(label,0,3,1,1)
grid.attach(self.__sgname_entry,1,3,1,1)
vbox.append(grid)
self.__game_variables = self.__create_variables_widget()
vbox.append(self.__game_variables)
page.set_child(vbox)
self.__stack.add_titled(page,"main","Game")
stack_page = self.__stack.get_page(page)
stack_page.set_icon_name('sgbackup')
def __create_windows_page(self):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
self.__set_widget_margin(vbox,5)
grid = Gtk.Grid()
label = Gtk.Label.new("Root directory:")
page.sgroot_entry = Gtk.Entry()
page.sgroot_entry.set_hexpand(True)
grid.attach(label,0,0,1,1)
grid.attach(page.sgroot_entry,1,0,1,1)
label = Gtk.Label.new("Backup directory:")
page.sgdir_entry = Gtk.Entry()
page.sgdir_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(page.sgdir_entry,1,1,1,1)
vbox.append(grid)
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")
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")
stack_page = self.__stack.get_page(page)
stack_page.set_icon_name("windows-svgrepo-com")
return page
def __create_linux_page(self):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
self.__set_widget_margin(vbox,5)
grid = Gtk.Grid()
label = Gtk.Label.new("Root directory:")
page.sgroot_entry = Gtk.Entry()
page.sgroot_entry.set_hexpand(True)
grid.attach(label,0,0,1,1)
grid.attach(page.sgroot_entry,1,0,1,1)
label = Gtk.Label.new("Backup directory:")
page.sgdir_entry = Gtk.Entry()
page.sgdir_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(page.sgdir_entry,1,1,1,1)
label = Gtk.Label.new("Executable")
page.binary_entry = Gtk.Entry()
page.binary_entry.set_hexpand(True)
grid.attach(label,0,2,1,1)
grid.attach(page.binary_entry,1,2,1,1)
vbox.append(grid)
page.variables = self.__create_variables_widget()
vbox.append(page.variables)
page.set_child(vbox)
self.__stack.add_titled(page,"linux","Linux")
stack_page = self.__stack.get_page(page)
stack_page.set_icon_name("linux-svgrepo-com")
return page
def __create_macos_page(self):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
self.__set_widget_margin(vbox,5)
grid = Gtk.Grid()
label = Gtk.Label.new("Root directory:")
page.sgroot_entry = Gtk.Entry()
page.sgroot_entry.set_hexpand(True)
grid.attach(label,0,0,1,1)
grid.attach(page.sgroot_entry,1,0,1,1)
label = Gtk.Label.new("Backup directory:")
page.sgdir_entry = Gtk.Entry()
page.sgdir_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(page.sgdir_entry,1,1,1,1)
label = Gtk.Label.new("Executable")
page.binary_entry = Gtk.Entry()
page.binary_entry.set_hexpand(True)
grid.attach(label,0,2,1,1)
grid.attach(page.binary_entry,1,2,1,1)
vbox.append(grid)
page.variables = self.__create_variables_widget()
vbox.append(page.variables)
page.set_child(vbox)
self.__stack.add_titled(page,"macos","MacOS")
stack_page = self.__stack.get_page(page)
stack_page.set_icon_name("apple-svgrepo-com")
return page
def __create_steam_page(self,name,title):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
self.__set_widget_margin(vbox,5)
grid = Gtk.Grid()
label = Gtk.Label.new("App ID:")
page.appid_entry = Gtk.Entry()
page.appid_entry.set_hexpand(True)
grid.attach(label,0,0,1,1)
grid.attach(page.appid_entry,1,0,1,1)
vbox.append(grid)
label = Gtk.Label.new("Root directory:")
page.sgroot_entry = Gtk.Entry()
page.sgroot_entry.set_hexpand(True)
grid.attach(label,0,1,1,1)
grid.attach(page.sgroot_entry,1,1,1,1)
label = Gtk.Label.new("Backup directory:")
page.sgdir_entry = Gtk.Entry()
page.sgdir_entry.set_hexpand(True)
grid.attach(label,0,2,1,1)
grid.attach(page.sgdir_entry,1,2,1,1)
page.variables = self.__create_variables_widget()
vbox.append(page.variables)
page.set_child(vbox)
self.__stack.add_titled(page,name,title)
stack_page = self.__stack.get_page(page)
stack_page.set_icon_name("steam-svgrepo-com")
return page
def __set_widget_margin(self,widget,margin):
widget.set_margin_start(margin)
widget.set_margin_end(margin)
widget.set_margin_top(margin)
widget.set_margin_bottom(margin)
def __create_variables_widget(self):
widget = Gtk.Frame.new("Variables")
vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL,0)
model = Gio.ListStore.new(GameVariableData)
selection = Gtk.SingleSelection.new(model)
selection.set_autoselect(False)
selection.set_can_unselect(True)
widget.columnview = Gtk.ColumnView.new(selection)
widget.columnview.set_vexpand(True)
widget.actions = Gtk.ActionBar()
icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
widget.add_button = Gtk.Button()
widget.add_button.set_child(icon)
widget.add_button.connect('clicked',
self._on_variables_add_button_clicked,
widget.columnview)
widget.actions.pack_start(widget.add_button)
icon = Gtk.Image.new_from_icon_name("document-edit-symbolic")
widget.edit_button = Gtk.Button()
widget.edit_button.set_child(icon)
widget.edit_button.set_sensitive(False)
widget.edit_button.connect('clicked',
self._on_variables_edit_buton_clicked,
widget.columnview)
widget.actions.pack_start(widget.edit_button)
icon = Gtk.Image.new_from_icon_name("list-remove-symbolic")
widget.remove_button = Gtk.Button()
widget.remove_button.set_child(icon)
widget.remove_button.set_sensitive(False)
widget.remove_button.connect('clicked',
self._on_variables_remove_button_clicked,
widget.columnview)
widget.actions.pack_start(widget.remove_button)
name_column = Gtk.ColumnViewColumn.new("Name",self.__variable_name_factory)
name_column.set_expand(True)
widget.columnview.append_column(name_column)
value_column = Gtk.ColumnViewColumn.new("Value",self.__variable_value_factory)
value_column.set_expand(True)
widget.columnview.append_column(value_column)
selection.connect('selection-changed',self._on_variable_selection_changed,widget)
vbox.append(widget.actions)
vbox.append(widget.columnview)
widget.set_child(vbox)
return widget
def __create_registry_key_widget(self,title):
widget = Gtk.Frame.new(title)
vbox=Gtk.Box.new(Gtk.Orientation.VERTICAL,2)
widget.actions = Gtk.ActionBar()
icon = Gtk.Image.new_from_icon_name("list-add-symbolic")
button = Gtk.Button.new()
button.set_child(icon)
button.connect('clicked',self._on_windows_regkey_add_button_clicked,widget)
widget.actions.pack_start(button)
model = Gio.ListStore.new(RegistryKeyData)
selection = Gtk.SingleSelection.new(model)
selection.set_autoselect(False)
selection.set_can_unselect(True)
factory = Gtk.SignalListItemFactory()
factory.connect('setup',self._on_windows_regkey_setup)
factory.connect('bind',self._on_windows_regkey_bind,widget)
widget.listview = Gtk.ListView.new(selection,factory)
vbox.append(widget.actions)
vbox.append(widget.listview)
widget.set_child(vbox)
return widget
def reset(self):
self.__active_switch.set_active(True)
self.__live_switch.set_active(True)
self.__name_entry.set_text("")
self.__sgname_entry.set_text("")
self.__game_variables.columnview.get_model().get_model().remove_all()
#windows
self.__windows.sgroot_entry.set_text("")
self.__windows.sgdir_entry.set_text("")
self.__windows.variables.columnview.get_model().get_model().remove_all()
self.__windows.lookup_regkeys.listview.get_model().get_model().remove_all()
self.__windows.installdir_regkeys.listview.get_model().get_model().remove_all()
#linux
self.__linux.sgroot_entry.set_text("")
self.__linux.sgdir_entry.set_text("")
self.__linux.binary_entry.set_text("")
self.__linux.variables.columnview.get_model().get_model().remove_all()
#linux
self.__macos.sgroot_entry.set_text("")
self.__macos.sgdir_entry.set_text("")
self.__macos.binary_entry.set_text("")
self.__macos.variables.columnview.get_model().get_model().remove_all()
#steam windows
self.__steam_windows.sgroot_entry.set_text("")
self.__steam_windows.sgdir_entry.set_text("")
self.__steam_windows.appid_entry.set_text("")
self.__steam_windows.variables.columnview.get_model().get_model().remove_all()
#steam linux
self.__steam_linux.sgroot_entry.set_text("")
self.__steam_linux.sgdir_entry.set_text("")
self.__steam_linux.appid_entry.set_text("")
self.__steam_linux.variables.columnview.get_model().get_model().remove_all()
#steam macos
self.__steam_macos.sgroot_entry.set_text("")
self.__steam_macos.sgdir_entry.set_text("")
self.__steam_macos.appid_entry.set_text("")
self.__steam_macos.variables.columnview.get_model().get_model().remove_all()
if self.__game:
self.__active_switch.set_active(self.__game.is_active)
self.__live_switch.set_active(self.__game.is_live)
self.__name_entry.set_text(self.__game.name)
self.__sgname_entry.set_text(self.__game.savegame_name)
for name,value in self.__game.variables.items():
self.__game_variables.get_model().get_model().append(GameVariableData(name,value))
if self.__game.windows:
self.__windows.sgroot_entry.set_text(self.__game.windows.savegame_root)
self.__windows.sgdir_entry.set_text(self.__game.windows.savegame_dir)
# set lookup regkeys
var_model = self.__windows.variables.columnview.get_model().get_model()
grk_model = self.__windows.lookup_regkeys.listview.get_model().get_model()
irk_model = self.__windows.installdir_regkeys.listview.get_model().get_model()
for rk in self.__game.windows.game_registry_keys:
grk_model.append(RegistryKeyData(rk))
#set installdir regkeys
for rk in self.__game.windows.installdir_registry_keys:
irk_model.append(RegistryKeyData(rk))
#set variables
for name,value in self.__game.windows.variables.items():
var_model.append(GameVariableData(name,value))
if self.__game.linux:
self.__linux.sgroot_entry.set_text(self.__game.linux.savegame_root)
self.__linux.sgdir_entry.set_text(self.__game.linux.savegame_dir)
self.__linux.binary_entry.set_text(self.__game.linux.binary)
var_model = self.__linux.variables.columnview.get_model().get_model()
for name,value in self.__game.linux.variables.items():
var_model.append(GameVariableData(name,value))
if self.__game.macos:
self.__macos.sgroot_entry.set_text(self.__game.linux.savegame_root)
self.__macos.sgdir_entry.set_text(self.__game.linux.savegame_dir)
self.__macos.binary_entry.set_text(self.__game.linux.binary)
var_model = self.__macos.variables.columnview.get_model().get_model()
for name,value in self.__game.linux.variables.items():
var_model.append(GameVariableData(name,value))
if self.__game.steam_windows:
self.__steam_windows.sgroot_entry.set_text(self.__game.steam_windows.savegame_root)
self.__steam_windows.sgdir_entry.set_text(self.__game.steam_windows.savegame_dir)
self.__steam_windows.appid_entry.set_text(self.__game.steam_windows.appid)
var_model = self.__steam_windows.variables.columnview.get_model().get_model()
for name,value in self.__game.steam_windows.variables.items():
var_model.append(GameVariableData(name,value))
if self.__game.steam_linux:
self.__steam_linux.sgroot_entry.set_text(self.__game.steam_linux.savegame_root)
self.__steam_linux.sgdir_entry.set_text(self.__game.steam_linux.savegame_dir)
self.__steam_linux.appid_entry.set_text(self.__game.steam_linux.appid)
var_model = self.__steam_linux.variables.columnview.get_model().get_model()
for name,value in self.__game.steam_linux.variables.items():
var_model.append(GameVariableData(name,value))
if self.__game.steam_macos:
self.__steam_macos.sgroot_entry.set_text(self.__game.steam_macos.savegame_root)
self.__steam_macos.sgdir_entry.set_text(self.__game.steam_macos.savegame_dir)
self.__steam_macos.appid_entry.set_text(self.__game.steam_macos.appid)
var_model = self.__steam_macos.variables.columnview.get_model().get_model()
for name,value in self.__game.steam_macos.variables.items():
var_model.append(GameVariableData(name,value))
# reset()
def _on_variable_name_setup(self,factory,item):
label = Gtk.Label()
item.set_child(label)
def _on_variable_name_bind(self,factory,item):
label = item.get_child()
data = item.get_item()
data.bind_property('name',label,'label',GObject.BindingFlags.SYNC_CREATE)
def _on_variable_value_setup(self,factory,item):
label = Gtk.Label()
item.set_child(label)
def _on_variable_value_bind(self,factory,item):
label = item.get_child()
data = item.get_item()
data.bind_property('value',label,'label',GObject.BindingFlags.SYNC_CREATE)
def _on_windows_regkey_setup(self,factory,item):
label = Gtk.EditableLabel()
item.set_child(label)
def _on_windows_regkey_bind(self,factory,item,widget):
label = item.get_child()
data = item.get_item()
label.set_text(data.regkey)
label.bind_property('text',data,'regkey',GObject.BindingFlags.DEFAULT)
label.connect('changed',self._on_windows_regkey_label_changed,widget)
if not label.get_text():
label.start_editing()
label.grab_focus()
def _on_stack_sidebar_selected_rows_changed(self,sidebar):
row = sidebar.get_selected_row()
self.__stack.set_visible_child_name(row.get_child().page_name)
def _on_variables_add_button_clicked(self,button,columnview):
dialog = GameVariableDialog(self,columnview)
dialog.present()
def _on_variables_remove_button_clicked(self,button,columnview):
selection = columnview.get_model()
model = selection.get_model()
selected = selection.get_selected()
if selected == Gtk.INVALID_LIST_POSITION:
return
model.remove(selected)
def _on_variables_edit_buton_clicked(self,button,columnview):
data = columnview.get_model().get_selected()
if data:
dialog = GameVariableDialog(self,columnview,data)
dialog.present()
def _on_variable_selection_changed(self,selection,position,n_items,var_widget):
if (selection.get_model().get_n_items() == 0) or (selection.get_selected() == Gtk.INVALID_LIST_POSITION):
var_widget.edit_button.set_sensitive(False)
var_widget.remove_button.set_sensitive(False)
else:
var_widget.edit_button.set_sensitive(True)
var_widget.remove_button.set_sensitive(True)
def _on_windows_regkey_add_button_clicked(self,button,widget):
widget.listview.get_model().get_model().append(RegistryKeyData())
def _on_windows_regkey_label_changed(self,label,widget):
if not label.get_text():
model = widget.listview.get_model().get_model()
i = 0
while i < model.get_n_items():
item = model.get_item(i)
if not item.regkey:
model.remove(i)
continue
i += 1

View File

@ -0,0 +1,97 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,Signal,Property
from ..settings import settings
class SettingsDialog(Gtk.Dialog):
def __init__(self,parent=None):
Gtk.Dialog.__init__(self)
if parent:
self.set_transient_for(parent)
self.set_default_size(800,600)
vbox = self.get_content_area()
paned = Gtk.Paned(orientation=Gtk.Orientation.HORIZONTAL)
paned.set_position(250)
self.__stack = Gtk.Stack()
self.__stack_sidebar = Gtk.StackSidebar.new()
self.__add_general_settings_page()
paned.set_start_child(self.__stack_sidebar)
paned.set_end_child(self.__stack)
paned.set_vexpand(True)
self.__stack_sidebar.set_stack(self.__stack)
vbox.append(paned)
self.add_button("Apply",Gtk.ResponseType.APPLY)
self.add_button("Cancel",Gtk.ResponseType.CANCEL)
def __add_general_settings_page(self):
page = Gtk.ScrolledWindow()
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
grid = Gtk.Grid()
label = Gtk.Label.new('Backup directory: ')
grid.attach(label,0,0,1,1)
self.__backupdir_label = Gtk.Label.new(settings.backup_dir)
self.__backupdir_label.set_hexpand(True)
grid.attach(self.__backupdir_label,1,0,1,1)
img = Gtk.Image.new_from_icon_name('document-open-symbolic')
img.set_pixel_size(16)
backupdir_button = Gtk.Button()
backupdir_button.set_child(img)
backupdir_button.connect('clicked',self._on_backupdir_button_clicked)
grid.attach(backupdir_button,2,0,1,1)
vbox.append(grid)
page.set_child(vbox)
self.add_page(page,"general","Generic settings")
def _on_backupdir_dialog_select_folder(self,dialog,result,*data):
try:
dir = dialog.select_folder_finish(result)
if dir is not None:
self.__backupdir_label.set_text(dir.get_path())
except:
pass
def _on_backupdir_button_clicked(self,button):
dialog = Gtk.FileDialog.new()
dialog.set_title("sgbackup: Choose backup folder")
dialog.select_folder(self,None,self._on_backupdir_dialog_select_folder)
def add_page(self,page,name,title):
self.__stack.add_titled(page,name,title)
def do_response(self,response):
if response == Gtk.ResponseType.APPLY:
self.emit('save')
settings.save()
self.destroy()
@Signal(name='save')
def do_save(self):
settings.backup_dir = self.__backupdir_label.get_text()

View File

@ -1,83 +0,0 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,GObject,Gio
from .appwindow import AppWindow
import logging; logger=logging.getLogger(__name__)
class Application(Gtk.Application):
def __init__(self,*args,**kwargs):
AppFlags = Gio.ApplicationFlags
kwargs['application_id'] = 'org.sgbackup.sgbackup'
kwargs['flags'] = AppFlags.FLAGS_NONE
Gtk.Application.__init__(self,*args,**kwargs)
self.__logger = logger.getChild('Application')
self.__builder = None
self.__appwindow = None
@property
def _logger(self):
return self.__logger
@GObject.Property
def appwindow(self):
return self.__appwindow
def do_startup(self):
self._logger.debug('do_startup()')
if not self.__builder:
self.__builder = Gtk.Builder.new()
Gtk.Application.do_startup(self)
action_about = Gio.SimpleAction.new('about',None)
action_about.connect('activate',self.on_action_about)
self.add_action(action_about)
action_quit = Gio.SimpleAction.new('quit',None)
action_quit.connect('activate',self.on_action_quit)
self.add_action(action_quit)
action_settings = Gio.SimpleAction.new('settings',None)
action_settings.connect('activate',self.on_action_settings)
self.add_action(action_settings)
# add accels
self.set_accels_for_action('app.quit',["<Primary>q"])
@GObject.Property
def builder(self):
return self.__builder
def do_activate(self):
self._logger.debug('do_activate()')
if not (self.__appwindow):
self.__appwindow = AppWindow(application=self)
self.appwindow.present()
def on_action_about(self,action,param):
pass
def on_action_settings(self,action,param):
pass
def on_action_quit(self,action,param):
self.quit()

View File

@ -1,6 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<interface> <interface>
<menu id='appmenu'> <menu id='appmenu'>
<section>
<submenu>
<attribute name='label' translatable='true'>_Game</attribute>
<item>
<attribute name='label' translatable='true'>_Add Game</attribute>
<attribute name='action'>app.new-game</attribute>
</item>
</submenu>
<submenu>
<attribute name='label' translatable='true'>_Steam</attribute>
</submenu>
<submenu>
<attribute name='label' translatable='true'>_Epic</attribute>
</submenu>
<submenu>
<attribute name='label' translatable='true'>_GoG</attribute>
</submenu>
</section>
<section> <section>
<item> <item>
<attribute name='label' translatable='true'>_Settings</attribute> <attribute name='label' translatable='true'>_Settings</attribute>

View File

@ -1,86 +0,0 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,Gio,GObject
import os
from .gameview import GameView
from .backupview import BackupView
class AppWindow(Gtk.ApplicationWindow):
def __init__(self,application=None,**kwargs):
kwargs['title'] = "SGBackup"
if (application is not None):
kwargs['application']=application
if (hasattr(application,'builder')):
builder = application.builder
else:
builder = Gtk.Builder.new()
Gtk.ApplicationWindow.__init__(self,**kwargs)
self.set_default_size(800,600)
self.__builder = builder
self.builder.add_from_file(os.path.join(os.path.dirname(__file__),'appmenu.ui'))
gmenu = self.builder.get_object('appmenu')
appmenu_popover = Gtk.PopoverMenu.new_from_model(gmenu)
image = Gtk.Image.new_from_icon_name('open-menu-symbolic')
menubutton = Gtk.MenuButton.new()
menubutton.set_popover(appmenu_popover)
menubutton.set_child(image)
headerbar = Gtk.HeaderBar.new()
headerbar.pack_start(menubutton)
self.set_titlebar(headerbar)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL)
self.__vpaned.set_hexpand(True)
self.__vpaned.set_vexpand(True)
self.__vpaned.set_wide_handle(True)
self.__gameview = GameView()
self.__vpaned.set_start_child(self.gameview)
self.__backupview = BackupView(self.gameview)
self.__vpaned.set_end_child(self.backupview)
self.__vpaned.set_resize_start_child(True)
self.__vpaned.set_resize_end_child(True)
vbox.append(self.__vpaned)
statusbar = Gtk.Statusbar()
statusbar.set_hexpand(True)
statusbar.set_vexpand(False)
statusbar.push(0,'Running ...')
vbox.append(statusbar)
self.set_child(vbox)
@GObject.Property
def builder(self):
return self.__builder
@GObject.Property
def backupview(self):
return self.__backupview
@GObject.Property
def gameview(self):
return self.__gameview

View File

@ -1,30 +0,0 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,Gio,GObject
from .gameview import GameView
class BackupView(Gtk.ScrolledWindow):
def __init__(self,gameview:GameView):
Gtk.ScrolledWindow.__init__(self)
self.__gameview = GameView
@GObject.Property
def gameview(self):
return self.__gameview

View File

@ -1,24 +0,0 @@
###############################################################################
# sgbackup - The SaveGame Backup tool #
# Copyright (C) 2024 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,Gio,GObject
class GameView(Gtk.ScrolledWindow):
def __init__(self):
Gtk.ScrolledWindow.__init__(self)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,21 @@
<?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>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 305 305" xml:space="preserve">
<g id="XMLID_228_">
<path id="XMLID_229_" d="M40.738,112.119c-25.785,44.745-9.393,112.648,19.121,153.82C74.092,286.523,88.502,305,108.239,305
c0.372,0,0.745-0.007,1.127-0.022c9.273-0.37,15.974-3.225,22.453-5.984c7.274-3.1,14.797-6.305,26.597-6.305
c11.226,0,18.39,3.101,25.318,6.099c6.828,2.954,13.861,6.01,24.253,5.815c22.232-0.414,35.882-20.352,47.925-37.941
c12.567-18.365,18.871-36.196,20.998-43.01l0.086-0.271c0.405-1.211-0.167-2.533-1.328-3.066c-0.032-0.015-0.15-0.064-0.183-0.078
c-3.915-1.601-38.257-16.836-38.618-58.36c-0.335-33.736,25.763-51.601,30.997-54.839l0.244-0.152
c0.567-0.365,0.962-0.944,1.096-1.606c0.134-0.661-0.006-1.349-0.386-1.905c-18.014-26.362-45.624-30.335-56.74-30.813
c-1.613-0.161-3.278-0.242-4.95-0.242c-13.056,0-25.563,4.931-35.611,8.893c-6.936,2.735-12.927,5.097-17.059,5.097
c-4.643,0-10.668-2.391-17.645-5.159c-9.33-3.703-19.905-7.899-31.1-7.899c-0.267,0-0.53,0.003-0.789,0.008
C78.894,73.643,54.298,88.535,40.738,112.119z"/>
<path id="XMLID_230_" d="M212.101,0.002c-15.763,0.642-34.672,10.345-45.974,23.583c-9.605,11.127-18.988,29.679-16.516,48.379
c0.155,1.17,1.107,2.073,2.284,2.164c1.064,0.083,2.15,0.125,3.232,0.126c15.413,0,32.04-8.527,43.395-22.257
c11.951-14.498,17.994-33.104,16.166-49.77C214.544,0.921,213.395-0.049,212.101,0.002z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 304.998 304.998" xml:space="preserve">
<g id="XMLID_91_">
<path id="XMLID_92_" d="M274.659,244.888c-8.944-3.663-12.77-8.524-12.4-15.777c0.381-8.466-4.422-14.667-6.703-17.117
c1.378-5.264,5.405-23.474,0.004-39.291c-5.804-16.93-23.524-42.787-41.808-68.204c-7.485-10.438-7.839-21.784-8.248-34.922
c-0.392-12.531-0.834-26.735-7.822-42.525C190.084,9.859,174.838,0,155.851,0c-11.295,0-22.889,3.53-31.811,9.684
c-18.27,12.609-15.855,40.1-14.257,58.291c0.219,2.491,0.425,4.844,0.545,6.853c1.064,17.816,0.096,27.206-1.17,30.06
c-0.819,1.865-4.851,7.173-9.118,12.793c-4.413,5.812-9.416,12.4-13.517,18.539c-4.893,7.387-8.843,18.678-12.663,29.597
c-2.795,7.99-5.435,15.537-8.005,20.047c-4.871,8.676-3.659,16.766-2.647,20.505c-1.844,1.281-4.508,3.803-6.757,8.557
c-2.718,5.8-8.233,8.917-19.701,11.122c-5.27,1.078-8.904,3.294-10.804,6.586c-2.765,4.791-1.259,10.811,0.115,14.925
c2.03,6.048,0.765,9.876-1.535,16.826c-0.53,1.604-1.131,3.42-1.74,5.423c-0.959,3.161-0.613,6.035,1.026,8.542
c4.331,6.621,16.969,8.956,29.979,10.492c7.768,0.922,16.27,4.029,24.493,7.035c8.057,2.944,16.388,5.989,23.961,6.913
c1.151,0.145,2.291,0.218,3.39,0.218c11.434,0,16.6-7.587,18.238-10.704c4.107-0.838,18.272-3.522,32.871-3.882
c14.576-0.416,28.679,2.462,32.674,3.357c1.256,2.404,4.567,7.895,9.845,10.724c2.901,1.586,6.938,2.495,11.073,2.495
c0.001,0,0,0,0.001,0c4.416,0,12.817-1.044,19.466-8.039c6.632-7.028,23.202-16,35.302-22.551c2.7-1.462,5.226-2.83,7.441-4.065
c6.797-3.768,10.506-9.152,10.175-14.771C282.445,250.905,279.356,246.811,274.659,244.888z M124.189,243.535
c-0.846-5.96-8.513-11.871-17.392-18.715c-7.26-5.597-15.489-11.94-17.756-17.312c-4.685-11.082-0.992-30.568,5.447-40.602
c3.182-5.024,5.781-12.643,8.295-20.011c2.714-7.956,5.521-16.182,8.66-19.783c4.971-5.622,9.565-16.561,10.379-25.182
c4.655,4.444,11.876,10.083,18.547,10.083c1.027,0,2.024-0.134,2.977-0.403c4.564-1.318,11.277-5.197,17.769-8.947
c5.597-3.234,12.499-7.222,15.096-7.585c4.453,6.394,30.328,63.655,32.972,82.044c2.092,14.55-0.118,26.578-1.229,31.289
c-0.894-0.122-1.96-0.221-3.08-0.221c-7.207,0-9.115,3.934-9.612,6.283c-1.278,6.103-1.413,25.618-1.427,30.003
c-2.606,3.311-15.785,18.903-34.706,21.706c-7.707,1.12-14.904,1.688-21.39,1.688c-5.544,0-9.082-0.428-10.551-0.651l-9.508-10.879
C121.429,254.489,125.177,250.583,124.189,243.535z M136.254,64.149c-0.297,0.128-0.589,0.265-0.876,0.411
c-0.029-0.644-0.096-1.297-0.199-1.952c-1.038-5.975-5-10.312-9.419-10.312c-0.327,0-0.656,0.025-1.017,0.08
c-2.629,0.438-4.691,2.413-5.821,5.213c0.991-6.144,4.472-10.693,8.602-10.693c4.85,0,8.947,6.536,8.947,14.272
C136.471,62.143,136.4,63.113,136.254,64.149z M173.94,68.756c0.444-1.414,0.684-2.944,0.684-4.532
c0-7.014-4.45-12.509-10.131-12.509c-5.552,0-10.069,5.611-10.069,12.509c0,0.47,0.023,0.941,0.067,1.411
c-0.294-0.113-0.581-0.223-0.861-0.329c-0.639-1.935-0.962-3.954-0.962-6.015c0-8.387,5.36-15.211,11.95-15.211
c6.589,0,11.95,6.824,11.95,15.211C176.568,62.78,175.605,66.11,173.94,68.756z M169.081,85.08
c-0.095,0.424-0.297,0.612-2.531,1.774c-1.128,0.587-2.532,1.318-4.289,2.388l-1.174,0.711c-4.718,2.86-15.765,9.559-18.764,9.952
c-2.037,0.274-3.297-0.516-6.13-2.441c-0.639-0.435-1.319-0.897-2.044-1.362c-5.107-3.351-8.392-7.042-8.763-8.485
c1.665-1.287,5.792-4.508,7.905-6.415c4.289-3.988,8.605-6.668,10.741-6.668c0.113,0,0.215,0.008,0.321,0.028
c2.51,0.443,8.701,2.914,13.223,4.718c2.09,0.834,3.895,1.554,5.165,2.01C166.742,82.664,168.828,84.422,169.081,85.08z
M205.028,271.45c2.257-10.181,4.857-24.031,4.436-32.196c-0.097-1.855-0.261-3.874-0.42-5.826
c-0.297-3.65-0.738-9.075-0.283-10.684c0.09-0.042,0.19-0.078,0.301-0.109c0.019,4.668,1.033,13.979,8.479,17.226
c2.219,0.968,4.755,1.458,7.537,1.458c7.459,0,15.735-3.659,19.125-7.049c1.996-1.996,3.675-4.438,4.851-6.372
c0.257,0.753,0.415,1.737,0.332,3.005c-0.443,6.885,2.903,16.019,9.271,19.385l0.927,0.487c2.268,1.19,8.292,4.353,8.389,5.853
c-0.001,0.001-0.051,0.177-0.387,0.489c-1.509,1.379-6.82,4.091-11.956,6.714c-9.111,4.652-19.438,9.925-24.076,14.803
c-6.53,6.872-13.916,11.488-18.376,11.488c-0.537,0-1.026-0.068-1.461-0.206C206.873,288.406,202.886,281.417,205.028,271.45z
M39.917,245.477c-0.494-2.312-0.884-4.137-0.465-5.905c0.304-1.31,6.771-2.714,9.533-3.313c3.883-0.843,7.899-1.714,10.525-3.308
c3.551-2.151,5.474-6.118,7.17-9.618c1.228-2.531,2.496-5.148,4.005-6.007c0.085-0.05,0.215-0.108,0.463-0.108
c2.827,0,8.759,5.943,12.177,11.262c0.867,1.341,2.473,4.028,4.331,7.139c5.557,9.298,13.166,22.033,17.14,26.301
c3.581,3.837,9.378,11.214,7.952,17.541c-1.044,4.909-6.602,8.901-7.913,9.784c-0.476,0.108-1.065,0.163-1.758,0.163
c-7.606,0-22.662-6.328-30.751-9.728l-1.197-0.503c-4.517-1.894-11.891-3.087-19.022-4.241c-5.674-0.919-13.444-2.176-14.732-3.312
c-1.044-1.171,0.167-4.978,1.235-8.337c0.769-2.414,1.563-4.91,1.998-7.523C41.225,251.596,40.499,248.203,39.917,245.477z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@ -0,0 +1,6 @@
<?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" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>steam</title>
<path d="M18.102 12.129c0-0 0-0 0-0.001 0-1.564 1.268-2.831 2.831-2.831s2.831 1.268 2.831 2.831c0 1.564-1.267 2.831-2.831 2.831-0 0-0 0-0.001 0h0c-0 0-0 0-0.001 0-1.563 0-2.83-1.267-2.83-2.83 0-0 0-0 0-0.001v0zM24.691 12.135c0-2.081-1.687-3.768-3.768-3.768s-3.768 1.687-3.768 3.768c0 2.081 1.687 3.768 3.768 3.768v0c2.080-0.003 3.765-1.688 3.768-3.767v-0zM10.427 23.76l-1.841-0.762c0.524 1.078 1.611 1.808 2.868 1.808 1.317 0 2.448-0.801 2.93-1.943l0.008-0.021c0.155-0.362 0.246-0.784 0.246-1.226 0-1.757-1.424-3.181-3.181-3.181-0.405 0-0.792 0.076-1.148 0.213l0.022-0.007 1.903 0.787c0.852 0.364 1.439 1.196 1.439 2.164 0 1.296-1.051 2.347-2.347 2.347-0.324 0-0.632-0.066-0.913-0.184l0.015 0.006zM15.974 1.004c-7.857 0.001-14.301 6.046-14.938 13.738l-0.004 0.054 8.038 3.322c0.668-0.462 1.495-0.737 2.387-0.737 0.001 0 0.002 0 0.002 0h-0c0.079 0 0.156 0.005 0.235 0.008l3.575-5.176v-0.074c0.003-3.12 2.533-5.648 5.653-5.648 3.122 0 5.653 2.531 5.653 5.653s-2.531 5.653-5.653 5.653h-0.131l-5.094 3.638c0 0.065 0.005 0.131 0.005 0.199 0 0.001 0 0.002 0 0.003 0 2.342-1.899 4.241-4.241 4.241-2.047 0-3.756-1.451-4.153-3.38l-0.005-0.027-5.755-2.383c1.841 6.345 7.601 10.905 14.425 10.905 8.281 0 14.994-6.713 14.994-14.994s-6.713-14.994-14.994-14.994c-0 0-0.001 0-0.001 0h0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 305 305" xml:space="preserve">
<g id="XMLID_108_">
<path id="XMLID_109_" d="M139.999,25.775v116.724c0,1.381,1.119,2.5,2.5,2.5H302.46c1.381,0,2.5-1.119,2.5-2.5V2.5
c0-0.726-0.315-1.416-0.864-1.891c-0.548-0.475-1.275-0.687-1.996-0.583L142.139,23.301
C140.91,23.48,139.999,24.534,139.999,25.775z"/>
<path id="XMLID_110_" d="M122.501,279.948c0.601,0,1.186-0.216,1.644-0.616c0.544-0.475,0.856-1.162,0.856-1.884V162.5
c0-1.381-1.119-2.5-2.5-2.5H2.592c-0.663,0-1.299,0.263-1.768,0.732c-0.469,0.469-0.732,1.105-0.732,1.768l0.006,98.515
c0,1.25,0.923,2.307,2.16,2.477l119.903,16.434C122.274,279.94,122.388,279.948,122.501,279.948z"/>
<path id="XMLID_138_" d="M2.609,144.999h119.892c1.381,0,2.5-1.119,2.5-2.5V28.681c0-0.722-0.312-1.408-0.855-1.883
c-0.543-0.475-1.261-0.693-1.981-0.594L2.164,42.5C0.923,42.669-0.001,43.728,0,44.98l0.109,97.521
C0.111,143.881,1.23,144.999,2.609,144.999z"/>
<path id="XMLID_169_" d="M302.46,305c0.599,0,1.182-0.215,1.64-0.613c0.546-0.475,0.86-1.163,0.86-1.887l0.04-140
c0-0.663-0.263-1.299-0.732-1.768c-0.469-0.469-1.105-0.732-1.768-0.732H142.499c-1.381,0-2.5,1.119-2.5,2.5v117.496
c0,1.246,0.918,2.302,2.151,2.476l159.961,22.504C302.228,304.992,302.344,305,302.46,305z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -19,9 +19,9 @@
import logging import logging
from . import gui from . import gui
from .gui.application import Application from .gui import Application
from .steam import SteamLibrary from .steam import SteamLibrary
import sys
logger=logging.getLogger(__name__) logger=logging.getLogger(__name__)
@ -37,6 +37,6 @@ def curses_main():
def gui_main(): def gui_main():
logger.debug("Running gui_main()") logger.debug("Running gui_main()")
gui.app = Application() gui._app = Application()
gui.app.run() gui._app.run()
return 0 return 0

View File

@ -23,6 +23,8 @@ import sys
from gi.repository import GLib,GObject from gi.repository import GLib,GObject
class Settings(GObject.GObject): class Settings(GObject.GObject):
__gtype_name__ = "Settings"
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -35,6 +37,13 @@ class Settings(GObject.GObject):
if (os.path.isfile(self.__config_file)): if (os.path.isfile(self.__config_file)):
with open(self.__config_file,'r') as conf: with open(self.__config_file,'r') as conf:
self.__configparser.read_file(conf) self.__configparser.read_file(conf)
if not os.path.isdir(self.config_dir):
os.makedirs(self.config_dir)
if not os.path.isdir(self.gameconf_dir):
os.makedirs(self.gameconf_dir)
@GObject.Property(nick="parser") @GObject.Property(nick="parser")
def parser(self)->ConfigParser: def parser(self)->ConfigParser:
@ -61,11 +70,12 @@ class Settings(GObject.GObject):
def backup_dir(self)->str: def backup_dir(self)->str:
if self.parser.has_option('sgbackup','backupDirectory'): if self.parser.has_option('sgbackup','backupDirectory'):
return self.parser.get('sgbackup','backupDirectory') return self.parser.get('sgbackup','backupDirectory')
return GLib.build_filename(GLib.build_filename(GLib.get_home_dir(),'SavagameBackups')) return os.path.join(GLib.get_home_dir(),'SavagameBackups')
@backup_dir.setter @backup_dir.setter
def backup_dir(self,directory:str): def backup_dir(self,directory:str):
if not os.path.isabs(directory): if not os.path.isabs(directory):
raise ValueError("\"backup_dir\" needs to be an absolute path!") raise ValueError("\"backup_dir\" needs to be an absolute path!")
self.ensure_section('sgbackup')
return self.parser.set('sgbackup','backupDirectory',directory) return self.parser.set('sgbackup','backupDirectory',directory)
@GObject.Property(type=str) @GObject.Property(type=str)
@ -77,6 +87,10 @@ class Settings(GObject.GObject):
def save(self): def save(self):
self.emit('save') self.emit('save')
def ensure_section(self,section:str):
if not self.parser.has_section(section):
self.parser.add_section(section)
@GObject.Signal(name='save',flags=GObject.SIGNAL_RUN_LAST,return_type=None,arg_types=()) @GObject.Signal(name='save',flags=GObject.SIGNAL_RUN_LAST,return_type=None,arg_types=())
def do_save(self): def do_save(self):
with open(self.config_file,'w') as ofile: with open(self.config_file,'w') as ofile:

View File

@ -23,8 +23,11 @@ import sys
import json import json
from .settings import settings from .settings import settings
from .game import STEAM_GAMES,STEAM_WINDOWS_GAMES,STEAM_LINUX_GAMES,STEAM_MACOS_GAMES
PLATFORM_WINDOWS = (sys.platform.lower() == 'win32') PLATFORM_WINDOWS = (sys.platform.lower() == 'win32')
PLATFORM_LINUX = (sys.platform.lower() in ('linux','freebsd','netbsd','openbsd','dragonfly'))
PLATFORM_MACOS = (sys.platform.lower() == 'macos')
from gi.repository.GObject import GObject,Property,Signal from gi.repository.GObject import GObject,Property,Signal
@ -69,8 +72,6 @@ class AcfFileParser(object):
return line_count,ret return line_count,ret
def parse_file(self,acf_file)->dict: def parse_file(self,acf_file)->dict:
if not os.path.isfile(acf_file): if not os.path.isfile(acf_file):
@ -86,6 +87,14 @@ class AcfFileParser(object):
raise RuntimeError("Not a acf file!") raise RuntimeError("Not a acf file!")
class IgnoreSteamApp(GObject): class IgnoreSteamApp(GObject):
__gtype_name__ = "sgbackup-steam-IgnoreSteamApp"
def __init__(self,appid:int,name:str,reason:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__reason = reason
@staticmethod @staticmethod
def new_from_dict(conf:dict): def new_from_dict(conf:dict):
if ('appid' in conf and 'name' in conf): if ('appid' in conf and 'name' in conf):
@ -95,12 +104,6 @@ class IgnoreSteamApp(GObject):
return SteamIgnoreApp(appid,name,reason) return SteamIgnoreApp(appid,name,reason)
return None return None
def __init__(self,appid:int,name:str,reason:str):
GObject.__init__(self)
self.__appid = int(appid)
self.__name = name
self.__reason = reason
@Property(type=int) @Property(type=int)
def appid(self)->str: def appid(self)->str:
@ -120,7 +123,6 @@ class IgnoreSteamApp(GObject):
def reason(self,reason:str): def reason(self,reason:str):
self.__reason = reason self.__reason = reason
def serialize(self): def serialize(self):
return { return {
'appid': self.appid, 'appid': self.appid,
@ -128,7 +130,10 @@ class IgnoreSteamApp(GObject):
'reason': self.reason, 'reason': self.reason,
} }
class SteamApp(GObject): class SteamApp(GObject):
__gtype_name__ = "sgbackup-steam-SteamApp"
def __init__(self,appid:int,name:str,installdir:str): def __init__(self,appid:int,name:str,installdir:str):
GObject.__init__(self) GObject.__init__(self)
self.__appid = int(appid) self.__appid = int(appid)
@ -159,8 +164,11 @@ class SteamApp(GObject):
def __eq__(self,other): def __eq__(self,other):
return self.appid == other.appid return self.appid == other.appid
class SteamLibrary(GObject): class SteamLibrary(GObject):
__gtype_name__ = "sgbackup-steam-SteamLibrary"
def __init__(self,library_path:str): def __init__(self,library_path:str):
GObject.__init__(self) GObject.__init__(self)
self.directory = library_path self.directory = library_path
@ -183,7 +191,7 @@ class SteamLibrary(GObject):
return Path(self.directory).resolve() return Path(self.directory).resolve()
@Property @Property
def steam_apps(self)->list: def steam_apps(self)->list[SteamApp]:
parser = AcfFileParser() parser = AcfFileParser()
appdir = self.path / "steamapps" appdir = self.path / "steamapps"
commondir = appdir / "common" commondir = appdir / "common"
@ -202,10 +210,12 @@ class SteamLibrary(GObject):
return sorted(ret) return sorted(ret)
class Steam(GObject): class Steam(GObject):
__gtype_name__ = "sgbackup-steam-Steam"
def __init__(self): def __init__(self):
GObject.__init__(self) GObject.__init__(self)
self.__libraries = [] self.__libraries = []
self.__ignore_apps = [] self.__ignore_apps = {}
if not self.steamlib_list_file.is_file(): if not self.steamlib_list_file.is_file():
if (PLATFORM_WINDOWS): if (PLATFORM_WINDOWS):
@ -229,7 +239,6 @@ class Steam(GObject):
except: except:
pass pass
ignore_apps = []
if self.ignore_apps_file.is_file(): if self.ignore_apps_file.is_file():
with open(str(self.ignore_apps_file),'r',encoding="utf-8") as ifile: with open(str(self.ignore_apps_file),'r',encoding="utf-8") as ifile:
ignore_list = json.loads(ifile.read()) ignore_list = json.loads(ifile.read())
@ -239,8 +248,8 @@ class Steam(GObject):
except: except:
continue continue
if ignore_app: if ignore_app:
self.__ignore_apps.append(ignore_app) self.__ignore_apps[ignore_app.appid] = ignore_app
self.__ignore_apps = sorted(ignore_apps)
#__init__() #__init__()
@Property @Property
@ -252,17 +261,21 @@ class Steam(GObject):
return Path(settings.config_dir).resolve / 'ignore_steamapps.json' return Path(settings.config_dir).resolve / 'ignore_steamapps.json'
@Property @Property
def libraries(self): def libraries(self)->list[SteamLibrary]:
return self.__libraries return self.__libraries
@Property @Property
def ignore_apps(self): def ignore_apps(self)->dict[int:IgnoreSteamApp]:
return self.__ignore_apps return self.__ignore_apps
def __write_steamlib_list_file(self): def __write_steamlib_list_file(self):
with open(self.steamlib_list_file,'w',encoding='utf-8') as ofile: with open(self.steamlib_list_file,'w',encoding='utf-8') as ofile:
ofile.write('\n'.join(str(sl.directory) for sl in self.libraries)) ofile.write('\n'.join(str(sl.directory) for sl in self.libraries))
def __write_ignore_steamapps_file(self):
with open(self.ignore_apps_file,'w',encoding='utf-8') as ofile:
ofile.write(json.dumps([i.serialize() for i in self.ignore_apps.values()]))
def add_library(self,steamlib:SteamLibrary|str): def add_library(self,steamlib:SteamLibrary|str):
if isinstance(steamlib,SteamLibrary): if isinstance(steamlib,SteamLibrary):
lib = steamlib lib = steamlib
@ -294,3 +307,46 @@ class Steam(GObject):
for i in sorted(delete_libs,reverse=True): for i in sorted(delete_libs,reverse=True):
del self.__libraries[i] del self.__libraries[i]
self.__write_steamlib_list_file() self.__write_steamlib_list_file()
def add_ignore_app(self,app:IgnoreSteamApp):
self.__ignore_apps[app.appid] = app
self.__write_ignore_steamapps_file()
def remove_ignore_app(self,app:IgnoreSteamApp|int):
if isinstance(app,IgnoreSteamApp):
appid = app.appid
else:
appid = int(app)
if appid in self.__ignore_apps:
del self.__ignore_apps[appid]
self.__write_ignore_steamapps_file()
def find_new_steamapps(self)->list[SteamApp]:
new_apps = []
for lib in self.libraries:
for app in lib.steam_apps:
if not app.appid in STEAM_GAMES and not app.appid in self.ignore_apps:
new_apps.append(app)
return sorted(new_apps)
def update_steam_apps(self):
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]
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]
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]
game.installdir = app.installdir
game.save()