refactor(ui): Improve user feedback and hard reset logic

This commit introduces several improvements to the user interface and application logic:

- **UI Feedback:**
  - Replaces the subtitle text change with a large, centered, temporary message for better visibility of feedback like "Settings saved" or "Hard reset successful".
  - Adds several new message strings to `pbp_app_config.py` for consistent UI text.

- **Hard Reset:**
  - Fixes a bug where encrypted drives were not unmounted during a hard reset.
  - The `_perform_hard_reset` function now correctly uses the existing `encryption_manager` instance to unmount all drives before deleting the configuration and restarting the application.
  - The application now provides clear feedback during and after the hard reset process.

- **Settings:**
  - Improves the "Advanced Settings" and "Settings" frames by adding success messages and preventing errors on empty selections.
  - The main application closing sequence (`on_closing`) is also made more robust, mirroring the new hard reset logic for unmounting drives.
This commit is contained in:
2025-09-14 21:44:46 +02:00
parent 6504758b7b
commit 34234d2d14
6 changed files with 81 additions and 50 deletions

View File

@@ -27,6 +27,7 @@ class EncryptionManager:
self.lock_file = AppConfig.LOCK_FILE_PATH
def _write_lock_file(self, data):
with open(self.lock_file, 'w') as f:
json.dump(data, f)

View File

@@ -377,6 +377,8 @@ class Msg:
"key_file_not_created": _("Key file not created."), # New
"backup_options": _("Backup Options"), # New
"hard_reset": _("Hard reset"),
"hard_reset_success": _("Hard reset successful.\nRestarting..."),
"hard_reset_info_manual_restart": _("The application must be restarted as drives are still mounted."),
"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"),
@@ -387,5 +389,7 @@ class Msg:
"password_empty_error": _("Password cannot be empty."),
"passwords_do_not_match_error": _("Passwords do not match."),
"ok": _("OK"),
"settings_saved": _("Settings saved successfully!"),
"delete_success": _("Deletion successful!"),
"unlock_backup": _("Unlock Backup"),
}

View File

