Reworked the modal ui's for launcher, settings, updates and game pre-loader.

This commit is contained in:
2026-08-31 22:03:47 +02:00
parent fe883047e0
commit 87dee93b86

View File

@@ -14,7 +14,10 @@ import sys
import threading
import time
import webbrowser
from typing import Callable, Mapping
from typing import TYPE_CHECKING, Callable, Mapping
if TYPE_CHECKING:
from tkinter import Frame, PhotoImage, Toplevel
from . import __version__
from .changelog import (
@@ -75,10 +78,17 @@ from .server_status import (
ServerStatusError,
fetch_server_status,
)
from .tk_chrome import (
centered_window_geometry,
create_dashboard_chrome,
create_window_chrome,
load_square_window_icon,
show_themed_dialog,
)
MAX_NEWS_CARDS = 3
WINDOW_SIZE = "1000x750"
WINDOW_SIZE = "1002x784"
NEWS_CARD_HEIGHT = 242
NEWS_IMAGE_WIDTH = 292
NEWS_IMAGE_HEIGHT = 118
@@ -517,7 +527,7 @@ def preloader_status_text(
return f"Step {step} of {total} · {value}"
def freeze_preloader_geometry(frame: tk.Misc) -> tuple[int, int]:
def freeze_preloader_geometry(frame: Frame) -> tuple[int, int]:
"""Lock a measured preloader frame so later status text cannot reflow it."""
frame.update_idletasks()
@@ -654,7 +664,7 @@ class TransferRateEstimator:
def run_dashboard(paths: AppPaths) -> None:
try:
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from tkinter import filedialog, ttk
except ImportError as exc:
raise DashboardError(
"Python Tk is missing. Install your distribution's python3-tk package "
@@ -665,29 +675,43 @@ def run_dashboard(paths: AppPaths) -> None:
root = tk.Tk(className=APPLICATION_WM_CLASS)
root.title(f"{APPLICATION_NAME} — Linux Launcher")
root.iconname(APPLICATION_NAME)
window_icon = None
window_icon: PhotoImage | None = None
window_icon_path = find_application_icon()
if window_icon_path is not None:
try:
window_icon = tk.PhotoImage(master=root, file=str(window_icon_path))
root.iconphoto(True, window_icon)
loaded_window_icon = load_square_window_icon(root, window_icon_path)
root.iconphoto(True, loaded_window_icon)
window_icon = loaded_window_icon
except (OSError, tk.TclError) as icon_error:
print(
f"Warning: could not load the launcher icon: {icon_error}",
file=sys.stderr,
)
def apply_window_icon(window: Toplevel) -> None:
icon = window_icon
if icon is not None:
window.iconphoto(False, icon)
root.geometry(WINDOW_SIZE)
root.resizable(False, False)
root.configure(background="#0c0908")
root.withdraw()
dashboard_chrome = create_dashboard_chrome(
root,
title="PRISTONTALE EU",
subtitle="NATIVE LINUX LAUNCHER",
)
dashboard_content = dashboard_chrome.content
try:
startup_settings = ensure_settings(paths)
except (OSError, ValueError) as startup_error:
root.deiconify()
messagebox.showerror(
"Launcher settings",
f"The launcher could not load its settings:\n\n{startup_error}",
parent=root,
show_themed_dialog(
root,
title="Launcher settings",
message=f"The launcher could not load its settings:\n\n{startup_error}",
kind="error",
)
root.destroy()
return
@@ -1091,7 +1115,7 @@ def run_dashboard(paths: AppPaths) -> None:
startup_started_at = time.monotonic()
startup_status = tk.StringVar(value="Loading dashboard contents…")
startup_overlay = tk.Frame(
root,
dashboard_content,
background="#0c0908",
borderwidth=0,
highlightthickness=0,
@@ -1289,7 +1313,7 @@ def run_dashboard(paths: AppPaths) -> None:
startup_timeout_after = None
begin_dashboard_transition(timed_out=True)
frame = ttk.Frame(root, style="Root.TFrame", padding=12)
frame = ttk.Frame(dashboard_content, style="Root.TFrame", padding=12)
frame.pack(fill="both", expand=True)
header = ttk.Frame(frame, style="Root.TFrame")
@@ -1535,6 +1559,7 @@ def run_dashboard(paths: AppPaths) -> None:
activity_window.geometry("800x400")
activity_window.resizable(False, False)
activity_window.configure(background="#0c0908")
apply_window_icon(activity_window)
log = tk.Text(
activity_window,
background="#100d0c",
@@ -1577,6 +1602,7 @@ def run_dashboard(paths: AppPaths) -> None:
preloader: tk.Toplevel | None = None
preloader_status: tk.StringVar | None = None
preloader_progress: ttk.Progressbar | None = None
preloader_image_references: list[tk.PhotoImage] = []
settings_window: tk.Toplevel | None = None
dashboard_debug_window: tk.Toplevel | None = None
dashboard_debug_output: tk.Text | None = None
@@ -1753,37 +1779,64 @@ def run_dashboard(paths: AppPaths) -> None:
if preloader is not None and preloader.winfo_exists():
return
window = tk.Toplevel(root)
window.withdraw()
preloader = window
window.title("Launching PristonTale EU")
window.configure(background="#0c0908")
window.resizable(False, False)
window.protocol("WM_DELETE_WINDOW", lambda: None)
window.transient(root)
window.attributes("-topmost", True)
apply_window_icon(window)
loader_chrome = create_window_chrome(
window,
title="",
subtitle="",
show_title_bar=False,
show_close=False,
show_minimize=False,
allow_close=False,
)
status_variable = tk.StringVar(value="Starting launcher process…")
preloader_status = status_variable
content = ttk.Frame(window, style="Root.TFrame", padding=26)
content.pack(fill="both", expand=True)
ttk.Label(content, text="PRISTONTALE EU", style="Brand.TLabel").pack(anchor="w")
ttk.Label(content, text="LAUNCHING THE CLIENT", style="Tagline.TLabel").pack(
anchor="w",
pady=(2, 18),
content = ttk.Frame(
loader_chrome.content,
style="Root.TFrame",
padding=(34, 26),
)
content.pack(fill="both", expand=True)
logo_source = window_icon
if logo_source is not None:
factor = max(
1,
math.ceil(logo_source.width() / 88),
math.ceil(logo_source.height() / 88),
)
modal_logo = logo_source.subsample(factor, factor)
preloader_image_references.clear()
preloader_image_references.append(modal_logo)
tk.Label(
content,
image=modal_logo,
background="#0c0908",
borderwidth=0,
highlightthickness=0,
).pack(anchor="center", pady=(0, 16))
ttk.Label(
content,
text="Please wait while the game prepares its window.",
text="Preparing the game window…",
style="PreloaderStatus.TLabel",
).pack(anchor="w")
anchor="center",
).pack(fill="x")
status_label = ttk.Label(
content,
textvariable=status_variable,
style="PreloaderMuted.TLabel",
width=PRELOADER_STATUS_CHARACTERS,
anchor="w",
anchor="center",
)
status_label.pack(
fill="x",
pady=(4, 10),
pady=(5, 11),
)
progress_widget = ttk.Progressbar(
content,
@@ -1795,12 +1848,23 @@ def run_dashboard(paths: AppPaths) -> None:
preloader_progress = progress_widget
progress_widget.pack(fill="x")
freeze_preloader_geometry(content)
root.update_idletasks()
window.update_idletasks()
width = window.winfo_reqwidth()
height = window.winfo_reqheight()
x = (window.winfo_screenwidth() - width) // 2
y = (window.winfo_screenheight() - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}")
window.geometry(
centered_window_geometry(
width,
height,
parent_x=root.winfo_rootx(),
parent_y=root.winfo_rooty(),
parent_width=root.winfo_width(),
parent_height=root.winfo_height(),
)
)
window.deiconify()
window.lift()
window.grab_set()
window.focus_force()
def set_preloader_status(value: str, step: int | None = None, total: int | None = None) -> None:
@@ -1812,10 +1876,15 @@ def run_dashboard(paths: AppPaths) -> None:
def close_game_preloader() -> None:
nonlocal preloader, preloader_status, preloader_progress
if preloader is not None and preloader.winfo_exists():
try:
preloader.grab_release()
except tk.TclError:
pass
preloader.destroy()
preloader = None
preloader_status = None
preloader_progress = None
preloader_image_references.clear()
def run_operation(
label: str,
@@ -2720,6 +2789,7 @@ def run_dashboard(paths: AppPaths) -> None:
window.geometry("900x640")
window.minsize(700, 440)
window.configure(background="#0c0908")
apply_window_icon(window)
debug_header = tk.Frame(window, background="#24170f", padx=16, pady=12)
debug_header.pack(fill="x")
tk.Label(
@@ -3103,10 +3173,14 @@ def run_dashboard(paths: AppPaths) -> None:
start_new_session=True,
)
except OSError as restart_error:
messagebox.showerror(
"Launcher update",
f"The update was installed, but the launcher could not restart: {restart_error}",
parent=root,
show_themed_dialog(
root,
title="Launcher update",
message=(
"The update was installed, but the launcher could not "
f"restart: {restart_error}"
),
kind="error",
)
else:
root.destroy()
@@ -3342,30 +3416,40 @@ def run_dashboard(paths: AppPaths) -> None:
return
if not check.available:
if required_reason is None:
messagebox.showinfo(
"Launcher update",
check.reason,
parent=root,
show_themed_dialog(
root,
title="Launcher is up to date",
message=check.reason,
kind="success",
)
else:
messagebox.showwarning(
"Compatibility update required",
"The installed game needs a launcher compatibility update:\n\n"
f"{required_reason}\n\n{check.reason}",
parent=root,
show_themed_dialog(
root,
title="Compatibility update required",
message=(
"The installed game needs a launcher compatibility update:\n\n"
f"{required_reason}\n\n{check.reason}"
),
kind="warning",
)
return
if not check.supported:
messagebox.showwarning(
"Manual launcher update required",
check.reason,
parent=root,
show_themed_dialog(
root,
title="Manual launcher update required",
message=check.reason,
kind="warning",
)
return
if not messagebox.askyesno(
"Update launcher",
f"Install signed launcher version {check.latest_version} and restart now?",
parent=root,
if not show_themed_dialog(
root,
title="Update launcher",
message=(
f"Install signed launcher version {check.latest_version} and restart now?"
),
kind="question",
confirm=True,
accept_text="INSTALL & RESTART",
):
return
run_operation(
@@ -3385,11 +3469,14 @@ def run_dashboard(paths: AppPaths) -> None:
official_snapshot,
)
except OSError as cache_error:
messagebox.showerror(
"Realm selection",
"The launcher could not save the official realm selection:\n\n"
f"{cache_error}",
parent=root,
show_themed_dialog(
root,
title="Realm selection",
message=(
"The launcher could not save the official realm selection:\n\n"
f"{cache_error}"
),
kind="error",
)
return
launch_settings: Settings | None = None
@@ -3469,21 +3556,36 @@ def run_dashboard(paths: AppPaths) -> None:
try:
current_settings = ensure_settings(paths)
except (OSError, ValueError) as error:
messagebox.showerror(
"Settings",
f"The launcher could not load its settings:\n\n{error}",
parent=root,
show_themed_dialog(
root,
title="Settings",
message=f"The launcher could not load its settings:\n\n{error}",
kind="error",
)
return
dialog = tk.Toplevel(root)
dialog.withdraw()
settings_window = dialog
dialog.title("PristonTale EU — Settings")
dialog.geometry("840x680")
dialog.geometry("842x714")
dialog.resizable(False, False)
dialog.configure(background="#0c0908")
dialog.transient(root)
apply_window_icon(dialog)
settings_chrome = create_window_chrome(
dialog,
title="PRISTONTALE EU",
subtitle="LAUNCHER SETTINGS",
show_close=True,
show_minimize=False,
allow_close=True,
)
settings_header = tk.Frame(dialog, background="#24170f", height=118)
settings_header = tk.Frame(
settings_chrome.content,
background="#24170f",
height=118,
)
settings_header.pack(fill="x")
settings_header.pack_propagate(False)
tk.Frame(settings_header, background="#d98927", width=5).pack(side="left", fill="y")
@@ -3511,7 +3613,11 @@ def run_dashboard(paths: AppPaths) -> None:
font=("Sans", 9),
).pack(anchor="w", pady=(2, 0))
content = ttk.Frame(dialog, style="Root.TFrame", padding=(16, 14, 16, 16))
content = ttk.Frame(
settings_chrome.content,
style="Root.TFrame",
padding=(16, 14, 16, 16),
)
content.pack(fill="both", expand=True)
content.columnconfigure(0, weight=1, uniform="settings-card")
content.columnconfigure(1, weight=1, uniform="settings-card")
@@ -3607,6 +3713,8 @@ def run_dashboard(paths: AppPaths) -> None:
settings_window.destroy()
settings_window = None
settings_chrome.set_close_command(close_settings)
def save_current_settings() -> None:
try:
updated_settings = update_settings(
@@ -3619,7 +3727,12 @@ def run_dashboard(paths: AppPaths) -> None:
game_source=game_source_value.get().strip() or None,
)
except (OSError, ValueError) as error:
messagebox.showerror("Settings", str(error), parent=dialog)
show_themed_dialog(
dialog,
title="Settings",
message=str(error),
kind="error",
)
return
append_log(
f"Saved launcher settings to {paths.settings_file}: "
@@ -3752,7 +3865,20 @@ def run_dashboard(paths: AppPaths) -> None:
).pack(side="left", padx=(8, 0))
dialog.protocol("WM_DELETE_WINDOW", close_settings)
dialog.update_idletasks()
dialog.geometry(
centered_window_geometry(
842,
714,
parent_x=root.winfo_rootx(),
parent_y=root.winfo_rooty(),
parent_width=root.winfo_width(),
parent_height=root.winfo_height(),
)
)
dialog.deiconify()
dialog.lift()
dialog.grab_set()
dialog.focus_set()
settings_button.configure(command=open_settings)
@@ -3779,10 +3905,11 @@ def run_dashboard(paths: AppPaths) -> None:
# and any active launcher operation have finished.
if operation_active or game_clients.active_count:
set_dashboard_debug(False)
root.iconify()
dashboard_chrome.minimize()
return
root.destroy()
dashboard_chrome.set_close_command(close_dashboard)
root.protocol("WM_DELETE_WINDOW", close_dashboard)
def update_news_viewport(_event: object | None = None) -> None: