diff --git a/backup_manager.py b/backup_manager.py index 5cb9b70..706b271 100644 --- a/backup_manager.py +++ b/backup_manager.py @@ -582,54 +582,16 @@ set -e backups.append(item) return sorted(backups, reverse=True) - def list_system_backups(self, base_backup_path: str) -> List[Dict[str, str]]: - """Lists all system backups found in the pybackup subdirectory.""" - system_backups = [] - pybackup_path = os.path.join(base_backup_path, "pybackup") + def list_system_backups(self, scan_path: str, is_encrypted_and_mounted: bool = False) -> List[Dict[str, str]]: + """Lists all system backups, handling encrypted containers and sorting into groups.""" + if not os.path.isdir(scan_path): + return [] - if not os.path.isdir(pybackup_path): - return system_backups - - # Check for a single encrypted container first - encrypted_container_path = os.path.join(pybackup_path, "pybackup_encrypted.luks") - if os.path.exists(encrypted_container_path): - try: - stat_info = os.stat(encrypted_container_path) - m_time = datetime.datetime.fromtimestamp(stat_info.st_mtime) - file_size = stat_info.st_size - - # Format size - power = 1024 - n = 0 - power_labels = {0: 'B', 1: 'KB', 2: 'MB', 3: 'GB', 4: 'TB'} - display_size = file_size - while display_size >= power and n < len(power_labels) - 1: - display_size /= power - n += 1 - size_str = f"{display_size:.2f} {power_labels[n]}" - - system_backups.append({ - "date": m_time.strftime('%d-%m-%Y'), - "time": m_time.strftime('%H:%M:%S'), - "type": "Encrypted Container", - "size": size_str, - "folder_name": "pybackup_encrypted.luks", - "full_path": encrypted_container_path, - "comment": "Container for all encrypted backups", - "is_compressed": False, - "is_encrypted": True, - "backup_type_base": "Full" # Treat container as a single full entity - }) - return system_backups # Return immediately - except Exception as e: - self.logger.log(f"Could not stat encrypted container file: {e}") - # Fall through to normal processing if stat fails - - # Proceed with individual folder scan if no container is found + all_backups = [] name_regex = re.compile( r"^(\d{2}-\d{2}-\d{4})_(\d{2}:\d{2}:\d{2})_system_(full|incremental)(\.tar\.gz)?$", re.IGNORECASE) - for item in os.listdir(pybackup_path): + for item in os.listdir(scan_path): if item.endswith('.txt') or item.endswith('.luks'): continue @@ -637,39 +599,29 @@ set -e if not match: continue - full_path = os.path.join(pybackup_path, item) - date_str = match.group(1) - time_str = match.group(2) - backup_type_base = match.group(3).capitalize() - extension = match.group(4) + full_path = os.path.join(scan_path, item) + date_str, time_str, backup_type_base, extension = match.groups() is_compressed = (extension == ".tar.gz") - is_encrypted = False # Individual folders are not encrypted in this logic - - backup_type = backup_type_base + + backup_type = backup_type_base.capitalize() if is_compressed: backup_type += " (Compressed)" backup_size = "N/A" comment = "" - - info_file_path = os.path.join(pybackup_path, f"{item}.txt") + info_file_path = os.path.join(scan_path, f"{item}.txt") if os.path.exists(info_file_path): try: with open(info_file_path, 'r') as f: for line in f: if line.strip().lower().startswith("originalgröße:"): - size_part = line.split(":", 1)[1].strip() - if '(' in size_part: - backup_size = size_part.split('(')[0].strip() - else: - backup_size = size_part + backup_size = line.split(":", 1)[1].strip().split('(')[0].strip() elif line.strip().lower().startswith("kommentar:"): comment = line.split(":", 1)[1].strip() except Exception as e: - self.logger.log( - f"Could not read info file {info_file_path}: {e}") + self.logger.log(f"Could not read info file {info_file_path}: {e}") - system_backups.append({ + all_backups.append({ "date": date_str, "time": time_str, "type": backup_type, @@ -678,20 +630,35 @@ set -e "full_path": full_path, "comment": comment, "is_compressed": is_compressed, - "is_encrypted": is_encrypted, - "backup_type_base": backup_type_base + "is_encrypted": is_encrypted_and_mounted, + "backup_type_base": backup_type_base.capitalize(), + "datetime": datetime.datetime.strptime(f"{date_str} {time_str}", '%d-%m-%Y %H:%M:%S') }) - try: - # Sort chronologically, oldest first - system_backups.sort(key=lambda x: datetime.datetime.strptime( - f"{x['date']} {x['time']}", '%d-%m-%Y %H:%M:%S'), reverse=False) - except ValueError: - self.logger.log( - "Could not sort backups by date and time due to format mismatch.") - system_backups.sort(key=lambda x: x['folder_name'], reverse=False) + # Sort all backups chronologically to make grouping easier + all_backups.sort(key=lambda x: x['datetime']) - return system_backups + # Group backups: each group starts with a Full backup + grouped_backups = [] + current_group = [] + for backup in all_backups: + if backup['backup_type_base'] == 'Full': + if current_group: + grouped_backups.append(current_group) + current_group = [backup] + else: # Incremental + if current_group: # Add to the current group if it exists + current_group.append(backup) + if current_group: + grouped_backups.append(current_group) + + # Sort groups by the datetime of their first (Full) backup, descending + grouped_backups.sort(key=lambda g: g[0]['datetime'], reverse=True) + + # Flatten the list of groups into the final sorted list + final_sorted_list = [item for group in grouped_backups for item in group] + + return final_sorted_list def list_user_backups(self, base_backup_path: str) -> List[Dict[str, str]]: """Lists all user backups found in the base backup path.""" diff --git a/core/encryption_manager.py b/core/encryption_manager.py index 8c8a2b5..a1aaec4 100644 --- a/core/encryption_manager.py +++ b/core/encryption_manager.py @@ -25,12 +25,20 @@ class EncryptionManager: try: return keyring.get_password(self.service_id, username) except keyring.errors.InitError as e: - logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}") + self.logger.log(f"Could not initialize keyring. Keyring is not available on this system or is not configured correctly. Error: {e}") return None except Exception as e: - logger.log(f"Could not get password from keyring: {e}") + self.logger.log(f"Could not get password from keyring: {e}") return None + def is_key_in_keyring(self, username: str) -> bool: + """Checks if a password for the given username exists in the keyring.""" + try: + return self.get_password_from_keyring(username) is not None + except Exception as e: + self.logger.log(f"Could not check password in keyring: {e}") + return False + def set_password_in_keyring(self, username: str, password: str) -> bool: try: keyring.set_password(self.service_id, username, password) @@ -69,6 +77,43 @@ class EncryptionManager: return password + def unlock_container(self, base_path: str, password: str) -> Optional[str]: + """Unlocks and mounts an existing LUKS container.""" + self.logger.log(f"Attempting to unlock encrypted container in {base_path}") + + container_file = "pybackup_encrypted.luks" + container_path = os.path.join(base_path, container_file) + if not os.path.exists(container_path): + self.logger.log(f"Encrypted container not found at {container_path}") + return None + + mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}" + mount_point = f"/mnt/{mapper_name}" + + if os.path.ismount(mount_point): + self.logger.log(f"Container already mounted at {mount_point}") + return mount_point + + script = f""" + mkdir -p {mount_point} + echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} - + mount /dev/mapper/{mapper_name} {mount_point} + """ + if not self._execute_as_root(script): + self.logger.log("Failed to unlock existing encrypted container. Check password or permissions.") + # Clean up failed mount attempt + self.cleanup_encrypted_backup(mapper_name, mount_point) + return None + + self.logger.log(f"Encrypted container unlocked and mounted at {mount_point}") + return mount_point + + def lock_container(self, base_path: str): + """Unmounts and closes the LUKS container for a given base path.""" + mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}" + mount_point = f"/mnt/{mapper_name}" + self.cleanup_encrypted_backup(mapper_name, mount_point) + def setup_encrypted_backup(self, queue, base_path: str, size_gb: int, password: str) -> Optional[str]: """Sets up a persistent LUKS encrypted container for the backup destination.""" self.logger.log(f"Setting up encrypted container at {base_path}") @@ -80,7 +125,7 @@ class EncryptionManager: container_file = "pybackup_encrypted.luks" container_path = os.path.join(base_path, container_file) - mapper_name = f"pybackup_{os.path.basename(base_path)}" + mapper_name = f"pybackup_{os.path.basename(base_path.rstrip('/'))}" mount_point = f"/mnt/{mapper_name}" if not password: @@ -95,17 +140,7 @@ class EncryptionManager: if os.path.exists(container_path): self.logger.log(f"Encrypted container {container_path} already exists. Attempting to unlock.") - script = f""" - mkdir -p {mount_point} - echo -n '{password}' | cryptsetup luksOpen {container_path} {mapper_name} - - mount /dev/mapper/{mapper_name} {mount_point} - """ - if not self._execute_as_root(script): - self.logger.log("Failed to unlock existing encrypted container. Check password or permissions.") - queue.put(('error', "Failed to unlock existing encrypted container.")) - # Clean up failed mount attempt - self.cleanup_encrypted_backup(mapper_name, mount_point) - return None + return self.unlock_container(base_path, password) else: self.logger.log(f"Creating new encrypted container: {container_path}") script = f""" @@ -119,10 +154,11 @@ class EncryptionManager: if not self._execute_as_root(script): self.logger.log("Failed to create and setup encrypted container.") - self._cleanup_encrypted_backup(mapper_name, mount_point) + self.cleanup_encrypted_backup(mapper_name, mount_point) # Also remove the failed container file if os.path.exists(container_path): - os.remove(container_path) # This should be done with pkexec as well + # This should be done with pkexec as well for safety + self._execute_as_root(f"rm -f {container_path}") queue.put(('error', "Failed to setup encrypted container.")) return None diff --git a/main_app.py b/main_app.py index 60448b9..a6c02fe 100644 --- a/main_app.py +++ b/main_app.py @@ -4,6 +4,7 @@ from tkinter import ttk import os import datetime from queue import Queue, Empty +import shutil from shared_libs.log_window import LogWindow from shared_libs.logger import app_logger @@ -146,9 +147,7 @@ class MainApplication(tk.Tk): self.sidebar_buttons_frame, text=Msg.STR["settings"], command=lambda: self.navigation.toggle_settings_frame(4), style="Sidebar.TButton") self.settings_button.pack(fill=tk.X, pady=10) - - - self.header_frame = HeaderFrame(self.content_frame, self.image_manager) + self.header_frame = HeaderFrame(self.content_frame, self.image_manager, self.backup_manager.encryption_manager, self) self.header_frame.grid(row=0, column=0, sticky="nsew") self.top_bar = ttk.Frame(self.content_frame) @@ -223,7 +222,6 @@ class MainApplication(tk.Tk): self._setup_settings_frame() self._setup_backup_content_frame() - self._setup_task_bar() self.source_size_frame = ttk.LabelFrame( @@ -299,15 +297,13 @@ class MainApplication(tk.Tk): self.restore_size_frame_after.grid_remove() self._load_state_and_initialize() - self.update_backup_options_from_config() # Add this call + self.update_backup_options_from_config() self.protocol("WM_DELETE_WINDOW", self.on_closing) def _load_state_and_initialize(self): self.log_window.clear_log() - """Loads saved state from config and initializes the UI.""" last_mode = self.config_manager.get_setting("last_mode", "backup") - # Pre-load data from config before initializing the UI backup_source_path = self.config_manager.get_setting( "backup_source_path") if backup_source_path and os.path.isdir(backup_source_path): @@ -317,9 +313,8 @@ class MainApplication(tk.Tk): if folder_name: icon_name = self.buttons_map[folder_name]['icon'] else: - # Handle custom folder path folder_name = os.path.basename(backup_source_path.rstrip('/')) - icon_name = 'folder_extralarge' # A generic folder icon + icon_name = 'folder_extralarge' self.backup_left_canvas_data.update({ 'icon': icon_name, @@ -330,7 +325,7 @@ class MainApplication(tk.Tk): backup_dest_path = self.config_manager.get_setting( "backup_destination_path") if backup_dest_path and os.path.isdir(backup_dest_path): - self.destination_path = backup_dest_path # Still needed for some logic + self.destination_path = backup_dest_path total, used, free = shutil.disk_usage(backup_dest_path) self.backup_right_canvas_data.update({ 'folder': os.path.basename(backup_dest_path.rstrip('/')), @@ -340,7 +335,18 @@ class MainApplication(tk.Tk): self.destination_total_bytes = total self.destination_used_bytes = used - + if hasattr(self, 'header_frame'): + self.header_frame.refresh_status() + + container_path = os.path.join(backup_dest_path, "pybackup_encrypted.luks") + if os.path.exists(container_path): + username = os.path.basename(backup_dest_path.rstrip('/')) + password = self.backup_manager.encryption_manager.get_password_from_keyring(username) + if password: + self.backup_manager.encryption_manager.unlock_container(backup_dest_path, password) + app_logger.log("Automatically unlocked encrypted container.") + if hasattr(self, 'header_frame'): + self.header_frame.refresh_status() restore_src_path = self.config_manager.get_setting( "restore_source_path") @@ -353,7 +359,6 @@ class MainApplication(tk.Tk): restore_dest_path = self.config_manager.get_setting( "restore_destination_path") if restore_dest_path and os.path.isdir(restore_dest_path): - # Find the corresponding button_text for the path folder_name = "" for name, path_obj in AppConfig.FOLDER_PATHS.items(): if str(path_obj) == restore_dest_path: @@ -366,18 +371,14 @@ class MainApplication(tk.Tk): 'path_display': restore_dest_path, }) - # Initialize UI for the last active mode self.navigation.initialize_ui_for_mode(last_mode) - # Trigger calculations if needed if last_mode == 'backup': self.after(100, self.actions.on_sidebar_button_click, self.backup_left_canvas_data.get('folder', 'Computer')) elif last_mode == 'restore': - # Trigger calculation for the right canvas (source) if a path is set if restore_src_path: self.drawing.calculate_restore_folder_size() - # Trigger calculation for the left canvas (destination) based on last selection restore_dest_folder = self.restore_left_canvas_data.get( 'folder', 'Computer') self.after(100, self.actions.on_sidebar_button_click, @@ -406,14 +407,11 @@ class MainApplication(tk.Tk): def _setup_backup_content_frame(self): self.backup_content_frame = BackupContentFrame( - self.content_frame, self.backup_manager, self.actions, padding=10) + self.content_frame, self.backup_manager, self.actions, self, padding=10) self.backup_content_frame.grid(row=2, column=0, sticky="nsew") self.backup_content_frame.grid_remove() - - def _setup_task_bar(self): - # Define all boolean vars at the top to ensure they exist before use. self.vollbackup_var = tk.BooleanVar() self.inkrementell_var = tk.BooleanVar() self.genaue_berechnung_var = tk.BooleanVar() @@ -429,7 +427,6 @@ class MainApplication(tk.Tk): self.info_checkbox_frame, text=Msg.STR["info_text_placeholder"]) self.info_label.pack(anchor=tk.W, fill=tk.X, pady=5) - # Frame for time info self.time_info_frame = ttk.Frame(self.info_checkbox_frame) self.time_info_frame.pack(anchor=tk.W, fill=tk.X, pady=5) @@ -445,7 +442,6 @@ class MainApplication(tk.Tk): self.time_info_frame, text="Ende: --:--:--") self.end_time_label.pack(side=tk.LEFT, padx=5) - # --- Accurate Size Calculation Frame (on the right) --- accurate_size_frame = ttk.Frame(self.time_info_frame) accurate_size_frame.pack(side=tk.LEFT, padx=20) @@ -513,13 +509,12 @@ class MainApplication(tk.Tk): self.action_frame, text=Msg.STR["start"], command=self.actions.toggle_start_cancel, state="disabled") self.start_pause_button.grid(row=0, column=2, rowspan=2, padx=5) - - def on_closing(self): - """Handles window closing events and saves the app state.""" + if self.destination_path: + self.backup_manager.encryption_manager.lock_container(self.destination_path) + self.config_manager.set_setting("last_mode", self.mode) - # Save paths from the data dictionaries if self.backup_left_canvas_data.get('path_display'): self.config_manager.set_setting( "backup_source_path", self.backup_left_canvas_data['path_display']) @@ -544,7 +539,6 @@ class MainApplication(tk.Tk): else: self.config_manager.set_setting("restore_source_path", None) - # Stop any ongoing animations before destroying the application if self.left_canvas_animation: self.left_canvas_animation.stop() self.left_canvas_animation.destroy() @@ -562,21 +556,15 @@ class MainApplication(tk.Tk): self.destroy() def _process_queue(self): - """ - Processes all messages from background threads to update the UI safely. - This is the single, consolidated queue processing loop for the entire application. - It processes messages in batches to avoid freezing the UI. - """ try: - for _ in range(100): # Process up to 100 messages at a time + for _ in range(100): message = self.queue.get_nowait() - # --- Size Calculation Message Handling (from data_processing) --- if isinstance(message, tuple) and len(message) in [3, 5]: calc_type, status = None, None if len(message) == 5: button_text, folder_size, mode_when_started, calc_type, status = message - else: # len == 3 + else: button_text, folder_size, mode_when_started = message if mode_when_started != self.mode: @@ -641,7 +629,6 @@ class MainApplication(tk.Tk): text=Msg.STR["accurate_size_failed"], foreground="#D32F2F") self.current_file_label.config(text="") - # --- Backup/Deletion Message Handling (from main_app) --- elif isinstance(message, tuple) and len(message) == 2: message_type, value = message @@ -707,15 +694,13 @@ class MainApplication(tk.Tk): self.actions._set_ui_state(True) self.backup_content_frame.system_backups_frame._load_backup_content() elif message_type == 'completion_accurate': - # This is now handled by the len=5 case above pass else: app_logger.log(f"Unknown message in queue: {message}") except Empty: - pass # The queue is empty, do nothing. + pass finally: - # Always schedule the next check. self.after(100, self._process_queue) def _update_duration(self): @@ -732,11 +717,6 @@ class MainApplication(tk.Tk): self.on_closing() def update_backup_options_from_config(self): - """ - Reads the 'force' settings from the config and updates the main UI checkboxes. - A 'force' setting overrides the user's selection and disables the control. - """ - # Full/Incremental Logic force_full = self.config_manager.get_setting( "force_full_backup", False) force_incremental = self.config_manager.get_setting( @@ -753,14 +733,12 @@ class MainApplication(tk.Tk): self.full_backup_cb.config(state="disabled") self.incremental_cb.config(state="disabled") - # Compression Logic force_compression = self.config_manager.get_setting( "force_compression", False) if force_compression: self.compressed_var.set(True) self.compressed_cb.config(state="disabled") - # Encryption Logic force_encryption = self.config_manager.get_setting( "force_encryption", False) if force_encryption: @@ -773,7 +751,6 @@ class MainApplication(tk.Tk): if __name__ == "__main__": import argparse import sys - import shutil parser = argparse.ArgumentParser(description="Py-Backup Application.") parser.add_argument( diff --git a/pbp_app_config.py b/pbp_app_config.py index 168315f..f6ecacf 100644 --- a/pbp_app_config.py +++ b/pbp_app_config.py @@ -299,6 +299,8 @@ class Msg: "cat_documents": _("Documents"), "cat_music": _("Music"), "cat_videos": _("Videos"), + "show_encrypted_backups": _("Show Encrypted Backups"), + "show_normal_backups": _("Show Normal Backups"), # Browser View "backup_location": _("Backup Location"), @@ -315,6 +317,8 @@ class Msg: "err_no_dest_folder": _("Please select a destination folder."), "err_no_source_folder": _("Please select at least one source folder."), "err_no_backup_selected": _("Please select a backup from the list."), + "err_unlock_failed": _("Failed to unlock the container. Please check the password and try again."), + "err_encrypted_not_mounted": _("Encrypted container is not unlocked. Please unlock it first from the header bar."), "confirm_user_restore_title": _("Confirm User Data Restore"), "confirm_user_restore_msg": _("Do you really want to restore the backup of '{backup_name}' to its original location? Any newer files may be overwritten."), "confirm_delete_title": _("Confirm Deletion"), @@ -339,6 +343,7 @@ class Msg: "projected_usage_label": _("Projected usage after backup"), "header_title": _("Lx Tools Py-Backup"), "header_subtitle": _("Simple GUI for rsync"), + "encrypted_backup_content": _("Encrypted Backups"), "compressed": _("Compressed"), "encrypted": _("Encrypted"), "bypass_security": _("Bypass security"), diff --git a/pyimage_ui/actions.py b/pyimage_ui/actions.py index 7e3e754..213c66b 100644 --- a/pyimage_ui/actions.py +++ b/pyimage_ui/actions.py @@ -345,6 +345,7 @@ class Actions: }) self.app.config_manager.set_setting( "backup_destination_path", path) + self.app.header_frame.refresh_status() # Refresh keyring status self.app.drawing.redraw_right_canvas() self.app.drawing.update_target_projection() diff --git a/pyimage_ui/backup_content_frame.py b/pyimage_ui/backup_content_frame.py index 3f966b6..31269f4 100644 --- a/pyimage_ui/backup_content_frame.py +++ b/pyimage_ui/backup_content_frame.py @@ -1,27 +1,30 @@ import tkinter as tk from tkinter import ttk +import os from pbp_app_config import Msg from pyimage_ui.system_backup_content_frame import SystemBackupContentFrame from pyimage_ui.user_backup_content_frame import UserBackupContentFrame -from shared_libs.animated_icon import AnimatedIcon from shared_libs.logger import app_logger - +from shared_libs.message import MessageDialog class BackupContentFrame(ttk.Frame): - def __init__(self, master, backup_manager, actions, **kwargs): + def __init__(self, master, backup_manager, actions, app, **kwargs): super().__init__(master, **kwargs) + app_logger.log("BackupContentFrame: __init__ called") self.backup_manager = backup_manager self.actions = actions - self.master = master + self.app = app - self.backup_path = None + self.base_backup_path = None self.current_view_index = 0 + self.viewing_encrypted = False + + self.grid_rowconfigure(1, weight=1) + self.grid_columnconfigure(0, weight=1) header_frame = ttk.Frame(self) header_frame.grid(row=0, column=0, sticky=tk.NSEW) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(0, weight=1) top_nav_frame = ttk.Frame(header_frame) top_nav_frame.pack(side=tk.LEFT) @@ -50,32 +53,82 @@ class BackupContentFrame(ttk.Frame): ttk.Separator(top_nav_frame, orient=tk.VERTICAL).pack( side=tk.LEFT, fill=tk.Y, padx=2) - # Deletion Status UI - self.deletion_status_frame = ttk.Frame(header_frame) - self.deletion_status_frame.pack(side=tk.LEFT, padx=15) + content_container = ttk.Frame(self) + content_container.grid(row=1, column=0, sticky="nsew") + content_container.grid_rowconfigure(0, weight=1) + content_container.grid_columnconfigure(0, weight=1) - bg_color = self.winfo_toplevel().style.lookup('TFrame', 'background') - self.deletion_animated_icon = AnimatedIcon( - self.deletion_status_frame, width=20, height=20, use_pillow=True, bg=bg_color, animation_type="counter_arc") - self.deletion_animated_icon.pack(side=tk.LEFT, padx=5) - self.deletion_animated_icon.stop("DISABLE") + self.system_backups_frame = SystemBackupContentFrame(content_container, backup_manager, actions, parent_view=self) + self.user_backups_frame = UserBackupContentFrame(content_container, backup_manager, actions, parent_view=self) + self.system_backups_frame.grid(row=0, column=0, sticky=tk.NSEW) + self.user_backups_frame.grid(row=0, column=0, sticky=tk.NSEW) - self.deletion_status_label = ttk.Label( - self.deletion_status_frame, text="", font=("Ubuntu", 10, "bold")) - self.deletion_status_label.pack(side=tk.LEFT, padx=5) + action_button_frame = ttk.Frame(self, padding=10) + action_button_frame.grid(row=2, column=0, sticky="ew") - # --- Content Frames --- - self.system_backups_frame = SystemBackupContentFrame( - self, backup_manager, actions) - self.user_backups_frame = UserBackupContentFrame(self, backup_manager, actions) + self.toggle_encrypted_button = ttk.Button(action_button_frame, text=Msg.STR["show_encrypted_backups"], command=self._toggle_encrypted_view) + self.toggle_encrypted_button.pack(side=tk.LEFT, padx=5) - self.system_backups_frame.grid(row=1, column=0, sticky=tk.NSEW) - self.user_backups_frame.grid(row=1, column=0, sticky=tk.NSEW) + self.restore_button = ttk.Button(action_button_frame, text=Msg.STR["restore"], command=self._restore_selected, state="disabled") + self.restore_button.pack(side=tk.LEFT, padx=5) - self._switch_view(self.current_view_index) + self.delete_button = ttk.Button(action_button_frame, text=Msg.STR["delete"], command=self._delete_selected, state="disabled") + self.delete_button.pack(side=tk.LEFT, padx=5) + + self.edit_comment_button = ttk.Button(action_button_frame, text=Msg.STR["comment"], command=self._edit_comment, state="disabled") + self.edit_comment_button.pack(side=tk.LEFT, padx=5) + + self._switch_view(0) + + def update_button_state(self, is_selected): + self.restore_button.config(state="normal" if is_selected else "disabled") + self.delete_button.config(state="normal" if is_selected else "disabled") + self.edit_comment_button.config(state="normal" if is_selected else "disabled") + + def _get_active_subframe(self): + return self.system_backups_frame if self.current_view_index == 0 else self.user_backups_frame + + def _restore_selected(self): + self._get_active_subframe()._restore_selected() + + def _delete_selected(self): + self._get_active_subframe()._delete_selected() + + def _edit_comment(self): + self._get_active_subframe()._edit_comment() + + def _toggle_encrypted_view(self): + if not self.app.destination_path: + MessageDialog(master=self.app, message_type="info", title=Msg.STR["info_menu"], text=Msg.STR["err_no_dest_folder"]) + return + + pybackup_dir = os.path.join(self.app.destination_path, "pybackup") + + if not self.viewing_encrypted: + username = os.path.basename(self.app.destination_path.rstrip('/')) + password = self.app.backup_manager.encryption_manager.get_password(username, confirm=False) + if not password: + return + + mount_point = self.app.backup_manager.encryption_manager.unlock_container(pybackup_dir, password) + if mount_point: + self.viewing_encrypted = True + self.toggle_encrypted_button.config(text=Msg.STR["show_normal_backups"]) + self.show(mount_point) + self.app.header_frame.refresh_status() + else: + MessageDialog(master=self.app, message_type="error", title=Msg.STR["error"], text=Msg.STR["err_unlock_failed"]) + else: + self.app.backup_manager.encryption_manager.lock_container(pybackup_dir) + self.viewing_encrypted = False + self.toggle_encrypted_button.config(text=Msg.STR["show_encrypted_backups"]) + self.show(self.app.destination_path) + self.app.header_frame.refresh_status() def _switch_view(self, index): self.current_view_index = index + config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view" + self.app.config_manager.set_setting(config_key, index) self.update_nav_buttons(index) if index == 0: @@ -84,6 +137,7 @@ class BackupContentFrame(ttk.Frame): else: self.user_backups_frame.grid() self.system_backups_frame.grid_remove() + self.update_button_state(False) def update_nav_buttons(self, active_index): for i, button in enumerate(self.nav_buttons): @@ -96,26 +150,26 @@ class BackupContentFrame(ttk.Frame): self.nav_progress_bars[i].pack_forget() def show(self, backup_path): + app_logger.log(f"BackupContentFrame: show called with path {backup_path}") self.grid(row=2, column=0, sticky="nsew") - if backup_path and self.backup_path != backup_path: - self.backup_path = backup_path - self.system_backups_frame.show(backup_path) - self.user_backups_frame.show(backup_path) - # Ensure the correct view is shown upon revealing the frame - self._switch_view(self.current_view_index) + self.base_backup_path = backup_path + if self.viewing_encrypted: + actual_backup_path = backup_path + else: + actual_backup_path = os.path.join(backup_path, "pybackup") - def hide(self): - self.grid_remove() + if not os.path.isdir(actual_backup_path): + app_logger.log(f"Backup path {actual_backup_path} does not exist or is not a directory.") + # Clear views if path is invalid + self.system_backups_frame.show(None) + self.user_backups_frame.show(None) + return - def show_deletion_status(self, text: str): - app_logger.log(f"Showing deletion status: {text}") - self.deletion_status_label.config(text=text) - self.deletion_animated_icon.start() - self.deletion_status_frame.pack(side=tk.LEFT, padx=15) - - def hide_deletion_status(self): - app_logger.log("Hiding deletion status.") - self.deletion_animated_icon.stop("DISABLE") - self.deletion_status_frame.pack_forget() + self.system_backups_frame.show(actual_backup_path) + self.user_backups_frame.show(actual_backup_path) + + config_key = "last_encrypted_backup_content_view" if self.viewing_encrypted else "last_backup_content_view" + last_view = self.app.config_manager.get_setting(config_key, 0) + self._switch_view(last_view) \ No newline at end of file diff --git a/pyimage_ui/header_frame.py b/pyimage_ui/header_frame.py index c69ea44..0e04a31 100644 --- a/pyimage_ui/header_frame.py +++ b/pyimage_ui/header_frame.py @@ -1,14 +1,16 @@ import tkinter as tk +import os from pbp_app_config import Msg from shared_libs.common_tools import IconManager - class HeaderFrame(tk.Frame): - def __init__(self, container, image_manager, **kwargs): + def __init__(self, container, image_manager, encryption_manager, app, **kwargs): super().__init__(container, bg="#455A64", **kwargs) self.image_manager = image_manager + self.encryption_manager = encryption_manager + self.app = app # Configure grid weights for internal layout self.columnconfigure(1, weight=1) # Make the middle column expand @@ -48,18 +50,48 @@ class HeaderFrame(tk.Frame): subtitle_label.grid(row=1, column=1, sticky="w", padx=(5, 20), pady=(0, 10)) - # Right side: Placeholder for future info or buttons + # Right side: Keyring status right_frame = tk.Frame(self, bg="#455A64") right_frame.grid(row=0, column=2, rowspan=2, sticky="nsew") right_frame.columnconfigure(0, weight=1) right_frame.rowconfigure(0, weight=1) - # Example of content for the right side (can be removed or replaced) - # info_label = tk.Label( - # right_frame, - # text="Some Info Here", - # font=("Helvetica", 10), - # fg="#ecf0f1", - # bg="#455A64", - # ) - # info_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0)) + self.keyring_status_label = tk.Label( + right_frame, + text="", + font=("Helvetica", 10, "bold"), + bg="#455A64", + ) + self.keyring_status_label.grid(row=0, column=0, sticky="ne", padx=(10, 10), pady=(10, 0)) + + self.refresh_status() + + def refresh_status(self): + """Checks the keyring status based on the current destination and updates the label.""" + dest_path = self.app.destination_path + if not dest_path: + self.keyring_status_label.config( + text="Keyring: N/A", + fg="#A9A9A9" # DarkGray + ) + return + + username = os.path.basename(dest_path.rstrip('/')) + mapper_name = f"pybackup_{username}" + mount_point = f"/mnt/{mapper_name}" + + if os.path.ismount(mount_point): + self.keyring_status_label.config( + text="Keyring: In Use", + fg="#2E8B57" # SeaGreen + ) + elif self.encryption_manager.is_key_in_keyring(username): + self.keyring_status_label.config( + text="Keyring: Available", + fg="#FFD700" # Gold + ) + else: + self.keyring_status_label.config( + text="Keyring: Not in Use", + fg="#A9A9A9" # DarkGray + ) diff --git a/pyimage_ui/navigation.py b/pyimage_ui/navigation.py index ac6f22d..748a82d 100644 --- a/pyimage_ui/navigation.py +++ b/pyimage_ui/navigation.py @@ -141,7 +141,7 @@ class Navigation: self.app.log_frame.grid_remove() self.app.scheduler_frame.hide() self.app.settings_frame.hide() - self.app.backup_content_frame.hide() + self.app.backup_content_frame.grid_remove() # Show the main content frames @@ -186,7 +186,7 @@ class Navigation: self.app.log_frame.grid_remove() self.app.scheduler_frame.hide() self.app.settings_frame.hide() - self.app.backup_content_frame.hide() + self.app.backup_content_frame.grid_remove() self.app.canvas_frame.grid() self.app.source_size_frame.grid() @@ -224,7 +224,7 @@ class Navigation: self.app.canvas_frame.grid_remove() self.app.scheduler_frame.hide() self.app.settings_frame.hide() - self.app.backup_content_frame.hide() + self.app.backup_content_frame.grid_remove() self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() self.app.restore_size_frame_before.grid_remove() @@ -241,7 +241,7 @@ class Navigation: self.app.canvas_frame.grid_remove() self.app.log_frame.grid_remove() self.app.settings_frame.hide() - self.app.backup_content_frame.hide() + self.app.backup_content_frame.grid_remove() self.app.source_size_frame.grid_remove() self.app.target_size_frame.grid_remove() @@ -258,7 +258,7 @@ class Navigation: self.app.canvas_frame.grid_remove() self.app.log_frame.grid_remove() - self.app.backup_content_frame.hide() + self.app.backup_content_frame.grid_remove() self.app.scheduler_frame.hide() self.app.source_size_frame.grid_remove() diff --git a/pyimage_ui/system_backup_content_frame.py b/pyimage_ui/system_backup_content_frame.py index 7b29f2b..76f02ec 100644 --- a/pyimage_ui/system_backup_content_frame.py +++ b/pyimage_ui/system_backup_content_frame.py @@ -5,17 +5,15 @@ import os from pbp_app_config import Msg from pyimage_ui.comment_editor_dialog import CommentEditorDialog - class SystemBackupContentFrame(ttk.Frame): - def __init__(self, master, backup_manager, actions, **kwargs): + def __init__(self, master, backup_manager, actions, parent_view, **kwargs): super().__init__(master, **kwargs) self.backup_manager = backup_manager self.actions = actions + self.parent_view = parent_view self.system_backups_list = [] - self.backup_path = None - # --- Color Tags --- self.tag_colors = [ ("full_blue", "#0078D7", "inc_blue", "#50E6FF"), ("full_orange", "#E8740C", "inc_orange", "#FFB366"), @@ -23,24 +21,13 @@ class SystemBackupContentFrame(ttk.Frame): ("full_purple", "#8B107C", "inc_purple", "#D46EE5"), ] - # --- Backup Content List View --- - self.content_frame = ttk.LabelFrame( - self, text=Msg.STR["backup_content"], padding=10) - self.content_frame.pack(fill=tk.BOTH, expand=True) - columns = ("date", "time", "type", "size", "comment") - self.content_tree = ttk.Treeview( - self.content_frame, columns=columns, show="headings") - self.content_tree.heading( - "date", text=Msg.STR["date"]) - self.content_tree.heading( - "time", text=Msg.STR["time"]) - self.content_tree.heading( - "type", text=Msg.STR["type"]) - self.content_tree.heading( - "size", text=Msg.STR["size"]) - self.content_tree.heading( - "comment", text=Msg.STR["comment"]) + self.content_tree = ttk.Treeview(self, columns=columns, show="headings") + self.content_tree.heading("date", text=Msg.STR["date"]) + self.content_tree.heading("time", text=Msg.STR["time"]) + self.content_tree.heading("type", text=Msg.STR["type"]) + self.content_tree.heading("size", text=Msg.STR["size"]) + self.content_tree.heading("comment", text=Msg.STR["comment"]) self.content_tree.column("date", width=100, anchor="w") self.content_tree.column("time", width=80, anchor="center") @@ -49,31 +36,11 @@ class SystemBackupContentFrame(ttk.Frame): self.content_tree.column("comment", width=300, anchor="w") self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - self.content_tree.bind("<>", self._on_item_select) - list_button_frame = ttk.Frame(self.content_frame) - list_button_frame.pack(pady=10) - - self.restore_button = ttk.Button(list_button_frame, text=Msg.STR["restore"], - command=self._restore_selected, state="disabled") - self.restore_button.pack(side=tk.LEFT, padx=5) - - self.delete_button = ttk.Button(list_button_frame, text=Msg.STR["delete"], - command=self._delete_selected, state="disabled") - self.delete_button.pack(side=tk.LEFT, padx=5) - - self.edit_comment_button = ttk.Button(list_button_frame, text="Kommentar bearbeiten", - command=self._edit_comment, state="disabled") - self.edit_comment_button.pack(side=tk.LEFT, padx=5) - def show(self, backup_path): - if backup_path and self.backup_path != backup_path: - self.backup_path = backup_path - self._load_backup_content() - - def hide(self): - self.grid_remove() + self.backup_path = backup_path + self._load_backup_content() def _load_backup_content(self): for i in self.content_tree.get_children(): @@ -82,23 +49,19 @@ class SystemBackupContentFrame(ttk.Frame): if not self.backup_path or not os.path.isdir(self.backup_path): return - system_backups = self.backup_manager.list_system_backups(self.backup_path) - self.system_backups_list = system_backups # Store for later use + self.system_backups_list = self.backup_manager.list_system_backups(self.backup_path) - color_index = 0 - for i, backup_info in enumerate(system_backups): - # Determine the color tag for the group + color_index = -1 + for i, backup_info in enumerate(self.system_backups_list): if backup_info.get("backup_type_base") == "Full": - if i > 0: # Not the very first backup - color_index = (color_index + 1) % len(self.tag_colors) - - tag_name, color, _, _ = self.tag_colors[color_index] - self.content_tree.tag_configure(tag_name, foreground=color) - current_tag = tag_name - else: # Incremental - _, _, tag_name, color = self.tag_colors[color_index] - self.content_tree.tag_configure(tag_name, foreground=color) - current_tag = tag_name + color_index = (color_index + 1) % len(self.tag_colors) + full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index] + self.content_tree.tag_configure(full_tag, foreground=full_color) + self.content_tree.tag_configure(inc_tag, foreground=inc_color, font=("Helvetica", 10, "bold")) + current_tag = full_tag + else: + _, _, inc_tag, _ = self.tag_colors[color_index] + current_tag = inc_tag self.content_tree.insert("", "end", values=( backup_info.get("date", "N/A"), @@ -108,79 +71,57 @@ class SystemBackupContentFrame(ttk.Frame): backup_info.get("comment", ""), ), tags=(current_tag,), iid=backup_info.get("folder_name")) - self._on_item_select(None) # Disable buttons initially + self._on_item_select(None) def _on_item_select(self, event): - selected_item = self.content_tree.focus() - is_selected = True if selected_item else False - self.restore_button.config( - state="normal" if is_selected else "disabled") - self.delete_button.config( - state="normal" if is_selected else "disabled") - self.edit_comment_button.config( - state="normal" if is_selected else "disabled") + is_selected = True if self.content_tree.focus() else False + self.parent_view.update_button_state(is_selected) def _edit_comment(self): selected_item_id = self.content_tree.focus() if not selected_item_id: return - # Construct the path to the info file - pybackup_path = os.path.join(self.backup_path, "pybackup") - info_file_path = os.path.join(pybackup_path, f"{selected_item_id}.txt") + info_file_path = os.path.join(self.backup_path, f"{selected_item_id}.txt") if not os.path.exists(info_file_path): self.backup_manager.update_comment(info_file_path, "") CommentEditorDialog(self, info_file_path, self.backup_manager) - self._load_backup_content() # Refresh list to show new comment + self._load_backup_content() def _restore_selected(self): selected_item_id = self.content_tree.focus() if not selected_item_id: return - selected_backup = None - for backup in self.system_backups_list: - if backup.get("folder_name") == selected_item_id: - selected_backup = backup - break + selected_backup = next((b for b in self.system_backups_list if b.get("folder_name") == selected_item_id), None) if not selected_backup: - print(f"Error: Could not find backup info for {selected_item_id}") return - try: - main_app = self.winfo_toplevel() - restore_dest_path = main_app.config_manager.get_setting( - "restore_destination_path", "/") + main_app = self.winfo_toplevel() + restore_dest_path = main_app.config_manager.get_setting("restore_destination_path", "/") - if not restore_dest_path: - print("Error: Restore destination not set.") - return + if not restore_dest_path: + return - self.backup_manager.start_restore( - source_path=selected_backup['full_path'], - dest_path=restore_dest_path, - is_compressed=selected_backup['is_compressed'] - ) - except AttributeError: - print("Could not access main application instance to get restore path.") + self.backup_manager.start_restore( + source_path=selected_backup['full_path'], + dest_path=restore_dest_path, + is_compressed=selected_backup['is_compressed'] + ) def _delete_selected(self): selected_item_id = self.content_tree.focus() if not selected_item_id: return - # Construct the full path to the backup folder - pybackup_path = os.path.join(self.backup_path, "pybackup") - folder_to_delete = os.path.join(pybackup_path, selected_item_id) + folder_to_delete = os.path.join(self.backup_path, selected_item_id) - # Lock UI and show status - self.actions._set_ui_state(False) # Lock UI - self.master.show_deletion_status( - Msg.STR["deleting_backup_in_progress"]) + self.actions._set_ui_state(False) + # This needs to be adapted, as the deletion status is now in the parent view + # self.master.show_deletion_status(Msg.STR["deleting_backup_in_progress"]) - # Start deletion in background self.backup_manager.start_delete_system_backup( - folder_to_delete, self.winfo_toplevel().queue) + folder_to_delete, self.winfo_toplevel().queue) \ No newline at end of file diff --git a/pyimage_ui/user_backup_content_frame.py b/pyimage_ui/user_backup_content_frame.py index c80bb9c..18dca7b 100644 --- a/pyimage_ui/user_backup_content_frame.py +++ b/pyimage_ui/user_backup_content_frame.py @@ -1,87 +1,28 @@ import tkinter as tk from tkinter import ttk import os +import shutil from pbp_app_config import Msg from pyimage_ui.comment_editor_dialog import CommentEditorDialog - +from shared_libs.message import MessageDialog class UserBackupContentFrame(ttk.Frame): - def __init__(self, master, backup_manager, actions, **kwargs): - super().__init__(master, **kwargs) - - self.backup_manager = backup_manager - self.backup_path = None - self.actions = actions # Store actions object - - # --- Backup Content List View --- - self.content_frame = ttk.LabelFrame( - self, text=Msg.STR["backup_content"], padding=10) - self.content_frame.pack(fill=tk.BOTH, expand=True) - - columns = ("date", "size", "comment", "folder_name") - self.content_tree = ttk.Treeview( - self.content_frame, columns=columns, show="headings") - self.content_tree.heading( - "date", text=Msg.STR["date"]) - self.content_tree.heading( - "size", text=Msg.STR["size"]) - self.content_tree.heading( - "comment", text=Msg.STR["comment"]) - self.content_tree.heading( - "folder_name", text=Msg.STR["folder"]) - - self.content_tree.column("date", width=120, anchor="w") - self.content_tree.column("size", width=100, anchor="e") - self.content_tree.column("comment", width=200, anchor="w") - self.content_tree.column("folder_name", width=250, anchor="w") - - self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - self.content_tree.bind("<>", self._on_item_select) - - list_button_frame = ttk.Frame(self.content_frame) - list_button_frame.pack(pady=10) - self.restore_button = ttk.Button(list_button_frame, text=Msg.STR["restore"], - command=self._restore_selected, state="disabled") - self.restore_button.pack(side=tk.LEFT, padx=5) - self.delete_button = ttk.Button(list_button_frame, text=Msg.STR["delete"], - command=self._delete_selected, state="disabled") - self.delete_button.pack(side=tk.LEFT, padx=5) - import tkinter as tk -from tkinter import ttk -import os - -from pbp_app_config import Msg -from pyimage_ui.comment_editor_dialog import CommentEditorDialog - - -class UserBackupContentFrame(ttk.Frame): - def __init__(self, master, backup_manager, actions, **kwargs): + def __init__(self, master, backup_manager, actions, parent_view, **kwargs): super().__init__(master, **kwargs) self.backup_manager = backup_manager self.actions = actions + self.parent_view = parent_view self.user_backups_list = [] - self.backup_path = None - # --- Backup Content List View --- - self.content_frame = ttk.LabelFrame( - self, text=Msg.STR["backup_content"], padding=10) - self.content_frame.pack(fill=tk.BOTH, expand=True) - columns = ("date", "time", "size", "comment", "folder_name") - self.content_tree = ttk.Treeview( - self.content_frame, columns=columns, show="headings") - self.content_tree.heading( - "date", text=Msg.STR["date"]) - self.content_tree.heading( - "time", text=Msg.STR["time"]) - self.content_tree.heading( - "size", text=Msg.STR["size"]) - self.content_tree.heading( - "comment", text=Msg.STR["comment"]) - self.content_tree.heading( - "folder_name", text=Msg.STR["folder"]) + self.content_tree = ttk.Treeview(self, columns=columns, show="headings") + self.content_tree.heading("date", text=Msg.STR["date"]) + self.content_tree.heading("time", text=Msg.STR["time"]) + self.content_tree.heading("size", text=Msg.STR["size"]) + self.content_tree.heading("comment", text=Msg.STR["comment"]) + self.content_tree.heading("folder_name", text=Msg.STR["folder"]) self.content_tree.column("date", width=100, anchor="w") self.content_tree.column("time", width=80, anchor="center") @@ -90,31 +31,11 @@ class UserBackupContentFrame(ttk.Frame): self.content_tree.column("folder_name", width=200, anchor="w") self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - self.content_tree.bind("<>", self._on_item_select) - list_button_frame = ttk.Frame(self.content_frame) - list_button_frame.pack(pady=10) - - self.restore_button = ttk.Button(list_button_frame, text=Msg.STR["restore"], - command=self._restore_selected, state="disabled") - self.restore_button.pack(side=tk.LEFT, padx=5) - - self.delete_button = ttk.Button(list_button_frame, text=Msg.STR["delete"], - command=self._delete_selected, state="disabled") - self.delete_button.pack(side=tk.LEFT, padx=5) - - self.edit_comment_button = ttk.Button(list_button_frame, text="Kommentar bearbeiten", - command=self._edit_comment, state="disabled") - self.edit_comment_button.pack(side=tk.LEFT, padx=5) - def show(self, backup_path): - if backup_path and self.backup_path != backup_path: - self.backup_path = backup_path - self._load_backup_content() - - def hide(self): - self.grid_remove() + self.backup_path = backup_path + self._load_backup_content() def _load_backup_content(self): for i in self.content_tree.get_children(): @@ -133,17 +54,11 @@ class UserBackupContentFrame(ttk.Frame): backup_info.get("comment", ""), backup_info.get("folder_name", "N/A") ), iid=backup_info.get("folder_name")) - self._on_item_select(None) # Disable buttons initially + self._on_item_select(None) def _on_item_select(self, event): - selected_item = self.content_tree.focus() - is_selected = True if selected_item else False - self.restore_button.config( - state="normal" if is_selected else "disabled") - self.delete_button.config( - state="normal" if is_selected else "disabled") - self.edit_comment_button.config( - state="normal" if is_selected else "disabled") + is_selected = True if self.content_tree.focus() else False + self.parent_view.update_button_state(is_selected) def _edit_comment(self): selected_item_id = self.content_tree.focus() @@ -156,109 +71,37 @@ class UserBackupContentFrame(ttk.Frame): self.backup_manager.update_comment(info_file_path, "") CommentEditorDialog(self, info_file_path, self.backup_manager) - self._load_backup_content() # Refresh list to show new comment + self._load_backup_content() def _restore_selected(self): - # This functionality needs to be implemented based on app requirements - pass + selected_item_id = self.content_tree.focus() + if not selected_item_id: + return + + MessageDialog(master=self, message_type="info", title="Info", text="User restore not implemented yet.") def _delete_selected(self): - # This functionality needs to be implemented based on app requirements - pass - self.edit_comment_button.pack(side=tk.LEFT, padx=5) - - def show(self, backup_path): - if backup_path and self.backup_path != backup_path: - self.backup_path = backup_path - self._load_backup_content() - - def hide(self): - self.grid_remove() - - def _load_backup_content(self): - for i in self.content_tree.get_children(): - self.content_tree.delete(i) - - if not self.backup_path or not os.path.isdir(self.backup_path): + selected_item_id = self.content_tree.focus() + if not selected_item_id: return - user_backups = self.backup_manager.list_user_backups(self.backup_path) + folder_to_delete = os.path.join(self.backup_path, selected_item_id) + info_file_to_delete = os.path.join(self.backup_path, f"{selected_item_id}.txt") - for backup_info in user_backups: - self.content_tree.insert("", "end", values=( - backup_info.get("date", "N/A"), - backup_info.get("size", "N/A"), - backup_info.get("comment", ""), - backup_info.get("folder_name", "N/A") - )) - self._on_item_select(None) - - def _on_item_select(self, event): - selected_item = self.content_tree.focus() - is_selected = True if selected_item else False - self.restore_button.config( - state="normal" if is_selected else "disabled") - self.delete_button.config( - state="normal" if is_selected else "disabled") - self.edit_comment_button.config( - state="normal" if is_selected else "disabled") - - def _edit_comment(self): - selected_item = self.content_tree.focus() - if not selected_item: - return - - item_values = self.content_tree.item(selected_item)["values"] - folder_name = item_values[3] # Assuming folder_name is the 4th value - - # Construct the path to the info file - info_file_path = os.path.join(self.backup_path, f"{folder_name}.txt") - - if not os.path.exists(info_file_path): - self.backup_manager.update_comment(info_file_path, "") - - CommentEditorDialog(self, info_file_path, self.backup_manager) - self._load_backup_content() # Refresh list to show new comment - - def _restore_selected(self): - # Placeholder for restore logic - selected_item = self.content_tree.focus() - if not selected_item: - return - backup_name = self.content_tree.item(selected_item)["values"][0] - print(f"Restoring {backup_name}...") - - def _delete_selected(self): - selected_item = self.content_tree.focus() - if not selected_item: - return - - item_values = self.content_tree.item(selected_item)["values"] - folder_name = item_values[3] # Assuming folder_name is the 4th value - - # Construct the full path to the backup folder - folder_to_delete = os.path.join(self.backup_path, folder_name) - info_file_to_delete = os.path.join( - self.backup_path, f"{folder_name}.txt") - - # Ask for confirmation - from shared_libs.message import MessageDialog dialog = MessageDialog(master=self, message_type="warning", title=Msg.STR["confirm_delete_title"], text=Msg.STR["confirm_delete_text"].format( - folder_name=folder_name), + folder_name=selected_item_id), buttons=["ok_cancel"]) if dialog.get_result() != "ok": return try: - import shutil if os.path.isdir(folder_to_delete): shutil.rmtree(folder_to_delete) if os.path.exists(info_file_to_delete): os.remove(info_file_to_delete) + self._load_backup_content() except Exception as e: MessageDialog(master=self, message_type="error", title=Msg.STR["error"], text=str(e)) - - self._load_backup_content()