v1.8.78: auto-updater — Gitea releases check, download, apply
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
271
gui/update_dialog.py
Normal file
271
gui/update_dialog.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user