Compare commits
6 Commits
7decaf46ef
...
3a59bccddc
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a59bccddc | |||
| 9406d3f0e2 | |||
| 34234d2d14 | |||
| 6504758b7b | |||
| ff640ca9ef | |||
| 7d64544c37 |
@@ -88,7 +88,11 @@ class BackupManager:
|
|||||||
|
|
||||||
if is_system:
|
if is_system:
|
||||||
return os.path.join(base_data_dir, "system")
|
return os.path.join(base_data_dir, "system")
|
||||||
|
# For user backups, store them directly in the mount point if encrypted.
|
||||||
|
elif is_encrypted:
|
||||||
|
return base_data_dir
|
||||||
else:
|
else:
|
||||||
|
# Keep old structure for unencrypted user backups
|
||||||
return os.path.join(base_data_dir, "user", source_name)
|
return os.path.join(base_data_dir, "user", source_name)
|
||||||
|
|
||||||
def check_for_full_backup(self, dest_path: str, source_name: str, is_encrypted: bool) -> bool:
|
def check_for_full_backup(self, dest_path: str, source_name: str, is_encrypted: bool) -> bool:
|
||||||
@@ -201,7 +205,9 @@ class BackupManager:
|
|||||||
full_system_cmd = f"mkdir -p '{rsync_dest}' && {rsync_cmd_str}"
|
full_system_cmd = f"mkdir -p '{rsync_dest}' && {rsync_cmd_str}"
|
||||||
command = ['pkexec', 'bash', '-c', full_system_cmd]
|
command = ['pkexec', 'bash', '-c', full_system_cmd]
|
||||||
else:
|
else:
|
||||||
rsync_command_parts.extend([source_path, rsync_dest])
|
# Add a trailing slash to the source path to ensure rsync copies the content, not the directory itself.
|
||||||
|
source_with_slash = source_path.rstrip('/') + '/'
|
||||||
|
rsync_command_parts.extend([source_with_slash, rsync_dest])
|
||||||
os.makedirs(rsync_dest, exist_ok=True)
|
os.makedirs(rsync_dest, exist_ok=True)
|
||||||
command = rsync_command_parts
|
command = rsync_command_parts
|
||||||
|
|
||||||
@@ -273,7 +279,7 @@ class BackupManager:
|
|||||||
if is_system:
|
if is_system:
|
||||||
command.extend(['pkexec', 'rsync', '-aAXHvn', '--stats'])
|
command.extend(['pkexec', 'rsync', '-aAXHvn', '--stats'])
|
||||||
else:
|
else:
|
||||||
command.extend(['rsync', '-avn', '--stats'])
|
command.extend(['rsync', '-ain', '--dry-run', '--stats'])
|
||||||
|
|
||||||
command.append(f"--link-dest={latest_backup_path}")
|
command.append(f"--link-dest={latest_backup_path}")
|
||||||
|
|
||||||
@@ -283,6 +289,9 @@ class BackupManager:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as dummy_dest:
|
with tempfile.TemporaryDirectory() as dummy_dest:
|
||||||
|
# For user backups, add a trailing slash to copy contents, not the dir itself.
|
||||||
|
if not is_system:
|
||||||
|
source_path = source_path.rstrip('/') + '/'
|
||||||
command.extend([source_path, dummy_dest])
|
command.extend([source_path, dummy_dest])
|
||||||
|
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
|
|||||||
@@ -141,11 +141,23 @@ do_resize() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Resizing LUKS volume."
|
log "Checking filesystem before resize."
|
||||||
cryptsetup resize "$mapper_name"
|
|
||||||
|
|
||||||
log "Checking and resizing filesystem."
|
|
||||||
e2fsck -fy "/dev/mapper/$mapper_name"
|
e2fsck -fy "/dev/mapper/$mapper_name"
|
||||||
|
|
||||||
|
log "Resizing LUKS volume and filesystem."
|
||||||
|
# Re-authenticate for resize, as some cryptsetup versions require it.
|
||||||
|
if [ -n "$LUKSPASS" ]; then
|
||||||
|
log "Resizing with password."
|
||||||
|
echo -n "$LUKSPASS" | cryptsetup resize "$mapper_name" -
|
||||||
|
elif [ -n "$key_file_arg" ]; then
|
||||||
|
log "Resizing with keyfile."
|
||||||
|
cryptsetup resize "$mapper_name" --key-file "$key_file_arg"
|
||||||
|
else
|
||||||
|
# This should not happen if luksOpen succeeded.
|
||||||
|
# Fallback to the previous non-interactive attempt.
|
||||||
|
log "No password or keyfile for resize, trying non-interactively."
|
||||||
|
cryptsetup resize "$mapper_name" < /dev/null
|
||||||
|
fi
|
||||||
resize2fs "/dev/mapper/$mapper_name"
|
resize2fs "/dev/mapper/$mapper_name"
|
||||||
|
|
||||||
# 4. Mount it again
|
# 4. Mount it again
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class EncryptionManager:
|
|||||||
self.lock_file = AppConfig.LOCK_FILE_PATH
|
self.lock_file = AppConfig.LOCK_FILE_PATH
|
||||||
|
|
||||||
def _write_lock_file(self, data):
|
def _write_lock_file(self, data):
|
||||||
|
|
||||||
with open(self.lock_file, 'w') as f:
|
with open(self.lock_file, 'w') as f:
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
|
|
||||||
@@ -93,33 +94,62 @@ class EncryptionManager:
|
|||||||
return os.path.join(pybackup_dir, container_filename)
|
return os.path.join(pybackup_dir, container_filename)
|
||||||
|
|
||||||
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
|
def get_key_file_path(self, base_dest_path: str, profile_name: str) -> str:
|
||||||
pybackup_dir = os.path.join(base_dest_path, "pybackup")
|
# base_dest_path is ignored for security. Keyfiles are stored centrally.
|
||||||
|
secure_key_dir = "/usr/local/etc/luks"
|
||||||
key_filename = f"luks_{profile_name}.keyfile"
|
key_filename = f"luks_{profile_name}.keyfile"
|
||||||
return os.path.join(pybackup_dir, key_filename)
|
return os.path.join(secure_key_dir, key_filename)
|
||||||
|
|
||||||
def create_and_add_key_file(self, base_dest_path: str, profile_name: str, password: str) -> Optional[str]:
|
def create_and_add_key_file(self, base_dest_path: str, profile_name: str, password: str) -> Optional[str]:
|
||||||
|
# Get the new secure path for the keyfile
|
||||||
|
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||||
|
secure_key_dir = os.path.dirname(key_file_path)
|
||||||
|
|
||||||
|
# Container path remains the same, on the destination drive
|
||||||
|
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||||
|
|
||||||
|
# This script will be executed as root. It creates the secure directory,
|
||||||
|
# generates the keyfile, sets its permissions, and adds it to the LUKS container.
|
||||||
|
# Using shlex.quote to be safe with paths
|
||||||
|
script = f"""
|
||||||
|
mkdir -p {shlex.quote(secure_key_dir)} && \
|
||||||
|
dd if=/dev/urandom of={shlex.quote(key_file_path)} bs=1 count=4096 && \
|
||||||
|
chmod 0400 {shlex.quote(key_file_path)} && \
|
||||||
|
cryptsetup luksAddKey {shlex.quote(container_path)} {shlex.quote(key_file_path)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._execute_as_root(script, password):
|
||||||
|
self.logger.log(
|
||||||
|
f"Successfully created keyfile at {key_file_path} and added to LUKS container.")
|
||||||
|
return key_file_path
|
||||||
|
else:
|
||||||
|
self.logger.log(
|
||||||
|
"Failed to create and add key file. The script might have failed midway. Attempting cleanup.")
|
||||||
|
# Attempt to clean up the keyfile if the script failed, as we can't be sure at which stage it failed.
|
||||||
|
cleanup_script = f"rm -f {shlex.quote(key_file_path)}"
|
||||||
|
self._execute_as_root(cleanup_script)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_key_file(self, base_dest_path: str, profile_name: str, password: str) -> bool:
|
||||||
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
key_file_path = self.get_key_file_path(base_dest_path, profile_name)
|
||||||
container_path = self.get_container_path(base_dest_path, profile_name)
|
container_path = self.get_container_path(base_dest_path, profile_name)
|
||||||
try:
|
|
||||||
with open(key_file_path, 'wb') as f:
|
|
||||||
f.write(os.urandom(4096))
|
|
||||||
os.chmod(key_file_path, 0o400)
|
|
||||||
self.logger.log(f"Generated new key file at {key_file_path}")
|
|
||||||
|
|
||||||
script = f'cryptsetup luksAddKey "{container_path}" "{key_file_path}"'
|
if not os.path.exists(key_file_path):
|
||||||
if self._execute_as_root(script, password):
|
self.logger.log(f"Keyfile for profile {profile_name} does not exist. Nothing to delete.")
|
||||||
self.logger.log(
|
return True # Considered a success as the desired state is "deleted"
|
||||||
"Successfully added key file to LUKS container.")
|
|
||||||
return key_file_path
|
# This script removes the key from the LUKS container and then deletes the keyfile.
|
||||||
else:
|
# It requires a valid password for the container to authorize the key removal.
|
||||||
self.logger.log("Failed to add key file to LUKS container.")
|
script = f"""
|
||||||
os.remove(key_file_path)
|
cryptsetup luksRemoveKey {shlex.quote(container_path)} {shlex.quote(key_file_path)} && \
|
||||||
return None
|
rm -f {shlex.quote(key_file_path)}
|
||||||
except Exception as e:
|
"""
|
||||||
self.logger.log(f"Error creating key file: {e}")
|
|
||||||
if os.path.exists(key_file_path):
|
if self._execute_as_root(script, password):
|
||||||
os.remove(key_file_path)
|
self.logger.log(f"Successfully removed keyfile for profile {profile_name}.")
|
||||||
return None
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.log(f"Failed to remove keyfile for profile {profile_name}.")
|
||||||
|
return False
|
||||||
|
|
||||||
def _get_password_and_keyfile(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[Optional[str], Optional[str]]:
|
def _get_password_and_keyfile(self, base_dest_path: str, profile_name: str, username: str) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""Gets the password (from cache, keyring, or user) or the keyfile path."""
|
"""Gets the password (from cache, keyring, or user) or the keyfile path."""
|
||||||
@@ -177,9 +207,11 @@ class EncryptionManager:
|
|||||||
free_space = shutil.disk_usage(mount_point).free
|
free_space = shutil.disk_usage(mount_point).free
|
||||||
required_space = int(source_size * 1.10)
|
required_space = int(source_size * 1.10)
|
||||||
|
|
||||||
if required_space > free_space:
|
BUFFER = 4 * 1024 * 1024 * 1024 # 4 GB
|
||||||
|
# Trigger resize if projected free space is less than the buffer
|
||||||
|
if free_space - required_space < BUFFER:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
f"Resize needed for {profile_name}. Free: {free_space}, Required: {required_space}")
|
f"Resize needed for {profile_name}. Free: {free_space}, Required: {required_space}, Buffer: {BUFFER}")
|
||||||
queue.put(
|
queue.put(
|
||||||
('status_update', f"Container für {profile_name} zu klein. Vergrößere..."))
|
('status_update', f"Container für {profile_name} zu klein. Vergrößere..."))
|
||||||
|
|
||||||
@@ -192,8 +224,11 @@ class EncryptionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
current_total = shutil.disk_usage(mount_point).total
|
current_total = shutil.disk_usage(mount_point).total
|
||||||
needed_additional = required_space - free_space - \
|
|
||||||
(4 * 1024 * 1024 * 1024) # (4 * 1024 * 1024 * 1024) = 4 GB
|
# How much space we need to add to meet the future demand and have the buffer left over.
|
||||||
|
# This is the deficit (required_space - free_space) plus the desired buffer.
|
||||||
|
needed_additional = (required_space - free_space) + BUFFER
|
||||||
|
|
||||||
new_total_size = current_total + needed_additional
|
new_total_size = current_total + needed_additional
|
||||||
new_total_size = math.ceil(new_total_size / 4096) * 4096
|
new_total_size = math.ceil(new_total_size / 4096) * 4096
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ class Msg:
|
|||||||
"ready_for_first_backup": _("Everything is ready for your first backup."),
|
"ready_for_first_backup": _("Everything is ready for your first backup."),
|
||||||
"backup_mode": _("Backup Mode"),
|
"backup_mode": _("Backup Mode"),
|
||||||
"backup_mode_info": _("Backup Mode: You can start a backup here."),
|
"backup_mode_info": _("Backup Mode: You can start a backup here."),
|
||||||
|
"encrypted_unmounted_warning": _("Accurate detection is only possible when the drive is mounted."),
|
||||||
"restore_mode_info": _("Restore Mode: You can start a restore here."),
|
"restore_mode_info": _("Restore Mode: You can start a restore here."),
|
||||||
"advanced_settings_title": _("Advanced Settings"),
|
"advanced_settings_title": _("Advanced Settings"),
|
||||||
"animation_settings_title": _("Animation Settings"),
|
"animation_settings_title": _("Animation Settings"),
|
||||||
@@ -256,7 +257,7 @@ class Msg:
|
|||||||
"log": _("Log"),
|
"log": _("Log"),
|
||||||
"full_backup": _("Full backup"),
|
"full_backup": _("Full backup"),
|
||||||
"incremental": _("Incremental"),
|
"incremental": _("Incremental"),
|
||||||
"incremental_backup": _("Incremental backup"), # New
|
"incremental_backup": _("Incremental backup"),
|
||||||
"test_run": _("Test run"),
|
"test_run": _("Test run"),
|
||||||
"start": _("Start"),
|
"start": _("Start"),
|
||||||
"cancel_backup": _("Cancel"),
|
"cancel_backup": _("Cancel"),
|
||||||
@@ -279,8 +280,8 @@ class Msg:
|
|||||||
"system_excludes": _("System Excludes"),
|
"system_excludes": _("System Excludes"),
|
||||||
"manual_excludes": _("Manual Excludes"),
|
"manual_excludes": _("Manual Excludes"),
|
||||||
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
|
"manual_excludes_info": _("Here, manually add files or folders to be excluded from the backup. Each entry should be on a new line."),
|
||||||
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
|
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
|
||||||
f"with the following content (replace 'user' with the correct username):\n"
|
f"with the following content (replace 'user' with the correct username):\n"
|
||||||
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
|
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
|
||||||
|
|
||||||
# Menus
|
# Menus
|
||||||
@@ -352,9 +353,9 @@ class Msg:
|
|||||||
"header_subtitle": _("Simple GUI for rsync"),
|
"header_subtitle": _("Simple GUI for rsync"),
|
||||||
"encrypted_backup_content": _("Encrypted Backups"),
|
"encrypted_backup_content": _("Encrypted Backups"),
|
||||||
"compressed": _("Compressed"),
|
"compressed": _("Compressed"),
|
||||||
"compression": _("Compression"), # New
|
"compression": _("Compression"),
|
||||||
"encrypted": _("Encrypted"),
|
"encrypted": _("Encrypted"),
|
||||||
"encryption": _("Encryption"), # New
|
"encryption": _("Encryption"),
|
||||||
"bypass_security": _("Bypass security"),
|
"bypass_security": _("Bypass security"),
|
||||||
"refresh_log": _("Refresh log"),
|
"refresh_log": _("Refresh log"),
|
||||||
"comment": _("Kommentar"),
|
"comment": _("Kommentar"),
|
||||||
@@ -369,15 +370,38 @@ class Msg:
|
|||||||
"sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."),
|
"sync_mode_trash_bin": _("Sync Mode: Archive. Files deleted from the source will be moved to a trash folder in the backup."),
|
||||||
"sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."),
|
"sync_mode_no_delete": _("Sync Mode: Additive. Files deleted from the source will be kept in the backup."),
|
||||||
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
|
"encryption_note_system_backup": _("Note: For system backups, encryption only applies to files directly within the /home directory. Folders are not automatically encrypted unless explicitly included in the backup."),
|
||||||
"keyfile_settings": _("Keyfile Settings"), # New
|
|
||||||
"backup_defaults_title": _("Backup Defaults"), # New
|
# Advanced Settings - Keyfile
|
||||||
"automation_settings_title": _("Automation Settings"), # New
|
"keyfile_settings": _("Keyfile Settings"),
|
||||||
"create_add_key_file": _("Create/Add Key File"), # New
|
"backup_defaults_title": _("Backup Defaults"),
|
||||||
"key_file_not_created": _("Key file not created."), # New
|
"automation_settings_title": _("Automation Settings"),
|
||||||
"backup_options": _("Backup Options"), # New
|
"create_add_key_file": _("Create/Add Key File"),
|
||||||
"hard_reset": _("Hard reset"),
|
"key_file_not_created": _("Key file not created."),
|
||||||
|
"keyfile_automation_info": _("A keyfile provides an alternative way to unlock an encrypted backup without entering a password, which is useful for automation.\n\n**Prerequisite:** You must first create at least one encrypted backup for a profile. This process will then add a keyfile to that existing encrypted container."),
|
||||||
|
"err_no_encrypted_container": _("No encrypted container found at the destination. Please create an encrypted backup for this profile first."),
|
||||||
|
"enter_existing_password_title": _("Enter Existing Password"),
|
||||||
|
"keyfile_creation_success": _("Keyfile created successfully!"),
|
||||||
|
"keyfile_creation_failed": _("Failed to create keyfile. See log for details."),
|
||||||
|
"keyfile_status_unknown": _("Keyfile status unknown (no destination set)."),
|
||||||
|
"keyfile_exists": _("Keyfile exists: {key_file_path}"),
|
||||||
|
"keyfile_not_found_for_dest": _("Keyfile has not been created for this destination."),
|
||||||
|
"select_profile_first": _("Please select at least one profile from the list."),
|
||||||
|
"current_destination": _("Current Target Destination"),
|
||||||
|
"no_destination_selected": _("None. Please select a destination in the main backup view."),
|
||||||
|
"sudoers_info_text": _(f"To run automated backups, an administrator must create a file in /etc/sudoers.d/\n"
|
||||||
|
f"with the following content (replace 'user' with the correct username):\n"
|
||||||
|
f"user ALL=(ALL) NOPASSWD: /path/to/pybackup-cli.py"),
|
||||||
|
|
||||||
|
# General Settings
|
||||||
|
"backup_options": _("Backup Options"),
|
||||||
|
"reset": _("Reset"),
|
||||||
|
"hard_reset_success": _("Hard reset successful.\nRestarting..."),
|
||||||
|
"default_reset_success": _("Default reset successful"),
|
||||||
|
"default_reset_failed": _("Default reset failed\nPlease click Delete now on Full_delete_config_settings..."),
|
||||||
|
"default_reset_info": _("Click to restore all settings to their original defaults. Your existing backup schedules and any manually added paths in the exclusion list will be preserved."),
|
||||||
"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."),
|
"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"),
|
"delete_now": _("Delete now"),
|
||||||
|
"default_config_settings": _("Default config settings"),
|
||||||
"full_delete_config_settings": _("Full delete config settings"),
|
"full_delete_config_settings": _("Full delete config settings"),
|
||||||
"password_required": _("Password Required"),
|
"password_required": _("Password Required"),
|
||||||
"enter_password_prompt": _("Please enter the password for the encrypted backup:"),
|
"enter_password_prompt": _("Please enter the password for the encrypted backup:"),
|
||||||
@@ -386,5 +410,7 @@ class Msg:
|
|||||||
"password_empty_error": _("Password cannot be empty."),
|
"password_empty_error": _("Password cannot be empty."),
|
||||||
"passwords_do_not_match_error": _("Passwords do not match."),
|
"passwords_do_not_match_error": _("Passwords do not match."),
|
||||||
"ok": _("OK"),
|
"ok": _("OK"),
|
||||||
|
"settings_saved": _("Settings saved successfully!"),
|
||||||
|
"delete_success": _("Deletion successful!"),
|
||||||
"unlock_backup": _("Unlock Backup"),
|
"unlock_backup": _("Unlock Backup"),
|
||||||
}
|
}
|
||||||
|
|||||||
28
main_app.py
28
main_app.py
@@ -54,16 +54,20 @@ class MainApplication(tk.Tk):
|
|||||||
self.style.configure("Green.Sidebar.TButton", foreground="green")
|
self.style.configure("Green.Sidebar.TButton", foreground="green")
|
||||||
|
|
||||||
# Custom button styles for BackupContentFrame
|
# Custom button styles for BackupContentFrame
|
||||||
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
|
self.style.configure(
|
||||||
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
|
"Success.TButton", foreground="#2E7D32") # Darker Green
|
||||||
|
self.style.configure(
|
||||||
|
"Danger.TButton", foreground="#C62828") # Darker Red
|
||||||
|
|
||||||
# Custom LabelFrame style for Mount frame
|
# Custom LabelFrame style for Mount frame
|
||||||
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
|
self.style.configure("Mount.TLabelFrame", bordercolor="#0078D7")
|
||||||
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
|
self.style.configure("Mount.TLabelFrame.Label", foreground="#0078D7")
|
||||||
|
|
||||||
# Custom button styles for BackupContentFrame
|
# Custom button styles for BackupContentFrame
|
||||||
self.style.configure("Success.TButton", foreground="#2E7D32") # Darker Green
|
self.style.configure(
|
||||||
self.style.configure("Danger.TButton", foreground="#C62828") # Darker Red
|
"Success.TButton", foreground="#2E7D32") # Darker Green
|
||||||
|
self.style.configure(
|
||||||
|
"Danger.TButton", foreground="#C62828") # Darker Red
|
||||||
|
|
||||||
self.style.configure("Switch2.TCheckbutton",
|
self.style.configure("Switch2.TCheckbutton",
|
||||||
background="#2b3e4f", foreground="white")
|
background="#2b3e4f", foreground="white")
|
||||||
@@ -435,7 +439,7 @@ class MainApplication(tk.Tk):
|
|||||||
restore_dest_folder)
|
restore_dest_folder)
|
||||||
self._process_queue()
|
self._process_queue()
|
||||||
self._update_sync_mode_display() # Call after loading state
|
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):
|
def _setup_log_window(self):
|
||||||
self.log_frame = ttk.Frame(self.content_frame)
|
self.log_frame = ttk.Frame(self.content_frame)
|
||||||
@@ -452,8 +456,8 @@ class MainApplication(tk.Tk):
|
|||||||
self.scheduler_frame.grid_remove()
|
self.scheduler_frame.grid_remove()
|
||||||
|
|
||||||
def _setup_settings_frame(self):
|
def _setup_settings_frame(self):
|
||||||
self.settings_frame = SettingsFrame(
|
self.settings_frame = SettingsFrame(self.content_frame, self.navigation, self.actions,
|
||||||
self.content_frame, self.navigation, self.actions, self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
|
self.backup_manager.encryption_manager, self.image_manager, self.config_manager, padding=(0, 10))
|
||||||
self.settings_frame.grid(row=2, column=0, sticky="nsew")
|
self.settings_frame.grid(row=2, column=0, sticky="nsew")
|
||||||
self.settings_frame.grid_remove()
|
self.settings_frame.grid_remove()
|
||||||
|
|
||||||
@@ -552,13 +556,10 @@ class MainApplication(tk.Tk):
|
|||||||
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
|
self.start_cancel_button.grid(row=0, column=2, rowspan=2, padx=5)
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
self.config_manager.set_setting(
|
# First, always attempt to unmount all encrypted drives
|
||||||
"refresh_log", self.refresh_log_var.get())
|
|
||||||
|
|
||||||
# Attempt to unmount all encrypted drives
|
|
||||||
unmount_success = self.backup_manager.encryption_manager.unmount_all()
|
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:
|
if not unmount_success and self.backup_manager.encryption_manager.mounted_destinations:
|
||||||
app_logger.log(
|
app_logger.log(
|
||||||
"WARNING: Not all encrypted drives could be unmounted. Preventing application closure.")
|
"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()
|
message_type="error", title=Msg.STR["unmount_failed_title"], text=Msg.STR["unmount_failed_message"]).show()
|
||||||
return # Prevent application from closing
|
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)
|
self.config_manager.set_setting("last_mode", self.mode)
|
||||||
|
|
||||||
if self.backup_left_canvas_data.get('path_display'):
|
if self.backup_left_canvas_data.get('path_display'):
|
||||||
@@ -592,6 +595,7 @@ class MainApplication(tk.Tk):
|
|||||||
else:
|
else:
|
||||||
self.config_manager.set_setting("restore_source_path", None)
|
self.config_manager.set_setting("restore_source_path", None)
|
||||||
|
|
||||||
|
# Finally, clean up UI resources and destroy the window
|
||||||
if self.left_canvas_animation:
|
if self.left_canvas_animation:
|
||||||
self.left_canvas_animation.stop()
|
self.left_canvas_animation.stop()
|
||||||
self.left_canvas_animation.destroy()
|
self.left_canvas_animation.destroy()
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class Actions:
|
|||||||
self.app.compressed_cb.config(state="normal")
|
self.app.compressed_cb.config(state="normal")
|
||||||
self.app.encrypted_cb.config(state="normal")
|
self.app.encrypted_cb.config(state="normal")
|
||||||
self.app.test_run_cb.config(state="normal")
|
self.app.test_run_cb.config(state="normal")
|
||||||
|
|
||||||
# Apply mutual exclusion rules for Option A
|
# Apply mutual exclusion rules for Option A
|
||||||
if self.app.compressed_var.get():
|
if self.app.compressed_var.get():
|
||||||
self.app.incremental_var.set(False)
|
self.app.incremental_var.set(False)
|
||||||
@@ -85,7 +85,7 @@ class Actions:
|
|||||||
self.app.incremental_cb.config(state="disabled")
|
self.app.incremental_cb.config(state="disabled")
|
||||||
self.app.encrypted_var.set(False)
|
self.app.encrypted_var.set(False)
|
||||||
self.app.encrypted_cb.config(state="disabled")
|
self.app.encrypted_cb.config(state="disabled")
|
||||||
|
|
||||||
if self.app.incremental_var.get() or self.app.encrypted_var.get():
|
if self.app.incremental_var.get() or self.app.encrypted_var.get():
|
||||||
self.app.compressed_var.set(False)
|
self.app.compressed_var.set(False)
|
||||||
self.app.compressed_cb.config(state="disabled")
|
self.app.compressed_cb.config(state="disabled")
|
||||||
@@ -397,47 +397,6 @@ class Actions:
|
|||||||
MessageDialog(message_type="error",
|
MessageDialog(message_type="error",
|
||||||
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
|
title=Msg.STR["error"], text=Msg.STR["path_not_found"].format(path=path)).show()
|
||||||
|
|
||||||
def reset_to_default_settings(self):
|
|
||||||
|
|
||||||
self.app.config_manager.set_setting("backup_destination_path", None)
|
|
||||||
self.app.config_manager.set_setting("restore_source_path", None)
|
|
||||||
self.app.config_manager.set_setting("restore_destination_path", None)
|
|
||||||
|
|
||||||
self.app.config_manager.remove_setting("backup_animation_type")
|
|
||||||
self.app.config_manager.remove_setting("calculation_animation_type")
|
|
||||||
self.app.config_manager.remove_setting("force_full_backup")
|
|
||||||
self.app.config_manager.remove_setting("force_incremental_backup")
|
|
||||||
self.app.config_manager.remove_setting("force_compression")
|
|
||||||
self.app.config_manager.remove_setting("force_encryption")
|
|
||||||
|
|
||||||
self.app.update_backup_options_from_config()
|
|
||||||
|
|
||||||
AppConfig.generate_and_write_final_exclude_list()
|
|
||||||
app_logger.log("Settings have been reset to default values.")
|
|
||||||
|
|
||||||
settings_frame = self.app.settings_frame
|
|
||||||
if settings_frame:
|
|
||||||
settings_frame.load_and_display_excludes()
|
|
||||||
settings_frame._load_hidden_files()
|
|
||||||
self.app.destination_path = None
|
|
||||||
self.app.start_cancel_button.config(state="disabled")
|
|
||||||
|
|
||||||
self.app.backup_left_canvas_data.clear()
|
|
||||||
self.app.backup_right_canvas_data.clear()
|
|
||||||
self.app.restore_left_canvas_data.clear()
|
|
||||||
self.app.restore_right_canvas_data.clear()
|
|
||||||
|
|
||||||
current_source = self.app.left_canvas_data.get('folder')
|
|
||||||
if current_source:
|
|
||||||
self.on_sidebar_button_click(current_source)
|
|
||||||
|
|
||||||
self.app.backup_content_frame.system_backups_frame._load_backup_content()
|
|
||||||
self.app.backup_content_frame.user_backups_frame._load_backup_content()
|
|
||||||
|
|
||||||
with message_box_animation(self.app.animated_icon):
|
|
||||||
MessageDialog(message_type="info",
|
|
||||||
title=Msg.STR["settings_reset_title"], text=Msg.STR["settings_reset_text"])
|
|
||||||
|
|
||||||
def _parse_size_string_to_bytes(self, size_str: str) -> int:
|
def _parse_size_string_to_bytes(self, size_str: str) -> int:
|
||||||
if not size_str or size_str == Msg.STR["calculating_size"]:
|
if not size_str or size_str == Msg.STR["calculating_size"]:
|
||||||
return 0
|
return 0
|
||||||
@@ -524,7 +483,8 @@ class Actions:
|
|||||||
|
|
||||||
self.app.task_progress.stop()
|
self.app.task_progress.stop()
|
||||||
self.app.task_progress.config(mode="determinate", value=0)
|
self.app.task_progress.config(mode="determinate", value=0)
|
||||||
self.app._update_info_label(Msg.STR["incremental_calc_cancelled"], color="#E8740C")
|
self.app._update_info_label(
|
||||||
|
Msg.STR["incremental_calc_cancelled"], color="#E8740C")
|
||||||
self.app.start_cancel_button.config(text=Msg.STR["start"])
|
self.app.start_cancel_button.config(text=Msg.STR["start"])
|
||||||
self._set_ui_state(True)
|
self._set_ui_state(True)
|
||||||
return
|
return
|
||||||
@@ -541,12 +501,14 @@ class Actions:
|
|||||||
delete_path)
|
delete_path)
|
||||||
app_logger.log(
|
app_logger.log(
|
||||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||||
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
|
self.app._update_info_label(
|
||||||
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||||
else:
|
else:
|
||||||
self.app.backup_manager.cancel_backup()
|
self.app.backup_manager.cancel_backup()
|
||||||
app_logger.log(
|
app_logger.log(
|
||||||
"Backup cancelled, but directory could not be deleted (path unknown).")
|
"Backup cancelled, but directory could not be deleted (path unknown).")
|
||||||
self.app._update_info_label("Backup cancelled, but directory could not be deleted (path unknown).")
|
self.app._update_info_label(
|
||||||
|
"Backup cancelled, but directory could not be deleted (path unknown).")
|
||||||
else:
|
else:
|
||||||
self.app.backup_manager.cancel_backup()
|
self.app.backup_manager.cancel_backup()
|
||||||
if delete_path:
|
if delete_path:
|
||||||
@@ -557,14 +519,17 @@ class Actions:
|
|||||||
shutil.rmtree(delete_path)
|
shutil.rmtree(delete_path)
|
||||||
app_logger.log(
|
app_logger.log(
|
||||||
Msg.STR["backup_cancelled_and_deleted_msg"])
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||||
self.app._update_info_label(Msg.STR["backup_cancelled_and_deleted_msg"])
|
self.app._update_info_label(
|
||||||
|
Msg.STR["backup_cancelled_and_deleted_msg"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app_logger.log(f"Error deleting backup directory: {e}")
|
app_logger.log(f"Error deleting backup directory: {e}")
|
||||||
self.app._update_info_label(f"Error deleting backup directory: {e}")
|
self.app._update_info_label(
|
||||||
|
f"Error deleting backup directory: {e}")
|
||||||
else:
|
else:
|
||||||
app_logger.log(
|
app_logger.log(
|
||||||
"Backup cancelled, but no path found to delete.")
|
"Backup cancelled, but no path found to delete.")
|
||||||
self.app._update_info_label("Backup cancelled, but no path found to delete.")
|
self.app._update_info_label(
|
||||||
|
"Backup cancelled, but no path found to delete.")
|
||||||
|
|
||||||
if hasattr(self.app, 'current_backup_path'):
|
if hasattr(self.app, 'current_backup_path'):
|
||||||
self.app.current_backup_path = None
|
self.app.current_backup_path = None
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
import os
|
import os
|
||||||
|
import glob
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from core.pbp_app_config import AppConfig, Msg
|
from core.pbp_app_config import AppConfig, Msg
|
||||||
@@ -17,6 +18,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.config_manager = config_manager
|
self.config_manager = config_manager
|
||||||
self.app_instance = app_instance
|
self.app_instance = app_instance
|
||||||
self.current_view_index = 0
|
self.current_view_index = 0
|
||||||
|
self.keyfile_profile_widgets = {}
|
||||||
|
|
||||||
nav_frame = ttk.Frame(self)
|
nav_frame = ttk.Frame(self)
|
||||||
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
|
nav_frame.pack(fill=tk.X, padx=10, pady=(0, 5))
|
||||||
@@ -166,28 +168,21 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
|
self.backup_defaults_frame, text=Msg.STR["encryption_note_system_backup"], wraplength=750, justify="left")
|
||||||
encryption_note.pack(anchor=tk.W, pady=5)
|
encryption_note.pack(anchor=tk.W, pady=5)
|
||||||
|
|
||||||
|
# --- Keyfile Settings Frame ---
|
||||||
self.keyfile_settings_frame = ttk.LabelFrame(
|
self.keyfile_settings_frame = ttk.LabelFrame(
|
||||||
view_container, text=Msg.STR["automation_settings_title"], padding=10)
|
view_container, text=Msg.STR["automation_settings_title"], padding=10)
|
||||||
|
|
||||||
|
keyfile_info_label = ttk.Label(
|
||||||
|
self.keyfile_settings_frame, text=Msg.STR["keyfile_automation_info"], justify="left", wraplength=750)
|
||||||
|
keyfile_info_label.pack(fill=tk.X, pady=(5, 15))
|
||||||
|
|
||||||
key_file_button = ttk.Button(
|
scan_button = ttk.Button(self.keyfile_settings_frame, text="Scan Destination for Encrypted Profiles", command=self._populate_keyfile_profiles_view)
|
||||||
self.keyfile_settings_frame, text=Msg.STR["create_add_key_file"], command=self._create_key_file)
|
scan_button.pack(pady=5)
|
||||||
key_file_button.grid(row=0, column=0, padx=5, pady=5)
|
|
||||||
|
|
||||||
self.key_file_status_var = tk.StringVar(
|
self.keyfile_profiles_frame = ttk.Frame(self.keyfile_settings_frame)
|
||||||
value=Msg.STR["key_file_not_created"])
|
self.keyfile_profiles_frame.pack(fill=tk.BOTH, expand=True, pady=10)
|
||||||
key_file_status_label = ttk.Label(
|
|
||||||
self.keyfile_settings_frame, textvariable=self.key_file_status_var, foreground="gray")
|
|
||||||
key_file_status_label.grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
|
||||||
|
|
||||||
sudoers_info_text = Msg.STR["sudoers_info_text"]
|
|
||||||
sudoers_info_label = ttk.Label(
|
|
||||||
self.keyfile_settings_frame, text=sudoers_info_text, justify="left")
|
|
||||||
|
|
||||||
sudoers_info_label.grid(
|
|
||||||
row=1, column=0, columnspan=2, sticky="w", padx=5, pady=5)
|
|
||||||
|
|
||||||
self.keyfile_settings_frame.columnconfigure(1, weight=1)
|
|
||||||
|
|
||||||
|
# --- Bottom Buttons (for most views) ---
|
||||||
self.bottom_button_frame = ttk.Frame(self)
|
self.bottom_button_frame = ttk.Frame(self)
|
||||||
self.bottom_button_frame.pack(pady=10)
|
self.bottom_button_frame.pack(pady=10)
|
||||||
|
|
||||||
@@ -206,7 +201,6 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self._load_animation_settings()
|
self._load_animation_settings()
|
||||||
self._load_backup_defaults()
|
self._load_backup_defaults()
|
||||||
self._load_manual_excludes()
|
self._load_manual_excludes()
|
||||||
self._update_key_file_status()
|
|
||||||
|
|
||||||
self._switch_view(self.current_view_index)
|
self._switch_view(self.current_view_index)
|
||||||
|
|
||||||
@@ -243,61 +237,140 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.config_manager.set_setting("use_trash_bin", use_trash)
|
self.config_manager.set_setting("use_trash_bin", use_trash)
|
||||||
self.config_manager.set_setting("no_trash_bin", no_trash)
|
self.config_manager.set_setting("no_trash_bin", no_trash)
|
||||||
|
|
||||||
def _create_key_file(self):
|
def _populate_keyfile_profiles_view(self):
|
||||||
if not self.app_instance.destination_path:
|
# Clear existing widgets
|
||||||
MessageDialog(message_type="error", title="Error",
|
for widget in self.keyfile_profiles_frame.winfo_children():
|
||||||
text="Please select a backup destination first.")
|
widget.destroy()
|
||||||
|
self.keyfile_profile_widgets.clear()
|
||||||
|
|
||||||
|
destination = self.app_instance.destination_path
|
||||||
|
if not destination:
|
||||||
|
MessageDialog(message_type="error", title=Msg.STR["error"],
|
||||||
|
text=Msg.STR["select_destination_first"]).show()
|
||||||
return
|
return
|
||||||
|
|
||||||
pybackup_dir = os.path.join(
|
pybackup_dir = os.path.join(destination, "pybackup")
|
||||||
self.app_instance.destination_path, "pybackup")
|
if not os.path.isdir(pybackup_dir):
|
||||||
container_path = os.path.join(pybackup_dir, "pybackup_encrypted.luks")
|
ttk.Label(self.keyfile_profiles_frame, text="No 'pybackup' directory found at destination.").pack()
|
||||||
if not os.path.exists(container_path):
|
|
||||||
MessageDialog(message_type="error", title="Error",
|
|
||||||
text="No encrypted container found at the destination.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
search_pattern = os.path.join(pybackup_dir, "pybackup_encrypted_*.luks")
|
||||||
|
found_containers = glob.glob(search_pattern)
|
||||||
|
|
||||||
|
if not found_containers:
|
||||||
|
ttk.Label(self.keyfile_profiles_frame, text="No encrypted profiles found at destination.").pack()
|
||||||
|
return
|
||||||
|
|
||||||
|
profiles = []
|
||||||
|
for container in found_containers:
|
||||||
|
filename = os.path.basename(container)
|
||||||
|
profile_name = filename.replace("pybackup_encrypted_", "").replace(".luks", "")
|
||||||
|
if profile_name:
|
||||||
|
profiles.append(profile_name)
|
||||||
|
|
||||||
|
# Master "All" checkbox
|
||||||
|
all_var = tk.BooleanVar()
|
||||||
|
all_check = ttk.Checkbutton(self.keyfile_profiles_frame, text="All Profiles", variable=all_var, style="Switch.TCheckbutton",
|
||||||
|
command=lambda v=all_var: self._on_master_keyfile_toggle(v, profiles))
|
||||||
|
all_check.pack(anchor="w", padx=5, pady=5)
|
||||||
|
ttk.Separator(self.keyfile_profiles_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
# Individual profile checkboxes
|
||||||
|
for profile_name in sorted(profiles):
|
||||||
|
profile_frame = ttk.Frame(self.keyfile_profiles_frame)
|
||||||
|
profile_frame.pack(fill=tk.X, padx=5, pady=2)
|
||||||
|
|
||||||
|
var = tk.BooleanVar()
|
||||||
|
check = ttk.Checkbutton(profile_frame, text=profile_name, variable=var, style="Switch.TCheckbutton")
|
||||||
|
check.pack(side=tk.LEFT)
|
||||||
|
# Use a lambda with default arguments to capture the current values
|
||||||
|
check.config(command=lambda p=profile_name, v=var: self._on_keyfile_checkbox_toggle(p, v))
|
||||||
|
|
||||||
|
status_label = ttk.Label(profile_frame, text="", foreground="gray")
|
||||||
|
status_label.pack(side=tk.LEFT, padx=10)
|
||||||
|
|
||||||
|
self.keyfile_profile_widgets[profile_name] = {"var": var, "status_label": status_label}
|
||||||
|
self._update_single_keyfile_status(profile_name)
|
||||||
|
|
||||||
|
def _on_master_keyfile_toggle(self, var, profiles):
|
||||||
|
if not var.get():
|
||||||
|
return # Do nothing on uncheck for safety
|
||||||
|
|
||||||
password_dialog = PasswordDialog(
|
password_dialog = PasswordDialog(
|
||||||
self, title="Enter Existing Password", confirm=False, translations=Msg.STR)
|
self, title=Msg.STR["enter_existing_password_title"], confirm=False, translations=Msg.STR)
|
||||||
password, _ = password_dialog.get_password()
|
password, _ = password_dialog.get_password()
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
|
var.set(False) # Uncheck the master switch
|
||||||
return
|
return
|
||||||
|
|
||||||
|
for profile_name in profiles:
|
||||||
|
self._create_key_file_for_profile(profile_name, password)
|
||||||
|
|
||||||
|
def _on_keyfile_checkbox_toggle(self, profile_name, var):
|
||||||
|
if var.get(): # If checked, create key
|
||||||
|
self._create_key_file_for_profile(profile_name)
|
||||||
|
else: # If unchecked, delete key
|
||||||
|
self._delete_key_file_for_profile(profile_name)
|
||||||
|
|
||||||
|
def _create_key_file_for_profile(self, profile_name, password=None):
|
||||||
|
header = self.app_instance.header_frame
|
||||||
|
destination = self.app_instance.destination_path
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
password_dialog = PasswordDialog(
|
||||||
|
self, title=f"{Msg.STR['enter_existing_password_title']} for {profile_name}", confirm=False, translations=Msg.STR)
|
||||||
|
password, _ = password_dialog.get_password()
|
||||||
|
if not password:
|
||||||
|
self._update_single_keyfile_status(profile_name) # Revert checkbox state
|
||||||
|
return
|
||||||
|
|
||||||
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
|
key_file_path = self.app_instance.backup_manager.encryption_manager.create_and_add_key_file(
|
||||||
self.app_instance.destination_path, password)
|
destination, profile_name, password)
|
||||||
|
|
||||||
if key_file_path:
|
if key_file_path:
|
||||||
MessageDialog(message_type="info", title="Success",
|
header.show_temporary_message(Msg.STR["keyfile_creation_success"])
|
||||||
text=f"Key file created and added successfully!\nPath: {key_file_path}")
|
|
||||||
else:
|
else:
|
||||||
MessageDialog(message_type="error", title="Error",
|
header.show_temporary_message(Msg.STR["keyfile_creation_failed"])
|
||||||
text="Failed to create or add key file. See log for details.")
|
|
||||||
|
self._update_single_keyfile_status(profile_name)
|
||||||
|
|
||||||
self._update_key_file_status()
|
def _delete_key_file_for_profile(self, profile_name):
|
||||||
|
header = self.app_instance.header_frame
|
||||||
|
destination = self.app_instance.destination_path
|
||||||
|
|
||||||
def _update_key_file_status(self):
|
password_dialog = PasswordDialog(
|
||||||
if not self.app_instance.destination_path:
|
self, title=f"Password to delete key for {profile_name}", confirm=False, translations=Msg.STR)
|
||||||
self.key_file_status_var.set(
|
password, _ = password_dialog.get_password()
|
||||||
"Key file status unknown (no destination set).")
|
if not password:
|
||||||
|
self._update_single_keyfile_status(profile_name) # Revert checkbox state
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine the profile_name based on the current source folder
|
success = self.app_instance.backup_manager.encryption_manager.delete_key_file(
|
||||||
source_name = self.app_instance.left_canvas_data.get('folder')
|
destination, profile_name, password)
|
||||||
if not source_name:
|
|
||||||
# Fallback or handle case where no source is selected, perhaps default to "system" or log an error
|
|
||||||
# For now, let's assume "system" if no specific folder is selected for keyfile status
|
|
||||||
profile_name = "system"
|
|
||||||
else:
|
|
||||||
profile_name = "system" if source_name == "Computer" else source_name
|
|
||||||
|
|
||||||
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
if success:
|
||||||
self.app_instance.destination_path, profile_name)
|
header.show_temporary_message("Keyfile successfully deleted.")
|
||||||
if os.path.exists(key_file_path):
|
|
||||||
self.key_file_status_var.set(f"Key file exists: {key_file_path}")
|
|
||||||
else:
|
else:
|
||||||
self.key_file_status_var.set(
|
header.show_temporary_message("Failed to delete keyfile.")
|
||||||
"Key file has not been created for this destination.")
|
|
||||||
|
self._update_single_keyfile_status(profile_name)
|
||||||
|
|
||||||
|
def _update_single_keyfile_status(self, profile_name):
|
||||||
|
widgets = self.keyfile_profile_widgets.get(profile_name)
|
||||||
|
if not widgets:
|
||||||
|
return
|
||||||
|
|
||||||
|
destination = self.app_instance.destination_path
|
||||||
|
key_file_path = self.app_instance.backup_manager.encryption_manager.get_key_file_path(
|
||||||
|
destination, profile_name)
|
||||||
|
|
||||||
|
if os.path.exists(key_file_path):
|
||||||
|
widgets["var"].set(True)
|
||||||
|
widgets["status_label"].config(text="Keyfile exists")
|
||||||
|
else:
|
||||||
|
widgets["var"].set(False)
|
||||||
|
widgets["status_label"].config(text="")
|
||||||
|
|
||||||
def _switch_view(self, index):
|
def _switch_view(self, index):
|
||||||
self.current_view_index = index
|
self.current_view_index = index
|
||||||
@@ -309,8 +382,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
self.animation_settings_frame.pack_forget()
|
self.animation_settings_frame.pack_forget()
|
||||||
self.backup_defaults_frame.pack_forget()
|
self.backup_defaults_frame.pack_forget()
|
||||||
|
|
||||||
# Show/hide the main action buttons based on the view
|
if index in [1, 2]:
|
||||||
if index == 1: # Manual Excludes view
|
|
||||||
self.bottom_button_frame.pack_forget()
|
self.bottom_button_frame.pack_forget()
|
||||||
else:
|
else:
|
||||||
self.bottom_button_frame.pack(pady=10)
|
self.bottom_button_frame.pack(pady=10)
|
||||||
@@ -326,7 +398,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
elif index == 2:
|
elif index == 2:
|
||||||
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
|
self.keyfile_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.info_label.config(text="")
|
self.info_label.config(text="")
|
||||||
self._update_key_file_status()
|
self._populate_keyfile_profiles_view()
|
||||||
elif index == 3:
|
elif index == 3:
|
||||||
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
|
self.animation_settings_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.info_label.config(text="")
|
self.info_label.config(text="")
|
||||||
@@ -357,8 +429,11 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
|
|
||||||
def _delete_manual_exclude(self):
|
def _delete_manual_exclude(self):
|
||||||
selected_indices = self.manual_excludes_listbox.curselection()
|
selected_indices = self.manual_excludes_listbox.curselection()
|
||||||
|
if not selected_indices:
|
||||||
|
return
|
||||||
for i in reversed(selected_indices):
|
for i in reversed(selected_indices):
|
||||||
self.manual_excludes_listbox.delete(i)
|
self.manual_excludes_listbox.delete(i)
|
||||||
|
self.app_instance.header_frame.show_temporary_message(Msg.STR["delete_success"])
|
||||||
self._apply_changes()
|
self._apply_changes()
|
||||||
|
|
||||||
def _reset_animation_settings(self):
|
def _reset_animation_settings(self):
|
||||||
@@ -550,13 +625,7 @@ class AdvancedSettingsFrame(ttk.Frame):
|
|||||||
f.write(f"{item}\n")
|
f.write(f"{item}\n")
|
||||||
|
|
||||||
# Show temporary success message in header
|
# Show temporary success message in header
|
||||||
self.app_instance.header_frame.show_temporary_message("Settings saved successfully!")
|
self.app_instance.header_frame.show_temporary_message(Msg.STR["settings_saved"])
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _cancel_changes(self):
|
def _cancel_changes(self):
|
||||||
self.pack_forget()
|
self.pack_forget()
|
||||||
|
|||||||
@@ -290,7 +290,21 @@ class Drawing:
|
|||||||
elif self.app.is_first_backup:
|
elif self.app.is_first_backup:
|
||||||
info_messages.append(Msg.STR["ready_for_first_backup"])
|
info_messages.append(Msg.STR["ready_for_first_backup"])
|
||||||
elif self.app.mode == "backup":
|
elif self.app.mode == "backup":
|
||||||
|
# Base message is always shown after calculation in backup mode
|
||||||
info_messages.append(Msg.STR["backup_mode_info"])
|
info_messages.append(Msg.STR["backup_mode_info"])
|
||||||
|
|
||||||
|
# Additionally, show a warning if encryption is on but the drive is not mounted
|
||||||
|
if self.app.encrypted_var.get():
|
||||||
|
dest_path = self.app.destination_path
|
||||||
|
profile_name = self.app.left_canvas_data.get('folder')
|
||||||
|
|
||||||
|
is_mounted = False
|
||||||
|
if dest_path and profile_name:
|
||||||
|
is_mounted = self.app.backup_manager.encryption_manager.is_mounted(
|
||||||
|
dest_path, profile_name)
|
||||||
|
|
||||||
|
if not is_mounted:
|
||||||
|
info_messages.append(Msg.STR["encrypted_unmounted_warning"])
|
||||||
else:
|
else:
|
||||||
info_messages.append(Msg.STR["restore_mode_info"])
|
info_messages.append(Msg.STR["restore_mode_info"])
|
||||||
|
|
||||||
|
|||||||
@@ -72,18 +72,32 @@ class HeaderFrame(tk.Frame):
|
|||||||
|
|
||||||
self.refresh_status()
|
self.refresh_status()
|
||||||
|
|
||||||
def show_temporary_message(self, message: str, duration_ms: int = 4000):
|
def show_temporary_message(self, message: str, duration_ms: int = 3000):
|
||||||
"""Displays a temporary message in the subtitle area."""
|
"""Displays a large, centered, temporary message over the header."""
|
||||||
# Cancel any previous after job to prevent conflicts
|
# If a message is already shown, cancel its removal and destroy it
|
||||||
if self._temp_message_after_id:
|
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
|
||||||
self.after_cancel(self._temp_message_after_id)
|
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)
|
self._temp_message_after_id = self.after(duration_ms, self._restore_subtitle)
|
||||||
|
|
||||||
def _restore_subtitle(self):
|
def _restore_subtitle(self):
|
||||||
"""Restores the original subtitle text and color."""
|
"""Destroys the temporary message label."""
|
||||||
self.subtitle_label.config(text=self.original_subtitle_text, fg="#bdc3c7")
|
if hasattr(self, '_temp_message_label') and self._temp_message_label.winfo_exists():
|
||||||
|
self._temp_message_label.destroy()
|
||||||
self._temp_message_after_id = None
|
self._temp_message_after_id = None
|
||||||
|
|
||||||
def refresh_status(self):
|
def refresh_status(self):
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from core.pbp_app_config import AppConfig, Msg
|
|||||||
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
|
from pyimage_ui.advanced_settings_frame import AdvancedSettingsFrame
|
||||||
from shared_libs.custom_file_dialog import CustomFileDialog
|
from shared_libs.custom_file_dialog import CustomFileDialog
|
||||||
from shared_libs.message import MessageDialog, PasswordDialog
|
from shared_libs.message import MessageDialog, PasswordDialog
|
||||||
|
from shared_libs.logger import app_logger
|
||||||
|
|
||||||
|
|
||||||
class SettingsFrame(ttk.Frame):
|
class SettingsFrame(ttk.Frame):
|
||||||
@@ -45,19 +46,12 @@ class SettingsFrame(ttk.Frame):
|
|||||||
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton")
|
self.button_frame, text=Msg.STR["add_to_exclude_list"], command=self._add_to_exclude_list, style="Gray.Toolbutton")
|
||||||
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
|
add_to_exclude_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
|
||||||
side=tk.LEFT, ipady=15, padx=5)
|
|
||||||
|
|
||||||
reset_button = ttk.Button(
|
|
||||||
self.button_frame, text=Msg.STR["default_settings"], command=self.actions.reset_to_default_settings, style="Gray.Toolbutton")
|
|
||||||
reset_button.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
||||||
side=tk.LEFT, ipady=15, padx=5)
|
side=tk.LEFT, ipady=15, padx=5)
|
||||||
|
|
||||||
# Right-aligned buttons
|
# Right-aligned buttons
|
||||||
hard_reset_button = ttk.Button(
|
hard_reset_button = ttk.Button(
|
||||||
self.button_frame, text=Msg.STR["hard_reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
|
self.button_frame, text=Msg.STR["reset"], command=self._toggle_hard_reset_view, style="Gray.Toolbutton")
|
||||||
hard_reset_button.pack(side=tk.LEFT, padx=5)
|
hard_reset_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
ttk.Separator(self.button_frame, orient=tk.VERTICAL).pack(
|
||||||
@@ -79,10 +73,6 @@ class SettingsFrame(ttk.Frame):
|
|||||||
self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes)
|
self.bottom_button_frame, text=Msg.STR["apply"], command=self._apply_changes)
|
||||||
apply_button.pack(side=tk.LEFT, padx=5)
|
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 ---
|
# --- Treeview for file/folder exclusion ---
|
||||||
self.tree_frame = ttk.LabelFrame(
|
self.tree_frame = ttk.LabelFrame(
|
||||||
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
|
self.trees_container, text=Msg.STR["user_defined_folder_settings"], padding=10)
|
||||||
@@ -121,50 +111,122 @@ class SettingsFrame(ttk.Frame):
|
|||||||
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
|
self.hidden_tree.bind("<Button-1>", self._toggle_include_status_hidden)
|
||||||
self.hidden_tree_frame.pack_forget() # Initially hidden
|
self.hidden_tree_frame.pack_forget() # Initially hidden
|
||||||
|
|
||||||
|
# --- Default Reset Frame (initially hidden) ---
|
||||||
|
self.default_reset_frame = ttk.LabelFrame(
|
||||||
|
self, text=Msg.STR["default_config_settings"], padding=10)
|
||||||
|
|
||||||
|
default_reset_label = ttk.Label(
|
||||||
|
self.default_reset_frame, text=Msg.STR["default_reset_info"], wraplength=400, justify=tk.CENTER)
|
||||||
|
default_reset_label.pack(pady=10, expand=True)
|
||||||
|
|
||||||
|
# Frame to center the button
|
||||||
|
default_reset_button_frame = ttk.Frame(self.default_reset_frame)
|
||||||
|
default_reset_button_frame.pack(pady=5)
|
||||||
|
|
||||||
|
reset_button = ttk.Button(
|
||||||
|
default_reset_button_frame, text=Msg.STR["default_settings"], command=self._reset_to_default_settings)
|
||||||
|
reset_button.pack() # Centered by default in its own frame
|
||||||
|
|
||||||
# --- Hard Reset Frame (initially hidden) ---
|
# --- Hard Reset Frame (initially hidden) ---
|
||||||
self.hard_reset_frame = ttk.LabelFrame(
|
self.hard_reset_frame = ttk.LabelFrame(
|
||||||
self, text=Msg.STR["full_delete_config_settings"], padding=10)
|
self, text=Msg.STR["full_delete_config_settings"], padding=10)
|
||||||
|
|
||||||
hard_reset_label = ttk.Label(
|
hard_reset_label = ttk.Label(
|
||||||
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.LEFT)
|
self.hard_reset_frame, text=Msg.STR["hard_reset_warning"], wraplength=400, justify=tk.CENTER)
|
||||||
hard_reset_label.pack(pady=10)
|
hard_reset_label.pack(pady=10, expand=True)
|
||||||
|
|
||||||
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
|
hard_reset_button_frame = ttk.Frame(self.hard_reset_frame)
|
||||||
hard_reset_button_frame.pack(pady=10)
|
hard_reset_button_frame.pack(pady=5)
|
||||||
|
|
||||||
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)
|
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() # Centered by default
|
||||||
|
|
||||||
cancel_hard_reset_button = ttk.Button(
|
# --- Bottom Cancel Button for Reset View (initially hidden) ---
|
||||||
hard_reset_button_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
|
self.reset_view_cancel_frame = ttk.Frame(self)
|
||||||
cancel_hard_reset_button.pack(side=tk.LEFT, padx=5)
|
self.reset_view_cancel_button = ttk.Button(
|
||||||
|
self.reset_view_cancel_frame, text=Msg.STR["cancel"], command=self._toggle_hard_reset_view)
|
||||||
|
self.reset_view_cancel_button.pack(pady=10)
|
||||||
|
|
||||||
self.hidden_files_visible = False
|
self.hidden_files_visible = False
|
||||||
self.hard_reset_visible = False
|
self.hard_reset_visible = False
|
||||||
# To hold the instance of AdvancedSettingsFrame
|
# To hold the instance of AdvancedSettingsFrame
|
||||||
self.advanced_settings_frame_instance = None
|
self.advanced_settings_frame_instance = None
|
||||||
|
|
||||||
def _perform_hard_reset(self):
|
def _reset_to_default_settings(self):
|
||||||
if self.encryption_manager.mounted_destinations:
|
app_instance = self.master.master.master
|
||||||
dialog = PasswordDialog(
|
header = app_instance.header_frame
|
||||||
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:
|
try:
|
||||||
shutil.rmtree(AppConfig.APP_DIR)
|
self.config_manager.set_setting("backup_destination_path", None)
|
||||||
# Restart the application
|
self.config_manager.set_setting("restore_source_path", None)
|
||||||
os.execl(sys.executable, sys.executable, *sys.argv)
|
self.config_manager.set_setting("restore_destination_path", None)
|
||||||
|
|
||||||
|
self.config_manager.remove_setting("backup_animation_type")
|
||||||
|
self.config_manager.remove_setting("calculation_animation_type")
|
||||||
|
self.config_manager.remove_setting("force_full_backup")
|
||||||
|
self.config_manager.remove_setting("force_incremental_backup")
|
||||||
|
self.config_manager.remove_setting("force_compression")
|
||||||
|
self.config_manager.remove_setting("force_encryption")
|
||||||
|
|
||||||
|
app_instance.update_backup_options_from_config()
|
||||||
|
|
||||||
|
AppConfig.generate_and_write_final_exclude_list()
|
||||||
|
app_logger.log("Settings have been reset to default values.")
|
||||||
|
|
||||||
|
self.load_and_display_excludes()
|
||||||
|
if self.hidden_files_visible:
|
||||||
|
self._load_hidden_files()
|
||||||
|
|
||||||
|
app_instance.destination_path = None
|
||||||
|
app_instance.start_cancel_button.config(state="disabled")
|
||||||
|
|
||||||
|
app_instance.backup_left_canvas_data.clear()
|
||||||
|
app_instance.backup_right_canvas_data.clear()
|
||||||
|
app_instance.restore_left_canvas_data.clear()
|
||||||
|
app_instance.restore_right_canvas_data.clear()
|
||||||
|
|
||||||
|
current_source = app_instance.left_canvas_data.get('folder')
|
||||||
|
if current_source:
|
||||||
|
self.actions.on_sidebar_button_click(current_source)
|
||||||
|
|
||||||
|
app_instance.backup_content_frame.system_backups_frame._load_backup_content()
|
||||||
|
app_instance.backup_content_frame.user_backups_frame._load_backup_content()
|
||||||
|
|
||||||
|
header.show_temporary_message(Msg.STR["default_reset_success"])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
MessageDialog(message_type="error", text=str(e)).show()
|
app_logger.log(f"Error resetting settings: {e}")
|
||||||
|
header.show_temporary_message(Msg.STR["default_reset_failed"])
|
||||||
|
|
||||||
|
def _perform_hard_reset(self):
|
||||||
|
try:
|
||||||
|
# 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"])
|
||||||
|
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=f"Hard reset failed: {e}").show()
|
||||||
|
|
||||||
def _toggle_hard_reset_view(self):
|
def _toggle_hard_reset_view(self):
|
||||||
self.hard_reset_visible = not self.hard_reset_visible
|
self.hard_reset_visible = not self.hard_reset_visible
|
||||||
@@ -172,10 +234,16 @@ class SettingsFrame(ttk.Frame):
|
|||||||
self.trees_container.pack_forget()
|
self.trees_container.pack_forget()
|
||||||
self.button_frame.pack_forget()
|
self.button_frame.pack_forget()
|
||||||
self.bottom_button_frame.pack_forget()
|
self.bottom_button_frame.pack_forget()
|
||||||
|
self.default_reset_frame.pack(
|
||||||
|
fill=tk.X, expand=True, padx=10, pady=5)
|
||||||
self.hard_reset_frame.pack(
|
self.hard_reset_frame.pack(
|
||||||
fill=tk.BOTH, expand=True, padx=10, pady=5)
|
fill=tk.X, expand=True, padx=10, pady=5)
|
||||||
|
self.reset_view_cancel_frame.pack(fill=tk.X, side=tk.BOTTOM)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
self.default_reset_frame.pack_forget()
|
||||||
self.hard_reset_frame.pack_forget()
|
self.hard_reset_frame.pack_forget()
|
||||||
|
self.reset_view_cancel_frame.pack_forget()
|
||||||
self.button_frame.pack(fill=tk.X, padx=10)
|
self.button_frame.pack(fill=tk.X, padx=10)
|
||||||
self.trees_container.pack(
|
self.trees_container.pack(
|
||||||
fill=tk.BOTH, expand=True, padx=10, pady=5)
|
fill=tk.BOTH, expand=True, padx=10, pady=5)
|
||||||
@@ -338,6 +406,10 @@ class SettingsFrame(ttk.Frame):
|
|||||||
if self.hidden_files_visible:
|
if self.hidden_files_visible:
|
||||||
self._load_hidden_files()
|
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):
|
def _open_advanced_settings(self):
|
||||||
# Hide main settings UI elements
|
# Hide main settings UI elements
|
||||||
self.trees_container.pack_forget() # Hide the container for treeviews
|
self.trees_container.pack_forget() # Hide the container for treeviews
|
||||||
|
|||||||
Reference in New Issue
Block a user