345 lines
12 KiB
Python
345 lines
12 KiB
Python
"""
|
|
Update Dialog for UmamusumeCardManager
|
|
Provides a modal dialog for the update process.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, scrolledtext
|
|
import threading
|
|
import webbrowser
|
|
from typing import Optional, Callable
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from updater.update_checker import check_for_updates, download_update, apply_update, get_current_version
|
|
from gui.theme import (
|
|
BG_DARK, BG_DARKEST, BG_MEDIUM, BG_LIGHT, BG_HIGHLIGHT,
|
|
ACCENT_PRIMARY, ACCENT_SECONDARY, ACCENT_SUCCESS, ACCENT_ERROR,
|
|
TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED,
|
|
FONT_HEADER, FONT_SUBHEADER, FONT_BODY, FONT_BODY_BOLD, FONT_SMALL,
|
|
create_styled_button
|
|
)
|
|
|
|
|
|
class UpdateDialog:
|
|
"""Modal dialog for checking and applying updates."""
|
|
|
|
def __init__(self, parent: tk.Tk, on_close_callback: Optional[Callable] = None):
|
|
self.parent = parent
|
|
self.on_close_callback = on_close_callback
|
|
self.update_info = None
|
|
self.download_thread = None
|
|
self.is_downloading = False
|
|
|
|
# Create the dialog window
|
|
self.dialog = tk.Toplevel(parent)
|
|
self.dialog.title("Check for Updates")
|
|
self.dialog.geometry("520x600")
|
|
self.dialog.resizable(True, True)
|
|
self.dialog.minsize(480, 500)
|
|
self.dialog.transient(parent)
|
|
self.dialog.grab_set()
|
|
|
|
# Center on parent
|
|
self.center_on_parent()
|
|
|
|
self.dialog.configure(bg=BG_DARK)
|
|
|
|
# Set up the UI
|
|
self.setup_ui()
|
|
|
|
# Start checking for updates
|
|
self.check_for_updates()
|
|
|
|
def center_on_parent(self):
|
|
"""Center the dialog on the parent window."""
|
|
self.dialog.update_idletasks()
|
|
parent_x = self.parent.winfo_x()
|
|
parent_y = self.parent.winfo_y()
|
|
parent_w = self.parent.winfo_width()
|
|
parent_h = self.parent.winfo_height()
|
|
|
|
dialog_w = 520
|
|
dialog_h = 600
|
|
|
|
x = parent_x + (parent_w - dialog_w) // 2
|
|
y = parent_y + (parent_h - dialog_h) // 2
|
|
|
|
self.dialog.geometry(f"{dialog_w}x{dialog_h}+{x}+{y}")
|
|
|
|
def setup_ui(self):
|
|
"""Set up the dialog UI."""
|
|
# Button frame (Create first to pack at bottom)
|
|
self.button_frame = tk.Frame(self.dialog, bg=BG_DARK, pady=20, padx=20)
|
|
self.button_frame.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
# Close button
|
|
self.close_button = create_styled_button(
|
|
self.button_frame,
|
|
text="Close",
|
|
command=self.close,
|
|
style_type='default'
|
|
)
|
|
self.close_button.pack(side=tk.RIGHT)
|
|
|
|
# Update button (hidden initially)
|
|
self.update_button = create_styled_button(
|
|
self.button_frame,
|
|
text="⬇️ Download & Install",
|
|
command=self.start_download,
|
|
style_type='accent'
|
|
)
|
|
# We don't pack it yet
|
|
|
|
# Main container
|
|
main_frame = tk.Frame(self.dialog, bg=BG_DARK, padx=25, pady=20)
|
|
main_frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Title
|
|
self.title_label = tk.Label(
|
|
main_frame,
|
|
text="🔄 Checking for Updates...",
|
|
font=FONT_HEADER,
|
|
bg=BG_DARK,
|
|
fg=ACCENT_PRIMARY
|
|
)
|
|
self.title_label.pack(pady=(0, 10))
|
|
|
|
# Status message
|
|
self.status_label = tk.Label(
|
|
main_frame,
|
|
text="Connecting to GitHub...",
|
|
font=FONT_BODY,
|
|
bg=BG_DARK,
|
|
fg=TEXT_MUTED,
|
|
wraplength=460
|
|
)
|
|
self.status_label.pack(pady=(0, 10))
|
|
|
|
# Version info frame
|
|
self.version_frame = tk.Frame(main_frame, bg=BG_MEDIUM, padx=15, pady=10)
|
|
self.version_frame.pack(fill=tk.X, pady=(0, 15))
|
|
|
|
self.current_version_label = tk.Label(
|
|
self.version_frame,
|
|
text=f"Current Version: v{get_current_version()}",
|
|
font=FONT_BODY,
|
|
bg=BG_MEDIUM,
|
|
fg=TEXT_SECONDARY
|
|
)
|
|
self.current_version_label.pack(anchor='w')
|
|
|
|
self.new_version_label = tk.Label(
|
|
self.version_frame,
|
|
text="Latest Version: Checking...",
|
|
font=FONT_BODY,
|
|
bg=BG_MEDIUM,
|
|
fg=TEXT_SECONDARY
|
|
)
|
|
self.new_version_label.pack(anchor='w')
|
|
|
|
# Release Notes Area
|
|
self.notes_label = tk.Label(
|
|
main_frame,
|
|
text="What's New:",
|
|
font=FONT_BODY_BOLD,
|
|
bg=BG_DARK,
|
|
fg=TEXT_PRIMARY
|
|
)
|
|
self.notes_label.pack(anchor='w', pady=(0, 5))
|
|
|
|
# Text box for release notes
|
|
self.notes_text = scrolledtext.ScrolledText(
|
|
main_frame,
|
|
height=10,
|
|
bg=BG_MEDIUM,
|
|
fg=TEXT_SECONDARY,
|
|
font=FONT_SMALL,
|
|
borderwidth=0,
|
|
highlightthickness=0,
|
|
padx=10,
|
|
pady=10
|
|
)
|
|
self.notes_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
|
self.notes_text.insert(tk.END, "Checking for release notes...")
|
|
self.notes_text.config(state=tk.DISABLED)
|
|
|
|
# Progress bar (hidden initially)
|
|
self.progress_frame = tk.Frame(main_frame, bg=BG_DARK)
|
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
|
|
|
|
self.progress_label = tk.Label(
|
|
self.progress_frame,
|
|
text="",
|
|
font=FONT_SMALL,
|
|
bg=BG_DARK,
|
|
fg=TEXT_MUTED
|
|
)
|
|
self.progress_label.pack(anchor='w', pady=(0, 5))
|
|
|
|
self.progress_bar = ttk.Progressbar(
|
|
self.progress_frame,
|
|
mode='indeterminate',
|
|
length=460
|
|
)
|
|
self.progress_bar.pack(fill=tk.X)
|
|
self.progress_bar.start(10)
|
|
|
|
def check_for_updates(self):
|
|
"""Check for updates in a background thread."""
|
|
def check():
|
|
self.update_info = check_for_updates()
|
|
self.dialog.after(0, self.update_check_complete)
|
|
|
|
thread = threading.Thread(target=check, daemon=True)
|
|
thread.start()
|
|
|
|
def update_check_complete(self):
|
|
"""Called when the update check is complete."""
|
|
self.progress_bar.stop()
|
|
self.progress_frame.pack_forget() # Hide progress bar when check is done
|
|
|
|
# Enable text box to update it
|
|
self.notes_text.config(state=tk.NORMAL)
|
|
self.notes_text.delete(1.0, tk.END)
|
|
|
|
if self.update_info:
|
|
# Update available!
|
|
self.title_label.config(text="🎉 Update Available!")
|
|
self.status_label.config(
|
|
text="A new version is available.",
|
|
fg=ACCENT_SUCCESS
|
|
)
|
|
self.new_version_label.config(
|
|
text=f"Latest Version: {self.update_info['new_version']}",
|
|
fg=ACCENT_SUCCESS
|
|
)
|
|
|
|
# Show Release Notes
|
|
notes = self.update_info.get('release_notes', 'No release notes available.')
|
|
self.notes_text.insert(tk.END, notes)
|
|
|
|
# Show update button
|
|
self.update_button.pack(side=tk.RIGHT, padx=(0, 10))
|
|
else:
|
|
# Up to date or error
|
|
self.title_label.config(text="✅ You're Up to Date!")
|
|
self.status_label.config(
|
|
text=f"You are running the latest version.",
|
|
fg=TEXT_SECONDARY
|
|
)
|
|
self.new_version_label.config(
|
|
text=f"Latest Version: v{get_current_version()}",
|
|
fg=ACCENT_SUCCESS
|
|
)
|
|
self.notes_text.insert(tk.END, "You are using the latest version of Umamusume Support Card Manager.\n\nEnjoy!")
|
|
|
|
self.notes_text.config(state=tk.DISABLED)
|
|
|
|
def start_download(self):
|
|
"""Start downloading the update."""
|
|
if self.is_downloading or not self.update_info:
|
|
return
|
|
|
|
self.is_downloading = True
|
|
self.update_button.config(state=tk.DISABLED, text="Downloading...")
|
|
self.close_button.config(state=tk.DISABLED)
|
|
|
|
self.title_label.config(text="⬇️ Downloading Update...")
|
|
self.status_label.config(text="Please wait...", fg=TEXT_MUTED)
|
|
|
|
# Configure progress bar for determinate mode
|
|
self.progress_frame.pack(fill=tk.X, pady=(0, 10)) # Show progress frame again
|
|
self.progress_bar.config(mode='determinate', maximum=100)
|
|
self.progress_bar.pack(fill=tk.X)
|
|
self.progress_bar['value'] = 0
|
|
|
|
def download():
|
|
def progress_callback(downloaded, total):
|
|
if total > 0:
|
|
percent = int((downloaded / total) * 100)
|
|
mb_downloaded = downloaded / (1024 * 1024)
|
|
mb_total = total / (1024 * 1024)
|
|
self.dialog.after(0, lambda: self.update_progress(percent, mb_downloaded, mb_total))
|
|
|
|
download_path = download_update(self.update_info['download_url'], progress_callback)
|
|
self.dialog.after(0, lambda: self.download_complete(download_path))
|
|
|
|
self.download_thread = threading.Thread(target=download, daemon=True)
|
|
self.download_thread.start()
|
|
|
|
def update_progress(self, percent: int, downloaded_mb: float, total_mb: float):
|
|
"""Update the progress bar."""
|
|
self.progress_bar['value'] = percent
|
|
self.progress_label.config(text=f"Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%)")
|
|
|
|
def download_complete(self, download_path: Optional[str]):
|
|
"""Called when the download is complete."""
|
|
self.is_downloading = False
|
|
|
|
if download_path:
|
|
self.title_label.config(text="✅ Download Complete!")
|
|
self.status_label.config(
|
|
text="Update ready to install.",
|
|
fg=ACCENT_SUCCESS
|
|
)
|
|
|
|
# Change button to install
|
|
self.update_button.config(
|
|
state=tk.NORMAL,
|
|
text="🔄 Install & Restart",
|
|
command=lambda: self.install_update(download_path)
|
|
)
|
|
self.close_button.config(state=tk.NORMAL)
|
|
else:
|
|
self.title_label.config(text="❌ Download Failed")
|
|
self.status_label.config(
|
|
text="Failed not download update.",
|
|
fg=ACCENT_ERROR
|
|
)
|
|
self.update_button.config(state=tk.NORMAL, text="⬇️ Retry Download")
|
|
self.close_button.config(state=tk.NORMAL)
|
|
|
|
def install_update(self, download_path: str):
|
|
"""Install the downloaded update."""
|
|
self.title_label.config(text="🔄 Installing Update...")
|
|
self.status_label.config(text="Applying update...", fg=TEXT_MUTED)
|
|
self.update_button.config(state=tk.DISABLED)
|
|
self.close_button.config(state=tk.DISABLED)
|
|
|
|
if apply_update(download_path):
|
|
# Exit the application - the updater script will restart it
|
|
self.dialog.after(1000, lambda: self.parent.quit())
|
|
else:
|
|
messagebox.showinfo(
|
|
"Manual Update Required",
|
|
f"The update was downloaded but cannot be applied automatically.\n\n"
|
|
f"Downloaded file location:\n{download_path}\n\n"
|
|
f"Please replace the current executable manually.",
|
|
parent=self.dialog
|
|
)
|
|
self.close()
|
|
|
|
|
|
def close(self):
|
|
"""Close the dialog."""
|
|
if self.on_close_callback:
|
|
self.on_close_callback()
|
|
self.dialog.destroy()
|
|
|
|
|
|
def show_update_dialog(parent: tk.Tk, on_close_callback: Optional[Callable] = None) -> UpdateDialog:
|
|
"""
|
|
Show the update dialog.
|
|
|
|
Args:
|
|
parent: The parent Tk window
|
|
on_close_callback: Optional callback when dialog is closed
|
|
|
|
Returns:
|
|
The UpdateDialog instance
|
|
"""
|
|
return UpdateDialog(parent, on_close_callback)
|