From 22144859d8498f1f184b43faf6ab183c72b451c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A9sir=C3=A9=20Werner=20Menrath?= Date: Thu, 11 Sep 2025 01:08:38 +0200 Subject: [PATCH] feat: Implement Hard Reset and Refactor PasswordDialog This commit introduces a new "Hard Reset" functionality in the settings, allowing users to reset the application to its initial state by deleting the configuration directory. Key changes include: - Added a "Hard Reset" button and a dedicated confirmation frame in `settings_frame.py`. - Implemented the logic to delete the `.config/py_backup` directory and restart the application. - Enhanced the hard reset process to unmount encrypted drives if they are mounted, prompting for a password if necessary. To improve modularity and maintainability, the PasswordDialog class has been refactored: - Moved PasswordDialog from `pyimage_ui/password_dialog.py` to `shared_libs/message.py`. - Updated all references and imports to the new location. - Externalized all user-facing strings in PasswordDialog for translation support. Additionally, several bug fixes and improvements were made: - Corrected object access hierarchy in `settings_frame.py` and `advanced_settings_frame.py` by passing manager instances directly. - Handled `FileNotFoundError` in `actions.py` when selecting remote backup destinations, preventing crashes and displaying "N/A" for disk usage. - Replaced incorrect `calculating_animation` reference with `animated_icon` in `actions.py`. - Added missing translation keys in `pbp_app_config.py`. --- core/backup_manager.py | 2 +- core/encryption_manager.py | 11 ++-- core/pbp_app_config.py | 12 +++++ main_app.py | 2 +- pyimage_ui/actions.py | 22 ++++---- pyimage_ui/advanced_settings_frame.py | 5 +- pyimage_ui/password_dialog.py | 69 ------------------------- pyimage_ui/settings_frame.py | 74 ++++++++++++++++++++++++--- 8 files changed, 103 insertions(+), 94 deletions(-) delete mode 100644 pyimage_ui/password_dialog.py diff --git a/core/backup_manager.py b/core/backup_manager.py index 71c0fc3..577c469 100644 --- a/core/backup_manager.py +++ b/core/backup_manager.py @@ -14,7 +14,7 @@ import tempfile import stat from core.pbp_app_config import AppConfig -from pyimage_ui.password_dialog import PasswordDialog +from shared_libs.message import PasswordDialog from core.encryption_manager import EncryptionManager diff --git a/core/encryption_manager.py b/core/encryption_manager.py index 96706d6..77f183f 100644 --- a/core/encryption_manager.py +++ b/core/encryption_manager.py @@ -8,8 +8,8 @@ import subprocess import math from typing import Optional, Tuple -from core.pbp_app_config import AppConfig -from pyimage_ui.password_dialog import PasswordDialog +from core.pbp_app_config import AppConfig, Msg +from shared_libs.message import PasswordDialog import json @@ -78,7 +78,7 @@ class EncryptionManager: self.password_cache[username] = password return password dialog = PasswordDialog( - self.app, title=f"Enter password for {username}", confirm=confirm) + self.app, title=f"Enter password for {username}", confirm=confirm, translations=Msg.STR) password, save_to_keyring = dialog.get_password() if password: self.password_cache[username] = password @@ -323,6 +323,11 @@ mount \"/dev/mapper/{mapper_name}\" \"{mount_point}\" for path in list(self.mounted_destinations): self.unmount_and_reset_owner(path, force_unmap=True) + def unmount_all_encrypted_drives(self, password: str) -> Tuple[bool, str]: + for path in list(self.mounted_destinations): + self.unmount_and_reset_owner(path, force_unmap=True) + return True, "Successfully unmounted all drives." + def _get_chown_command(self, mount_point: str, is_system: bool) -> str: if not is_system: try: diff --git a/core/pbp_app_config.py b/core/pbp_app_config.py index 29b6284..de38c3d 100644 --- a/core/pbp_app_config.py +++ b/core/pbp_app_config.py @@ -369,4 +369,16 @@ class Msg: "create_add_key_file": _("Create/Add Key File"), # New "key_file_not_created": _("Key file not created."), # New "backup_options": _("Backup Options"), # New + "hard_reset": _("Hard reset"), + "hard_reset_warning": _("This will reset the application to its initial state, as if it were opened for the first time. This can be useful if you are experiencing problems with the app. Clicking 'Delete now' will delete the '.config/py_backup' folder in your home directory without an additional dialog. The application will then automatically restart."), + "delete_now": _("Delete now"), + "full_delete_config_settings": _("Full delete config settings"), + "password_required": _("Password Required"), + "enter_password_prompt": _("Please enter the password for the encrypted backup:"), + "confirm_password_prompt": _("Confirm password:"), + "save_to_keyring": _("Save password to system keyring"), + "password_empty_error": _("Password cannot be empty."), + "passwords_do_not_match_error": _("Passwords do not match."), + "ok": _("OK"), + "unlock_backup": _("Unlock Backup"), } diff --git a/main_app.py b/main_app.py index f3df9cb..216e6fb 100644 --- a/main_app.py +++ b/main_app.py @@ -436,7 +436,7 @@ class MainApplication(tk.Tk): def _setup_settings_frame(self): self.settings_frame = SettingsFrame( - self.content_frame, self.navigation, self.actions, padding=10) + self.content_frame, self.navigation, self.actions, self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=10) self.settings_frame.grid(row=2, column=0, sticky="nsew") self.settings_frame.grid_remove() diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index d44ea7b..9cd4616 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -334,7 +334,7 @@ class Actions: try: if self.app.mode == "backup": if self.app.destination_path: - self.app.backup_manager.encryption_manager.unmount( + self.app.backup_manager.encryption_manager.unmount_and_reset_owner( self.app.destination_path) self.app.destination_path = path @@ -352,14 +352,16 @@ class Actions: f.write(f"{backup_root_to_exclude}\n") except IOError as e: app_logger.log(f"Error updating exclusion list: {e}") - - total, used, free = shutil.disk_usage(path) - self.app.destination_total_bytes = total - self.app.destination_used_bytes = used - size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB" + try: + total, used, free = shutil.disk_usage(path) + self.app.destination_total_bytes = total + self.app.destination_used_bytes = used + size_str = f"{used / (1024**3):.2f} GB / {total / (1024**3):.2f} GB" + except FileNotFoundError: + size_str = "N/A" self.app.right_canvas_data.update({ - 'folder': os.path.basename(path.rstrip('/')), + 'folder': os.path.basename(path.rstrip('/')) if not path.startswith(('ssh:', 'sftp:')) else path, 'path_display': path, 'size': size_str }) @@ -375,7 +377,7 @@ 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('/')) if not path.startswith(('ssh:', 'sftp:')) else path, 'path_display': path, 'size': '' }) @@ -385,7 +387,7 @@ class Actions: self.app.start_pause_button.config(state="normal") except FileNotFoundError: - with message_box_animation(self.app.calculating_animation): + with message_box_animation(self.app.animated_icon): MessageDialog(master=self.app, message_type="error", title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show() @@ -692,4 +694,4 @@ class Actions: is_encrypted=is_encrypted, mode=mode, use_trash_bin=use_trash_bin, - no_trash_bin=no_trash_bin) + no_trash_bin=no_trash_bin) \ No newline at end of file diff --git a/pyimage_ui/advanced_settings_frame.py b/pyimage_ui/advanced_settings_frame.py index 543646c..c246cbb 100644 --- a/pyimage_ui/advanced_settings_frame.py +++ b/pyimage_ui/advanced_settings_frame.py @@ -6,8 +6,7 @@ from pathlib import Path from core.pbp_app_config import AppConfig, Msg from shared_libs.animated_icon import AnimatedIcon from pyimage_ui.shared_logic import enforce_backup_type_exclusivity -from shared_libs.message import MessageDialog -from pyimage_ui.password_dialog import PasswordDialog +from shared_libs.message import MessageDialog, PasswordDialog class AdvancedSettingsFrame(ttk.Frame): @@ -248,7 +247,7 @@ class AdvancedSettingsFrame(ttk.Frame): return password_dialog = PasswordDialog( - self, title="Enter Existing Password", confirm=False) + self, title="Enter Existing Password", confirm=False, translations=Msg.STR) password, _ = password_dialog.get_password() if not password: diff --git a/pyimage_ui/password_dialog.py b/pyimage_ui/password_dialog.py deleted file mode 100644 index badb632..0000000 --- a/pyimage_ui/password_dialog.py +++ /dev/null @@ -1,69 +0,0 @@ -import tkinter as tk -from tkinter import ttk, messagebox - - -class PasswordDialog(tk.Toplevel): - def __init__(self, parent, title="Password Required", confirm=True): - super().__init__(parent) - self.title(title) - self.parent = parent - self.password = None - self.save_to_keyring = tk.BooleanVar() - self.confirm = confirm - - self.transient(parent) - self.grab_set() - - ttk.Label(self, text="Please enter the password for the encrypted backup:").pack( - padx=20, pady=10) - self.password_entry = ttk.Entry(self, show="*") - self.password_entry.pack(padx=20, pady=5, fill="x", expand=True) - self.password_entry.focus_set() - - if self.confirm: - ttk.Label(self, text="Confirm password:").pack(padx=20, pady=10) - self.confirm_entry = ttk.Entry(self, show="*") - self.confirm_entry.pack(padx=20, pady=5, fill="x", expand=True) - - self.save_to_keyring_cb = ttk.Checkbutton( - self, text="Save password to system keyring", variable=self.save_to_keyring) - self.save_to_keyring_cb.pack(padx=20, pady=10) - - button_frame = ttk.Frame(self) - button_frame.pack(pady=10) - - ok_button = ttk.Button(button_frame, text="OK", command=self.on_ok) - ok_button.pack(side="left", padx=5) - cancel_button = ttk.Button( - button_frame, text="Cancel", command=self.on_cancel) - cancel_button.pack(side="left", padx=5) - - self.bind("", lambda event: self.on_ok()) - self.bind("", lambda event: self.on_cancel()) - - self.wait_window(self) - - def on_ok(self): - password = self.password_entry.get() - - if not password: - messagebox.showerror( - "Error", "Password cannot be empty.", parent=self) - return - - if self.confirm: - confirm = self.confirm_entry.get() - if password != confirm: - messagebox.showerror( - "Error", "Passwords do not match.", parent=self) - return - - self.password = password - self.destroy() - - def on_cancel(self): - self.password = None - self.destroy() - - def get_password(self): - return self.password, self.save_to_keyring.get() diff --git a/pyimage_ui/settings_frame.py b/pyimage_ui/settings_frame.py index 55302bb..e88e7f3 100644 --- a/pyimage_ui/settings_frame.py +++ b/pyimage_ui/settings_frame.py @@ -1,20 +1,25 @@ import tkinter as tk from tkinter import ttk import os +import shutil +import sys from pathlib import Path from core.pbp_app_config import AppConfig, Msg from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame from shared_libs.custom_file_dialog import CustomFileDialog -from shared_libs.message import MessageDialog +from shared_libs.message import MessageDialog, PasswordDialog class SettingsFrame(ttk.Frame): - def __init__(self, master, navigation, actions, **kwargs): + def __init__(self, master, navigation, actions, encryption_manager, image_manager, config_manager, **kwargs): super().__init__(master, **kwargs) self.navigation = navigation self.actions = actions + self.encryption_manager = encryption_manager + self.image_manager = image_manager + self.config_manager = config_manager self.pbp_app_config = AppConfig() self.user_exclude_patterns = [] @@ -61,6 +66,26 @@ class SettingsFrame(ttk.Frame): self.hidden_tree.bind("", self._toggle_include_status_hidden) self.hidden_tree_frame.pack_forget() # Initially hidden + # --- Hard Reset Frame (initially hidden) --- + self.hard_reset_frame = ttk.LabelFrame( + self, text=Msg.STR["full_delete_config_settings"], padding=10) + + hard_reset_label = ttk.Label( + self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.LEFT) + hard_reset_label.pack(pady=10) + + hard_reset_button_frame = ttk.Frame(self.hard_reset_frame) + hard_reset_button_frame.pack(pady=10) + + delete_now_button = ttk.Button( + hard_reset_button_frame, text=Msg.STR["delete_now"], command=self._perform_hard_reset) + delete_now_button.pack(side=tk.LEFT, padx=5) + + cancel_hard_reset_button = ttk.Button( + hard_reset_button_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view) + cancel_hard_reset_button.pack(side=tk.LEFT, padx=5) + + # --- Action Buttons --- self.button_frame = ttk.Frame(self) self.button_frame.pack(fill=tk.X, padx=10, pady=10) @@ -68,9 +93,9 @@ class SettingsFrame(ttk.Frame): self.show_hidden_button = ttk.Button( self.button_frame, command=self._toggle_hidden_files_view, style="TButton.Borderless.Round") self.show_hidden_button.pack(side=tk.LEFT) - self.unhide_icon = self.master.master.master.image_manager.get_icon( + self.unhide_icon = self.image_manager.get_icon( 'hide') - self.hide_icon = self.master.master.master.image_manager.get_icon( + self.hide_icon = self.image_manager.get_icon( 'unhide') self.show_hidden_button.config(image=self.unhide_icon) @@ -90,14 +115,49 @@ class SettingsFrame(ttk.Frame): self.button_frame, text=Msg.STR["advanced"], command=self._open_advanced_settings) advanced_button.pack(side=tk.LEFT, padx=5) + hard_reset_button = ttk.Button( + self.button_frame, text=Msg.STR["hard_reset"], command=self._toggle_hard_reset_view) + hard_reset_button.pack(side=tk.RIGHT, padx=5) + reset_button = ttk.Button( self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings) reset_button.pack(side=tk.RIGHT) self.hidden_files_visible = False + self.hard_reset_visible = False # To hold the instance of AdvancedSettingsFrame self.advanced_settings_frame_instance = None + def _perform_hard_reset(self): + if self.encryption_manager.mounted_destinations: + dialog = PasswordDialog(self, title=Msg.STR["unlock_backup"], confirm=False, translations=Msg.STR) + password, _ = dialog.get_password() + if not password: + return + + success, message = self.encryption_manager.unmount_all_encrypted_drives(password) + if not success: + MessageDialog(message_type="error", text=message).show() + return + + try: + shutil.rmtree(AppConfig.APP_DIR) + # Restart the application + os.execl(sys.executable, sys.executable, *sys.argv) + except Exception as e: + MessageDialog(message_type="error", text=str(e)).show() + + def _toggle_hard_reset_view(self): + self.hard_reset_visible = not self.hard_reset_visible + if self.hard_reset_visible: + self.trees_container.pack_forget() + self.button_frame.pack_forget() + self.hard_reset_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + else: + self.hard_reset_frame.pack_forget() + self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) + self.button_frame.pack(fill=tk.X, padx=10, pady=10) + def _add_to_exclude_list(self) -> bool: result = MessageDialog("ask", Msg.STR["exclude_dialog_text"], title=Msg.STR["add_to_exclude_list"], buttons=[ Msg.STR["add_folder_button"], Msg.STR["add_file_button"]]).show() @@ -111,7 +171,7 @@ class SettingsFrame(ttk.Frame): dialog.destroy() else: dialog = CustomFileDialog( - self, filetypes=[("All Files", "*.*")], title=Msg.STR["add_to_exclude_list"]) + self, filetypes=[("All Files", "*.*")]) self.wait_window(dialog) path = dialog.get_result() dialog.destroy() @@ -265,7 +325,7 @@ class SettingsFrame(ttk.Frame): if not self.advanced_settings_frame_instance: self.advanced_settings_frame_instance = AdvancedSettingsFrame( self, # Parent is now self (SettingsFrame) - config_manager=self.master.master.master.config_manager, + config_manager=self.config_manager, app_instance=self.master.master.master, show_main_settings_callback=self._show_main_settings ) @@ -333,4 +393,4 @@ class SettingsFrame(ttk.Frame): 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]), tags=(new_tag,)) + new_status, current_values[1], current_values[2]), tags=(new_tag,)) \ No newline at end of file