272 lines
9.3 KiB
Python
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()
|