From 70021916bce04b6184aa6ca9b4aa89db28639eef Mon Sep 17 00:00:00 2001 From: deajan Date: Wed, 22 Oct 2025 12:48:26 +0200 Subject: [PATCH] GUI: WIP wizard theming --- npbackup/__version__.py | 4 +- npbackup/configuration.py | 33 ++++- npbackup/gui/__main__.py | 11 +- npbackup/gui/buttons.py | 3 +- npbackup/gui/config.py | 3 + npbackup/gui/constants.py | 3 +- npbackup/gui/wizard.py | 177 ++++++++++++++++-------- npbackup/requirements.txt | 1 + npbackup/translations/generic.en.yml | 1 + npbackup/translations/generic.fr.yml | 1 + npbackup/translations/wizard_gui.en.yml | 7 + npbackup/translations/wizard_gui.fr.yml | 7 + 12 files changed, 183 insertions(+), 68 deletions(-) diff --git a/npbackup/__version__.py b/npbackup/__version__.py index 91547c5..6fad219 100644 --- a/npbackup/__version__.py +++ b/npbackup/__version__.py @@ -9,8 +9,8 @@ __site__ = "https://www.netperfect.fr/npbackup" __description__ = "NetPerfect Backup Client" __copyright__ = "Copyright (C) 2022-2025 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2025092901" -__version__ = "3.0.4" +__build__ = "2025102201" +__version__ = "3.1.0-dev" import sys diff --git a/npbackup/configuration.py b/npbackup/configuration.py index 8ff363e..5f5cc91 100644 --- a/npbackup/configuration.py +++ b/npbackup/configuration.py @@ -7,8 +7,8 @@ __intname__ = "npbackup.configuration" __author__ = "Orsiris de Jong" __copyright__ = "Copyright (C) 2022-2025 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2025100401" -__version__ = "npbackup 3.0.4+" +__build__ = "2025102201" +__version__ = "npbackup 3.1.0+" from typing import Tuple, Optional, List, Any, Union @@ -32,7 +32,7 @@ from npbackup.key_management import AES_KEY, EARLIER_AES_KEY, IS_PRIV_BUILD, get from npbackup.__version__ import __version__ as MAX_CONF_VERSION MIN_MIGRATABLE_CONF_VERSION = "3.0.0" -MIN_CONF_VERSION = "3.0.4" +MIN_CONF_VERSION = "3.1.0" sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))) @@ -249,6 +249,31 @@ empty_config_dict = { "full_concurrency": False, # Allow multiple npbackup instances to run at the same time "repo_aware_concurrency": False, # Allow multiple npbackup instances to run at the same time, but only for different repos }, + "presets": { + "adds_to_existing": True, + "replaces_existing": False, + "retention_policies": { + "gfs": { + "keep_daily": 30, + "keep_weekly": 4, + "keep_monthly": 12, + "keep_yearly": 3, + "keep_within": True, + "group_by_host": True, + "group_by_tags": True, + "group_by_paths": False, + "ntp_server": None, + "keep_tags": [], + "apply_on_tags": [], + } + }, + }, + "destinations": { + "default_destination": { + "repo_uri": None, + "repo_password": None, + } + }, } @@ -946,6 +971,8 @@ def _load_config_file(config_file: Path) -> Union[bool, dict]: def load_config(config_file: Path) -> Optional[dict]: + if not isinstance(config_file, Path): + config_file = Path(config_file) full_config = _load_config_file(config_file) if not full_config: return None diff --git a/npbackup/gui/__main__.py b/npbackup/gui/__main__.py index 5d1e0e6..bf22020 100644 --- a/npbackup/gui/__main__.py +++ b/npbackup/gui/__main__.py @@ -60,6 +60,7 @@ from npbackup.__version__ import version_dict, version_string from npbackup.__debug__ import _DEBUG, _NPBACKUP_ALLOW_AUTOUPGRADE_DEBUG from npbackup.restic_wrapper import ResticRunner from npbackup.restic_wrapper import schema +import sv_ttk logger = getLogger() @@ -1128,10 +1129,11 @@ def _main_gui(viewer_mode: bool): alpha_channel=1.0, default_button_element_size=(16, 1), right_click_menu=right_click_menu, - finalize=True, + use_ttk_buttons=True, ) - - # Auto reisze table to window size + window.finalize() + sv_ttk.set_theme("light") + # Auto resize table to window size window["snapshot-list"].expand(True, True) window.read(timeout=0.01) @@ -1140,7 +1142,8 @@ def _main_gui(viewer_mode: bool): if repo_config: try: - current_state, backup_tz, snapshot_list = get_gui_data(repo_config) + # current_state, backup_tz, snapshot_list = get_gui_data(repo_config) + raise TypeError # WIP except (TypeError, ValueError): current_state = None backup_tz = None diff --git a/npbackup/gui/buttons.py b/npbackup/gui/buttons.py index 1648328..3770db6 100644 --- a/npbackup/gui/buttons.py +++ b/npbackup/gui/buttons.py @@ -111,7 +111,8 @@ def RoundedButton( metadata=None, btn_size=(None, None), ): - + if button_color is None: + button_color = sg.theme_button_color() if btn_size != (None, None): btn_width: int = btn_size[0] btn_height: int = btn_size[1] diff --git a/npbackup/gui/config.py b/npbackup/gui/config.py index 073ae20..ce2498d 100644 --- a/npbackup/gui/config.py +++ b/npbackup/gui/config.py @@ -41,6 +41,7 @@ from resources.customization import ( from npbackup.task import create_scheduled_task from npbackup.gui.helpers import quick_close_simplegui_window from npbackup.gui.constants import combo_boxes, byte_units +import sv_ttk logger = getLogger() @@ -2628,6 +2629,7 @@ Google Cloud storage: GOOGLE_PROJECT_ID GOOGLE_APPLICATION_CREDENTIALS\n\ if config_file: window.set_title(f"Configuration - {config_file}") + # sv_ttk.set_theme("light") while True: event, values = window.read() # Get object type for various delete operations @@ -2822,6 +2824,7 @@ Google Cloud storage: GOOGLE_PROJECT_ID GOOGLE_APPLICATION_CREDENTIALS\n\ tree = env_variables_tree option_key = "env.env_variables" + # It's --ADD- and not --ADD-- since we include --ADD-*-- STYLE events if event.startswith("--ADD-"): icon = TREE_ICON if "ENV-VARIABLE" in event or "ENCRYPTED-ENV-VARIABLE" in event: diff --git a/npbackup/gui/constants.py b/npbackup/gui/constants.py index e5d32d1..f7e9288 100644 --- a/npbackup/gui/constants.py +++ b/npbackup/gui/constants.py @@ -39,7 +39,8 @@ combo_boxes = { }, "retention_options": { "GFS": _t("wizard_gui.retention_gfs"), - "30days": _t("wizard_gui.retention_30days"), + "14days": _t("wizard_gui.retention_14_days"), + "30days": _t("wizard_gui.retention_30_days"), "keep_all": _t("wizard_gui.retention_keep_all"), }, "backends": { diff --git a/npbackup/gui/wizard.py b/npbackup/gui/wizard.py index cdc256d..10cb707 100644 --- a/npbackup/gui/wizard.py +++ b/npbackup/gui/wizard.py @@ -34,77 +34,85 @@ from resources.customization import ( TXT_COLOR_LDR, SIMPLEGUI_THEME, THEME_CHOOSER_ICON, + TREE_ICON, ) from npbackup.gui.constants import combo_boxes, byte_units -from npbackup.core.i18n_helper import _t +from npbackup.core.i18n_helper import _t, _locale from npbackup.gui.buttons import RoundedButton +import npbackup.configuration +import sv_ttk +CONFIG_FILE = "npbackup.conf" # WIP override via --config-file sg.LOOK_AND_FEEL_TABLE["CLEAR"] = LOOK_AND_FEEL_TABLE["CLEAR"] sg.LOOK_AND_FEEL_TABLE["DARK"] = LOOK_AND_FEEL_TABLE["DARK"] sg.theme(SIMPLEGUI_THEME) logger = getLogger() +add_source_menu = [ + "-ADD-SOURCE-", + [ + _t("generic.add_files"), + _t("generic.add_folder"), + _t("wizard_gui.add_system"), + _t("wizard_gui.add_hyper_v"), + _t("wizard_gui.add_kvm"), + ], +] + +date_options = { + "format": "%Y-%m-%d", + "default_date_m_d_y": ( + datetime.now().month, + datetime.now().day, + datetime.now().year, + ), + "close_when_date_chosen": True, +} + +conf = npbackup.configuration.load_config(CONFIG_FILE) +if not conf: + conf = npbackup.configuration.get_default_config() +try: + retention_policies = list(conf.g("presets.retention_policies").keys()) +except Exception: + retention_policies = {} + +backup_paths_tree = sg.TreeData() +# retention_policies = list(combo_boxes["retention_options"].values()) + wizard_layouts = { "wizard_layout_1": [ [ sg.Text( - textwrap.fill(f"{_t('wizard_gui.select_backup_sources')}", 70), - size=(None, None), - expand_x=True, - justification="c", + textwrap.fill(f"{_t('wizard_gui.select_backup_sources')}"), + size=(40, 1), + expand_x=False, + font=("Helvetica", 16), + ), + sg.Push(), + sg.ButtonMenu( + _t("generic.add"), + menu_def=add_source_menu, + key="-ADD-SOURCE-MENU-", + button_color=(TXT_COLOR_LDR, BG_COLOR_LDR), ), ], [ - sg.Input(visible=False, key="--ADD-PATHS-FILE--", enable_events=True), - sg.FilesBrowse( - "", # _t("generic.add_files" - target="--ADD-PATHS-FILE--", - key="--ADD-PATHS-FILE-BUTTON--", - # button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"]) - ), - sg.Input(visible=False, key="--ADD-PATHS-FOLDER--", enable_events=True), - sg.FolderBrowse( - "", # _t("generic.add_folder"), - target="--ADD-PATHS-FOLDER--", - key="--ADD-PATHS-FOLDER-BUTTON--", - # button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"]) - ), - sg.Button( - "", # _t("generic.add_manually"), - key="--ADD-PATHS-MANUALLY--", - border_width=0, - # button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"]) - ), - sg.Button( - "", # _t("generic.remove_selected"), - key="--REMOVE-PATHS--", - border_width=0, - # button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"]) - ), - sg.Button( - "", - key="-ADD-WINDOWS-SYSTEM-", - border_width=0, - ), - sg.Button( - "", - key="-ADD-HYPERV-", - border_width=0, - ), - sg.Button( - "", - key="-ADD-KVM-", - border_width=0, + sg.Text( + textwrap.fill(f"{_t('wizard_gui.select_backup_sources_description')}"), + size=(80, 2), + expand_x=False, + justification="L", ), ], [ sg.Tree( sg.TreeData(), key="backup_opts.paths", - headings=[], - col0_heading=_t("config_gui.backup_sources"), + headings=["Type", "Details"], + # col0_heading=_t("config_gui.backup_sources"), expand_x=True, expand_y=True, header_text_color=TXT_COLOR_LDR, @@ -126,6 +134,30 @@ wizard_layouts = { ], ], "wizard_layout_3": [ + [sg.Text(_t("wizard_gui.step_3"), font=("Helvetica", 16))], + [ + sg.Input("YYYY/MM/DD", key="-FIRST-BACKUP-DATE-", size=(12, 1)), + sg.Combo( + values=[h for h in range(0, 24)], + default_value=0, + key="-FIRST-BACKUP-HOUR-", + size=(3, 1), + ), + sg.Text(" : "), + sg.Combo( + values=[m for m in range(0, 60)], + default_value=0, + key="-FIRST-BACKUP-MINUTE-", + size=(3, 1), + ), + ], + [ + sg.CalendarButton( + "Calendar", target="-FIRST-BACKUP-DATE-", key="CALENDAR", **date_options + ), + ], + ], + "wizard_layout_4": [ [ sg.Column( [ @@ -168,18 +200,18 @@ wizard_layouts = { ), ], ], - "wizard_layout_4": [ + "wizard_layout_5": [ [sg.T(_t("wizard_gui.retention_settings"), font=("Helvetica", 16))], [ sg.Combo( - values=list(combo_boxes["retention_options"].values()), - default_value=next(iter(combo_boxes["retention_options"])), + values=retention_policies, + default_value=retention_policies[0], key="-RETENTION-TYPE-", enable_events=True, ) ], ], - "wizard_layout_5": [ + "wizard_layout_6": [ [sg.Text(_t("wizard_gui.end_user_experience"), font=("Helvetica", 16))], [ sg.Checkbox( @@ -189,7 +221,7 @@ wizard_layouts = { ) ], ], - "wizard_layout_6": [ + "wizard_layout_7": [ [sg.Text(_t("wizard_gui.end_user_experience"), font=("Helvetica", 16))], ], } @@ -208,7 +240,7 @@ for i in range(1, len(wizard_layouts)): [ RoundedButton( str(i), - button_color=("#FAFAFA", "#ADADAD"), + button_color=(TXT_COLOR_LDR, BG_COLOR_LDR), border_width=0, key=f"-BREADCRUMB-{i}-", btn_size=(30, 30), @@ -249,7 +281,7 @@ wizard_layout = [ border_width=0, ), RoundedButton( - _t("generic.start"), + _t("generic.next"), key="-NEXT-", button_color=(TXT_COLOR_LDR, BG_COLOR_LDR), border_width=0, @@ -266,8 +298,8 @@ wizard_layout = [ def start_wizard(): CURRENT_THEME = SIMPLEGUI_THEME - NUMBER_OF_TABS = len(wizard_tabs) + 1 - current_tab = 0 + NUMBER_OF_TABS = len(wizard_tabs) + current_tab = 1 wizard = sg.Window( "NPBackup Wizard", layout=wizard_layout, @@ -286,7 +318,7 @@ def start_wizard(): wizard.TKroot.after(60000, _reskin_job) def set_active_tab(active_number): - for tab_index in range(1, NUMBER_OF_TABS): + for tab_index in range(1, NUMBER_OF_TABS + 1): if tab_index != active_number: wizard[f"-TAB{tab_index}-"].Update(visible=False) wizard[f"-BREADCRUMB-{tab_index}-"].Update( @@ -296,9 +328,13 @@ def start_wizard(): wizard[f"-BREADCRUMB-{active_number}-"].Update(button_color=("#3F2DCB", None)) wizard.finalize() + # Widget theming from https://github.com/rdbende/Sun-Valley-ttk-theme?tab=readme-ov-file + sv_ttk.set_theme("light") set_active_tab(1) + while True: event, values = wizard.read() + print(event, values) if event == sg.WIN_CLOSED or event == _t("generic.cancel"): break if event == "-THEME-": @@ -337,6 +373,33 @@ def start_wizard(): set_active_tab(current_tab) elif current_tab == 1: break + if event == "-ADD-SOURCE-MENU-": + node = None + if values["-ADD-SOURCE-MENU-"] == _t("generic.add_files"): + sg.FileBrowse(_t("generic.add_files"), target="backup_opts.paths") + node = sg.popup_get_file("Add files clicked", no_window=True) + elif values["-ADD-SOURCE-MENU-"] == _t("generic.add_folder"): + node = sg.popup_get_folder("Add folder clicked", no_window=True) + elif values["-ADD-SOURCE-MENU-"] == _t("wizard_gui.add_system"): + sg.popup("Add Windows system clicked", keep_on_top=True) + elif values["-ADD-SOURCE-MENU-"] == _t("wizard_gui.add_hyper_v"): + sg.popup("Add Hyper-V virtual machines clicked", keep_on_top=True) + elif values["-ADD-SOURCE-MENU-"] == _t("wizard_gui.add_kvm"): + sg.popup("Add KVM virtual machines clicked", keep_on_top=True) + if node: + icon = TREE_ICON + tree = backup_paths_tree + # Check if node is ADD-PATH-FILES which can contain multiple elements separated by semicolon + if ";" in node: + for path in node.split(";"): + if tree.tree_dict.get(path): + tree.delete(path) + tree.insert("", path, path, path, icon=icon) + else: + if tree.tree_dict.get(node): + tree.delete(node) + tree.insert("", node, node, node, icon=icon) + wizard["backup_opts.paths"].update(values=tree) wizard.close() diff --git a/npbackup/requirements.txt b/npbackup/requirements.txt index a47b8fd..c59b74e 100644 --- a/npbackup/requirements.txt +++ b/npbackup/requirements.txt @@ -14,6 +14,7 @@ ofunctions.mailer>=1.3.0 # keep in mind that freesimplegui might higher required python version in the future freesimplegui==5.2.0.post1 reskinner==4.0.0 +sv_ttk pillow requests ruamel.yaml diff --git a/npbackup/translations/generic.en.yml b/npbackup/translations/generic.en.yml index 60aa6f6..2eeda0d 100644 --- a/npbackup/translations/generic.en.yml +++ b/npbackup/translations/generic.en.yml @@ -71,6 +71,7 @@ en: bad_file: Bad file file_does_not_exist: File does not exist + add: Add add_files: Add files add_folder: Add folder add_manually: Add manually diff --git a/npbackup/translations/generic.fr.yml b/npbackup/translations/generic.fr.yml index b1bb8e4..d803dd4 100644 --- a/npbackup/translations/generic.fr.yml +++ b/npbackup/translations/generic.fr.yml @@ -71,6 +71,7 @@ fr: bad_file: Fichier erroné file_does_not_exist: Fichier inexistant + add: Ajouter add_files: Ajouter fichiers add_folder: Ajouter dossier add_manually: Ajouter manuellement diff --git a/npbackup/translations/wizard_gui.en.yml b/npbackup/translations/wizard_gui.en.yml index 19fdc31..2ecb098 100644 --- a/npbackup/translations/wizard_gui.en.yml +++ b/npbackup/translations/wizard_gui.en.yml @@ -17,5 +17,12 @@ en: step_4: Review summary step_5: Finalize setup + add_system: Add Windows system + add_hyper_v: Add Hyper-V virtual machines + add_kvm: Add KVM virtual machines + retention_gfs: GFS Retention + retention_30_days: 30 Days Retention + retention_14_days: 14 Days Retention + retention_keep_all: Keep All Backups diff --git a/npbackup/translations/wizard_gui.fr.yml b/npbackup/translations/wizard_gui.fr.yml index 7337482..fc49ec3 100644 --- a/npbackup/translations/wizard_gui.fr.yml +++ b/npbackup/translations/wizard_gui.fr.yml @@ -17,4 +17,11 @@ fr: step_4: Revoir le résumé step_5: Finaliser la configuration + add_system: Ajouter un système Windows + add_hyper_v: Ajouter des machines virtuelles Hyper-V + add_kvm: Ajouter des machines virtuelles KVM + retention_gfs: Rétention GFS + retention_30_days: Rétention de 30 jours + retention_14_days: Rétention de 14 jours + retention_keep_all: Conserver toutes les sauvegardes \ No newline at end of file