diff --git a/core/backup_manager.py b/core/backup_manager.py index de44a44..2903ada 100644 --- a/core/backup_manager.py +++ b/core/backup_manager.py @@ -450,6 +450,7 @@ class BackupManager: user_backups = sorted([b for b in all_backups if not b["is_system"]], key=lambda x: x['datetime'], reverse=True) + # Group system backups grouped_system_backups = [] temp_group = [] for backup in reversed(system_backups): @@ -470,7 +471,39 @@ class BackupManager: final_system_list = [ item for group in grouped_system_backups for item in group] - return final_system_list, user_backups + # Group user backups by source, then by chains + user_backups_by_source = {} + for backup in user_backups: + source = backup.get('source', 'Unknown') + if source not in user_backups_by_source: + user_backups_by_source[source] = [] + user_backups_by_source[source].append(backup) + + final_user_list = [] + for source in sorted(user_backups_by_source.keys()): + source_backups = user_backups_by_source[source] + + grouped_source_backups = [] + temp_group = [] + for backup in reversed(source_backups): + if backup['backup_type_base'] == 'Full': + if temp_group: + grouped_source_backups.append(temp_group) + temp_group = [backup] + else: + if not temp_group: + grouped_source_backups.append([backup]) + else: + temp_group.append(backup) + if temp_group: + grouped_source_backups.append(temp_group) + + grouped_source_backups.sort(key=lambda g: g[0]['datetime'], reverse=True) + + for group in grouped_source_backups: + final_user_list.extend(group) + + return final_system_list, final_user_list def _find_latest_backup(self, profile_path: str, source_name: str) -> Optional[str]: self.logger.log( diff --git a/pyimage_ui/system_backup_content_frame.py b/pyimage_ui/system_backup_content_frame.py index f1b1fa7..9d727e9 100644 --- a/pyimage_ui/system_backup_content_frame.py +++ b/pyimage_ui/system_backup_content_frame.py @@ -15,13 +15,6 @@ class SystemBackupContentFrame(ttk.Frame): self.system_backups_list = [] self.backup_path = None - self.tag_colors = [ - ("full_blue", "#0078D7", "inc_blue", "#50E6FF"), - ("full_orange", "#E8740C", "inc_orange", "#FFB366"), - ("full_green", "#107C10", "inc_green", "#50E680"), - ("full_purple", "#8B107C", "inc_purple", "#D46EE5"), - ] - columns = ("date", "time", "type", "size", "comment") self.content_tree = ttk.Treeview( self, columns=columns, show="headings") @@ -31,11 +24,11 @@ class SystemBackupContentFrame(ttk.Frame): self.content_tree.heading("size", text=Msg.STR["size"]) self.content_tree.heading("comment", text=Msg.STR["comment"]) - self.content_tree.column("date", width=80, anchor="center") - self.content_tree.column("time", width=60, anchor="center") - self.content_tree.column("type", width=140, anchor="center") + self.content_tree.column("date", width=100, anchor="center") + self.content_tree.column("time", width=100, anchor="center") + self.content_tree.column("type", width=180, anchor="w") self.content_tree.column("size", width=90, anchor="center") - self.content_tree.column("comment", width=350, anchor="w") + self.content_tree.column("comment", width=310, anchor="w") self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.content_tree.bind("<>", self._on_item_select) @@ -52,27 +45,28 @@ class SystemBackupContentFrame(ttk.Frame): if not self.system_backups_list: return + colors = ["#0078D7", "#E8740C", "#107C10", "#8B107C", "#005A9E"] color_index = -1 + current_color_tag = "" + for i, backup_info in enumerate(self.system_backups_list): if backup_info.get("backup_type_base") == "Full": - color_index = (color_index + 1) % len(self.tag_colors) - full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index] + color_index = (color_index + 1) % len(colors) + current_color_tag = f"color_{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 + current_color_tag, foreground=colors[color_index]) + + backup_type_display = backup_info.get("type", "N/A") + if backup_info.get("backup_type_base") != "Full": + backup_type_display = f"▲ {backup_type_display}" self.content_tree.insert("", "end", values=( backup_info.get("date", "N/A"), backup_info.get("time", "N/A"), - backup_info.get("type", "N/A"), + backup_type_display, backup_info.get("size", "N/A"), backup_info.get("comment", ""), - ), tags=(current_tag,), iid=backup_info.get("folder_name")) + ), tags=(current_color_tag,), iid=backup_info.get("folder_name")) self._on_item_select(None) @@ -90,16 +84,15 @@ class SystemBackupContentFrame(ttk.Frame): if not selected_backup: return - is_encrypted = selected_backup.get('is_encrypted', False) - info_file_name = f"{selected_item_id}{'_encrypted' if is_encrypted else ''}.txt" - info_file_path = os.path.join( - self.backup_path, "pybackup", info_file_name) - - if not os.path.exists(info_file_path): - self.backup_manager.update_comment(info_file_path, "") + info_file_path = selected_backup.get('info_file_path') + if not info_file_path or not os.path.isfile(info_file_path): + MessageDialog(self, message_type="error", title="Error", + text=f"Metadata file not found: {info_file_path}") + return CommentEditorDialog(self, info_file_path, self.backup_manager) - self.parent_view.show(self.backup_path) + self.parent_view.show( + self.backup_path, self.system_backups_list) # Refresh view def _restore_selected(self): selected_item_id = self.content_tree.focus() @@ -122,7 +115,7 @@ class SystemBackupContentFrame(ttk.Frame): self.backup_manager.start_restore( source_path=selected_backup['full_path'], dest_path=restore_dest_path, - is_compressed=selected_backup['is_compressed'] + is_compressed=selected_backup.get('is_compressed', False) ) def _delete_selected(self): @@ -142,7 +135,6 @@ class SystemBackupContentFrame(ttk.Frame): if is_encrypted: username = os.path.basename(self.backup_path.rstrip('/')) - # Get password in the UI thread before starting the background task password = self.backup_manager.encryption_manager.get_password( username, confirm=False) if not password: diff --git a/pyimage_ui/user_backup_content_frame.py b/pyimage_ui/user_backup_content_frame.py index f97184d..e0a6142 100644 --- a/pyimage_ui/user_backup_content_frame.py +++ b/pyimage_ui/user_backup_content_frame.py @@ -17,13 +17,6 @@ class UserBackupContentFrame(ttk.Frame): self.user_backups_list = [] self.backup_path = None - self.tag_colors = [ - ("full_blue", "#0078D7", "inc_blue", "#50E6FF"), - ("full_orange", "#E8740C", "inc_orange", "#FFB366"), - ("full_green", "#107C10", "inc_green", "#50E680"), - ("full_purple", "#8B107C", "inc_purple", "#D46EE5"), - ] - columns = ("date", "time", "type", "size", "folder_name", "comment") self.content_tree = ttk.Treeview( self, columns=columns, show="headings") @@ -34,12 +27,12 @@ class UserBackupContentFrame(ttk.Frame): self.content_tree.heading("folder_name", text=Msg.STR["folder"]) self.content_tree.heading("comment", text=Msg.STR["comment"]) - self.content_tree.column("date", width=80, anchor="center") - self.content_tree.column("time", width=60, anchor="center") - self.content_tree.column("type", width=140, anchor="center") + self.content_tree.column("date", width=100, anchor="center") + self.content_tree.column("time", width=100, anchor="center") + self.content_tree.column("type", width=180, anchor="w") self.content_tree.column("size", width=90, anchor="center") self.content_tree.column("folder_name", width=130, anchor="center") - self.content_tree.column("comment", width=220, anchor="w") + self.content_tree.column("comment", width=180, anchor="w") self.content_tree.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.content_tree.bind("<>", self._on_item_select) @@ -56,28 +49,29 @@ class UserBackupContentFrame(ttk.Frame): if not self.user_backups_list: return + colors = ["#0078D7", "#E8740C", "#107C10", "#8B107C", "#005A9E"] color_index = -1 + current_color_tag = "" + for i, backup_info in enumerate(self.user_backups_list): if backup_info.get("backup_type_base") == "Full": - color_index = (color_index + 1) % len(self.tag_colors) - full_tag, full_color, inc_tag, inc_color = self.tag_colors[color_index] + color_index = (color_index + 1) % len(colors) + current_color_tag = f"color_{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 + current_color_tag, foreground=colors[color_index]) + + backup_type_display = backup_info.get("type", "N/A") + if backup_info.get("backup_type_base") != "Full": + backup_type_display = f"▲ {backup_type_display}" self.content_tree.insert("", "end", values=( backup_info.get("date", "N/A"), backup_info.get("time", "N/A"), - backup_info.get("type", "N/A"), + backup_type_display, backup_info.get("size", "N/A"), backup_info.get("source", "N/A"), backup_info.get("comment", "") - ), tags=(current_tag,), iid=backup_info.get("folder_name")) + ), tags=(current_color_tag,), iid=backup_info.get("folder_name")) self._on_item_select(None) def _on_item_select(self, event): @@ -94,16 +88,15 @@ class UserBackupContentFrame(ttk.Frame): if not selected_backup: return - # Use the direct path to the info file, which we added to the backup dict info_file_path = selected_backup.get('info_file_path') if not info_file_path or not os.path.isfile(info_file_path): - MessageDialog(self, message_type="error", title="Error", text=f"Metadata file not found: {info_file_path}") + MessageDialog(self, message_type="error", title="Error", + text=f"Metadata file not found: {info_file_path}") return CommentEditorDialog(self, info_file_path, self.backup_manager) - - # Refresh the view to show the new comment - self.parent_view.show(self.backup_path) + + self.parent_view.show(self.backup_path, self.user_backups_list) def _restore_selected(self): selected_item_id = self.content_tree.focus() @@ -129,9 +122,8 @@ class UserBackupContentFrame(ttk.Frame): password = None if is_encrypted: - # For encrypted backups, the base_dest_path is the path to the container's parent directory - # We assume the logic to get the username/keyring entry is handled by the encryption manager - password = self.backup_manager.encryption_manager.get_password(confirm=False) + password = self.backup_manager.encryption_manager.get_password( + confirm=False) if not password: self.actions.logger.log( "Password entry cancelled, aborting deletion.") @@ -141,12 +133,11 @@ class UserBackupContentFrame(ttk.Frame): self.parent_view.show_deletion_status( Msg.STR["deleting_backup_in_progress"]) - # The info_file_path is no longer needed as it's inside the folder_to_delete self.backup_manager.start_delete_backup( path_to_delete=folder_to_delete, is_encrypted=is_encrypted, is_system=False, - base_dest_path=self.backup_path, # This is the root destination folder + base_dest_path=self.backup_path, password=password, queue=self.winfo_toplevel().queue )