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`.
This commit is contained in:
2025-09-11 01:08:38 +02:00
parent 8a70fe2320
commit 22144859d8
8 changed files with 103 additions and 94 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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"),
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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:

View File

@@ -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("<Return>", lambda event: self.on_ok())
self.bind("<Escape>", 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()

View File

@@ -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("<Button-1>", 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,))