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