Fixed some UI issues with updater.

This commit is contained in:
2026-09-10 00:13:05 +02:00
parent a362b0bb8e
commit 9644de3ff5
6 changed files with 214 additions and 73 deletions

View File

@@ -4,4 +4,4 @@ from .paths import AppPaths
__all__ = ["AppPaths", "__version__"]
__version__ = "0.9.8"
__version__ = "0.9.11"

View File

@@ -7,7 +7,6 @@ from typing import Any, Callable
from ...paths import AppPaths
from ...self_update import (
LauncherRemoteHistory,
LauncherRemoteRelease,
LauncherUpdateCheck,
LauncherUpdateHistory,
inspect_launcher_update_history,
@@ -52,6 +51,46 @@ def release_notes_preview(markdown: str | None) -> str:
return preview
def update_version_heading(
running_version: str,
latest_version: str,
channel: str,
*,
update_available: bool,
) -> str:
"""Describe update versions without confusing an older channel head as current."""
if update_available:
return f"Update: {running_version} → {latest_version}"
return (
f"Current version: {running_version}\n"
f"Latest {channel.title()} release: {latest_version}"
)
def release_history_state(
release_status: str,
*,
is_running: bool,
local_active: bool,
local_retained: bool,
is_older: bool,
) -> str:
"""Label running and installed releases without treating them as synonymous."""
if is_running:
return "Current"
if local_active:
return "Installed"
if release_status != "active":
return release_status.title()
if local_retained:
return "Retained locally"
if is_older:
return "Available rollback"
return "Published"
class LauncherUpdateDialog:
"""Own the update window and expose small refresh/error entry points."""
@@ -97,6 +136,8 @@ class LauncherUpdateDialog:
self._history: Any = None
self._history_filter_value: Any = None
self._history_items: dict[str, tuple[str, str, str | None, bool]] = {}
self._release_notes_frame: Any = None
self._release_notes_text: Any = None
self._release_notes_button: Any = None
self._check_button: Any = None
self._rollback_button: Any = None
@@ -120,7 +161,7 @@ class LauncherUpdateDialog:
dialog.withdraw()
self._window = dialog
dialog.title("PristonTale EU — Launcher Updates")
dialog.resizable(False, False)
dialog.resizable(False, True)
dialog.configure(background="#0c0908")
dialog.transient(self._root)
self._apply_window_icon(dialog)
@@ -201,6 +242,53 @@ class LauncherUpdateDialog:
sticky="e",
padx=(12, 0),
)
self._release_notes_frame = ttk.Frame(
overview,
style="SettingsCard.TFrame",
)
self._release_notes_frame.grid(
row=2,
column=0,
columnspan=2,
sticky="ew",
pady=(10, 0),
)
self._release_notes_frame.columnconfigure(0, weight=1)
ttk.Label(
self._release_notes_frame,
text="WHAT'S NEW",
style="SettingsText.TLabel",
).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 4))
self._release_notes_text = tk.Text(
self._release_notes_frame,
name="launcher_update_release_notes",
width=1,
height=3,
wrap="word",
background="#100d0c",
foreground="#c8b8a8",
selectbackground="#6a482d",
selectforeground="#ffffff",
font=("Sans", 8),
borderwidth=0,
highlightthickness=0,
padx=8,
pady=6,
state="disabled",
takefocus=False,
)
release_notes_scrollbar = ttk.Scrollbar(
self._release_notes_frame,
orient="vertical",
style="News.Vertical.TScrollbar",
command=self._release_notes_text.yview,
)
self._release_notes_text.configure(
yscrollcommand=release_notes_scrollbar.set,
)
self._release_notes_text.grid(row=1, column=0, sticky="ew")
release_notes_scrollbar.grid(row=1, column=1, sticky="ns", padx=(6, 0))
self._release_notes_frame.grid_remove()
history_card = ttk.LabelFrame(
content,
@@ -244,7 +332,7 @@ class LauncherUpdateDialog:
history_card,
columns=("version", "channel", "state", "installed"),
show="headings",
height=6,
height=4,
style="Update.Treeview",
selectmode="browse",
)
@@ -333,8 +421,8 @@ class LauncherUpdateDialog:
self._check_again()
dialog.update_idletasks()
updates_width, updates_height = fitted_window_size(
760,
620,
820,
680,
scale=self._ui_scale,
screen_width=dialog.winfo_screenwidth(),
screen_height=dialog.winfo_screenheight(),
@@ -365,6 +453,8 @@ class LauncherUpdateDialog:
assert self._status_value is not None
assert self._details_value is not None
assert self._history is not None
assert self._release_notes_frame is not None
assert self._release_notes_text is not None
assert self._release_notes_button is not None
assert self._check_button is not None
assert self._rollback_button is not None
@@ -381,10 +471,11 @@ class LauncherUpdateDialog:
if check is None:
self._status_value.set("Checking the signed update channel…")
self._details_value.set(
"Installed launcher "
"Running launcher "
+ self._launcher_version
+ " · release information pending"
)
self._set_release_notes_preview("")
self._install_button.configure(state="disabled")
self._refresh_history_actions(busy=busy)
return
@@ -400,13 +491,14 @@ class LauncherUpdateDialog:
for release in (
remote_history.releases if remote_history is not None else ()
)
if release.version == check.current_version and release.status != "active"
if release.version == self._launcher_version
and release.status != "active"
),
None,
)
if installed_status is not None:
status_prefix += (
f"Warning: installed launcher {check.current_version} is "
f"Warning: running launcher {self._launcher_version} is "
f"{installed_status}; install the next active eligible release.\n"
)
self._status_value.set(status_prefix + check.reason)
@@ -418,18 +510,24 @@ class LauncherUpdateDialog:
if check.manifest.channel == "prerelease"
else ""
)
summary_line = f"{release_summary}\n" if not notes_preview else ""
version_line = update_version_heading(
self._launcher_version,
check.latest_version,
check.manifest.channel,
update_available=check.available,
)
details = (
f"{check.current_version} → {check.latest_version}\n"
f"{version_line}\n"
f"{release_title}{prerelease_label}\n"
f"{release_summary}\n"
f"{summary_line}"
f"Channel: {check.manifest.channel.title()} · "
f"{format_bytes(check.manifest.archive_size)} · "
f"published {check.manifest.published_at} · "
f"checked {check.checked_at or 'time unavailable'}"
)
if notes_preview:
details += f"\n\nWhat's new:\n{notes_preview}"
self._details_value.set(details)
self._set_release_notes_preview(notes_preview)
self._install_button.configure(
state=(
"normal"
@@ -442,6 +540,20 @@ class LauncherUpdateDialog:
)
self._refresh_history_actions(busy=busy)
def _set_release_notes_preview(self, preview: str) -> None:
assert self._release_notes_frame is not None
assert self._release_notes_text is not None
self._release_notes_text.configure(state="normal")
self._release_notes_text.delete("1.0", "end")
if preview:
self._release_notes_text.insert("1.0", preview)
self._release_notes_text.configure(state="disabled")
self._release_notes_text.yview_moveto(0.0)
if preview:
self._release_notes_frame.grid()
else:
self._release_notes_frame.grid_remove()
def _populate_history(self, *, busy: bool) -> None:
assert self._history is not None
assert self._history_filter_value is not None
@@ -461,16 +573,22 @@ class LauncherUpdateDialog:
else ()
)
}
current_version = (
installed_current_version = (
self._installed_history.current_version
if self._installed_history is not None
and self._installed_history.current_version is not None
else self._launcher_version
)
try:
current_semantic = SemanticVersion.parse(current_version)
installed_current_semantic = SemanticVersion.parse(
installed_current_version
)
except ReleaseModelError:
current_semantic = None
installed_current_semantic = None
try:
running_channel = SemanticVersion.parse(self._launcher_version).channel
except ReleaseModelError:
running_channel = "unknown"
filter_name = str(self._history_filter_value.get()).casefold()
def channel_visible(channel: str) -> bool:
@@ -478,20 +596,27 @@ class LauncherUpdateDialog:
remote_history = self._get_remote_history()
remote_versions: set[str] = set()
displayed_versions: set[str] = set()
if remote_history is not None:
for index, release in enumerate(remote_history.releases):
if not channel_visible(release.channel):
continue
remote_versions.add(release.version)
displayed_versions.add(release.version)
local = installed.get(release.version)
is_older = (
current_semantic is not None
and SemanticVersion.parse(release.version) < current_semantic
installed_current_semantic is not None
and SemanticVersion.parse(release.version)
< installed_current_semantic
)
can_rollback = release.status == "active" and is_older
state = self._remote_release_state(
release,
local_active=bool(local is not None and local.active),
local_active = bool(local is not None and local.active)
can_rollback = (
release.status == "active" and is_older and not local_active
)
state = release_history_state(
release.status,
is_running=release.version == self._launcher_version,
local_active=local_active,
local_retained=local is not None,
is_older=is_older,
)
@@ -517,14 +642,23 @@ class LauncherUpdateDialog:
for index, release in enumerate(self._installed_history.releases):
if release.version in remote_versions or not channel_visible(release.channel):
continue
displayed_versions.add(release.version)
try:
is_older = (
current_semantic is not None
and SemanticVersion.parse(release.version) < current_semantic
installed_current_semantic is not None
and SemanticVersion.parse(release.version)
< installed_current_semantic
)
except ReleaseModelError:
is_older = False
state = "Current" if release.active else "Retained locally"
state = release_history_state(
"active",
is_running=release.version == self._launcher_version,
local_active=release.active,
local_retained=True,
is_older=is_older,
)
can_rollback = is_older and not release.active
iid = f"local:{index}:{release.version}"
self._history.insert(
"",
@@ -541,8 +675,30 @@ class LauncherUpdateDialog:
release.version,
release.channel,
None,
is_older,
can_rollback,
)
if (
self._launcher_version not in displayed_versions
and channel_visible(running_channel)
):
iid = f"running:{self._launcher_version}"
self._history.insert(
"",
0,
iid=iid,
values=(
self._launcher_version,
running_channel.title(),
"Current",
"Running now",
),
)
self._history_items[iid] = (
self._launcher_version,
running_channel,
None,
False,
)
if not self._history.get_children():
iid = "placeholder"
self._history.insert(
@@ -567,24 +723,6 @@ class LauncherUpdateDialog:
self._history.focus(selected_iid)
self._refresh_history_actions(busy=busy)
@staticmethod
def _remote_release_state(
release: LauncherRemoteRelease,
*,
local_active: bool,
local_retained: bool,
is_older: bool,
) -> str:
if local_active:
return "Current"
if release.status != "active":
return release.status.title()
if local_retained:
return "Retained locally"
if is_older:
return "Available rollback"
return "Published"
def _history_selected(self, _event: object | None = None) -> None:
self._refresh_history_actions(busy=self._is_busy())
@@ -621,6 +759,7 @@ class LauncherUpdateDialog:
assert self._check_button is not None
self._status_value.set("The signed update channel could not be checked.")
self._details_value.set(message)
self._set_release_notes_preview("")
self._check_button.configure(state="normal")
def _check_again(self) -> None:
@@ -630,6 +769,7 @@ class LauncherUpdateDialog:
assert self._install_button is not None
self._status_value.set("Checking the signed update channel…")
self._details_value.set("Authenticating the selected channel manifests and signatures.")
self._set_release_notes_preview("")
self._check_button.configure(state="disabled")
self._install_button.configure(state="disabled")
self._request_check()
@@ -637,8 +777,9 @@ class LauncherUpdateDialog:
def _view_release_notes(self) -> None:
selected = self._selected_history_item()
if selected is not None:
if selected[2] is not None:
self._open_article(selected[2])
notes_url = selected[2]
if notes_url is not None:
self._open_article(notes_url)
return
check = self._get_check()
if check is not None and check.manifest.notes_url is not None: