Added proper hotbar/menu icons and added a preloader for the launcher.

This commit is contained in:
2026-08-30 12:06:15 +02:00
parent 2a7da33160
commit 994fda5cc7
3 changed files with 567 additions and 39 deletions

View File

@@ -8,3 +8,4 @@ Terminal=false
Categories=Game;RolePlaying;
Keywords=PristonTale;MMORPG;Wine;Proton;
StartupNotify=true
StartupWMClass=PristonTaleEU

View File

@@ -10,8 +10,13 @@ import sys
import time
from collections.abc import Collection, Iterable, Sequence
from .desktop_identity import (
APPLICATION_NAME,
APPLICATION_WM_CLASS,
APPLICATION_WM_INSTANCE,
)
WINDOW_TITLE = "PristonTale EU"
WINDOW_TITLE = APPLICATION_NAME
WINDOW_DISCOVERY_TIMEOUT_SECONDS = 60.0
WINDOW_READY_DELAY_SECONDS = 0.5
_X_ERROR_HANDLER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
@@ -66,6 +71,13 @@ class _MotifWmHints(ctypes.Structure):
]
class _XClassHint(ctypes.Structure):
_fields_ = [
("res_name", ctypes.c_char_p),
("res_class", ctypes.c_char_p),
]
class _X11:
def __init__(self) -> None:
library_path = ctypes.util.find_library("X11") or "libX11.so.6"
@@ -135,6 +147,33 @@ class _X11:
self.library.XMapRaised(self.display, window)
self.library.XFlush(self.display)
def set_window_identity(
self,
window: int,
*,
resource_name: str = APPLICATION_WM_INSTANCE,
resource_class: str = APPLICATION_WM_CLASS,
) -> None:
"""Replace Proton's generic steamapp identity before taskbar matching."""
for label, value in (
("resource name", resource_name),
("resource class", resource_class),
):
if not value or "\x00" in value:
raise GameboxRuntimeError(f"invalid X11 {label}")
hints = _XClassHint(
res_name=resource_name.encode("utf-8"),
res_class=resource_class.encode("utf-8"),
)
if self.library.XSetClassHint(
self.display,
window,
ctypes.byref(hints),
) == 0:
raise GameboxRuntimeError("could not set the game window identity")
self.library.XSync(self.display, 0)
def suppress_wm_ping(self, window: int) -> bool:
"""Stop the WM hang prompt for one window, preserving its other protocols."""
ping_atom = int(self.library.XInternAtom(self.display, b"_NET_WM_PING", 0))
@@ -265,6 +304,12 @@ class _X11:
ctypes.c_int,
]
self.library.XChangeProperty.restype = ctypes.c_int
self.library.XSetClassHint.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(_XClassHint),
]
self.library.XSetClassHint.restype = ctypes.c_int
self.library.XGetWMProtocols.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
@@ -325,6 +370,18 @@ def _prepare_game_window(
mapper.map_window(window, borderless=borderless)
def _apply_game_window_identity(mapper: _X11, window: int) -> None:
try:
mapper.set_window_identity(window)
except Exception as exc:
print(
f"Warning: could not assign the PristonTale EU taskbar identity ({exc}); "
"continuing normally",
file=sys.stderr,
flush=True,
)
def run_game_window(
command: Sequence[str],
window_title: str = WINDOW_TITLE,
@@ -371,6 +428,7 @@ def run_game_window(
game_window = window
game_title = title
game_window_created_at = now
_apply_game_window_identity(mapper, window)
print(
f"Game window created; preparing it: {title}",
file=sys.stderr,

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import base64
from collections import deque
from dataclasses import dataclass
import json
import math
import os
@@ -17,6 +18,11 @@ from typing import Callable, Mapping
from . import __version__
from .depot import DepotClient, DepotError, DepotSnapshot
from .desktop_identity import (
APPLICATION_NAME,
APPLICATION_WM_CLASS,
find_application_icon,
)
from .install_status import (
GameInstallationStatus,
PrimaryAction,
@@ -59,12 +65,89 @@ ACTIVITY_LOG_TRIM_LINES = 500
TRANSFER_RATE_WINDOW_SECONDS = 3.0
TRANSFER_RATE_MINIMUM_INTERVAL_SECONDS = 0.2
TRANSFER_RATE_IDLE_SECONDS = 2.0
DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS = 650
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS = 10_000
DASHBOARD_TRANSITION_STEP_MILLISECONDS = 16
DASHBOARD_TRANSITION_STEPS = 9
DASHBOARD_TRANSITION_MINIMUM_OPACITY = 0.18
DASHBOARD_STARTUP_COMPONENTS = frozenset(
{
"broadcast",
"compatibility",
"launcher_update",
"logo",
"news",
"news_artwork",
"version",
}
)
@dataclass(frozen=True)
class LauncherUpdatePresentation:
"""Visual treatment for one authenticated launcher-update result."""
button_text: str
button_style: str
badge_text: str | None
class DashboardError(RuntimeError):
"""Raised when the native dashboard cannot be displayed."""
def launcher_update_presentation(
check: LauncherUpdateCheck,
) -> LauncherUpdatePresentation:
"""Make authenticated updates visually distinct from the normal version button."""
if check.available and check.supported:
return LauncherUpdatePresentation(
button_text=f"UPDATE LAUNCHER · {check.latest_version}",
button_style="LauncherUpdate.TButton",
badge_text="SIGNED UPDATE AVAILABLE",
)
if check.available:
return LauncherUpdatePresentation(
button_text=f"MANUAL UPDATE · {check.latest_version}",
button_style="LauncherManualUpdate.TButton",
badge_text="MANUAL UPDATE REQUIRED",
)
return LauncherUpdatePresentation(
button_text=f"LAUNCHER {check.current_version}",
button_style="Secondary.TButton",
badge_text=None,
)
def dashboard_preloader_delay(
started_at: float,
*,
timestamp: float | None = None,
) -> int:
"""Keep the startup preloader visible long enough to avoid a brief flash."""
now = time.monotonic() if timestamp is None else timestamp
elapsed_milliseconds = round(max(0.0, now - started_at) * 1000)
return max(0, DASHBOARD_PRELOADER_MINIMUM_MILLISECONDS - elapsed_milliseconds)
def dashboard_content_ready(ready_components: set[str] | frozenset[str]) -> bool:
"""Return whether every user-visible startup component has settled."""
return DASHBOARD_STARTUP_COMPONENTS.issubset(ready_components)
def dashboard_transition_opacity(step: int, *, fading_in: bool) -> float:
"""Return one clamped opacity step for the loader/dashboard cross-fade."""
progress = min(max(step, 0), DASHBOARD_TRANSITION_STEPS) / DASHBOARD_TRANSITION_STEPS
opacity_range = 1.0 - DASHBOARD_TRANSITION_MINIMUM_OPACITY
if fading_in:
return DASHBOARD_TRANSITION_MINIMUM_OPACITY + opacity_range * progress
return 1.0 - opacity_range * progress
def launcher_restart_spec(
paths: AppPaths,
environment: Mapping[str, str] | None = None,
@@ -193,17 +276,31 @@ def run_dashboard(paths: AppPaths) -> None:
) from exc
paths.create_base_directories()
root = tk.Tk()
root.title("PristonTale EU — Linux Launcher")
root = tk.Tk(className=APPLICATION_WM_CLASS)
root.title(f"{APPLICATION_NAME} — Linux Launcher")
root.iconname(APPLICATION_NAME)
window_icon = 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)
except (OSError, tk.TclError) as icon_error:
print(
f"Warning: could not load the launcher icon: {icon_error}",
file=sys.stderr,
)
root.geometry(WINDOW_SIZE)
root.resizable(False, False)
root.configure(background="#0c0908")
root.withdraw()
try:
ensure_settings(paths)
except (OSError, ValueError) as error:
except (OSError, ValueError) as startup_error:
root.deiconify()
messagebox.showerror(
"Launcher settings",
f"The launcher could not load its settings:\n\n{error}",
f"The launcher could not load its settings:\n\n{startup_error}",
parent=root,
)
root.destroy()
@@ -227,6 +324,25 @@ def run_dashboard(paths: AppPaths) -> None:
foreground="#a8998a",
font=("Sans", 8),
)
style.configure("Splash.TFrame", background="#191311")
style.configure(
"SplashBrand.TLabel",
background="#191311",
foreground="#f5d18b",
font=("Georgia", 22, "bold"),
)
style.configure(
"SplashTagline.TLabel",
background="#191311",
foreground="#d8aa61",
font=("Sans", 8, "bold"),
)
style.configure(
"SplashStatus.TLabel",
background="#191311",
foreground="#c9baaa",
font=("Sans", 9),
)
style.configure(
"Section.TLabel",
background="#191311",
@@ -397,6 +513,43 @@ def run_dashboard(paths: AppPaths) -> None:
background=[("active", "#271b10"), ("disabled", "#171412")],
foreground=[("active", "#ffd17c"), ("disabled", "#887a70")],
)
style.configure(
"LauncherUpdate.TButton",
background="#f4bd61",
foreground="#24170f",
font=("Sans", 8, "bold"),
padding=(12, 8),
bordercolor="#ffe3a3",
borderwidth=2,
relief="flat",
)
style.map(
"LauncherUpdate.TButton",
background=[("active", "#ffd47f"), ("disabled", "#7b5d37")],
foreground=[("active", "#120b06"), ("disabled", "#d0bea2")],
)
style.configure(
"LauncherManualUpdate.TButton",
background="#a63f2f",
foreground="#fff4e9",
font=("Sans", 8, "bold"),
padding=(12, 8),
bordercolor="#e98769",
borderwidth=2,
relief="flat",
)
style.map(
"LauncherManualUpdate.TButton",
background=[("active", "#c95842"), ("disabled", "#60352e")],
foreground=[("active", "#ffffff"), ("disabled", "#c9ada5")],
)
style.configure(
"UpdateBadge.TLabel",
background="#d98927",
foreground="#1a1008",
font=("Sans", 7, "bold"),
padding=(7, 3),
)
style.configure(
"Activity.TButton",
background="#0c0908",
@@ -428,6 +581,203 @@ def run_dashboard(paths: AppPaths) -> None:
darkcolor="#b66c25",
)
startup_started_at = time.monotonic()
startup_status = tk.StringVar(value="Loading dashboard contents…")
startup_overlay = tk.Frame(
root,
background="#0c0908",
borderwidth=0,
highlightthickness=0,
)
startup_overlay.place(x=0, y=0, relwidth=1, relheight=1)
startup_border = tk.Frame(
startup_overlay,
background="#d98927",
borderwidth=0,
highlightthickness=0,
)
startup_border.place(relx=0.5, rely=0.5, anchor="center")
startup_shell = ttk.Frame(
startup_border,
style="Splash.TFrame",
padding=(26, 22),
)
startup_shell.pack(fill="both", expand=True, padx=1, pady=1)
ttk.Label(
startup_shell,
text="PRISTONTALE EU",
style="SplashBrand.TLabel",
).pack(anchor="w")
ttk.Label(
startup_shell,
text="NATIVE LINUX LAUNCHER",
style="SplashTagline.TLabel",
).pack(anchor="w", pady=(2, 17))
ttk.Label(
startup_shell,
textvariable=startup_status,
style="SplashStatus.TLabel",
).pack(anchor="w", pady=(0, 8))
startup_progress = ttk.Progressbar(
startup_shell,
style="Depot.Horizontal.TProgressbar",
mode="indeterminate",
length=420,
)
startup_progress.pack(fill="x")
startup_progress.start(12)
startup_ready_components: set[str] = set()
startup_transition_started = False
startup_ready_after: str | None = None
startup_timeout_after: str | None = None
root.deiconify()
root.lift()
startup_overlay.lift()
root.update()
def update_startup_progress(message: str) -> None:
if startup_transition_started:
return
startup_progress.stop()
startup_progress.configure(
mode="determinate",
maximum=len(DASHBOARD_STARTUP_COMPONENTS),
value=len(startup_ready_components),
)
startup_status.set(
f"{message} · {len(startup_ready_components)}/"
f"{len(DASHBOARD_STARTUP_COMPONENTS)}"
)
def cancel_startup_after(identifier: str | None) -> None:
if identifier is None:
return
try:
root.after_cancel(identifier)
except tk.TclError:
pass
def set_startup_component_pending(component: str) -> None:
nonlocal startup_ready_after
if startup_transition_started or component not in DASHBOARD_STARTUP_COMPONENTS:
return
if component not in startup_ready_components:
return
startup_ready_components.remove(component)
cancel_startup_after(startup_ready_after)
startup_ready_after = None
update_startup_progress("Loading dashboard content…")
def mark_startup_component_ready(component: str, message: str) -> None:
if startup_transition_started or component not in DASHBOARD_STARTUP_COMPONENTS:
return
startup_ready_components.add(component)
update_startup_progress(message)
maybe_start_dashboard_transition()
def set_root_opacity(value: float) -> bool:
try:
root.attributes("-alpha", value)
except tk.TclError:
return False
return True
def finish_dashboard_transition() -> None:
set_root_opacity(1.0)
root.lift()
root.focus_set()
def fade_dashboard_in(step: int = 0) -> None:
if not set_root_opacity(
dashboard_transition_opacity(step, fading_in=True)
):
finish_dashboard_transition()
return
if step >= DASHBOARD_TRANSITION_STEPS:
finish_dashboard_transition()
return
root.after(
DASHBOARD_TRANSITION_STEP_MILLISECONDS,
lambda: fade_dashboard_in(step + 1),
)
def swap_loader_for_dashboard(*, fade_supported: bool) -> None:
if startup_overlay.winfo_exists():
startup_overlay.destroy()
root.update_idletasks()
if fade_supported:
fade_dashboard_in()
else:
finish_dashboard_transition()
def fade_loader_out(step: int = 0) -> None:
fade_supported = set_root_opacity(
dashboard_transition_opacity(step, fading_in=False)
)
if not fade_supported or step >= DASHBOARD_TRANSITION_STEPS:
swap_loader_for_dashboard(fade_supported=fade_supported)
return
root.after(
DASHBOARD_TRANSITION_STEP_MILLISECONDS,
lambda: fade_loader_out(step + 1),
)
def begin_dashboard_transition(*, timed_out: bool = False) -> None:
nonlocal startup_transition_started
nonlocal startup_ready_after, startup_timeout_after
if startup_transition_started:
return
startup_transition_started = True
cancel_startup_after(startup_ready_after)
cancel_startup_after(startup_timeout_after)
startup_ready_after = None
startup_timeout_after = None
if timed_out:
settle_pending_news_artwork()
if "version" not in startup_ready_components:
version.set(
f"Launcher {__version__} · game build check continuing in background"
)
startup_progress.stop()
startup_progress.configure(
mode="determinate",
maximum=len(DASHBOARD_STARTUP_COMPONENTS),
value=len(DASHBOARD_STARTUP_COMPONENTS),
)
startup_status.set(
"Opening dashboard with available content…"
if timed_out
else "Opening dashboard…"
)
startup_overlay.update_idletasks()
root.after(80, fade_loader_out)
def maybe_start_dashboard_transition() -> None:
nonlocal startup_ready_after
if startup_transition_started or not dashboard_content_ready(
startup_ready_components
):
return
remaining = dashboard_preloader_delay(startup_started_at)
if remaining <= 0:
begin_dashboard_transition()
return
if startup_ready_after is None:
startup_ready_after = root.after(
remaining,
start_dashboard_after_minimum,
)
def start_dashboard_after_minimum() -> None:
nonlocal startup_ready_after
startup_ready_after = None
maybe_start_dashboard_transition()
def timeout_startup_preloader() -> None:
nonlocal startup_timeout_after
startup_timeout_after = None
begin_dashboard_transition(timed_out=True)
frame = ttk.Frame(root, style="Root.TFrame", padding=12)
frame.pack(fill="both", expand=True)
@@ -441,6 +791,14 @@ def run_dashboard(paths: AppPaths) -> None:
highlightthickness=0,
borderwidth=0,
)
logo.create_oval(4, 4, 38, 38, outline="#d98927", width=2)
logo.create_text(
21,
21,
text="PT",
fill="#f5d18b",
font=("Georgia", 9, "bold"),
)
logo.pack(side="left", padx=(0, 9))
brand = ttk.Frame(header, style="Root.TFrame")
brand.pack(side="left", fill="x", expand=True)
@@ -488,7 +846,14 @@ def run_dashboard(paths: AppPaths) -> None:
system_actions = ttk.Frame(controls, style="Surface.TFrame", padding=12)
system_actions.grid(row=0, column=1, sticky="nsew", padx=(4, 0))
ttk.Label(system_actions, text="LAUNCHER", style="Section.TLabel").pack(anchor="w")
system_heading = ttk.Frame(system_actions, style="Surface.TFrame")
system_heading.pack(fill="x")
ttk.Label(system_heading, text="LAUNCHER", style="Section.TLabel").pack(side="left")
launcher_update_badge = ttk.Label(
system_heading,
text="",
style="UpdateBadge.TLabel",
)
system_button_row = ttk.Frame(system_actions, style="Surface.TFrame")
system_button_row.pack(anchor="w", fill="x", pady=(6, 0))
doctor_button = ttk.Button(system_button_row, text="SYSTEM CHECK", style="Secondary.TButton")
@@ -609,6 +974,7 @@ def run_dashboard(paths: AppPaths) -> None:
transfer_rate = TransferRateEstimator()
news_image_targets: dict[int, tk.Canvas] = {}
news_image_references: dict[int, tk.PhotoImage] = {}
startup_news_images_pending: set[int] = set()
logo_image_references: list[tk.PhotoImage] = []
preloader: tk.Toplevel | None = None
preloader_status: tk.StringVar | None = None
@@ -980,6 +1346,11 @@ def run_dashboard(paths: AppPaths) -> None:
text="Official news is temporarily unavailable. Your cached game can still play.",
style="Muted.TLabel",
).pack(anchor="w", padx=8, pady=8)
startup_news_images_pending.clear()
mark_startup_component_ready(
"news_artwork",
"News layout ready",
)
return
visible_articles = articles[:MAX_NEWS_CARDS]
for column in range(len(visible_articles)):
@@ -1046,32 +1417,53 @@ def run_dashboard(paths: AppPaths) -> None:
load_news_images(visible_articles)
def load_news_images(articles: tuple[NewsArticle, ...]) -> None:
image_articles = tuple(
article for article in articles if article.image_url is not None
)
startup_news_images_pending.clear()
startup_news_images_pending.update(article.id for article in image_articles)
if startup_news_images_pending:
set_startup_component_pending("news_artwork")
else:
mark_startup_component_ready(
"news_artwork",
"News artwork ready",
)
def worker(news_article: NewsArticle) -> None:
image = fetch_news_image(news_article, paths.news_image_cache)
if image is not None:
events.put(("news_image", (news_article.id, image)))
events.put(("news_image", (news_article.id, image)))
for article in articles:
if article.image_url is not None:
threading.Thread(
target=worker,
args=(article,),
name="ptlinux-news-image",
daemon=True,
).start()
for article in image_articles:
threading.Thread(
target=worker,
args=(article,),
name="ptlinux-news-image",
daemon=True,
).start()
def install_news_image(value: object) -> None:
if (
not isinstance(value, tuple)
or len(value) != 2
or not isinstance(value[0], int)
or not isinstance(value[1], bytes)
or (value[1] is not None and not isinstance(value[1], bytes))
):
return
article_id, image_data = value
target = news_image_targets.get(article_id)
if target is None or not target.winfo_exists():
return
if image_data is None:
target.delete("all")
target.create_text(
NEWS_IMAGE_WIDTH // 2,
NEWS_IMAGE_HEIGHT // 2,
text="Artwork unavailable",
fill="#a8998a",
font=("Sans", 8),
)
return
try:
image = tk.PhotoImage(data=base64.b64encode(image_data), format="png")
except tk.TclError:
@@ -1104,6 +1496,32 @@ def run_dashboard(paths: AppPaths) -> None:
target.delete("all")
target.create_image(target_width // 2, target_height // 2, image=image)
def complete_news_image(value: object) -> None:
install_news_image(value)
if not isinstance(value, tuple) or not value or not isinstance(value[0], int):
return
startup_news_images_pending.discard(value[0])
if not startup_news_images_pending:
mark_startup_component_ready(
"news_artwork",
"News artwork ready",
)
def settle_pending_news_artwork() -> None:
for article_id in tuple(startup_news_images_pending):
target = news_image_targets.get(article_id)
if target is None or not target.winfo_exists():
continue
target.delete("all")
target.create_text(
NEWS_IMAGE_WIDTH // 2,
NEWS_IMAGE_HEIGHT // 2,
text="Artwork unavailable",
fill="#a8998a",
font=("Sans", 8),
)
startup_news_images_pending.clear()
def install_official_logo(value: object) -> None:
if not isinstance(value, bytes):
return
@@ -1122,8 +1540,7 @@ def run_dashboard(paths: AppPaths) -> None:
def load_official_logo() -> None:
def worker() -> None:
image = fetch_official_logo(paths.logo_cache)
if image is not None:
events.put(("logo_image", image))
events.put(("logo_image", image))
threading.Thread(target=worker, name="ptlinux-logo", daemon=True).start()
@@ -1191,6 +1608,10 @@ def run_dashboard(paths: AppPaths) -> None:
local_status = inspect_game_installation(paths)
refresh_primary_action(update_activity=update_activity)
if not local_status.installed or game_import_requires_verification(paths):
mark_startup_component_ready(
"compatibility",
"Game state ready",
)
return
def worker() -> None:
@@ -1215,21 +1636,18 @@ def run_dashboard(paths: AppPaths) -> None:
) -> None:
nonlocal launcher_update_check
launcher_update_check = check
if check.available and check.supported:
launcher_update_button.configure(
text=f"UPDATE TO {check.latest_version}",
state="normal",
)
elif check.available:
launcher_update_button.configure(
text="MANUAL UPDATE NEEDED",
state="normal",
)
presentation = launcher_update_presentation(check)
launcher_update_button.configure(
text=presentation.button_text,
style=presentation.button_style,
state="normal",
)
if presentation.badge_text is not None:
launcher_update_badge.configure(text=presentation.badge_text)
if not launcher_update_badge.winfo_manager():
launcher_update_badge.pack(side="right")
else:
launcher_update_button.configure(
text=f"LAUNCHER {__version__}",
state="normal",
)
launcher_update_badge.pack_forget()
if log_reason:
append_log(check.reason)
@@ -1339,12 +1757,24 @@ def run_dashboard(paths: AppPaths) -> None:
root.iconify()
elif kind == "broadcast":
show_broadcast(value if isinstance(value, Broadcast) else None)
mark_startup_component_ready(
"broadcast",
"Broadcast ready",
)
elif kind == "news" and isinstance(value, tuple):
show_news(value)
mark_startup_component_ready(
"news",
"News cards ready",
)
elif kind == "news_image":
install_news_image(value)
complete_news_image(value)
elif kind == "logo_image":
install_official_logo(value)
mark_startup_component_ready(
"logo",
"Launcher artwork ready",
)
elif (
kind == "compatibility"
and isinstance(value, tuple)
@@ -1368,28 +1798,50 @@ def run_dashboard(paths: AppPaths) -> None:
f"{checked.reason or 'unknown release profile'}"
)
refresh_primary_action(update_activity=update_activity)
mark_startup_component_ready(
"compatibility",
"Game compatibility ready",
)
elif kind == "version" and isinstance(value, DepotSnapshot):
version.set(
f"Launcher {__version__} · {value.version} · depot {value.build}"
)
refresh_primary_action(value)
mark_startup_component_ready(
"version",
"Game version ready",
)
elif kind == "version_error":
version.set(
f"Launcher {__version__} · game build check unavailable"
)
refresh_primary_action()
mark_startup_component_ready(
"version",
"Game version check complete",
)
elif kind == "launcher_update_check" and isinstance(
value,
LauncherUpdateCheck,
):
show_launcher_update_check(value)
mark_startup_component_ready(
"launcher_update",
"Launcher update status ready",
)
elif kind == "launcher_update_error" and isinstance(value, str):
if launcher_update_check is None:
launcher_update_button.configure(
text="CHECK LAUNCHER",
style="Secondary.TButton",
state="normal",
)
launcher_update_badge.pack_forget()
append_log(f"Launcher update check unavailable: {value}")
mark_startup_component_ready(
"launcher_update",
"Launcher update check complete",
)
except queue.Empty:
pass
if operation_active:
@@ -1399,14 +1851,16 @@ def run_dashboard(paths: AppPaths) -> None:
if speed_text.get() != "0 B/s" and transfer_rate.is_idle():
speed_text.set("0 B/s")
update_elapsed_indicator()
root.after(
dashboard_poll_interval(
poll_delay = (
ACTIVE_POLL_INTERVAL_MILLISECONDS
if not startup_transition_started
else dashboard_poll_interval(
operation_active=operation_active,
game_window_active=game_window_active,
pending_events=not events.empty(),
),
poll_events,
)
)
root.after(poll_delay, poll_events)
def command_for(*arguments: str) -> list[str]:
return [sys.executable, "-m", "ptlinux", *arguments]
@@ -1414,7 +1868,12 @@ def run_dashboard(paths: AppPaths) -> None:
def run_launcher_update(required_reason: str | None = None) -> None:
check = launcher_update_check
if check is None:
launcher_update_button.configure(text="CHECKING…", state="disabled")
launcher_update_badge.pack_forget()
launcher_update_button.configure(
text="CHECKING…",
style="Secondary.TButton",
state="disabled",
)
if required_reason is not None:
status.set("Checking for a compatible launcher update…")
progress_text.set("Play remains disabled until this release is supported.")
@@ -1824,6 +2283,9 @@ def run_dashboard(paths: AppPaths) -> None:
news_content.bind("<Configure>", update_news_viewport)
news_canvas.bind("<Configure>", resize_news_content)
startup_overlay.lift()
startup_status.set("Restoring dashboard content…")
startup_overlay.update_idletasks()
show_broadcast(read_cached_broadcast(paths.broadcast_cache))
show_news(read_cached_news(paths.news_cache))
cached_launcher_update = read_cached_launcher_update_check(
@@ -1835,7 +2297,14 @@ def run_dashboard(paths: AppPaths) -> None:
refresh_runtime_compatibility()
poll_events()
load_official_logo()
update_startup_progress("Loading online dashboard content…")
startup_overlay.update_idletasks()
startup_timeout_after = root.after(
DASHBOARD_PRELOADER_TIMEOUT_MILLISECONDS,
timeout_startup_preloader,
)
refresh_remote_content()
maybe_start_dashboard_transition()
try:
root.mainloop()
except KeyboardInterrupt: