"""Backup restore dialog - lists all backups and offers restore/delete actions.""" import tkinter as tk from tkinter import messagebox class BackupDialog(tk.Toplevel): """Modal dialog for browsing and restoring backups.""" PAGE_SIZE = 50 def __init__(self, parent, backup_manager, on_restore=None): super().__init__(parent) self.backup_manager = backup_manager self.on_restore = on_restore self._visible_count = self.PAGE_SIZE self.title("Backup Manager") self.geometry("520x520") self.minsize(400, 400) self.configure(bg="#2D2D3D") self.transient(parent) self.grab_set() self._build_ui() self._refresh() def _build_ui(self): header = tk.Frame(self, bg="#1E1E2E") header.pack(fill="x") tk.Label(header, text="Backups", bg="#1E1E2E", fg="white", font=("Segoe UI", 12, "bold"), pady=10).pack(side="left", padx=14) info = tk.Label(self, text="Automatic backups are taken after changes are made.\n" "Select a backup to restore or delete it.", bg="#2D2D3D", fg="#AAAAAA", font=("Segoe UI", 9), justify="left") info.pack(anchor="w", padx=14, pady=(8, 4)) list_frame = tk.Frame(self, bg="#2D2D3D") list_frame.pack(fill="both", expand=True, padx=14, pady=(4, 8)) header_row = tk.Frame(list_frame, bg="#1E1E2E") header_row.pack(fill="x") tk.Label(header_row, text="When", bg="#1E1E2E", fg="#888888", font=("Segoe UI", 9, "bold"), anchor="w").pack(side="left", fill="x", expand=True, padx=(8, 4), pady=4) tk.Label(header_row, text="Size", bg="#1E1E2E", fg="#888888", font=("Segoe UI", 9, "bold"), anchor="w", width=10).pack(side="left", padx=4, pady=4) canvas_frame = tk.Frame(list_frame, bg="#1E1E2E") canvas_frame.pack(fill="both", expand=True) self._list_canvas = tk.Canvas(canvas_frame, bg="#1E1E2E", highlightthickness=0) scrollbar = tk.Scrollbar(canvas_frame, orient="vertical", command=self._list_canvas.yview) self._list_inner = tk.Frame(self._list_canvas, bg="#1E1E2E") self._list_inner.bind("", lambda e: self._list_canvas.configure( scrollregion=self._list_canvas.bbox("all"))) self._list_canvas.create_window((0, 0), window=self._list_inner, anchor="nw", tags="inner") self._list_canvas.configure(yscrollcommand=scrollbar.set) self._list_canvas.bind("", lambda e: self._list_canvas.itemconfig("inner", width=e.width)) self._list_canvas.bind_all("", lambda e: self._list_canvas.yview_scroll( -1 * (e.delta // 120), "units")) self._list_canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") footer = tk.Frame(self, bg="#1E1E2E") footer.pack(fill="x", side="bottom") self._status_label = tk.Label(footer, text="", bg="#1E1E2E", fg="#AAAAAA", font=("Segoe UI", 9)) self._status_label.pack(side="left", padx=14, pady=10) tk.Button(footer, text="Close", command=self.destroy, bg="#3D3D5C", fg="white", font=("Segoe UI", 9), relief="flat", padx=14, pady=4).pack(side="right", padx=8, pady=8) tk.Button(footer, text="Create Backup Now", command=self._create_now, bg="#4A4A6A", fg="white", font=("Segoe UI", 9), relief="flat", padx=10, pady=4).pack(side="right", padx=4, pady=8) def _refresh(self): """Rebuild the backup list.""" for widget in self._list_inner.winfo_children(): widget.destroy() backups = self.backup_manager.list_backups() total_count = len(backups) if self._visible_count < self.PAGE_SIZE: self._visible_count = self.PAGE_SIZE shown = min(self._visible_count, total_count) if not backups: tk.Label(self._list_inner, text="No backups yet.", bg="#1E1E2E", fg="#666666", font=("Segoe UI", 10, "italic"), pady=20).pack() else: for i, info in enumerate(backups[:shown]): self._create_row(info, i) remaining = total_count - shown if remaining > 0: self._create_load_more_row(remaining) total = self.backup_manager.total_size_formatted() if total_count and shown < total_count: label = (f"Showing {shown} of {total_count} backup" f"{'s' if total_count != 1 else ''} • {total} on disk") else: label = (f"{total_count} backup{'s' if total_count != 1 else ''}" f" • {total} on disk") self._status_label.config(text=label) def _create_load_more_row(self, remaining): row = tk.Frame(self._list_inner, bg="#1E1E2E") row.pack(fill="x", pady=6) take = min(self.PAGE_SIZE, remaining) tk.Button(row, text=f"Load {take} more ({remaining} remaining)", command=self._load_more, bg="#3D3D5C", fg="white", font=("Segoe UI", 9), relief="flat", padx=14, pady=4).pack(pady=4) def _load_more(self): self._visible_count += self.PAGE_SIZE self._refresh() # Jump to the bottom so the newly-loaded rows are visible self._list_canvas.update_idletasks() self._list_canvas.yview_moveto(1.0) def _create_row(self, info, index): bg = "#222230" if index % 2 == 0 else "#1E1E2E" hover_bg = "#2A2A3A" row = tk.Frame(self._list_inner, bg=bg, cursor="hand2") row.pack(fill="x") ts_label = tk.Label(row, text=info.human_readable_time(), bg=bg, fg="white", font=("Segoe UI", 10), anchor="w", padx=8, pady=6) ts_label.pack(side="left", fill="x", expand=True) size_label = tk.Label(row, text=info.human_readable_size(), bg=bg, fg="#AAAAAA", font=("Segoe UI", 9), anchor="w", width=10, padx=4, pady=6) size_label.pack(side="left") del_btn = tk.Button(row, text="×", bg="#3D3D5C", fg="#FF7777", font=("Segoe UI", 11, "bold"), relief="flat", width=2, command=lambda p=info.path: self._delete_one(p)) del_btn.pack(side="right", padx=(4, 8), pady=3) restore_btn = tk.Button(row, text="Restore", bg="#27AE60", fg="white", font=("Segoe UI", 9), relief="flat", padx=10, pady=2, command=lambda p=info.path: self._restore_one(p)) restore_btn.pack(side="right", padx=4, pady=3) for w in (row, ts_label, size_label): w.bind("", lambda e, r=row: self._set_bg(r, hover_bg)) w.bind("", lambda e, r=row, b=bg: self._set_bg(r, b)) def _set_bg(self, row, color): row.config(bg=color) for c in row.winfo_children(): if isinstance(c, tk.Label): c.config(bg=color) def _delete_one(self, path): if not messagebox.askyesno("Delete Backup", f"Delete this backup?\n\n{path.name}\n\n" "This cannot be undone.", parent=self): return self.backup_manager.delete_backup(path) self._refresh() def _restore_one(self, path): if not messagebox.askyesno("Restore Backup", "Restoring will replace ALL current profiles,\n" "settings, and images with the backup's contents.\n\n" f"Restore from:\n{path.name}?\n\n" "A safety backup of the current state will be\n" "created automatically before restoring.", parent=self): return self.backup_manager.create_backup() # safety snapshot so the user can undo if self.on_restore: ok = self.on_restore(path) if ok: messagebox.showinfo("Restore Complete", "Backup restored successfully.", parent=self) self.destroy() else: messagebox.showerror("Restore Failed", "Could not restore the backup.\n" "Check the console for details.", parent=self) self._refresh() def _create_now(self): path = self.backup_manager.create_backup() if path: self._refresh() else: messagebox.showerror("Backup Failed", "Could not create a backup.\n" "Check the console for details.", parent=self)