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:
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user