Files
server-manager/gui/update_dialog.py
2026-03-05 04:09:47 -05:00

272 lines
9.3 KiB
Python

"""
Update dialog and banner — shows available updates, changelog, download progress.
"""
import threading
import customtkinter as ctk
from core.i18n import t
class UpdateBanner(ctk.CTkFrame):
"""Thin banner bar shown when an update is available."""
def __init__(self, parent, on_update=None, on_skip=None, on_dismiss=None):
super().__init__(parent, fg_color="#1e3a5f", corner_radius=8, height=40)
self.pack_propagate(False)
self._on_update = on_update
self._on_skip = on_skip
self._on_dismiss = on_dismiss
self._label = ctk.CTkLabel(
self, text="", font=ctk.CTkFont(size=13),
text_color="#e0e0e0"
)
self._label.pack(side="left", padx=12)
# Dismiss X
dismiss_btn = ctk.CTkButton(
self, text="\u2715", width=28, height=28,
corner_radius=14, fg_color="transparent", hover_color="#374151",
text_color="#9ca3af", font=ctk.CTkFont(size=12),
command=self._dismiss,
)
dismiss_btn.pack(side="right", padx=(0, 6))
# Skip button
self._skip_btn = ctk.CTkButton(
self, text=t("update_skip"), width=70, height=28,
corner_radius=6, fg_color="#4b5563", hover_color="#374151",
font=ctk.CTkFont(size=12),
command=self._skip,
)
self._skip_btn.pack(side="right", padx=(0, 4))
# Update button
self._update_btn = ctk.CTkButton(
self, text=t("update_install"), width=90, height=28,
corner_radius=6, fg_color="#2563eb", hover_color="#1d4ed8",
font=ctk.CTkFont(size=12, weight="bold"),
command=self._do_update,
)
self._update_btn.pack(side="right", padx=(0, 4))
def set_info(self, version: str, downloaded: bool = False):
"""Update the banner text."""
text = t("update_available").format(version=version)
if downloaded:
text += f" \u2714 {t('update_downloaded')}"
self._label.configure(text=text)
def _do_update(self):
if self._on_update:
self._on_update()
def _skip(self):
if self._on_skip:
self._on_skip()
self.pack_forget()
def _dismiss(self):
if self._on_dismiss:
self._on_dismiss()
self.pack_forget()
class UpdateDialog(ctk.CTkToplevel):
"""Modal dialog showing update details, changelog, and download progress."""
def __init__(self, parent, info: dict, downloaded_path: str = None,
on_install=None, on_skip=None):
super().__init__(parent)
self.title(t("update_available_title"))
self.geometry("500x420")
self.resizable(False, False)
self.transient(parent)
self.grab_set()
self._info = info
self._downloaded_path = downloaded_path
self._on_install = on_install
self._on_skip = on_skip
self._downloading = False
self._build_ui()
# Center on parent
self.update_idletasks()
px = parent.winfo_x() + (parent.winfo_width() - 500) // 2
py = parent.winfo_y() + (parent.winfo_height() - 420) // 2
self.geometry(f"+{px}+{py}")
def _build_ui(self):
from version import __version__
# Header
header = ctk.CTkFrame(self, fg_color="transparent")
header.pack(fill="x", padx=20, pady=(20, 10))
ctk.CTkLabel(
header, text="ServerManager",
font=ctk.CTkFont(size=18, weight="bold"),
).pack(anchor="w")
# Version info
ver_frame = ctk.CTkFrame(header, fg_color="transparent")
ver_frame.pack(fill="x", pady=(8, 0))
ctk.CTkLabel(
ver_frame, text=f"{t('update_current')}: v{__version__}",
font=ctk.CTkFont(size=13), text_color="#9ca3af",
).pack(side="left")
ctk.CTkLabel(
ver_frame, text=" \u2192 ",
font=ctk.CTkFont(size=13), text_color="#6b7280",
).pack(side="left")
ctk.CTkLabel(
ver_frame, text=f"v{self._info['version']}",
font=ctk.CTkFont(size=14, weight="bold"), text_color="#22c55e",
).pack(side="left")
# File size
size_mb = self._info.get("size", 0) / (1024 * 1024)
if size_mb > 0:
ctk.CTkLabel(
ver_frame, text=f" ({size_mb:.1f} MB)",
font=ctk.CTkFont(size=12), text_color="#6b7280",
).pack(side="left")
# Changelog
ctk.CTkLabel(
self, text=t("update_changelog"),
font=ctk.CTkFont(size=13, weight="bold"),
).pack(anchor="w", padx=20, pady=(10, 4))
changelog_frame = ctk.CTkFrame(self, fg_color="#1a1a2e", corner_radius=8)
changelog_frame.pack(fill="both", expand=True, padx=20, pady=(0, 10))
self._changelog_text = ctk.CTkTextbox(
changelog_frame, font=ctk.CTkFont(size=12),
fg_color="transparent", wrap="word",
activate_scrollbars=True,
)
self._changelog_text.pack(fill="both", expand=True, padx=8, pady=8)
changelog = self._info.get("changelog", "")
if changelog:
self._changelog_text.insert("1.0", changelog)
else:
self._changelog_text.insert("1.0", t("update_no_changelog"))
self._changelog_text.configure(state="disabled")
# Progress bar (hidden initially)
self._progress_frame = ctk.CTkFrame(self, fg_color="transparent")
self._progress_bar = ctk.CTkProgressBar(
self._progress_frame, width=400, height=8,
)
self._progress_bar.set(0)
self._progress_bar.pack(fill="x", padx=20)
self._progress_label = ctk.CTkLabel(
self._progress_frame, text="",
font=ctk.CTkFont(size=11), text_color="#9ca3af",
)
self._progress_label.pack(pady=(2, 0))
# Buttons
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
btn_frame.pack(fill="x", padx=20, pady=(0, 16))
self._install_btn = ctk.CTkButton(
btn_frame,
text=t("update_install") if self._downloaded_path else t("update_download_install"),
width=140, height=34, corner_radius=8,
fg_color="#2563eb", hover_color="#1d4ed8",
font=ctk.CTkFont(size=13, weight="bold"),
command=self._on_install_click,
)
self._install_btn.pack(side="right", padx=(8, 0))
ctk.CTkButton(
btn_frame, text=t("update_later"),
width=80, height=34, corner_radius=8,
fg_color="#4b5563", hover_color="#374151",
font=ctk.CTkFont(size=13),
command=self.destroy,
).pack(side="right", padx=(8, 0))
ctk.CTkButton(
btn_frame, text=t("update_skip"),
width=100, height=34, corner_radius=8,
fg_color="#4b5563", hover_color="#374151",
font=ctk.CTkFont(size=13),
command=self._on_skip_click,
).pack(side="right")
def _on_install_click(self):
if self._downloaded_path:
# Already downloaded — apply
if self._on_install:
self._on_install(self._downloaded_path)
else:
# Need to download first
self._start_download()
def _start_download(self):
if self._downloading:
return
self._downloading = True
self._install_btn.configure(state="disabled", text=t("update_downloading"))
self._progress_frame.pack(fill="x", padx=20, pady=(0, 8))
def _download():
from core.updater import UpdateChecker
# Access the updater from parent app
app = self.master
if hasattr(app, "updater"):
path = app.updater.download_update(
self._info["url"],
progress_cb=self._on_progress,
)
self.after(0, lambda: self._download_complete(path))
else:
self.after(0, lambda: self._download_complete(None))
threading.Thread(target=_download, daemon=True).start()
def _on_progress(self, downloaded: int, total: int):
"""Called from download thread."""
pct = downloaded / total if total > 0 else 0
mb_done = downloaded / (1024 * 1024)
mb_total = total / (1024 * 1024)
self.after(0, lambda: self._update_progress(pct, mb_done, mb_total))
def _update_progress(self, pct: float, mb_done: float, mb_total: float):
self._progress_bar.set(pct)
self._progress_label.configure(
text=f"{mb_done:.1f} / {mb_total:.1f} MB ({pct * 100:.0f}%)"
)
def _download_complete(self, path: str):
self._downloading = False
if path:
self._downloaded_path = path
self._install_btn.configure(
state="normal", text=t("update_restart"),
)
self._progress_bar.set(1.0)
self._progress_label.configure(text=t("update_ready"))
else:
self._install_btn.configure(
state="normal", text=t("update_download_install"),
)
self._progress_frame.pack_forget()
self._progress_label.configure(text=t("update_error"))
def _on_skip_click(self):
if self._on_skip:
self._on_skip(self._info["version"])
self.destroy()