221 lines
9.4 KiB
Python
221 lines
9.4 KiB
Python
"""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("<Configure>",
|
||
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("<Configure>",
|
||
lambda e: self._list_canvas.itemconfig("inner", width=e.width))
|
||
self._list_canvas.bind_all("<MouseWheel>",
|
||
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("<Enter>", lambda e, r=row: self._set_bg(r, hover_bg))
|
||
w.bind("<Leave>", 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)
|