Compare commits

...

5 Commits

17 changed files with 267 additions and 133 deletions

View File

@@ -204,10 +204,13 @@ class Msg:
"back": _("Back"),
"active": _("Active"),
"source": _("Source"),
"select_source": _("Select Source"),
"sources": _("Sources"),
"destination": _("Destination"),
"system_backup_info": _("System Backup"),
"system_restore_info": _("System Restore"),
"system_restore": _("System Restore"),
"select_source_first": _("Please select a source first"),
"user_backup_info": _("User Backup"),
"user_restore_info": _("User Restore"),
"select_destination_first": _("Please select a destination first"),

View File

@@ -249,14 +249,9 @@ class MainApplication(tk.Tk):
target_label_frame, text="0.00 GB / 0.00 GB")
self.target_size_label.pack(side=tk.RIGHT)
self.restore_size_frame = ttk.Frame(self.content_frame)
self.restore_size_frame.grid(row=4, column=0, sticky="ew", padx=10, pady=5, rowspan=2)
self.restore_size_frame.grid_columnconfigure(0, weight=1)
self.restore_size_frame.grid_remove()
self.restore_size_frame_before = ttk.LabelFrame(
self.restore_size_frame, text="Before Restoration", padding=10)
self.restore_size_frame_before.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
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(
@@ -270,8 +265,8 @@ class MainApplication(tk.Tk):
self.restore_size_label_before.pack(side=tk.RIGHT)
self.restore_size_frame_after = ttk.LabelFrame(
self.restore_size_frame, text="After Restoration", padding=10)
self.restore_size_frame_after.grid(row=1, column=0, sticky="ew", padx=10, pady=5)
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(
@@ -289,6 +284,9 @@ class MainApplication(tk.Tk):
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)
@@ -298,6 +296,23 @@ class MainApplication(tk.Tk):
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):
@@ -339,7 +354,7 @@ class MainApplication(tk.Tk):
self.navigation.initialize_ui_for_mode(last_mode)
# Trigger calculations if needed
if last_mode == 'backup' and backup_dest_path:
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':
@@ -351,10 +366,6 @@ class MainApplication(tk.Tk):
'folder', 'Computer')
self.after(100, self.actions.on_sidebar_button_click,
restore_dest_folder)
else:
# Default action if no specific state to restore
self.after(100, self.actions.on_sidebar_button_click, "Computer")
self.data_processing.process_queue()
def _setup_log_window(self):
@@ -445,6 +456,12 @@ class MainApplication(tk.Tk):
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'])

View File

@@ -20,7 +20,7 @@ class Actions:
def _check_for_first_backup(self):
# This logic only applies if no override from advanced settings is active
if self.app.full_backup_cb.cget('state') != 'normal':
self.app.is_first_backup = False # Ensure this is reset if controls are disabled
self.app.is_first_backup = False # Ensure this is reset if controls are disabled
return
# A "first backup" situation exists if the destination is selected but empty.
@@ -48,7 +48,8 @@ class Actions:
def on_sidebar_button_click(self, button_text):
self.app.drawing.reset_projection_canvases()
if not self.app.canvas_frame.winfo_viewable():
self.app.navigation.toggle_mode(self.app.mode, trigger_calculation=False)
self.app.navigation.toggle_mode(
self.app.mode, trigger_calculation=False)
self.app.log_window.clear_log()
folder_path = AppConfig.FOLDER_PATHS.get(button_text)
@@ -73,16 +74,17 @@ class Actions:
if button_text == "Computer":
if self.app.mode == 'backup':
extra_info = Msg.STR["system_backup_info"]
else: # restore
else: # restore
extra_info = Msg.STR["system_restore_info"]
else: # User folder
else: # User folder
if self.app.mode == 'backup':
extra_info = Msg.STR["user_backup_info"]
else: # restore
else: # restore
extra_info = Msg.STR["user_restore_info"]
# Unified logic for starting a calculation on the left canvas
self._start_left_canvas_calculation(button_text, str(folder_path), icon_name, extra_info)
self._start_left_canvas_calculation(
button_text, str(folder_path), icon_name, extra_info)
def _start_left_canvas_calculation(self, button_text, folder_path, icon_name, extra_info):
self.app.start_pause_button.config(state="disabled")
@@ -98,6 +100,18 @@ class Actions:
self.app.drawing.redraw_left_canvas()
return
if self.app.mode == 'restore' and not self.app.right_canvas_data.get('path_display'):
self.app.left_canvas_data.update({
'icon': icon_name,
'folder': button_text,
'path_display': folder_path,
'size': Msg.STR["select_source_first"],
'calculating': False,
'extra_info': extra_info
})
self.app.drawing.redraw_left_canvas()
return
if self.app.calculation_thread and self.app.calculation_thread.is_alive():
self.app.calculation_stop_event.set()
@@ -126,8 +140,9 @@ class Actions:
if self.app.mode == 'backup':
self._check_for_first_backup()
else: # restore mode
self.app.config_manager.set_setting("restore_destination_path", folder_path)
else: # restore mode
self.app.config_manager.set_setting(
"restore_destination_path", folder_path)
def on_right_canvas_click(self, event):
self.app.drawing.reset_projection_canvases()
@@ -171,11 +186,12 @@ class Actions:
size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB"
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')),
'path_display': path,
'size': size_str
})
self.app.config_manager.set_setting("backup_destination_path", path)
self.app.config_manager.set_setting(
"backup_destination_path", path)
self.app.drawing.redraw_right_canvas()
self.app.drawing.update_target_projection()
self.app.start_pause_button.config(state="normal")
@@ -187,11 +203,12 @@ class Actions:
elif self.app.mode == "restore":
self.app.right_canvas_data.update({
'folder': os.path.basename(path.rstrip('/')),
'folder': os.path.basename(path.rstrip('/')),
'path_display': path,
'size': ''
})
self.app.config_manager.set_setting("restore_source_path", path)
self.app.config_manager.set_setting(
"restore_source_path", path)
self.app.drawing.calculate_restore_folder_size()
self.app.start_pause_button.config(state="normal")
@@ -221,22 +238,22 @@ class Actions:
# Update the main UI to reflect the cleared settings
self.app.update_backup_options_from_config()
self.app.backup_left_canvas_data.clear()
self.app.backup_right_canvas_data.clear()
self.app.restore_left_canvas_data.clear()
self.app.restore_right_canvas_data.clear()
AppConfig.generate_and_write_final_exclude_list()
app_logger.log("Settings have been reset to default values.")
settings_frame = self.app.settings_frame
if settings_frame:
settings_frame.load_and_display_excludes()
settings_frame._load_hidden_files()
self.app.destination_path = None
self.app.navigation.initialize_ui_for_mode(self.app.mode)
self.app.start_pause_button.config(state="disabled")
# Clear the canvases and reset the UI to its initial state for the current mode
self.app.backup_left_canvas_data.clear()
self.app.backup_right_canvas_data.clear()
self.app.restore_left_canvas_data.clear()
self.app.restore_right_canvas_data.clear()
with message_box_animation(self.app.animated_icon):
MessageDialog(master=self.app, message_type="info",
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
@@ -249,10 +266,12 @@ class Actions:
self.app.start_pause_button["text"] = "Pause"
self.app.animated_icon.destroy()
backup_animation_type = self.app.config_manager.get_setting("backup_animation_type", "counter_arc")
backup_animation_type = self.app.config_manager.get_setting(
"backup_animation_type", "counter_arc")
self.app.animated_icon = AnimatedIcon(
self.app.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=backup_animation_type)
self.app.animated_icon.pack(side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.start()
if self.app.mode == "backup":
@@ -268,7 +287,8 @@ class Actions:
self.app.animated_icon.destroy()
self.app.animated_icon = AnimatedIcon(
self.app.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type="blink")
self.app.animated_icon.pack(side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.start()
self.app.backup_manager.pause_backup()
@@ -277,11 +297,13 @@ class Actions:
self.app.start_pause_button["text"] = "Pause"
self.app.animated_icon.destroy()
backup_animation_type = self.app.config_manager.get_setting("backup_animation_type", "counter_arc")
backup_animation_type = self.app.config_manager.get_setting(
"backup_animation_type", "counter_arc")
self.app.animated_icon = AnimatedIcon(
self.app.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=backup_animation_type)
self.app.animated_icon.pack(side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app.task_progress)
self.app.animated_icon.start()
self.app.backup_manager.resume_backup()
@@ -306,7 +328,7 @@ class Actions:
MessageDialog(master=self.app, message_type="error",
title=Msg.STR["error"], text=Msg.STR["system_backup_in_home_error"])
return
exclude_file_paths = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
exclude_file_paths.append(AppConfig.GENERATED_EXCLUDE_LIST_PATH)
@@ -330,4 +352,4 @@ class Actions:
source, dest, False, is_dry_run=is_dry_run, exclude_files=None, on_progress=self.update_task_progress, on_completion=self.on_backup_completion, on_error=self.on_backup_error)
def update_task_progress(self, percentage):
self.app.task_progress["value"] = percentage
self.app.task_progress["value"] = percentage

View File

@@ -8,6 +8,7 @@ from app_config import AppConfig, Msg
from shared_libs.animated_icon import AnimatedIcon
from pyimage_ui.shared_logic import enforce_backup_type_exclusivity
class AdvancedSettingsFrame(tk.Toplevel):
def __init__(self, master, config_manager, app_instance, **kwargs):
super().__init__(master, **kwargs)
@@ -17,46 +18,60 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.app_instance = app_instance
# --- Warning Label ---
warning_label = ttk.Label(self, text=Msg.STR["advanced_settings_warning"], wraplength=780, justify="center")
warning_label = ttk.Label(
self, text=Msg.STR["advanced_settings_warning"], wraplength=780, justify="center")
warning_label.pack(pady=10, fill=tk.X, padx=10)
# --- Treeview for system folder exclusion ---
self.tree_frame = ttk.LabelFrame(self, text=Msg.STR["exclude_system_folders"], padding=10)
self.tree_frame = ttk.LabelFrame(
self, text=Msg.STR["exclude_system_folders"], padding=10)
self.tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
columns = ("included", "name", "path")
self.tree = ttk.Treeview(self.tree_frame, columns=columns, show="headings")
self.tree = ttk.Treeview(
self.tree_frame, columns=columns, show="headings")
self.tree.heading("included", text=Msg.STR["in_backup"])
self.tree.heading("name", text=Msg.STR["name"])
self.tree.heading("path", text=Msg.STR["path"])
self.tree.column("path", anchor="center")
self.tree.column("name", anchor="center")
self.tree.column("included", width=100, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tree.tag_configure("backup_dest_exclude", foreground="gray")
self.tree.bind("<Button-1>", self._toggle_include_status)
# --- Animation Settings ---
animation_frame = ttk.LabelFrame(self, text=Msg.STR["animation_settings_title"], padding=10)
animation_frame = ttk.LabelFrame(
self, text=Msg.STR["animation_settings_title"], padding=10)
animation_frame.pack(fill=tk.X, padx=10, pady=5)
animation_types = ["counter_arc", "double_arc", "line", "blink"]
ttk.Label(animation_frame, text=Msg.STR["backup_animation_label"]).grid(row=0, column=0, sticky="w", pady=2)
ttk.Label(animation_frame, text=Msg.STR["backup_animation_label"]).grid(
row=0, column=0, sticky="w", pady=2)
self.backup_anim_var = tk.StringVar()
self.backup_anim_combo = ttk.Combobox(animation_frame, textvariable=self.backup_anim_var, values=animation_types, state="readonly")
self.backup_anim_combo = ttk.Combobox(
animation_frame, textvariable=self.backup_anim_var, values=animation_types, state="readonly")
self.backup_anim_combo.grid(row=0, column=1, sticky="ew", padx=5)
ttk.Label(animation_frame, text=Msg.STR["calc_animation_label"]).grid(row=1, column=0, sticky="w", pady=2)
ttk.Label(animation_frame, text=Msg.STR["calc_animation_label"]).grid(
row=1, column=0, sticky="w", pady=2)
self.calc_anim_var = tk.StringVar()
self.calc_anim_combo = ttk.Combobox(animation_frame, textvariable=self.calc_anim_var, values=animation_types, state="readonly")
self.calc_anim_combo = ttk.Combobox(
animation_frame, textvariable=self.calc_anim_var, values=animation_types, state="readonly")
self.calc_anim_combo.grid(row=1, column=1, sticky="ew", padx=5)
reset_button = ttk.Button(animation_frame, text=Msg.STR["default_settings"], command=self._reset_animation_settings)
reset_button = ttk.Button(
animation_frame, text=Msg.STR["default_settings"], command=self._reset_animation_settings)
reset_button.grid(row=0, column=2, rowspan=2, padx=10)
animation_frame.columnconfigure(1, weight=1)
# --- Backup Default Settings ---
defaults_frame = ttk.LabelFrame(self, text="Backup Defaults", padding=10)
defaults_frame = ttk.LabelFrame(
self, text="Backup Defaults", padding=10)
defaults_frame.pack(fill=tk.X, padx=10, pady=5)
self.force_full_var = tk.BooleanVar()
@@ -64,22 +79,30 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.force_compression_var = tk.BooleanVar()
self.force_encryption_var = tk.BooleanVar()
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity(self.force_full_var, self.force_incremental_var, self.force_full_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity(self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_compression"], variable=self.force_compression_var).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_encryption"], variable=self.force_encryption_var).pack(anchor=tk.W)
ttk.Separator(defaults_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
encryption_note = ttk.Label(defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_full_backup"], variable=self.force_full_var, command=lambda: enforce_backup_type_exclusivity(
self.force_full_var, self.force_incremental_var, self.force_full_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_incremental_backup"], variable=self.force_incremental_var, command=lambda: enforce_backup_type_exclusivity(
self.force_incremental_var, self.force_full_var, self.force_incremental_var.get())).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_compression"],
variable=self.force_compression_var).pack(anchor=tk.W)
ttk.Checkbutton(defaults_frame, text=Msg.STR["force_encryption"],
variable=self.force_encryption_var).pack(anchor=tk.W)
ttk.Separator(defaults_frame, orient=tk.HORIZONTAL).pack(
fill=tk.X, pady=5)
encryption_note = ttk.Label(
defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
encryption_note.pack(anchor=tk.W, pady=5)
# --- Action Buttons ---
button_frame = ttk.Frame(self)
button_frame.pack(pady=10)
ttk.Button(button_frame, text=Msg.STR["apply"], command=self._apply_changes).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self.destroy).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text=Msg.STR["apply"], command=self._apply_changes).pack(
side=tk.LEFT, padx=5)
ttk.Button(button_frame, text=Msg.STR["cancel"], command=self.destroy).pack(
side=tk.LEFT, padx=5)
self._load_system_folders()
self._load_animation_settings()
@@ -100,14 +123,20 @@ class AdvancedSettingsFrame(tk.Toplevel):
self.app_instance.update_backup_options_from_config()
def _load_backup_defaults(self):
self.force_full_var.set(self.config_manager.get_setting("force_full_backup", False))
self.force_incremental_var.set(self.config_manager.get_setting("force_incremental_backup", False))
self.force_compression_var.set(self.config_manager.get_setting("force_compression", False))
self.force_encryption_var.set(self.config_manager.get_setting("force_encryption", False))
self.force_full_var.set(
self.config_manager.get_setting("force_full_backup", False))
self.force_incremental_var.set(
self.config_manager.get_setting("force_incremental_backup", False))
self.force_compression_var.set(
self.config_manager.get_setting("force_compression", False))
self.force_encryption_var.set(
self.config_manager.get_setting("force_encryption", False))
def _load_animation_settings(self):
backup_anim = self.config_manager.get_setting("backup_animation_type", "counter_arc")
calc_anim = self.config_manager.get_setting("calculation_animation_type", "double_arc")
backup_anim = self.config_manager.get_setting(
"backup_animation_type", "counter_arc")
calc_anim = self.config_manager.get_setting(
"calculation_animation_type", "double_arc")
self.backup_anim_var.set(backup_anim)
self.calc_anim_var.set(calc_anim)
@@ -132,18 +161,26 @@ class AdvancedSettingsFrame(tk.Toplevel):
continue
is_user_excluded = f"{item_path_str}/*" in user_patterns
included_text = Msg.STR["no"] if is_user_excluded else Msg.STR["yes"]
items_to_display[item_path_str] = (included_text, item.name, item_path_str)
items_to_display[item_path_str] = (
included_text, item.name, item_path_str)
if self.app_instance and self.app_instance.destination_path:
backup_root_path = Path(f"/{self.app_instance.destination_path.strip('/').split('/')[0]}")
backup_root_path = Path(
f"/{self.app_instance.destination_path.strip('/').split('/')[0]}")
backup_root_path_str = str(backup_root_path.absolute())
items_to_display[backup_root_path_str] = (Msg.STR["no"], backup_root_path.name, backup_root_path_str)
items_to_display[backup_root_path_str] = (
Msg.STR["no"], backup_root_path.name, backup_root_path_str)
for item_path_str in sorted(items_to_display.keys()):
tags = ()
item_values = items_to_display[item_path_str]
tag = "yes" if item_values[0] == Msg.STR["yes"] else "no"
# Special tag for the backup destination, which is always excluded and read-only
if self.app_instance and self.app_instance.destination_path and item_path_str == str(Path(f"/{self.app_instance.destination_path.strip('/').split('/')[0]}").absolute()):
tags = ("backup_dest_exclude",)
self.tree.insert("", "end", values=items_to_display[item_path_str], tags=tags)
tags = ("backup_dest_exclude", tag)
else:
tags = (tag,)
self.tree.insert("", "end", values=item_values, tags=tags)
def _toggle_include_status(self, event):
item_id = self.tree.identify_row(event.y)
@@ -153,15 +190,25 @@ class AdvancedSettingsFrame(tk.Toplevel):
return
current_values = self.tree.item(item_id, 'values')
new_status = Msg.STR["yes"] if current_values[0] == Msg.STR["no"] else Msg.STR["no"]
self.tree.item(item_id, values=(new_status, current_values[1], current_values[2]))
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.tree.item(item_id, values=(
new_status, current_values[1], current_values[2]), tags=(new_tag,))
def _apply_changes(self):
self.config_manager.set_setting("backup_animation_type", self.backup_anim_var.get())
self.config_manager.set_setting("calculation_animation_type", self.calc_anim_var.get())
self.config_manager.set_setting("force_full_backup", self.force_full_var.get())
self.config_manager.set_setting("force_incremental_backup", self.force_incremental_var.get())
self.config_manager.set_setting("force_compression", self.force_compression_var.get())
self.config_manager.set_setting("force_encryption", self.force_encryption_var.get())
self.config_manager.set_setting(
"backup_animation_type", self.backup_anim_var.get())
self.config_manager.set_setting(
"calculation_animation_type", self.calc_anim_var.get())
self.config_manager.set_setting(
"force_full_backup", self.force_full_var.get())
self.config_manager.set_setting(
"force_incremental_backup", self.force_incremental_var.get())
self.config_manager.set_setting(
"force_compression", self.force_compression_var.get())
self.config_manager.set_setting(
"force_encryption", self.force_encryption_var.get())
if self.app_instance:
self.app_instance.update_backup_options_from_config()
@@ -171,17 +218,18 @@ class AdvancedSettingsFrame(tk.Toplevel):
# Create a new one
bg_color = self.app_instance.style.lookup('TFrame', 'background')
backup_animation_type = self.backup_anim_var.get()
initial_animation_type = "blink"
if backup_animation_type == "line":
initial_animation_type = "line"
self.app_instance.animated_icon = AnimatedIcon(
self.app_instance.action_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type=initial_animation_type)
# Pack it in the correct order
self.app_instance.animated_icon.pack(side=tk.LEFT, padx=5, before=self.app_instance.task_progress)
self.app_instance.animated_icon.pack(
side=tk.LEFT, padx=5, before=self.app_instance.task_progress)
# Set the correct state
self.app_instance.animated_icon.stop("DISABLE")
self.app_instance.animated_icon.animation_type = backup_animation_type
@@ -204,7 +252,8 @@ class AdvancedSettingsFrame(tk.Toplevel):
existing_patterns = []
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
existing_patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
existing_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
preserved_patterns = []
for pattern in existing_patterns:
@@ -222,23 +271,26 @@ class AdvancedSettingsFrame(tk.Toplevel):
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
for path in final_excludes:
f.write(f"{path}\n")
self.destroy()
if self.app_instance:
current_source = self.app_instance.left_canvas_data.get('folder')
if current_source:
self.app_instance.actions.on_sidebar_button_click(current_source)
self.app_instance.actions.on_sidebar_button_click(
current_source)
def _load_exclude_patterns(self):
generated_patterns = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
generated_patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
generated_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
user_patterns = []
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
user_patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
user_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
return generated_patterns, user_patterns

View File

@@ -6,6 +6,7 @@ import re
from app_config import Msg
class BackupContentFrame(ttk.Frame):
def __init__(self, master, backup_manager, **kwargs):
super().__init__(master, **kwargs)
@@ -21,11 +22,14 @@ class BackupContentFrame(ttk.Frame):
columns = ("name", "date", "size")
self.content_tree = ttk.Treeview(
self.content_frame, columns=columns, show="headings")
self.content_tree.heading("name", text=Msg.STR["name"])
self.content_tree.heading("date", text=Msg.STR["date"])
self.content_tree.heading("size", text=Msg.STR["size"])
self.content_tree.heading(
"name", text=Msg.STR["name"])
self.content_tree.heading(
"date", text=Msg.STR["date"])
self.content_tree.heading(
"size", text=Msg.STR["size"])
for col in columns:
self.content_tree.column(col, width=150)
self.content_tree.column(col, width=150, anchor="center")
self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
list_button_frame = ttk.Frame(self.content_frame)
@@ -59,8 +63,8 @@ class BackupContentFrame(ttk.Frame):
match = re.match(r"(\d{4}-\d{2}-\d{2})", backup_name)
if match:
backup_date = match.group(1)
backup_size = ""
backup_size = ""
self.content_tree.insert("", "end", values=(
backup_name, backup_date, backup_size

View File

@@ -392,3 +392,12 @@ class Drawing:
# Also reset the main info label
self.app.info_label.config(text="")
def reset_all_canvases(self):
self.reset_projection_canvases()
self.app.left_canvas.delete("all")
self.app.right_canvas.delete("all")
self.app.left_canvas_data.clear()
self.app.right_canvas_data.clear()
self.redraw_left_canvas()
self.redraw_right_canvas()

View File

@@ -14,9 +14,9 @@ class Navigation:
self.app.calculation_stop_event.set()
if self.app.right_calculation_thread and self.app.right_calculation_thread.is_alive():
self.app.right_calculation_stop_event.set()
# Stop all calculation animations and update UI
self.app.drawing._stop_calculating_animation() # Stops both left and right
self.app.drawing._stop_calculating_animation() # Stops both left and right
if self.app.left_canvas_data:
self.app.left_canvas_data['calculating'] = False
if self.app.right_canvas_data:
@@ -35,7 +35,8 @@ class Navigation:
active_index = 0
# Restore backup destination disk metrics
backup_dest_path = self.app.backup_right_canvas_data.get('path_display')
backup_dest_path = self.app.backup_right_canvas_data.get(
'path_display')
if backup_dest_path and os.path.isdir(backup_dest_path):
try:
total, used, free = shutil.disk_usage(backup_dest_path)
@@ -54,7 +55,8 @@ class Navigation:
active_index = 1
# Restore restore destination disk metrics
restore_dest_path = self.app.restore_left_canvas_data.get('path_display')
restore_dest_path = self.app.restore_left_canvas_data.get(
'path_display')
if restore_dest_path and os.path.isdir(restore_dest_path):
try:
total, used, free = shutil.disk_usage(restore_dest_path)
@@ -77,10 +79,10 @@ class Navigation:
'path_display': '',
'size': ''
})
else: # In restore mode, left is destination
else: # In restore mode, left is destination
self.app.left_canvas_data.update({
'icon': 'computer_extralarge', # Default icon
'folder': Msg.STR["select_destination"],
'icon': 'computer_extralarge',
'folder': 'Computer',
'path_display': '',
'size': ''
})
@@ -89,7 +91,7 @@ class Navigation:
# Right canvas is always the destination/HDD view conceptually
self.app.right_canvas_data.update({
'icon': 'hdd_extralarge',
'folder': Msg.STR["select_destination"] if mode == 'backup' else Msg.STR["source"],
'folder': Msg.STR["select_destination"] if mode == 'backup' else Msg.STR["select_source"],
'path_display': '',
'size': ''
})
@@ -98,28 +100,34 @@ class Navigation:
if mode == "backup":
self.app.source_size_frame.grid()
self.app.target_size_frame.grid()
self.app.restore_size_frame.grid_remove()
self.app.mode_button_icon = self.app.image_manager.get_icon("forward_extralarge")
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.mode_button_icon = self.app.image_manager.get_icon(
"forward_extralarge")
self.app.info_label.config(text=Msg.STR["backup_mode_info"])
self.app.full_backup_cb.config(state="normal")
self.app.incremental_cb.config(state="normal")
self.app.compressed_cb.config(state="normal")
self.app.encrypted_cb.config(state="normal")
self.app.bypass_security_cb.config(state='disabled') # This one is mode-dependent
self.app.bypass_security_cb.config(
state='disabled') # This one is mode-dependent
# Let the central config function handle the state of these checkboxes
self.app.update_backup_options_from_config()
else: # restore
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame.grid()
self.app.mode_button_icon = self.app.image_manager.get_icon("back_extralarge")
self.app.restore_size_frame_before.grid()
self.app.restore_size_frame_after.grid()
self.app.mode_button_icon = self.app.image_manager.get_icon(
"back_extralarge")
self.app.info_label.config(text=Msg.STR["restore_mode_info"])
self.app.full_backup_cb.config(state='disabled')
self.app.incremental_cb.config(state='disabled')
self.app.compressed_cb.config(state='disabled')
self.app.encrypted_cb.config(state='disabled')
self.app.bypass_security_cb.config(state='normal') # This one is mode-dependent
self.app.bypass_security_cb.config(
state='normal') # This one is mode-dependent
self.app.mode_button.config(image=self.app.mode_button_icon)
self.app.drawing.update_nav_buttons(active_index)
self.app.drawing.redraw_left_canvas()
@@ -157,8 +165,9 @@ class Navigation:
if trigger_calculation:
# Always trigger the calculation for the left canvas, which is controlled by the sidebar
current_folder_on_left = self.app.left_canvas_data.get('folder')
if current_folder_on_left:
self.app.actions.on_sidebar_button_click(current_folder_on_left)
if current_folder_on_left and current_folder_on_left in self.app.buttons_map:
self.app.actions.on_sidebar_button_click(
current_folder_on_left)
# Additionally, in restore mode, trigger calculation for the right canvas if a source is set
if self.app.mode == 'restore':
@@ -187,7 +196,8 @@ class Navigation:
self.app.backup_content_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.log_frame.grid()
self.app.top_bar.grid()
self._update_task_bar_visibility("log")
@@ -203,7 +213,8 @@ class Navigation:
self.app.backup_content_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.scheduler_frame.show()
self.app.top_bar.grid()
self._update_task_bar_visibility("scheduler")
@@ -219,7 +230,8 @@ class Navigation:
self.app.scheduler_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.settings_frame.show()
self.app.top_bar.grid()
self._update_task_bar_visibility("settings")
@@ -241,7 +253,8 @@ class Navigation:
self.app.settings_frame.hide()
self.app.source_size_frame.grid_remove()
self.app.target_size_frame.grid_remove()
self.app.restore_size_frame.grid_remove()
self.app.restore_size_frame_before.grid_remove()
self.app.restore_size_frame_after.grid_remove()
self.app.backup_content_frame.show(self.app.destination_path)
self.app.top_bar.grid()
self._update_task_bar_visibility("scheduler")

View File

@@ -26,7 +26,7 @@ class SchedulerFrame(ttk.Frame):
self.jobs_tree.heading("destination", text=Msg.STR["destination"])
self.jobs_tree.heading("sources", text=Msg.STR["sources"])
for col in columns:
self.jobs_tree.column(col, width=100)
self.jobs_tree.column(col, width=100, anchor="center")
self.jobs_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self._load_scheduled_jobs()
@@ -149,7 +149,7 @@ class SchedulerFrame(ttk.Frame):
if job_type == "user":
command += f" --sources "
for s in job_sources:
command += f'\"{s}\" ' # This line has an issue with escaping
command += f'\"{s}\" ' # This line has an issue with escaping
comment = f"{self.backup_manager.app_tag}; type:{job_type}; freq:{job_frequency}; dest:{dest}"
if job_type == "user":
@@ -174,7 +174,7 @@ class SchedulerFrame(ttk.Frame):
for job in jobs:
self.jobs_tree.insert("", "end", values=(
job["active"], job["type"], job["frequency"], job["destination"], ", ".join(
job["sources"])
job["sources"])
), iid=job["id"])
def _remove_selected_job(self):