@@ -54,16 +54,20 @@ class MainApplication(tk.Tk):
self.style.configure("Green.Sidebar.TButton", foreground="green")
# Custom button styles for BackupContentFrame
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
self.style.configure(
"Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure(
"Danger.TButton", foreground="#C62828") # Darker Red
# Custom LabelFrame style for Mount frame
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
# Custom button styles for BackupContentFrame
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
self.style.configure(
"Success.TButton", foreground="#2E7D32") # Darker Green
self.style.configure(
"Danger.TButton", foreground="#C62828") # Darker Red
self.style.configure("Switch2.TCheckbutton",
background="#2b3e4f", foreground="white")
@@ -435,7 +439,7 @@ class MainApplication(tk.Tk):
restore_dest_folder)
self._process_queue()
self._update_sync_mode_display() # Call after loading state
self.update_backup_options_from_config() # Apply defaults on startup
self.update_backup_options_from_config() # Apply defaults on startup
def _setup_log_window(self):
self.log_frame = ttk.Frame(self.content_frame)
@@ -552,13 +556,10 @@ class MainApplication(tk.Tk):
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
def on_closing(self):
self.config_manager.set_setting(
"refresh_log", self.refresh_log_var.get())
# Attempt to unmount all encrypted drives
# First, always attempt to unmount all encrypted drives
unmount_success = self.backup_manager.encryption_manager.unmount_all()
# Check if any drives are still mounted after the attempt
# If unmounting fails, show an error and prevent the app from closing
if not unmount_success and self.backup_manager.encryption_manager.mounted_destinations:
app_logger.log(
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
@@ -566,6 +567,8 @@ class MainApplication(tk.Tk):
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
return # Prevent application from closing
self.config_manager.set_setting(
"refresh_log", self.refresh_log_var.get())
self.config_manager.set_setting("last_mode", self.mode)
if self.backup_left_canvas_data.get('path_display'):
@@ -592,6 +595,7 @@ class MainApplication(tk.Tk):
else:
self.config_manager.set_setting("restore_source_path", None)
# Finally, clean up UI resources and destroy the window
if self.left_canvas_animation:
self.left_canvas_animation.stop()
self.left_canvas_animation.destroy()

View File

@@ -357,8 +357,11 @@ class AdvancedSettingsFrame(ttk.Frame):
def _delete_manual_exclude(self):
selected_indices = self.manual_excludes_listbox.curselection()
if not selected_indices:
return
for i in reversed(selected_indices):
self.manual_excludes_listbox.delete(i)
self.app_instance.header_frame.show_temporary_message(Msg.STR["delete_success"])
self._apply_changes()
def _reset_animation_settings(self):
@@ -550,13 +553,7 @@ class AdvancedSettingsFrame(ttk.Frame):
f.write(f"{item}\n")
# Show temporary success message in header
self.app_instance.header_frame.show_temporary_message("Settings saved successfully!")
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.header_frame.show_temporary_message(Msg.STR["settings_saved"])
def _cancel_changes(self):
self.pack_forget()

View File

@@ -72,18 +72,32 @@ class HeaderFrame(tk.Frame):
self.refresh_status()
def show_temporary_message(self, message: str, duration_ms: int = 4000):
"""Displays a temporary message in the subtitle area."""
# Cancel any previous after job to prevent conflicts
if self._temp_message_after_id:
self.after_cancel(self._temp_message_after_id)
def show_temporary_message(self, message: str, duration_ms: int = 3000):
"""Displays a large, centered, temporary message over the header."""
# If a message is already shown, cancel its removal and destroy it
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
if self._temp_message_after_id:
self.after_cancel(self._temp_message_after_id)
self._temp_message_label.destroy()
self.subtitle_label.config(text=message, fg="#2ECC71") # Green color for success
# Create a new label for the message
self._temp_message_label = tk.Label(
self, # Place it directly in the HeaderFrame
text=message,
font=("Helvetica", 16, "bold"),
fg="#2ECC71", # Green color for success
bg="#455A64" # Same background as header
)
# Place it in the center of the header frame
self._temp_message_label.place(relx=0.5, rely=0.5, anchor="center")
# Schedule its destruction
self._temp_message_after_id = self.after(duration_ms, self._restore_subtitle)
def _restore_subtitle(self):
"""Restores the original subtitle text and color."""
self.subtitle_label.config(text=self.original_subtitle_text, fg="#bdc3c7")
"""Destroys the temporary message label."""
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
self._temp_message_label.destroy()
self._temp_message_after_id = None
def refresh_status(self):

View File

@@ -6,9 +6,11 @@ import sys
from pathlib import Path
from core.pbp_app_config import AppConfig, Msg
from core.backup_manager import BackupManager
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
from shared_libs.custom_file_dialog import CustomFileDialog
from shared_libs.message import MessageDialog, PasswordDialog
from shared_libs.logger import app_logger
class SettingsFrame(ttk.Frame):
@@ -79,10 +81,6 @@ class SettingsFrame(ttk.Frame):
self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes)
apply_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(self.bottom_button_frame, text=Msg.STR["cancel"],
command=lambda: self.navigation.toggle_mode("backup", 0))
cancel_button.pack(side=tk.LEFT, padx=5)
# --- Treeview for file/folder exclusion ---
self.tree_frame = ttk.LabelFrame(
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
@@ -132,13 +130,13 @@ class SettingsFrame(ttk.Frame):
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
hard_reset_button_frame.pack(pady=10)
delete_now_button = ttk.Button(
self.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)
self.delete_now_button.pack(side=tk.LEFT, padx=5)
cancel_hard_reset_button = ttk.Button(
self.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)
self.cancel_hard_reset_button.pack(side=tk.LEFT, padx=5)
self.hidden_files_visible = False
self.hard_reset_visible = False
@@ -146,25 +144,34 @@ class SettingsFrame(ttk.Frame):
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)
# First, always attempt to unmount all encrypted drives
unmount_success = self.encryption_manager.unmount_all()
# If unmounting fails, show an error and prevent the app from closing
if not unmount_success and self.encryption_manager.mounted_destinations:
app_logger.log(
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
MessageDialog(
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
return # Prevent application from closing
# Try to delete the config directory without elevated rights
if AppConfig.APP_DIR.exists():
shutil.rmtree(AppConfig.APP_DIR)
app_instance = self.master.master.master
header = app_instance.header_frame
header.show_temporary_message(Msg.STR["hard_reset_success"])
AppConfig.ensure_directories()
self.after(2000, lambda: os.execl(
sys.executable, sys.executable, *sys.argv))
except Exception as e:
MessageDialog(message_type="error", text=str(e)).show()
MessageDialog(message_type="error",
text=f"Hard reset failed: {e}").show()
def _toggle_hard_reset_view(self):
self.hard_reset_visible = not self.hard_reset_visible
@@ -338,6 +345,10 @@ class SettingsFrame(ttk.Frame):
if self.hidden_files_visible:
self._load_hidden_files()
# Show success message and stay on the settings page
self.master.master.master.header_frame.show_temporary_message(
Msg.STR["settings_saved"])
def _open_advanced_settings(self):
# Hide main settings UI elements
self.trees_container.pack_forget() # Hide the container for treeviews