Files
Py-Backup/pyimage_ui/settings_frame.py
Désiré Werner Menrath 22144859d8 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`.
2025-09-11 01:08:38 +02:00

396 lines
16 KiB
Python

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, PasswordDialog
class SettingsFrame(ttk.Frame):
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 = []
# --- Container for Treeviews ---
self.trees_container = ttk.Frame(self)
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# --- Treeview for file/folder exclusion ---
self.tree_frame = ttk.LabelFrame(
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
self.tree_frame.pack(fill=tk.BOTH, expand=True)
columns = ("included", "name", "path")
self.tree = ttk.Treeview(
self.tree_frame, columns=columns, show="headings")
self.tree.heading("included", text=Msg.STR["in_backup"])
self.tree.heading("name", text=Msg.STR["name"])
self.tree.heading("path", text=Msg.STR["path"])
self.tree.tag_configure("yes", background="#89b4fe")
self.tree.tag_configure("no", background="#4c92ed")
self.tree.column("included", width=100, anchor="center")
self.tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.tree.bind("<Button-1>", self._toggle_include_status)
# --- Treeview for hidden files (initially hidden) ---
self.hidden_tree_frame = ttk.LabelFrame(
self.trees_container, text=Msg.STR["hidden_files_and_folders"], padding=10)
self.hidden_tree = ttk.Treeview(
self.hidden_tree_frame, columns=columns, show="headings")
self.hidden_tree.heading("included", text=Msg.STR["in_backup"])
self.hidden_tree.heading("name", text=Msg.STR["name"])
self.hidden_tree.heading("path", text=Msg.STR["path"])
self.hidden_tree.column("included", width=100, anchor="center")
self.hidden_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.hidden_tree.tag_configure("yes", background="#89b4fe")
self.hidden_tree.tag_configure("no", background="#4c92ed")
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)
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.image_manager.get_icon(
'hide')
self.hide_icon = self.image_manager.get_icon(
'unhide')
self.show_hidden_button.config(image=self.unhide_icon)
add_to_exclude_button = ttk.Button(
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list)
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
apply_button = ttk.Button(
self.button_frame, text=Msg.STR["apply"], command=self._apply_changes)
apply_button.pack(side=tk.LEFT, padx=5)
cancel_button = ttk.Button(self.button_frame, text=Msg.STR["cancel"],
command=lambda: self.navigation.toggle_mode("backup", 0))
cancel_button.pack(side=tk.LEFT, padx=5)
advanced_button = ttk.Button(
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()
path = None
if result:
dialog = CustomFileDialog(
self, mode="dir", title=Msg.STR["add_to_exclude_list"])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
else:
dialog = CustomFileDialog(
self, filetypes=[("All Files", "*.*")])
self.wait_window(dialog)
path = dialog.get_result()
dialog.destroy()
if path:
try:
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
lines = {line.strip() for line in f if line.strip()}
except FileNotFoundError:
lines = set()
new_entry = f"{path}/*" if os.path.isdir(path) else path
lines.add(new_entry)
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'w') as f:
for line in sorted(list(lines)):
f.write(f"{line}\n")
self.load_and_display_excludes()
if hasattr(self, 'advanced_settings_frame_instance') and self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance._load_manual_excludes()
def show(self):
self.grid(row=2, column=0, sticky="nsew")
self.load_and_display_excludes()
def hide(self):
self.grid_remove()
def _load_exclude_patterns(self):
all_patterns = []
if AppConfig.GENERATED_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.GENERATED_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
if AppConfig.MANUAL_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.MANUAL_EXCLUDE_LIST_PATH, 'r') as f:
all_patterns.extend(
[line.strip() for line in f if line.strip() and not line.startswith('#')])
return all_patterns
def load_and_display_excludes(self):
# Clear existing items
for i in self.tree.get_children():
self.tree.delete(i)
exclude_patterns = self._load_exclude_patterns()
home_dir = Path.home()
items_to_display = []
for item in home_dir.iterdir():
if not item.name.startswith('.'):
item_path_str = str(item.absolute())
is_excluded = f"{item_path_str}/*" in exclude_patterns or item_path_str in exclude_patterns
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
items_to_display.append(
(included_text, item.name, item_path_str))
# Sort by "Im Backup" status
items_to_display.sort(key=lambda x: x[0], reverse=True)
for item in items_to_display:
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status(self, event):
item_id = self.tree.identify_row(event.y)
if not item_id:
return
current_values = self.tree.item(item_id, 'values')
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
new_tag = "yes" if new_status == Msg.STR["yes"] else "no"
self.tree.item(item_id, values=(
new_status, current_values[1], current_values[2]), tags=(new_tag,))
def _apply_changes(self):
# Get all paths displayed in the treeviews
tree_paths = set()
for item_id in self.tree.get_children():
values = self.tree.item(item_id, 'values')
tree_paths.add(values[2])
if self.hidden_files_visible:
for item_id in self.hidden_tree.get_children():
values = self.hidden_tree.item(item_id, 'values')
tree_paths.add(values[2])
# Get the new excludes from the treeviews
new_excludes = []
for item_id in self.tree.get_children():
values = self.tree.item(item_id, 'values')
if values[0] == Msg.STR["no"]:
path = values[2]
if os.path.isdir(path):
new_excludes.append(f"{path}/*")
else:
new_excludes.append(path)
if self.hidden_files_visible:
for item_id in self.hidden_tree.get_children():
values = self.hidden_tree.item(item_id, 'values')
if values[0] == Msg.STR["no"]:
path = values[2]
if os.path.isdir(path):
new_excludes.append(f"{path}/*")
else:
new_excludes.append(path)
# Load existing user patterns
existing_user_patterns = []
if AppConfig.USER_EXCLUDE_LIST_PATH.exists():
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'r') as f:
existing_user_patterns = [
line.strip() for line in f if line.strip() and not line.startswith('#')]
# Preserve patterns that are not managed by this view
preserved_patterns = []
for pattern in existing_user_patterns:
clean_pattern = pattern.replace('/*', '')
if clean_pattern not in tree_paths:
preserved_patterns.append(pattern)
# Combine preserved patterns with new excludes from this view
final_excludes = list(set(preserved_patterns + new_excludes))
with open(AppConfig.USER_EXCLUDE_LIST_PATH, 'w') as f:
for path in final_excludes:
f.write(f"{path}\n")
# Reload the excludes to show the changes
self.load_and_display_excludes()
if self.hidden_files_visible:
self._load_hidden_files()
def _open_advanced_settings(self):
# Hide main settings UI elements
self.trees_container.pack_forget() # Hide the container for treeviews
self.button_frame.pack_forget()
# Create AdvancedSettingsFrame if not already created
if not self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance = AdvancedSettingsFrame(
self, # Parent is now self (SettingsFrame)
config_manager=self.config_manager,
app_instance=self.master.master.master,
show_main_settings_callback=self._show_main_settings
)
# Pack the AdvancedSettingsFrame
self.advanced_settings_frame_instance.pack(fill=tk.BOTH, expand=True)
def _show_main_settings(self):
# Hide advanced settings frame
if self.advanced_settings_frame_instance:
self.advanced_settings_frame_instance.pack_forget()
# Show main settings UI elements
# Re-pack the container for treeviews
self.trees_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Re-pack the button frame
self.button_frame.pack(fill=tk.X, padx=10, pady=10)
def _toggle_hidden_files_view(self):
self.hidden_files_visible = not self.hidden_files_visible
if self.hidden_files_visible:
self.tree_frame.pack_forget()
self.hidden_tree_frame.pack(fill=tk.BOTH, expand=True)
self._load_hidden_files()
self.show_hidden_button.config(image=self.hide_icon)
else:
self.hidden_tree_frame.pack_forget()
self.tree_frame.pack(fill=tk.BOTH, expand=True)
self.show_hidden_button.config(image=self.unhide_icon)
def _load_hidden_files(self):
# Clear existing items
for i in self.hidden_tree.get_children():
self.hidden_tree.delete(i)
exclude_patterns = self._load_exclude_patterns()
home_dir = Path.home()
items_to_display = []
for item in home_dir.iterdir():
if item.name.startswith('.'):
item_path_str = str(item.absolute())
is_excluded = f"{item_path_str}/*" in exclude_patterns or f"{item_path_str}" in exclude_patterns
included_text = Msg.STR["no"] if is_excluded else Msg.STR["yes"]
items_to_display.append(
(included_text, item.name, item_path_str))
# Sort by name
items_to_display.sort(key=lambda x: x[1])
for item in items_to_display:
tag = "yes" if item[0] == Msg.STR["yes"] else "no"
self.hidden_tree.insert("", "end", values=item, tags=(tag,))
def _toggle_include_status_hidden(self, event):
item_id = self.hidden_tree.identify_row(event.y)
if not item_id:
return
current_values = self.hidden_tree.item(item_id, 'values')
current_status = current_values[0]
new_status = Msg.STR["yes"] if current_status == Msg.STR["no"] else Msg.STR["no"]
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,))