feat(ui): Refine "Backup Content" view display logic

This commit implements several UI/UX improvements for the "Backup Content" list view based on user feedback.

- feat(ui): User backups are now grouped by their full/incremental chains, similar to system backups, for a more logical and organized view.
- feat(ui): The color scheme for backup chains has been simplified. Each chain (a full backup and its incrementals) now shares a single color to improve visual grouping.
- feat(ui): Incremental backups are now denoted by a ▲ icon in the Type column instead of a different color or font style, providing a clear and clean indicator.
- fix(ui): Adjusted all column widths in the backup lists to ensure all data (especially Date and Time) is fully visible without truncation.
This commit is contained in:
2025-09-09 14:22:37 +02:00
parent 94afeb5d45
commit 94a44881e6
3 changed files with 81 additions and 65 deletions

View File

@@ -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(

View File

@@ -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("<<TreeviewSelect>>", 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:

View File

@@ -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("<<TreeviewSelect>>", 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
)