GUI: WIP wizard

This commit is contained in:
deajan 2025-10-21 11:29:06 +02:00
parent 779d2ddaf7
commit acbdb8d4f2
14 changed files with 554 additions and 37 deletions

View file

@ -31,6 +31,7 @@ import _tkinter
import npbackup.configuration
import npbackup.common
from resources.customization import (
LOOK_AND_FEEL_TABLE,
OEM_STRING,
OEM_LOGO,
BG_COLOR_LDR,
@ -71,7 +72,8 @@ __repo_aware_concurrency = False
# Also prevents showing errors when config was just changed
GUI_STATUS_IGNORE_ERRORS = True
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)
sg.SetOptions(icon=OEM_ICON)

95
npbackup/gui/buttons.py Normal file
View file

@ -0,0 +1,95 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of npbackup
__intname__ = "npbackup.gui.buttons"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2023-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025102101"
from PIL import Image, ImageDraw
import FreeSimpleGUI as sg
def backgroundPNG(MAX_W, MAX_H, backgroundColor=None):
background = Image.new("RGBA", (MAX_W, MAX_H), color=backgroundColor)
draw = ImageDraw.Draw(background)
return [background, draw]
def roundCorners(im, rad):
"""
Rounds the corners of an image to given radius
"""
mask = Image.new("L", im.size)
if rad > min(*im.size) // 2:
rad = min(*im.size) // 2
draw = ImageDraw.Draw(mask)
draw.ellipse((0, 0, rad * 2, rad * 2), fill=255)
draw.ellipse((0, im.height - rad * 2 -2, rad * 2, im.height-1) , fill=255)
draw.ellipse((im.width - rad * 2, 1, im.width, rad * 2), fill=255)
draw.ellipse(
(im.width - rad * 2, im.height - rad * 2, im.width-1, im.height-1), fill=255
)
draw.rectangle([rad, 0, im.width - rad, im.height], fill=255)
draw.rectangle([0, rad, im.width, im.height - rad], fill=255)
mask = superSample(mask, 8)
im.putalpha(mask)
return im
def superSample(image, sample):
"""
Supersample an image for better edges
image: image object
sample: sampling multiplicator int(suggested: 2, 4, 8)
"""
w, h = image.size
image = image.resize((int(w * sample), int(h * sample)), resample=Image.LANCZOS)
image = image.resize((image.width // sample, image.height // sample), resample=Image.LANCZOS)
return image
def image_to_data(im):
"""
This is for Pysimplegui library
Converts image into data to be used inside GUIs
"""
from io import BytesIO
with BytesIO() as output:
im.save(output, format="PNG")
data = output.getvalue()
return data
def RoundedButton(button_text=' ', corner_radius=0, button_type=sg.BUTTON_TYPE_READ_FORM, target=(None, None),
tooltip=None, file_types=sg.FILE_TYPES_ALL_FILES, initial_folder=None, default_extension='',
disabled=False, change_submits=False, enable_events=False,
image_size=(None, None), image_subsample=None, border_width=None, size=(None, None),
auto_size_button=None, button_color=None, disabled_button_color=None, highlight_colors=None,
mouseover_colors=(None, None), use_ttk_buttons=None, font=None, bind_return_key=False, focus=False,
pad=None, key=None, right_click_menu=None, expand_x=False, expand_y=False, visible=True,
metadata=None):
button_img = backgroundPNG(50*5, 50*5, button_color[1])
button_img[0] = roundCorners(button_img[0], 30*5)
button_img[0] = button_img[0].resize((50, 50), resample=Image.LANCZOS)
btn_img = image_to_data(button_img[0])
if button_color is None:
button_color = sg.theme_button_color()
return sg.Button(button_text=button_text, button_type=button_type, target=target, tooltip=tooltip,
file_types=file_types, initial_folder=initial_folder, default_extension=default_extension,
disabled=disabled, change_submits=change_submits, enable_events=enable_events,
image_data=btn_img, image_size=image_size,
image_subsample=image_subsample, border_width=border_width, size=size,
auto_size_button=auto_size_button, button_color=(button_color[0], sg.theme_background_color()),
disabled_button_color=disabled_button_color, highlight_colors=highlight_colors,
mouseover_colors=mouseover_colors, use_ttk_buttons=use_ttk_buttons, font=font,
bind_return_key=bind_return_key, focus=focus, pad=pad, key=key, right_click_menu=right_click_menu,
expand_x=expand_x, expand_y=expand_y, visible=visible, metadata=metadata)

View file

@ -40,6 +40,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
logger = getLogger()
@ -94,33 +95,7 @@ def config_gui(full_config: dict, config_file: str):
suppress_key_guessing=True,
)
combo_boxes = {
"repo_opts.compression": {
"auto": _t("config_gui.auto"),
"max": _t("config_gui.max"),
"off": _t("config_gui.off"),
},
"backup_opts.source_type": {
"folder_list": _t("config_gui.folder_list"),
"files_from": _t("config_gui.files_from"),
"files_from_verbatim": _t("config_gui.files_from_verbatim"),
"files_from_raw": _t("config_gui.files_from_raw"),
"stdin_from_command": _t("config_gui.stdin_from_command"),
},
"backup_opts.priority": {
"low": _t("config_gui.low"),
"normal": _t("config_gui.normal"),
"high": _t("config_gui.high"),
},
"permissions": {
"backup": _t("config_gui.backup_perms"),
"restore": _t("config_gui.restore_perms"),
"restore_only": _t("config_gui.restore_only_perms"),
"full": _t("config_gui.full_perms"),
},
}
byte_units = ["B", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB", "PB", "PiB"]
def get_objects() -> List[str]:
"""

47
npbackup/gui/constants.py Normal file
View file

@ -0,0 +1,47 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of npbackup
__intname__ = "npbackup.gui.constants"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2023-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025102101"
from npbackup.core.i18n_helper import _t
combo_boxes = {
"repo_opts.compression": {
"auto": _t("config_gui.auto"),
"max": _t("config_gui.max"),
"off": _t("config_gui.off"),
},
"backup_opts.source_type": {
"folder_list": _t("config_gui.folder_list"),
"files_from": _t("config_gui.files_from"),
"files_from_verbatim": _t("config_gui.files_from_verbatim"),
"files_from_raw": _t("config_gui.files_from_raw"),
"stdin_from_command": _t("config_gui.stdin_from_command"),
},
"backup_opts.priority": {
"low": _t("config_gui.low"),
"normal": _t("config_gui.normal"),
"high": _t("config_gui.high"),
},
"permissions": {
"backup": _t("config_gui.backup_perms"),
"restore": _t("config_gui.restore_perms"),
"restore_only": _t("config_gui.restore_only_perms"),
"full": _t("config_gui.full_perms"),
},
"retention_options": {
"GFS": _t("wizard_gui.retention_gfs"),
"30days": _t("wizard_gui.retention_30days"),
"keep_all": _t("wizard_gui.retention_keep_all"),
},
}
byte_units = ["B", "KB", "KiB", "MB", "MiB", "GB", "GiB", "TB", "TiB", "PB", "PiB"]

View file

@ -7,7 +7,7 @@ __intname__ = "npbackup.gui.helpers"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2023-2025 NetInvent"
__license__ = "GPL-3.0-only"
__build__ = "2025070302"
__build__ = "2025102101"
from typing import Tuple, Union
@ -29,11 +29,12 @@ from resources.customization import (
from npbackup.core.runner import NPBackupRunner
from npbackup.__debug__ import _DEBUG
from npbackup.__env__ import GUI_CHECK_INTERVAL
from resources.customization import SIMPLEGUI_THEME, OEM_ICON
from resources.customization import SIMPLEGUI_THEME, OEM_ICON, LOOK_AND_FEEL_TABLE
logger = getLogger()
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)
sg.SetOptions(icon=OEM_ICON)

359
npbackup/gui/wizard.py Normal file
View file

@ -0,0 +1,359 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# This file is part of npbackup
__intname__ = "npbackup.gui.wizard"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2022-2025 NetInvent"
__license__ = "GPL-3.0-only"
from typing import List, Optional, Tuple
import sys
import os
import re
import gc
import textwrap
from argparse import ArgumentParser
from pathlib import Path
from logging import getLogger
import ofunctions.logger_utils
from datetime import datetime, timezone
import dateutil
from time import sleep
from ruamel.yaml.comments import CommentedMap
import atexit
from ofunctions.process import kill_childs
from ofunctions.threading import threaded
from ofunctions.misc import BytesConverter
import FreeSimpleGUI as sg
from psg_reskinner import animated_reskin, reskin
import _tkinter
import npbackup.configuration
import npbackup.common
from resources.customization import (
LOOK_AND_FEEL_TABLE,
OEM_STRING,
OEM_LOGO,
BG_COLOR_LDR,
TXT_COLOR_LDR,
GUI_STATE_OK_BUTTON,
GUI_STATE_OLD_BUTTON,
GUI_STATE_UNKNOWN_BUTTON,
LOADER_ANIMATION,
FOLDER_ICON,
FILE_ICON,
SYMLINK_ICON,
IRREGULAR_FILE_ICON,
LICENSE_TEXT,
SIMPLEGUI_THEME,
SIMPLEGUI_DARK_THEME,
OEM_ICON,
SHORT_PRODUCT_NAME,
THEME_CHOOSER_ICON,
# WIZARD
ADD_FOLDER,
ADD_FILE,
ADD_PROPERTY,
REMOVE_PROPERTY,
HYPERV,
KVM,
WINDOWS_SYSTEM,
BACKEND_LOCAL,
BACKEND_SFTP,
BACKEND_B2,
BACKEND_S3,
BACKEND_REST,
BACKEND_WASABI,
BACKEND_GOOGLE,
BACKEND_AZURE
)
from npbackup.gui.config import config_gui, ask_manager_password
from npbackup.gui.operations import operations_gui
from npbackup.gui.helpers import get_anon_repo_uri, gui_thread_runner, HideWindow
from npbackup.gui.handle_window import handle_current_window
from npbackup.gui.constants import combo_boxes, byte_units
from npbackup.core.i18n_helper import _t
from npbackup.core import upgrade_runner
from npbackup.path_helper import CURRENT_DIR
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
from npbackup.gui.buttons import RoundedButton
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()
wizard_layout_1 = [
[sg.Text(_t("wizard_gui.welcome", font=("Helvetica", 16)))],
[sg.Push()],
[sg.Text(_t("wizard_gui.welcome_description"))],
]
wizard_layout_2 = [
[
sg.Text(
textwrap.fill(f"{_t('wizard_gui.select_backup_sources')}", 70),
size=(None, None),
expand_x=True,
justification='c',
),
],
[
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--",
image_data=ADD_FILE,
border_width=0,
#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--",
image_data=ADD_FOLDER,
border_width=0,
#button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"])
),
sg.Button(
"", # _t("generic.add_manually"),
key="--ADD-PATHS-MANUALLY--",
image_source=ADD_PROPERTY,
border_width=0,
#button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"])
),
sg.Button(
"", # _t("generic.remove_selected"),
key="--REMOVE-PATHS--",
image_data=REMOVE_PROPERTY,
border_width=0,
#button_color=(None, sg.LOOK_AND_FEEL_TABLE[SIMPLEGUI_THEME]["BACKGROUND"])
),
sg.Button(
"",
image_data=WINDOWS_SYSTEM,
key="-ADD-WINDOWS-SYSTEM-",
border_width=0,
),
sg.Button(
"",
image_data=HYPERV,
key="-ADD-HYPERV-",
border_width=0,
),
sg.Button(
"",
image_data=KVM,
key="-ADD-KVM-",
border_width=0,
),
],
[
sg.Tree(
sg.TreeData(),
key="backup_opts.paths",
headings=[],
col0_heading=_t("config_gui.backup_sources"),
expand_x=True,
expand_y=True,
header_text_color=TXT_COLOR_LDR,
header_background_color=BG_COLOR_LDR,
)
],
]
wizard_layout_3 = [
[
sg.Text(_t("wizard_gui.backup_location", font=("Helvetica", 16)))
],
[
sg.Input(visible=False, key="--ADD-DESTINATION-FOLDER--", enable_events=True),
sg.FolderBrowse("", image_data=BACKEND_LOCAL, key="-BACKEND-LOCAL", border_width=0, target="--ADD-DESTINATION-FOLDER--"),
sg.Button(image_data=BACKEND_SFTP, key="-BACKEND-SFTP", border_width=0),
sg.Button(image_data=BACKEND_B2, key="-BACKEND-B2", border_width=0),
sg.Button(image_data=BACKEND_S3, key="-BACKEND-S3", border_width=0),
],
[
sg.Text(" HDD "),
sg.Text(" SFTP "),
sg.Text(" B2 "),
sg.Text(" S3 "),
],
[
sg.Button(image_data=BACKEND_REST, key="-BACKEND-REST", border_width=0),
sg.Button(image_data=BACKEND_GOOGLE, key="-BACKEND-GOOGLE", border_width=0),
sg.Button(image_data=BACKEND_AZURE, key="-BACKEND-AZURE", border_width=0),
sg.Button(image_data=BACKEND_WASABI, key="-BACKEND-WASABI", border_width=0),
],
[
sg.Text(" REST "),
sg.Text(" Google "),
sg.Text(" Azure "),
sg.Text(" Wasabi ")
]
]
wizard_layout_4 = [
[
sg.Column(
[
[
sg.Column(
[
[
sg.Button(
"+", key="--ADD-BACKUP-TAG--", size=(3, 1)
)
],
[
sg.Button(
"-",
key="--REMOVE-BACKUP-TAG--",
size=(3, 1),
)
],
],
pad=0,
size=(40, 80),
),
sg.Column(
[
[
sg.Tree(
sg.TreeData(),
key="backup_opts.tags",
headings=[],
col0_heading="Tags",
col0_width=30,
num_rows=3,
expand_x=True,
expand_y=True,
)
]
],
pad=0,
size=(300, 80),
),
],
],
pad=0,
),
],
]
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"])), key="-RETENTION-TYPE-", enable_events=True)],
]
wizard_layout_6 = [
[
sg.Text(_t("wizard_gui.end_user_experience", font=("Helvetica", 16)))
],
[
sg.Checkbox(_t("wizard_gui.disable_config_button"), key="-DISABLE-CONFIG-BUTTON-", default=True)
]
]
wizard_layout_7 = [
[sg.Text(_t("wizard_gui.end_user_experience", font=("Helvetica", 16)))],
]
wizard_breadcrumbs = [
[RoundedButton('1', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-1-')],
[RoundedButton('2', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-2-')],
[RoundedButton('3', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-3-')],
[RoundedButton('4', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-4-')],
[RoundedButton('5', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-5-')],
[RoundedButton('6', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-6-')],
[RoundedButton('7', button_color=("#FAFAFA", "#ADADAD"), border_width=0, key='-BREADCRUMB-7-')],
]
wizard_tabs = [
sg.Column(wizard_layout_1, element_justification='c', key='-TAB1-'),
sg.Column(wizard_layout_2, element_justification='c', key='-TAB2-'),
sg.Column(wizard_layout_3, element_justification='c', key='-TAB3-'),
sg.Column(wizard_layout_4, element_justification='c', key='-TAB4-'),
sg.Column(wizard_layout_5, element_justification='c', key='-TAB5-'),
sg.Column(wizard_layout_6, element_justification='c', key='-TAB6-'),
sg.Column(wizard_layout_7, element_justification='c', key='-TAB7-'),
]
wizard_layout = [
[sg.Push(), sg.Image(source=THEME_CHOOSER_ICON, key="-THEME-",enable_events=True)],
[sg.Column(wizard_breadcrumbs, element_justification="L"), sg.Column([wizard_tabs], expand_x=True, expand_y=True)],
[sg.Button(_t("generic.cancel"), key="-PREVIOUS-"), sg.Button(_t("generic.start"), key="-NEXT-")]
]
def start_wizard():
CURRENT_THEME = SIMPLEGUI_THEME
NUMBER_OF_TABS = len(wizard_tabs)
current_tab = 1
wizard = sg.Window("NPBackup Wizard",
layout=wizard_layout,
size=(800, 500),
element_justification='c',)
def _reskin_job():
nonlocal CURRENT_THEME
animated_reskin(
window=wizard,
new_theme=CURRENT_THEME,
theme_function=sg.theme,
lf_table=LOOK_AND_FEEL_TABLE,
)
wizard.TKroot.after(60000, _reskin_job)
while True:
event, values = wizard.read()
if event == sg.WIN_CLOSED or event == _t("generic.cancel"):
break
if event == "-THEME-":
if CURRENT_THEME != "DARK":
CURRENT_THEME = "DARK"
else:
CURRENT_THEME = "CLEAR"
_reskin_job()
continue
if event == "-NEXT-":
if current_tab < NUMBER_OF_TABS:
current_tab += 1
wizard["-NEXT-"].update(_t("generic.finish") if current_tab == NUMBER_OF_TABS else _t("generic.next"))
wizard["-PREVIOUS-"].update(_t("generic.cancel") if current_tab == 1 else _t("generic.previous"))
for tab_index in range(1, NUMBER_OF_TABS + 1):
if tab_index != current_tab:
wizard[f"-TAB{tab_index}-"].Update(visible=False)
wizard[f"-BREADCRUMB-{tab_index}-"].Update(button_color=("#FAFAFA", None))
wizard[f"-TAB{current_tab}-"].Update(visible=True)
wizard[f"-BREADCRUMB-{current_tab}-"].Update(button_color=("#3F2DCB", None))
elif current_tab == NUMBER_OF_TABS:
sg.popup(_t("wizard_gui.thank_you"), keep_on_top=True)
break
if event == "-PREVIOUS-":
if current_tab > 1:
current_tab -= 1
wizard["-NEXT-"].update(_t("generic.finish") if current_tab == NUMBER_OF_TABS else _t("generic.next"))
wizard["-PREVIOUS-"].update(_t("generic.cancel") if current_tab == 1 else _t("generic.previous"))
for tab_index in range(1, NUMBER_OF_TABS + 1):
if tab_index != current_tab:
wizard[f"-TAB{tab_index}-"].Update(visible=False)
wizard[f"-BREADCRUMB-{tab_index}-"].Update(button_color=("#FAFAFA", None))
wizard[f"-TAB{current_tab}-"].Update(visible=True)
wizard[f"-BREADCRUMB-{current_tab}-"].Update(button_color=("#3F2DCB", None))
elif current_tab == 1:
break
wizard.close()
start_wizard()

View file

@ -8,6 +8,7 @@ en:
encrypted_data: Encrypted_Data
compression: Compression
backup_paths: Backup paths
backup_sources: Backup sources
use_fs_snapshot: Use VSS snapshots
ignore_cloud_files: Ignore in-cloud files
windows_only: Windows only

View file

@ -9,6 +9,7 @@ fr:
encrypted_data: Donnée_Chiffrée
compression: Compression
backup_paths: Chemins à sauvegarder
backup_sources: Éléments à sauvegarder
use_fs_snapshot: Utiliser les instantanés VSS
ignore_cloud_files: Exclure le fichiers dans le cloud
windows_only: Windows seulement

View file

@ -10,6 +10,10 @@ en:
change: Change
close: Close
finished: Finished
next: next
previous: previous
start: Start
finish: Finish
yes: Yes
no: No

View file

@ -10,6 +10,10 @@ fr:
change: Changer
close: Fermer
finished: Terminé
next: suivant
previous: Précédent
start: Démarrer
finish: Terminer
yes: Oui
no: Non

View file

@ -0,0 +1,11 @@
en:
wizard_start: Start
select_backup_sources: Select backup sources
backup_source: Files & folders
backup_location: Backup location
backup_options: Backup options
summary: Summary
welcome: Welcome to the NPBackup wizard
welcome_description: This wizard will guide you through the initial configuration of NPBackup.
thank_you: Thank you for using NPBackup!

View file

@ -0,0 +1,11 @@
fr:
wizard_start: Début
select_backup_sources: Sélectionner les éléments à sauvegarder
backup_source: Fichiers & dossiers
backup_location: Emplacement de la sauvegarde
backup_options: Options de sauvegarde
summary: Résumé
welcome: Bienvenue dans l'assistant NPBackup
welcome_description: Cet assistant vous guidera à travers la configuration initiale de NPBackup.
thank_you: Merci d'utiliser NPBackup!

File diff suppressed because one or more lines are too long

View file

@ -55,6 +55,7 @@ def update_custom_icons():
"LOADING_ANIMATION": "loading.gif",
"OEM_LOGO": "oem_logo.png",
"OEM_ICON": "oem_icon.png",
"THEME_CHOOSER_ICON": "theme_chooser_icon.png",
}
resources_dir = os.path.join(BASEDIR, os.path.pardir, "resources")