""" 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()