View File

@@ -33,6 +33,10 @@ class SettingsFrame(ttk.Frame):
self.tree.heading("included", text=Msg.STR["in_backup"])
self.tree.heading("name", text=Msg.STR["name"])
self.tree.heading("path", text=Msg.STR["path"])
self.tree.tag_configure("yes", background="#89b4fe")
self.tree.tag_configure("no", background="#4c92ed")
self.tree.column("included", width=100, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
@@ -46,8 +50,13 @@ class SettingsFrame(ttk.Frame):
self.hidden_tree.heading("included", text=Msg.STR["in_backup"])
self.hidden_tree.heading("name", text=Msg.STR["name"])
self.hidden_tree.heading("path", text=Msg.STR["path"])
self.hidden_tree.column("included", width=100, anchor="center")
self.hidden_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.hidden_tree.tag_configure("yes", background="#89b4fe")
self.hidden_tree.tag_configure("no", background="#4c92ed")
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
self.hidden_tree_frame.pack_forget() # Initially hidden
@@ -126,7 +135,8 @@ class SettingsFrame(ttk.Frame):
items_to_display.sort(key=lambda x: x[0], reverse=True)
for item in items_to_display:
self.tree.insert("", "end", values=item)
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status(self, event):
item_id = self.tree.identify_row(event.y)
@@ -137,8 +147,10 @@ class SettingsFrame(ttk.Frame):
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.tree.item(item_id, values=(
new_status, current_values[1], current_values[2]))
new_status, current_values[1], current_values[2]), tags=(new_tag,))
def _apply_changes(self):
# Get all paths displayed in the treeviews
@@ -175,7 +187,8 @@ class SettingsFrame(ttk.Frame):
existing_patterns = []
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
existing_patterns = [line.strip() for line in f if line.strip() and not line.startswith('#')]
existing_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
# Preserve patterns that are not managed by this view
preserved_patterns = []
@@ -196,18 +209,16 @@ class SettingsFrame(ttk.Frame):
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
for path in final_excludes:
f.write(f"{path}\n")
self.hide()
# Trigger recalculation
current_source = self.master.master.master.left_canvas_data.get('folder')
if current_source:
self.master.master.master.actions.on_sidebar_button_click(current_source)
# Reload the excludes to show the changes
self.load_and_display_excludes()
if self.hidden_files_visible:
self._load_hidden_files()
def _open_advanced_settings(self):
advanced_settings_window = AdvancedSettingsFrame(
self.master,
config_manager=self.master.master.master.config_manager,
self.master,
config_manager=self.master.master.master.config_manager,
app_instance=self.master.master.master
)
advanced_settings_window.grab_set()
@@ -246,7 +257,8 @@ class SettingsFrame(ttk.Frame):
items_to_display.sort(key=lambda x: x[1])
for item in items_to_display:
self.hidden_tree.insert("", "end", values=item)
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.hidden_tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status_hidden(self, event):
item_id = self.hidden_tree.identify_row(event.y)
@@ -257,5 +269,7 @@ class SettingsFrame(ttk.Frame):
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.hidden_tree.item(item_id, values=(
new_status, current_values[1], current_values[2]))
new_status, current_values[1], current_values[2]), tags=(new_tag,))