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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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,))
|
||||
Reference in New Issue
Block a user