2
3 Dieses Commit enthält zwei wesentliche Verbesserungen:
4
5 1. **Flexible Auswahl des Backup-Typs:** Der Benutzer kann jetzt manuell die
Erstellung eines vollständigen Backups auswählen, auch wenn bereits ein früheres
vollständiges Backup vorhanden ist. Die Anwendung wechselt in diesem Fall nicht mehr
automatisch zu einem inkrementellen Backup.
6
7 2. **Korrektur der Füllstandsanzeige:** Die Füllstandsanzeige zeigt jetzt die
voraussichtliche Backup-Größe sowohl für vollständige als auch für inkrementelle
Backups korrekt an. Dies wurde erreicht, indem sichergestellt wurde, dass die
Quellgröße in allen Fällen korrekt berechnet und an die Benutzeroberfläche übergeben
wird.
684 lines
30 KiB
Python
684 lines
30 KiB
Python
#!/usr/bin/python3
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import os
|
|
import datetime
|
|
from queue import Queue, Empty
|
|
|
|
from shared_libs.log_window import LogWindow
|
|
from shared_libs.logger import app_logger
|
|
from shared_libs.animated_icon import AnimatedIcon
|
|
from shared_libs.common_tools import IconManager
|
|
from shared_libs.config_manager import ConfigManager
|
|
from backup_manager import BackupManager
|
|
from pbp_app_config import AppConfig, Msg
|
|
from pyimage_ui.scheduler_frame import SchedulerFrame
|
|
from pyimage_ui.backup_content_frame import BackupContentFrame
|
|
from pyimage_ui.header_frame import HeaderFrame
|
|
from pyimage_ui.settings_frame import SettingsFrame
|
|
from core.data_processing import DataProcessing
|
|
from pyimage_ui.drawing import Drawing
|
|
from pyimage_ui.navigation import Navigation
|
|
from pyimage_ui.actions import Actions
|
|
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
|
|
|
|
|
|
class MainApplication(tk.Tk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
AppConfig.ensure_directories()
|
|
|
|
self.title(AppConfig.UI_CONFIG["window_title"])
|
|
self.geometry("1000x800")
|
|
|
|
self.style = ttk.Style()
|
|
self.tk.call("source", "/usr/share/TK-Themes/water.tcl")
|
|
self.tk.call("set_theme", "light")
|
|
self.style.configure("Custom.TFrame", background="#2b3e4f")
|
|
self.style.configure("Sidebar.TButton", background="#2b3e4f",
|
|
foreground="white", font=("Ubuntu", 12, "bold"))
|
|
self.style.layout("Sidebar.TButton", self.style.layout(
|
|
"SidebarHover.TButton.Borderless.Round"))
|
|
|
|
self.style.map("Toolbutton", background=[
|
|
("active", "#000000")], foreground=[("active", "black")])
|
|
|
|
self.style.layout("Gray.Toolbutton",
|
|
self.style.layout("Toolbutton"))
|
|
self.style.configure("Gray.Toolbutton", foreground="gray")
|
|
|
|
main_frame = ttk.Frame(self)
|
|
main_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.grid_rowconfigure(0, weight=1)
|
|
self.grid_columnconfigure(0, weight=1)
|
|
|
|
sidebar = ttk.Frame(main_frame, style="Custom.TFrame")
|
|
sidebar.grid(row=0, column=0, sticky="nsew")
|
|
main_frame.grid_rowconfigure(0, weight=1)
|
|
main_frame.grid_columnconfigure(0, weight=0, minsize=180)
|
|
main_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
self.content_frame = ttk.Frame(main_frame)
|
|
self.content_frame.grid(row=0, column=1, sticky="nsew")
|
|
self.content_frame.grid_rowconfigure(0, weight=0)
|
|
self.content_frame.grid_rowconfigure(1, weight=0)
|
|
self.content_frame.grid_rowconfigure(2, weight=1)
|
|
self.content_frame.grid_rowconfigure(3, weight=0)
|
|
self.content_frame.grid_rowconfigure(4, weight=0)
|
|
self.content_frame.grid_rowconfigure(5, weight=0)
|
|
self.content_frame.grid_rowconfigure(6, weight=0)
|
|
self.content_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self.backup_manager = BackupManager(app_logger)
|
|
self.queue = Queue()
|
|
self.image_manager = IconManager()
|
|
|
|
self.config_manager = ConfigManager(AppConfig.SETTINGS_FILE)
|
|
self.data_processing = DataProcessing(self)
|
|
self.drawing = Drawing(self)
|
|
self.navigation = Navigation(self)
|
|
self.actions = Actions(self)
|
|
|
|
self.backup_is_running = False
|
|
self.start_time = None
|
|
|
|
self.calculation_thread = None
|
|
self.calculation_stop_event = None
|
|
self.source_larger_than_partition = False
|
|
self.accurate_calculation_running = False
|
|
self.is_first_backup = False
|
|
|
|
self.left_canvas_animation = None
|
|
self.right_canvas_animation = None
|
|
self.right_calculation_thread = None
|
|
self.right_calculation_stop_event = None
|
|
self.destination_path = None
|
|
self.source_size_bytes = 0
|
|
self.destination_used_bytes = 0
|
|
self.destination_total_bytes = 0
|
|
|
|
self.backup_left_canvas_data = {}
|
|
self.backup_right_canvas_data = {}
|
|
self.restore_left_canvas_data = {}
|
|
self.restore_right_canvas_data = {}
|
|
|
|
self.left_canvas_data = {}
|
|
self.right_canvas_data = {}
|
|
self.restore_destination_folder_size_bytes = 0
|
|
self.restore_source_size_bytes = 0
|
|
try:
|
|
lx_backup_icon = self.image_manager.get_icon('lx_backup_large')
|
|
if lx_backup_icon:
|
|
self.iconphoto(True, lx_backup_icon)
|
|
except Exception:
|
|
pass
|
|
|
|
lx_backup_label = ttk.Label(
|
|
sidebar, image=lx_backup_icon, background="#2b3e4f")
|
|
lx_backup_label.image = lx_backup_icon
|
|
lx_backup_label.pack(pady=10)
|
|
|
|
self.sidebar_buttons_frame = ttk.Frame(sidebar, style="Custom.TFrame")
|
|
self.sidebar_buttons_frame.pack(pady=20)
|
|
|
|
self.buttons_map = {
|
|
"Computer": {"icon": "computer_extralarge"},
|
|
"Documents": {"icon": "documents_extralarge"},
|
|
"Pictures": {"icon": "pictures_extralarge"},
|
|
"Music": {"icon": "music_extralarge"},
|
|
"Videos": {"icon": "video_extralarge_folder"},
|
|
}
|
|
|
|
for text, data in self.buttons_map.items():
|
|
button = ttk.Button(self.sidebar_buttons_frame, text=text, style="Sidebar.TButton",
|
|
command=lambda t=text: self.actions.on_sidebar_button_click(t))
|
|
button.pack(fill=tk.X, pady=10)
|
|
|
|
self.schedule_dialog_button = ttk.Button(
|
|
self.sidebar_buttons_frame, text=Msg.STR["scheduling"], command=lambda: self.navigation.toggle_scheduler_frame(3), style="Sidebar.TButton")
|
|
self.schedule_dialog_button.pack(fill=tk.X, pady=10)
|
|
|
|
self.settings_button = ttk.Button(
|
|
self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton")
|
|
self.settings_button.pack(fill=tk.X, pady=10)
|
|
|
|
self.header_frame = HeaderFrame(self.content_frame, self.image_manager)
|
|
self.header_frame.grid(row=0, column=0, sticky="nsew")
|
|
|
|
self.top_bar = ttk.Frame(self.content_frame)
|
|
self.top_bar.grid(row=1, column=0, sticky="ew", pady=10)
|
|
|
|
top_nav_frame = ttk.Frame(self.top_bar)
|
|
top_nav_frame.pack(side=tk.LEFT)
|
|
|
|
self.nav_buttons_defs = [
|
|
(Msg.STR["backup_menu"], lambda: self.navigation.toggle_mode(
|
|
"backup", 0, trigger_calculation=True)),
|
|
(Msg.STR["restore"], lambda: self.navigation.toggle_mode(
|
|
"restore", 1, trigger_calculation=True)),
|
|
(Msg.STR["backup_content"],
|
|
lambda: self.navigation.toggle_backup_content_frame(2)),
|
|
(Msg.STR["scheduling"],
|
|
lambda: self.navigation.toggle_scheduler_frame(3)),
|
|
(Msg.STR["settings"],
|
|
lambda: self.navigation.toggle_settings_frame(4)),
|
|
(Msg.STR["log"], lambda: self.navigation.toggle_log_window(5)),
|
|
]
|
|
|
|
self.nav_buttons = []
|
|
self.nav_progress_bars = []
|
|
|
|
for i, (text, command) in enumerate(self.nav_buttons_defs):
|
|
button_frame = ttk.Frame(top_nav_frame)
|
|
button_frame.pack(side=tk.LEFT, padx=5)
|
|
button = ttk.Button(button_frame, text=text,
|
|
command=command, style="TButton.Borderless.Round")
|
|
button.pack(side=tk.TOP)
|
|
self.nav_buttons.append(button)
|
|
progress_bar = ttk.Progressbar(
|
|
button_frame, orient="horizontal", length=50, mode="determinate", style="Small.Horizontal.TProgressbar")
|
|
progress_bar.pack_forget()
|
|
self.nav_progress_bars.append(progress_bar)
|
|
|
|
if i < len(self.nav_buttons_defs) - 1:
|
|
ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack(
|
|
side=tk.LEFT, fill=tk.Y, padx=2)
|
|
|
|
self.canvas_frame = ttk.Frame(self.content_frame)
|
|
self.canvas_frame.grid(
|
|
row=2, column=0, sticky="nsew", padx=10, pady=(15, 5))
|
|
self.canvas_frame.grid_columnconfigure(0, weight=1)
|
|
self.canvas_frame.grid_columnconfigure(2, weight=1)
|
|
self.canvas_frame.grid_rowconfigure(0, weight=1)
|
|
|
|
self.left_canvas = tk.Canvas(
|
|
self.canvas_frame, relief="solid", borderwidth=1)
|
|
self.left_canvas.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
|
self.left_canvas.bind("<Configure>", self.drawing.redraw_left_canvas)
|
|
|
|
button_frame = ttk.Frame(self.canvas_frame)
|
|
button_frame.grid(row=0, column=1, padx=15)
|
|
|
|
self.mode_button_icon = self.image_manager.get_icon(
|
|
"forward_extralarge")
|
|
self.mode_button = ttk.Button(
|
|
button_frame, image=self.mode_button_icon, command=self.navigation.toggle_mode, style="TButton.Borderless.Round")
|
|
self.mode_button.pack(pady=30)
|
|
|
|
self.right_canvas = tk.Canvas(
|
|
self.canvas_frame, relief="solid", borderwidth=1)
|
|
self.right_canvas.grid(row=0, column=2, sticky="nsew", padx=5, pady=5)
|
|
self.right_canvas.bind("<Configure>", self.drawing.redraw_right_canvas)
|
|
self.right_canvas.bind(
|
|
"<Button-1>", self.actions.on_right_canvas_click)
|
|
|
|
self._setup_log_window()
|
|
self._setup_scheduler_frame()
|
|
self._setup_settings_frame()
|
|
self._setup_backup_content_frame()
|
|
|
|
self._setup_task_bar()
|
|
|
|
self.source_size_frame = ttk.LabelFrame(
|
|
self.content_frame, text=Msg.STR["source"], padding=10)
|
|
self.source_size_frame.grid(
|
|
row=4, column=0, sticky="ew", padx=10, pady=5)
|
|
self.source_size_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self.source_size_canvas = tk.Canvas(
|
|
self.source_size_frame, height=20, relief="solid", borderwidth=1)
|
|
self.source_size_canvas.grid(row=0, column=0, sticky="ew")
|
|
|
|
source_label_frame = ttk.Frame(self.source_size_frame)
|
|
source_label_frame.grid(row=1, column=0, sticky="ew")
|
|
self.source_size_label = ttk.Label(
|
|
source_label_frame, text="0.00 GB / 0.00 GB")
|
|
self.source_size_label.pack(side=tk.RIGHT)
|
|
|
|
self.target_size_frame = ttk.LabelFrame(
|
|
self.content_frame, text=Msg.STR["projected_usage_label"], padding=10)
|
|
self.target_size_frame.grid(
|
|
row=5, column=0, sticky="ew", padx=10, pady=5)
|
|
self.target_size_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
self.target_size_canvas = tk.Canvas(
|
|
self.target_size_frame, height=20, relief="solid", borderwidth=1)
|
|
self.target_size_canvas.grid(row=0, column=0, sticky="ew")
|
|
|
|
target_label_frame = ttk.Frame(self.target_size_frame)
|
|
target_label_frame.grid(row=1, column=0, sticky="ew")
|
|
self.target_size_label = ttk.Label(
|
|
target_label_frame, text="0.00 GB / 0.00 GB")
|
|
self.target_size_label.pack(side=tk.RIGHT)
|
|
|
|
self.restore_size_frame_before = ttk.LabelFrame(
|
|
self.content_frame, text="Before Restoration", padding=10)
|
|
self.restore_size_frame_before.grid(
|
|
row=4, column=0, sticky="ew", padx=10, pady=5)
|
|
self.restore_size_frame_before.grid_columnconfigure(0, weight=1)
|
|
|
|
self.restore_size_canvas_before = tk.Canvas(
|
|
self.restore_size_frame_before, height=20, relief="solid", borderwidth=1)
|
|
self.restore_size_canvas_before.grid(row=0, column=0, sticky="ew")
|
|
|
|
restore_label_frame_before = ttk.Frame(self.restore_size_frame_before)
|
|
restore_label_frame_before.grid(row=1, column=0, sticky="ew")
|
|
self.restore_size_label_before = ttk.Label(
|
|
restore_label_frame_before, text="0.00 GB / 0.00 GB")
|
|
self.restore_size_label_before.pack(side=tk.RIGHT)
|
|
|
|
self.restore_size_frame_after = ttk.LabelFrame(
|
|
self.content_frame, text="After Restoration", padding=10)
|
|
self.restore_size_frame_after.grid(
|
|
row=5, column=0, sticky="ew", padx=10, pady=5)
|
|
self.restore_size_frame_after.grid_columnconfigure(0, weight=1)
|
|
|
|
self.restore_size_canvas_after = tk.Canvas(
|
|
self.restore_size_frame_after, height=20, relief="solid", borderwidth=1)
|
|
self.restore_size_canvas_after.grid(row=0, column=0, sticky="ew")
|
|
|
|
restore_label_frame_after = ttk.Frame(self.restore_size_frame_after)
|
|
restore_label_frame_after.grid(row=1, column=0, sticky="ew")
|
|
|
|
self.restore_size_label_after = ttk.Label(
|
|
restore_label_frame_after, text="0.00 GB / 0.00 GB")
|
|
self.restore_size_label_after.pack(side=tk.RIGHT)
|
|
|
|
self.restore_size_label_diff = ttk.Label(
|
|
restore_label_frame_after, text="")
|
|
self.restore_size_label_diff.pack(side=tk.RIGHT)
|
|
|
|
self.restore_size_frame_before.grid_remove()
|
|
self.restore_size_frame_after.grid_remove()
|
|
|
|
self._load_state_and_initialize()
|
|
self.update_backup_options_from_config() # Add this call
|
|
self.protocol("WM_DELETE_WINDOW", self.on_closing)
|
|
|
|
def _load_state_and_initialize(self):
|
|
"""Loads saved state from config and initializes the UI."""
|
|
last_mode = self.config_manager.get_setting("last_mode", "backup")
|
|
|
|
# Pre-load data from config before initializing the UI
|
|
backup_source_path = self.config_manager.get_setting(
|
|
"backup_source_path")
|
|
if backup_source_path and os.path.isdir(backup_source_path):
|
|
folder_name = next((name for name, path_obj in AppConfig.FOLDER_PATHS.items(
|
|
) if str(path_obj) == backup_source_path), None)
|
|
|
|
if folder_name:
|
|
icon_name = self.buttons_map[folder_name]['icon']
|
|
else:
|
|
# Handle custom folder path
|
|
folder_name = os.path.basename(backup_source_path.rstrip('/'))
|
|
icon_name = 'folder_extralarge' # A generic folder icon
|
|
|
|
self.backup_left_canvas_data.update({
|
|
'icon': icon_name,
|
|
'folder': folder_name,
|
|
'path_display': backup_source_path,
|
|
})
|
|
|
|
backup_dest_path = self.config_manager.get_setting(
|
|
"backup_destination_path")
|
|
if backup_dest_path and os.path.isdir(backup_dest_path):
|
|
self.destination_path = backup_dest_path # Still needed for some logic
|
|
total, used, free = shutil.disk_usage(backup_dest_path)
|
|
self.backup_right_canvas_data.update({
|
|
'folder': os.path.basename(backup_dest_path.rstrip('/')),
|
|
'path_display': backup_dest_path,
|
|
'size': f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
|
|
})
|
|
self.destination_total_bytes = total
|
|
self.destination_used_bytes = used
|
|
|
|
restore_src_path = self.config_manager.get_setting(
|
|
"restore_source_path")
|
|
if restore_src_path and os.path.isdir(restore_src_path):
|
|
self.restore_right_canvas_data.update({
|
|
'folder': os.path.basename(restore_src_path.rstrip('/')),
|
|
'path_display': restore_src_path,
|
|
})
|
|
|
|
restore_dest_path = self.config_manager.get_setting(
|
|
"restore_destination_path")
|
|
if restore_dest_path and os.path.isdir(restore_dest_path):
|
|
# Find the corresponding button_text for the path
|
|
folder_name = ""
|
|
for name, path_obj in AppConfig.FOLDER_PATHS.items():
|
|
if str(path_obj) == restore_dest_path:
|
|
folder_name = name
|
|
break
|
|
if folder_name:
|
|
self.restore_left_canvas_data.update({
|
|
'icon': self.buttons_map[folder_name]['icon'],
|
|
'folder': folder_name,
|
|
'path_display': restore_dest_path,
|
|
})
|
|
|
|
# Initialize UI for the last active mode
|
|
self.navigation.initialize_ui_for_mode(last_mode)
|
|
|
|
# Trigger calculations if needed
|
|
if last_mode == 'backup':
|
|
self.after(100, self.actions.on_sidebar_button_click,
|
|
self.backup_left_canvas_data.get('folder', 'Computer'))
|
|
elif last_mode == 'restore':
|
|
# Trigger calculation for the right canvas (source) if a path is set
|
|
if restore_src_path:
|
|
self.drawing.calculate_restore_folder_size()
|
|
# Trigger calculation for the left canvas (destination) based on last selection
|
|
restore_dest_folder = self.restore_left_canvas_data.get(
|
|
'folder', 'Computer')
|
|
self.after(100, self.actions.on_sidebar_button_click,
|
|
restore_dest_folder)
|
|
self.data_processing.process_queue()
|
|
|
|
def _setup_log_window(self):
|
|
self.log_frame = ttk.Frame(self.content_frame)
|
|
self.log_window = LogWindow(self.log_frame)
|
|
self.log_window.pack(fill=tk.BOTH, expand=True)
|
|
app_logger.init_logger(self.log_window.log_message)
|
|
self.log_frame.grid(row=2, column=0, sticky="nsew")
|
|
self.log_frame.grid_remove()
|
|
|
|
def _setup_scheduler_frame(self):
|
|
self.scheduler_frame = SchedulerFrame(
|
|
self.content_frame, self.backup_manager, padding=10)
|
|
self.scheduler_frame.grid(row=2, column=0, sticky="nsew")
|
|
self.scheduler_frame.grid_remove()
|
|
|
|
def _setup_settings_frame(self):
|
|
self.settings_frame = SettingsFrame(
|
|
self.content_frame, self.navigation, self.actions, padding=10)
|
|
self.settings_frame.grid(row=2, column=0, sticky="nsew")
|
|
self.settings_frame.grid_remove()
|
|
|
|
def _setup_backup_content_frame(self):
|
|
self.backup_content_frame = BackupContentFrame(
|
|
self.content_frame, self.backup_manager, padding=10)
|
|
self.backup_content_frame.grid(row=2, column=0, sticky="nsew")
|
|
self.backup_content_frame.grid_remove()
|
|
|
|
def _setup_task_bar(self):
|
|
# Define all boolean vars at the top to ensure they exist before use.
|
|
self.vollbackup_var = tk.BooleanVar()
|
|
self.inkrementell_var = tk.BooleanVar()
|
|
self.genaue_berechnung_var = tk.BooleanVar()
|
|
self.testlauf_var = tk.BooleanVar()
|
|
self.compressed_var = tk.BooleanVar()
|
|
self.encrypted_var = tk.BooleanVar()
|
|
self.bypass_security_var = tk.BooleanVar()
|
|
|
|
self.info_checkbox_frame = ttk.Frame(self.content_frame, padding=10)
|
|
self.info_checkbox_frame.grid(row=3, column=0, sticky="ew")
|
|
|
|
self.info_label = ttk.Label(
|
|
self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"])
|
|
self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5)
|
|
|
|
# Frame for time info
|
|
self.time_info_frame = ttk.Frame(self.info_checkbox_frame)
|
|
self.time_info_frame.pack(anchor=tk.W, fill=tk.X, pady=5)
|
|
|
|
self.start_time_label = ttk.Label(self.time_info_frame, text="Start: --:--:--")
|
|
self.start_time_label.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.end_time_label = ttk.Label(self.time_info_frame, text="Ende: --:--:--")
|
|
self.end_time_label.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.duration_label = ttk.Label(self.time_info_frame, text="Dauer: --:--:--")
|
|
self.duration_label.pack(side=tk.LEFT, padx=5)
|
|
|
|
# --- Accurate Size Calculation Frame (on the right) ---
|
|
accurate_size_frame = ttk.Frame(self.time_info_frame)
|
|
accurate_size_frame.pack(side=tk.LEFT, padx=20)
|
|
|
|
self.accurate_size_cb = ttk.Checkbutton(accurate_size_frame, text=Msg.STR["accurate_size_cb_label"],
|
|
variable=self.genaue_berechnung_var, command=self.actions.on_toggle_accurate_size_calc)
|
|
self.accurate_size_cb.pack(side=tk.LEFT, padx=5)
|
|
|
|
accurate_size_info_label = ttk.Label(accurate_size_frame, text=Msg.STR["accurate_size_info_label"], foreground="gray")
|
|
accurate_size_info_label.pack(side=tk.LEFT)
|
|
|
|
checkbox_frame = ttk.Frame(self.info_checkbox_frame)
|
|
checkbox_frame.pack(fill=tk.X, pady=5)
|
|
|
|
self.full_backup_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["full_backup"],
|
|
variable=self.vollbackup_var, command=lambda: self.actions.handle_backup_type_change('voll'))
|
|
self.full_backup_cb.pack(side=tk.LEFT, padx=5)
|
|
self.incremental_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["incremental"],
|
|
variable=self.inkrementell_var, command=lambda: self.actions.handle_backup_type_change('inkrementell'))
|
|
self.incremental_cb.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.compressed_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["compressed"],
|
|
variable=self.compressed_var)
|
|
self.compressed_cb.pack(side=tk.LEFT, padx=5)
|
|
self.encrypted_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["encrypted"],
|
|
variable=self.encrypted_var)
|
|
self.encrypted_cb.pack(side=tk.LEFT, padx=5)
|
|
self.test_run_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["test_run"],
|
|
variable=self.testlauf_var)
|
|
self.test_run_cb.pack(side=tk.LEFT, padx=5)
|
|
self.bypass_security_cb = ttk.Checkbutton(checkbox_frame, text=Msg.STR["bypass_security"],
|
|
variable=self.bypass_security_var)
|
|
self.bypass_security_cb.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.action_frame = ttk.Frame(self.content_frame, padding=10)
|
|
self.action_frame.grid(row=6, column=0, sticky="ew")
|
|
self.action_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
bg_color = self.style.lookup('TFrame', 'background')
|
|
backup_animation_type = self.config_manager.get_setting(
|
|
"backup_animation_type", "counter_arc")
|
|
initial_animation_type = "blink"
|
|
if backup_animation_type == "line":
|
|
initial_animation_type = "line"
|
|
|
|
self.animated_icon = AnimatedIcon(
|
|
self.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=initial_animation_type)
|
|
self.animated_icon.grid(row=0, column=0, rowspan=2, padx=5)
|
|
self.animated_icon.stop("DISABLE")
|
|
self.animated_icon.animation_type = backup_animation_type
|
|
|
|
progress_container = ttk.Frame(self.action_frame)
|
|
progress_container.grid(row=0, column=1, sticky="ew", padx=5)
|
|
progress_container.grid_columnconfigure(0, weight=1)
|
|
|
|
self.current_file_label = ttk.Label(
|
|
progress_container, text="", anchor="w")
|
|
self.current_file_label.grid(row=0, column=0, sticky="ew")
|
|
|
|
self.task_progress = ttk.Progressbar(
|
|
progress_container, orient="horizontal", length=100, mode="determinate")
|
|
self.task_progress.grid(row=1, column=0, sticky="ew", pady=(5, 0))
|
|
|
|
self.start_pause_button = ttk.Button(
|
|
self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled")
|
|
self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5)
|
|
|
|
def on_closing(self):
|
|
"""Handles window closing events and saves the app state."""
|
|
self.config_manager.set_setting("last_mode", self.mode)
|
|
|
|
# Save paths from the data dictionaries
|
|
if self.backup_left_canvas_data.get('path_display'):
|
|
self.config_manager.set_setting(
|
|
"backup_source_path", self.backup_left_canvas_data['path_display'])
|
|
else:
|
|
self.config_manager.set_setting("backup_source_path", None)
|
|
|
|
if self.backup_right_canvas_data.get('path_display'):
|
|
self.config_manager.set_setting(
|
|
"backup_destination_path", self.backup_right_canvas_data['path_display'])
|
|
else:
|
|
self.config_manager.set_setting("backup_destination_path", None)
|
|
|
|
if self.restore_left_canvas_data.get('path_display'):
|
|
self.config_manager.set_setting(
|
|
"restore_destination_path", self.restore_left_canvas_data['path_display'])
|
|
else:
|
|
self.config_manager.set_setting("restore_destination_path", None)
|
|
|
|
if self.restore_right_canvas_data.get('path_display'):
|
|
self.config_manager.set_setting(
|
|
"restore_source_path", self.restore_right_canvas_data['path_display'])
|
|
else:
|
|
self.config_manager.set_setting("restore_source_path", None)
|
|
|
|
# Stop any ongoing animations before destroying the application
|
|
if self.left_canvas_animation:
|
|
self.left_canvas_animation.stop()
|
|
self.left_canvas_animation.destroy()
|
|
self.left_canvas_animation = None
|
|
if self.right_canvas_animation:
|
|
self.right_canvas_animation.stop()
|
|
self.right_canvas_animation.destroy()
|
|
self.right_canvas_animation = None
|
|
if self.animated_icon:
|
|
self.animated_icon.stop()
|
|
self.animated_icon.destroy()
|
|
self.animated_icon = None
|
|
|
|
app_logger.log(Msg.STR["app_quit"])
|
|
self.destroy()
|
|
|
|
def _process_backup_queue(self):
|
|
"""Processes messages from the backup thread queue to update the UI safely."""
|
|
try:
|
|
while True:
|
|
message = self.queue.get_nowait()
|
|
|
|
# Check if it's a backup status message (2-element tuple)
|
|
if isinstance(message, tuple) and len(message) == 2:
|
|
message_type, value = message
|
|
|
|
if message_type == 'progress':
|
|
self.task_progress["value"] = value
|
|
self.info_label.config(text=f"Fortschritt: {value}%") # Update progress text
|
|
elif message_type == 'file_update':
|
|
# Truncate long text to avoid window resizing issues
|
|
max_len = 120
|
|
if len(value) > max_len:
|
|
value = "..." + value[-max_len:]
|
|
self.current_file_label.config(text=value)
|
|
elif message_type == 'error':
|
|
self.animated_icon.stop("DISABLE")
|
|
self.start_pause_button["text"] = "Start"
|
|
self.backup_is_running = False
|
|
elif message_type == 'completion':
|
|
self.animated_icon.stop("DISABLE")
|
|
self.start_pause_button["text"] = "Start"
|
|
self.task_progress["value"] = 0
|
|
self.current_file_label.config(text="Backup finished.")
|
|
|
|
if self.start_time:
|
|
end_time = datetime.datetime.now()
|
|
duration = end_time - self.start_time
|
|
total_seconds = int(duration.total_seconds())
|
|
hours, remainder = divmod(total_seconds, 3600)
|
|
minutes, seconds = divmod(remainder, 60)
|
|
duration_str = f"{hours:02}:{minutes:02}:{seconds:02}"
|
|
end_str = end_time.strftime("%H:%M:%S")
|
|
self.end_time_label.config(text=f"Ende: {end_str}")
|
|
self.duration_label.config(text=f"Dauer: {duration_str}")
|
|
self.start_time = None
|
|
|
|
self.backup_is_running = False
|
|
self.actions._set_ui_state(True) # Re-enable UI
|
|
elif message_type == 'completion_accurate':
|
|
if value == 'success':
|
|
self.current_file_label.config(text=Msg.STR["accurate_size_success"])
|
|
else:
|
|
self.current_file_label.config(text=Msg.STR["accurate_size_failed"])
|
|
self.actions._set_ui_state(True) # Re-enable UI
|
|
else:
|
|
# This message is not for us (likely for DataProcessing), put it back and yield.
|
|
self.queue.put(message)
|
|
break
|
|
|
|
except Empty:
|
|
pass # Queue is empty, do nothing
|
|
|
|
# Reschedule the queue check
|
|
self.after(100, self._process_backup_queue)
|
|
|
|
def quit(self):
|
|
self.on_closing()
|
|
|
|
def update_backup_options_from_config(self):
|
|
"""
|
|
Reads the 'force' settings from the config and updates the main UI checkboxes.
|
|
A 'force' setting overrides the user's selection and disables the control.
|
|
"""
|
|
# Full/Incremental Logic
|
|
force_full = self.config_manager.get_setting(
|
|
"force_full_backup", False)
|
|
force_incremental = self.config_manager.get_setting(
|
|
"force_incremental_backup", False)
|
|
|
|
if force_full:
|
|
self.vollbackup_var.set(True)
|
|
self.inkrementell_var.set(False)
|
|
self.full_backup_cb.config(state="disabled")
|
|
self.incremental_cb.config(state="disabled")
|
|
elif force_incremental:
|
|
self.vollbackup_var.set(False)
|
|
self.inkrementell_var.set(True)
|
|
self.full_backup_cb.config(state="disabled")
|
|
self.incremental_cb.config(state="disabled")
|
|
|
|
# Compression Logic
|
|
force_compression = self.config_manager.get_setting(
|
|
"force_compression", False)
|
|
if force_compression:
|
|
self.compressed_var.set(True)
|
|
self.compressed_cb.config(state="disabled")
|
|
|
|
# Encryption Logic
|
|
force_encryption = self.config_manager.get_setting(
|
|
"force_encryption", False)
|
|
if force_encryption:
|
|
self.encrypted_var.set(True)
|
|
self.encrypted_cb.config(state="disabled")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
import sys
|
|
import shutil
|
|
|
|
parser = argparse.ArgumentParser(description="Py-Backup Application.")
|
|
parser.add_argument(
|
|
"--backup-type", choices=['user', 'system'], help="Type of backup to perform.")
|
|
parser.add_argument(
|
|
"--destination", help="Destination directory for the backup.")
|
|
parser.add_argument("--sources", nargs='+',
|
|
help="List of sources for user backup (e.g., Picture, Documents).")
|
|
parser.add_argument("--mode", choices=['full', 'incremental'],
|
|
default='incremental', help="Mode for system backup.")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.backup_type and args.destination:
|
|
from shared_libs.logger import app_logger as cli_logger
|
|
backup_manager = BackupManager(cli_logger)
|
|
|
|
if args.backup_type == 'user':
|
|
if not args.sources:
|
|
print("Error: --sources are required for user backup.")
|
|
sys.exit(1)
|
|
backup_manager._run_user_backup(
|
|
args.sources, args.destination, None, None)
|
|
|
|
elif args.backup_type == 'system':
|
|
backup_manager._run_system_backup(
|
|
args.destination, args.mode, None, None)
|
|
|
|
sys.exit(0)
|
|
|
|
else:
|
|
app = MainApplication()
|
|
app.mainloop()
|