Files
m5stack-automation-tool/widgets/backup_dialog.py
T
2026-07-17 15:29:53 -04:00

221 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)