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`.
396 lines
16 KiB
Python
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,)) |