Updated the update system to support multi channels (stable / pre-release).
This commit is contained in:
@@ -349,12 +349,13 @@ def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) ->
|
||||
"Settings.TCombobox",
|
||||
fieldbackground=[
|
||||
("disabled", "#171412"),
|
||||
("active", "#100d0c"),
|
||||
("readonly", "#100d0c"),
|
||||
],
|
||||
background=[
|
||||
("disabled", "#171412"),
|
||||
("pressed", "#32251f"),
|
||||
("active", "#2a201c"),
|
||||
("active", "#191311"),
|
||||
("readonly", "#191311"),
|
||||
],
|
||||
foreground=[
|
||||
|
||||
@@ -6,10 +6,13 @@ from typing import Any, Callable
|
||||
|
||||
from ...paths import AppPaths
|
||||
from ...self_update import (
|
||||
LauncherRemoteHistory,
|
||||
LauncherRemoteRelease,
|
||||
LauncherUpdateCheck,
|
||||
LauncherUpdateHistory,
|
||||
inspect_launcher_update_history,
|
||||
)
|
||||
from ...update_resolver import PRERELEASE_CHANNEL, ReleaseModelError, SemanticVersion
|
||||
from ...tk_chrome import (
|
||||
centered_window_geometry,
|
||||
create_window_chrome,
|
||||
@@ -19,6 +22,36 @@ from ..viewmodels.dashboard import fitted_window_size
|
||||
from ..viewmodels.downloader import format_bytes
|
||||
|
||||
|
||||
MAX_RELEASE_NOTES_PREVIEW_LINES = 6
|
||||
MAX_RELEASE_NOTES_PREVIEW_CHARACTERS = 900
|
||||
|
||||
|
||||
def release_notes_preview(markdown: str | None) -> str:
|
||||
"""Return a compact, readable changelog excerpt for the update dialog."""
|
||||
|
||||
if not markdown:
|
||||
return ""
|
||||
lines: list[str] = []
|
||||
truncated = False
|
||||
for raw_line in markdown.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or (not lines and line.startswith("# ")):
|
||||
continue
|
||||
if line == "---":
|
||||
break
|
||||
if len(lines) >= MAX_RELEASE_NOTES_PREVIEW_LINES:
|
||||
truncated = True
|
||||
break
|
||||
lines.append(line)
|
||||
preview = "\n".join(lines)
|
||||
if len(preview) > MAX_RELEASE_NOTES_PREVIEW_CHARACTERS:
|
||||
preview = preview[:MAX_RELEASE_NOTES_PREVIEW_CHARACTERS].rstrip()
|
||||
truncated = True
|
||||
if truncated:
|
||||
preview = preview.rstrip() + "\n…"
|
||||
return preview
|
||||
|
||||
|
||||
class LauncherUpdateDialog:
|
||||
"""Own the update window and expose small refresh/error entry points."""
|
||||
|
||||
@@ -32,6 +65,8 @@ class LauncherUpdateDialog:
|
||||
window_icon: Any | None,
|
||||
apply_window_icon: Callable[[Any], None],
|
||||
get_check: Callable[[], LauncherUpdateCheck | None],
|
||||
get_remote_history: Callable[[], LauncherRemoteHistory | None],
|
||||
get_selected_channel: Callable[[], str],
|
||||
is_busy: Callable[[], bool],
|
||||
request_check: Callable[[], None],
|
||||
install_and_restart: Callable[[str], None],
|
||||
@@ -46,6 +81,8 @@ class LauncherUpdateDialog:
|
||||
self._window_icon = window_icon
|
||||
self._apply_window_icon = apply_window_icon
|
||||
self._get_check = get_check
|
||||
self._get_remote_history = get_remote_history
|
||||
self._get_selected_channel = get_selected_channel
|
||||
self._is_busy = is_busy
|
||||
self._request_check = request_check
|
||||
self._install_and_restart = install_and_restart
|
||||
@@ -58,6 +95,8 @@ class LauncherUpdateDialog:
|
||||
self._status_value: Any = None
|
||||
self._details_value: Any = None
|
||||
self._history: Any = None
|
||||
self._history_filter_value: Any = None
|
||||
self._history_items: dict[str, tuple[str, str, str | None, bool]] = {}
|
||||
self._release_notes_button: Any = None
|
||||
self._check_button: Any = None
|
||||
self._rollback_button: Any = None
|
||||
@@ -103,7 +142,7 @@ class LauncherUpdateDialog:
|
||||
header_copy.pack(side="left", fill="both", expand=True, padx=20, pady=14)
|
||||
tk.Label(
|
||||
header_copy,
|
||||
text="SIGNED STABLE CHANNEL",
|
||||
text="SIGNED RELEASE CHANNEL",
|
||||
background="#24170f",
|
||||
foreground="#d8aa61",
|
||||
font=("Sans", 8, "bold"),
|
||||
@@ -165,24 +204,54 @@ class LauncherUpdateDialog:
|
||||
|
||||
history_card = ttk.LabelFrame(
|
||||
content,
|
||||
text="INSTALLED HISTORY",
|
||||
text="VERSION HISTORY",
|
||||
style="SettingsCard.TLabelframe",
|
||||
padding=12,
|
||||
)
|
||||
history_card.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
|
||||
history_card.columnconfigure(0, weight=1)
|
||||
history_card.rowconfigure(0, weight=1)
|
||||
history_card.rowconfigure(1, weight=1)
|
||||
filter_row = ttk.Frame(history_card, style="Root.TFrame")
|
||||
filter_row.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 8))
|
||||
filter_row.columnconfigure(0, weight=1)
|
||||
ttk.Label(
|
||||
filter_row,
|
||||
text="Signed Gitea releases and retained local versions",
|
||||
style="SettingsHint.TLabel",
|
||||
).grid(row=0, column=0, sticky="w")
|
||||
self._history_filter_value = tk.StringVar(
|
||||
value=(
|
||||
"All"
|
||||
if self._get_selected_channel() == PRERELEASE_CHANNEL
|
||||
else "Stable"
|
||||
)
|
||||
)
|
||||
history_filter = ttk.Combobox(
|
||||
filter_row,
|
||||
name="launcher_update_channel_filter",
|
||||
textvariable=self._history_filter_value,
|
||||
values=("All", "Stable", "Prerelease"),
|
||||
state="readonly",
|
||||
style="Settings.TCombobox",
|
||||
cursor="hand2",
|
||||
exportselection=False,
|
||||
takefocus=True,
|
||||
width=12,
|
||||
)
|
||||
history_filter.grid(row=0, column=1, sticky="e", padx=(12, 0))
|
||||
history_filter.bind("<<ComboboxSelected>>", lambda _event: self.refresh())
|
||||
self._history = ttk.Treeview(
|
||||
history_card,
|
||||
columns=("version", "state", "installed"),
|
||||
columns=("version", "channel", "state", "installed"),
|
||||
show="headings",
|
||||
height=6,
|
||||
style="Update.Treeview",
|
||||
selectmode="browse",
|
||||
)
|
||||
self._history.heading("version", text="VERSION", anchor="w")
|
||||
self._history.heading("channel", text="CHANNEL", anchor="w")
|
||||
self._history.heading("state", text="STATE", anchor="w")
|
||||
self._history.heading("installed", text="INSTALLED", anchor="w")
|
||||
self._history.heading("installed", text="DATE", anchor="w")
|
||||
self._history.column(
|
||||
"version",
|
||||
width=round(120 * self._ui_scale),
|
||||
@@ -190,6 +259,13 @@ class LauncherUpdateDialog:
|
||||
stretch=False,
|
||||
anchor="w",
|
||||
)
|
||||
self._history.column(
|
||||
"channel",
|
||||
width=round(105 * self._ui_scale),
|
||||
minwidth=round(80 * self._ui_scale),
|
||||
stretch=False,
|
||||
anchor="w",
|
||||
)
|
||||
self._history.column(
|
||||
"state",
|
||||
width=round(150 * self._ui_scale),
|
||||
@@ -204,14 +280,15 @@ class LauncherUpdateDialog:
|
||||
stretch=True,
|
||||
anchor="w",
|
||||
)
|
||||
self._history.grid(row=0, column=0, sticky="nsew")
|
||||
self._history.grid(row=1, column=0, sticky="nsew")
|
||||
self._history.bind("<<TreeviewSelect>>", self._history_selected)
|
||||
history_scrollbar = ttk.Scrollbar(
|
||||
history_card,
|
||||
orient="vertical",
|
||||
style="News.Vertical.TScrollbar",
|
||||
command=self._history.yview,
|
||||
)
|
||||
history_scrollbar.grid(row=0, column=1, sticky="ns", padx=(6, 0))
|
||||
history_scrollbar.grid(row=1, column=1, sticky="ns", padx=(6, 0))
|
||||
self._history.configure(yscrollcommand=history_scrollbar.set)
|
||||
|
||||
footer = ttk.Frame(content, style="SettingsFooter.TFrame", padding=(12, 9))
|
||||
@@ -298,37 +375,8 @@ class LauncherUpdateDialog:
|
||||
except RuntimeError as error:
|
||||
self._installed_history = None
|
||||
self._append_log(f"Could not inspect launcher update history: {error}")
|
||||
for item in self._history.get_children():
|
||||
self._history.delete(item)
|
||||
if self._installed_history is not None:
|
||||
for release in self._installed_history.releases:
|
||||
state_label = (
|
||||
"Current"
|
||||
if release.active
|
||||
else "Rollback target"
|
||||
if release.previous
|
||||
else "Retained"
|
||||
)
|
||||
self._history.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(release.version, state_label, release.installed_at),
|
||||
)
|
||||
if not self._history.get_children():
|
||||
self._history.insert(
|
||||
"",
|
||||
"end",
|
||||
values=(self._launcher_version, "Running", "Not recorded"),
|
||||
)
|
||||
busy = self._is_busy()
|
||||
rollback_available = (
|
||||
self._installed_history is not None
|
||||
and self._installed_history.previous_version is not None
|
||||
and not busy
|
||||
)
|
||||
self._rollback_button.configure(
|
||||
state="normal" if rollback_available else "disabled"
|
||||
)
|
||||
self._populate_history(busy=busy)
|
||||
self._check_button.configure(state="disabled" if busy else "normal")
|
||||
if check is None:
|
||||
self._status_value.set("Checking the signed update channel…")
|
||||
@@ -338,21 +386,50 @@ class LauncherUpdateDialog:
|
||||
+ " · release information pending"
|
||||
)
|
||||
self._install_button.configure(state="disabled")
|
||||
self._release_notes_button.configure(state="disabled")
|
||||
self._refresh_history_actions(busy=busy)
|
||||
return
|
||||
status_prefix = (
|
||||
f"Compatibility update required: {self._required_reason}\n"
|
||||
if self._required_reason is not None
|
||||
else ""
|
||||
)
|
||||
remote_history = self._get_remote_history()
|
||||
installed_status = next(
|
||||
(
|
||||
release.status
|
||||
for release in (
|
||||
remote_history.releases if remote_history is not None else ()
|
||||
)
|
||||
if release.version == check.current_version and release.status != "active"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if installed_status is not None:
|
||||
status_prefix += (
|
||||
f"Warning: installed launcher {check.current_version} is "
|
||||
f"{installed_status}; install the next active eligible release.\n"
|
||||
)
|
||||
self._status_value.set(status_prefix + check.reason)
|
||||
self._details_value.set(
|
||||
f"Current {check.current_version} · Latest {check.latest_version} · "
|
||||
f"{check.manifest.channel.title()} channel · "
|
||||
release_title = check.manifest.title or f"Launcher {check.latest_version}"
|
||||
release_summary = check.manifest.summary or "No release summary was provided."
|
||||
notes_preview = release_notes_preview(check.manifest.notes_markdown)
|
||||
prerelease_label = (
|
||||
" · PRERELEASE BUILD"
|
||||
if check.manifest.channel == "prerelease"
|
||||
else ""
|
||||
)
|
||||
details = (
|
||||
f"{check.current_version} → {check.latest_version}\n"
|
||||
f"{release_title}{prerelease_label}\n"
|
||||
f"{release_summary}\n"
|
||||
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._install_button.configure(
|
||||
state=(
|
||||
"normal"
|
||||
@@ -363,6 +440,178 @@ class LauncherUpdateDialog:
|
||||
self._release_notes_button.configure(
|
||||
state="normal" if check.manifest.notes_url is not None else "disabled"
|
||||
)
|
||||
self._refresh_history_actions(busy=busy)
|
||||
|
||||
def _populate_history(self, *, busy: bool) -> None:
|
||||
assert self._history is not None
|
||||
assert self._history_filter_value is not None
|
||||
selected_version = None
|
||||
selection = self._history.selection()
|
||||
if selection:
|
||||
selected = self._history_items.get(selection[0])
|
||||
selected_version = selected[0] if selected is not None else None
|
||||
for item in self._history.get_children():
|
||||
self._history.delete(item)
|
||||
self._history_items.clear()
|
||||
installed = {
|
||||
release.version: release
|
||||
for release in (
|
||||
self._installed_history.releases
|
||||
if self._installed_history is not None
|
||||
else ()
|
||||
)
|
||||
}
|
||||
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)
|
||||
except ReleaseModelError:
|
||||
current_semantic = None
|
||||
filter_name = str(self._history_filter_value.get()).casefold()
|
||||
|
||||
def channel_visible(channel: str) -> bool:
|
||||
return filter_name == "all" or filter_name == channel
|
||||
|
||||
remote_history = self._get_remote_history()
|
||||
remote_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)
|
||||
local = installed.get(release.version)
|
||||
is_older = (
|
||||
current_semantic is not None
|
||||
and SemanticVersion.parse(release.version) < 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_retained=local is not None,
|
||||
is_older=is_older,
|
||||
)
|
||||
iid = f"remote:{index}:{release.version}"
|
||||
self._history.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=iid,
|
||||
values=(
|
||||
release.version,
|
||||
release.channel.title(),
|
||||
state,
|
||||
release.release_date,
|
||||
),
|
||||
)
|
||||
self._history_items[iid] = (
|
||||
release.version,
|
||||
release.channel,
|
||||
release.release_notes_url,
|
||||
can_rollback,
|
||||
)
|
||||
if self._installed_history is not None:
|
||||
for index, release in enumerate(self._installed_history.releases):
|
||||
if release.version in remote_versions or not channel_visible(release.channel):
|
||||
continue
|
||||
try:
|
||||
is_older = (
|
||||
current_semantic is not None
|
||||
and SemanticVersion.parse(release.version) < current_semantic
|
||||
)
|
||||
except ReleaseModelError:
|
||||
is_older = False
|
||||
state = "Current" if release.active else "Retained locally"
|
||||
iid = f"local:{index}:{release.version}"
|
||||
self._history.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=iid,
|
||||
values=(
|
||||
release.version,
|
||||
release.channel.title(),
|
||||
state,
|
||||
release.installed_at,
|
||||
),
|
||||
)
|
||||
self._history_items[iid] = (
|
||||
release.version,
|
||||
release.channel,
|
||||
None,
|
||||
is_older,
|
||||
)
|
||||
if not self._history.get_children():
|
||||
iid = "placeholder"
|
||||
self._history.insert(
|
||||
"",
|
||||
"end",
|
||||
iid=iid,
|
||||
values=(self._launcher_version, "Unknown", "Running", "Not recorded"),
|
||||
)
|
||||
default_version = selected_version
|
||||
if default_version is None and self._installed_history is not None:
|
||||
default_version = self._installed_history.previous_version
|
||||
selected_iid = next(
|
||||
(
|
||||
iid
|
||||
for iid, item in self._history_items.items()
|
||||
if item[0] == default_version
|
||||
),
|
||||
None,
|
||||
)
|
||||
if selected_iid is not None:
|
||||
self._history.selection_set(selected_iid)
|
||||
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())
|
||||
|
||||
def _selected_history_item(self) -> tuple[str, str, str | None, bool] | None:
|
||||
assert self._history is not None
|
||||
selection = self._history.selection()
|
||||
return self._history_items.get(selection[0]) if selection else None
|
||||
|
||||
def _refresh_history_actions(self, *, busy: bool) -> None:
|
||||
assert self._rollback_button is not None
|
||||
assert self._release_notes_button is not None
|
||||
selected = self._selected_history_item()
|
||||
self._rollback_button.configure(
|
||||
state=(
|
||||
"normal"
|
||||
if selected is not None and selected[3] and not busy
|
||||
else "disabled"
|
||||
)
|
||||
)
|
||||
if selected is None:
|
||||
check = self._get_check()
|
||||
notes_url = check.manifest.notes_url if check is not None else None
|
||||
else:
|
||||
notes_url = selected[2]
|
||||
self._release_notes_button.configure(
|
||||
state="normal" if notes_url is not None else "disabled"
|
||||
)
|
||||
|
||||
def show_error(self, message: str) -> None:
|
||||
if self._window is None or not self._window.winfo_exists():
|
||||
@@ -380,12 +629,17 @@ class LauncherUpdateDialog:
|
||||
assert self._check_button is not None
|
||||
assert self._install_button is not None
|
||||
self._status_value.set("Checking the signed update channel…")
|
||||
self._details_value.set("Authenticating the stable manifest and signature.")
|
||||
self._details_value.set("Authenticating the selected channel manifests and signatures.")
|
||||
self._check_button.configure(state="disabled")
|
||||
self._install_button.configure(state="disabled")
|
||||
self._request_check()
|
||||
|
||||
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])
|
||||
return
|
||||
check = self._get_check()
|
||||
if check is not None and check.manifest.notes_url is not None:
|
||||
self._open_article(check.manifest.notes_url)
|
||||
@@ -399,6 +653,7 @@ class LauncherUpdateDialog:
|
||||
title="Update launcher",
|
||||
message=(
|
||||
f"Install signed launcher {check.latest_version} "
|
||||
f"from the {check.manifest.channel} channel "
|
||||
f"({format_bytes(check.manifest.archive_size)}) and restart now?"
|
||||
),
|
||||
kind="question",
|
||||
@@ -413,15 +668,53 @@ class LauncherUpdateDialog:
|
||||
|
||||
def _rollback_update(self) -> None:
|
||||
history = self._installed_history
|
||||
if history is None or history.previous_version is None:
|
||||
selected = self._selected_history_item()
|
||||
if selected is not None and selected[3]:
|
||||
target_version, target_channel, _notes_url, _can_rollback = selected
|
||||
elif history is not None and history.previous_version is not None:
|
||||
target_version = history.previous_version
|
||||
target = next(
|
||||
(
|
||||
release
|
||||
for release in history.releases
|
||||
if release.version == target_version
|
||||
),
|
||||
None,
|
||||
)
|
||||
target_channel = target.channel if target is not None else "stable"
|
||||
else:
|
||||
return
|
||||
current_release = next(
|
||||
(
|
||||
release
|
||||
for release in (history.releases if history is not None else ())
|
||||
if release.active
|
||||
),
|
||||
None,
|
||||
)
|
||||
current_channel = (
|
||||
current_release.channel
|
||||
if current_release is not None
|
||||
else SemanticVersion.parse(self._launcher_version).channel
|
||||
)
|
||||
channel_warning = ""
|
||||
if current_channel != target_channel:
|
||||
channel_warning = (
|
||||
f"\n\nThis crosses from {current_channel.title()} to "
|
||||
f"{target_channel.title()}. Older releases may use different "
|
||||
"configuration schemas."
|
||||
)
|
||||
if not show_themed_dialog(
|
||||
self._window,
|
||||
title="Roll back launcher",
|
||||
message=(
|
||||
f"Switch from launcher {history.current_version or self._launcher_version} "
|
||||
f"to {history.previous_version} and restart?\n\n"
|
||||
f"Switch from launcher "
|
||||
f"{(history.current_version if history is not None else None) or self._launcher_version} "
|
||||
f"to {target_version} and restart?\n\n"
|
||||
"The target manifest, signature, archive, and file hashes will be "
|
||||
"authenticated before activation.\n\n"
|
||||
"The current release remains available as the next rollback target."
|
||||
+ channel_warning
|
||||
),
|
||||
kind="warning",
|
||||
confirm=True,
|
||||
@@ -429,6 +722,5 @@ class LauncherUpdateDialog:
|
||||
window_icon=self._window_icon,
|
||||
):
|
||||
return
|
||||
target_version = history.previous_version
|
||||
self.close()
|
||||
self._rollback_and_restart(target_version)
|
||||
|
||||
@@ -66,9 +66,13 @@ from ..overlay import RuntimeCompatibility, inspect_runtime_compatibility
|
||||
from ..paths import AppPaths
|
||||
from ..settings import Settings, ensure_settings, load_settings, update_settings
|
||||
from ..self_update import (
|
||||
LauncherRemoteHistory,
|
||||
LauncherUpdateCheck,
|
||||
cache_launcher_release_history,
|
||||
cache_launcher_update_check,
|
||||
check_for_launcher_update,
|
||||
fetch_launcher_release_history,
|
||||
read_cached_launcher_release_history,
|
||||
read_cached_launcher_update_check,
|
||||
)
|
||||
from ..server_status import RealmServerStatus
|
||||
@@ -665,6 +669,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
latest_installation_status: GameInstallationStatus | None = None
|
||||
available_start_options: tuple[DepotStartOption, ...] = (DEFAULT_DEPOT_START_OPTION,)
|
||||
preferred_realm = startup_settings.realm
|
||||
selected_update_channel = startup_settings.update_channel
|
||||
latest_server_statuses: tuple[RealmServerStatus, ...] | None = None
|
||||
latest_server_status_check: ServerStatusCheck | None = None
|
||||
server_status_placeholder = "Checking official server status…"
|
||||
@@ -674,6 +679,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
dashboard_debug_state_error: str | None = None
|
||||
last_dashboard_debug_render = ""
|
||||
launcher_update_check: LauncherUpdateCheck | None = None
|
||||
launcher_release_history: LauncherRemoteHistory | None = None
|
||||
runtime_compatibility: RuntimeCompatibility | None = None
|
||||
compatibility_check_generation = 0
|
||||
controls_are_stacked: bool | None = None
|
||||
@@ -2282,14 +2288,18 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
launcher_update_dialog.refresh()
|
||||
|
||||
def refresh_launcher_update_check() -> None:
|
||||
requested_channel = selected_update_channel
|
||||
|
||||
def worker() -> None:
|
||||
try:
|
||||
check = check_for_launcher_update()
|
||||
check = check_for_launcher_update(channel=requested_channel)
|
||||
except RuntimeError as update_error:
|
||||
events.put(("launcher_update_error", str(update_error)))
|
||||
events.put(
|
||||
("launcher_update_error", (requested_channel, str(update_error)))
|
||||
)
|
||||
else:
|
||||
cache_launcher_update_check(paths.launcher_update_check_cache, check)
|
||||
events.put(("launcher_update_check", check))
|
||||
events.put(("launcher_update_check", (requested_channel, check)))
|
||||
|
||||
threading.Thread(
|
||||
target=worker,
|
||||
@@ -2297,6 +2307,24 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def history_worker() -> None:
|
||||
try:
|
||||
history = fetch_launcher_release_history()
|
||||
except RuntimeError as history_error:
|
||||
events.put(("launcher_history_error", str(history_error)))
|
||||
else:
|
||||
cache_launcher_release_history(
|
||||
paths.launcher_release_history_cache,
|
||||
history,
|
||||
)
|
||||
events.put(("launcher_release_history", history))
|
||||
|
||||
threading.Thread(
|
||||
target=history_worker,
|
||||
name="ptlinux-release-history",
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def refresh_remote_content() -> None:
|
||||
def broadcast_worker() -> None:
|
||||
events.put(("broadcast", fetch_latest_broadcast(paths.broadcast_cache)))
|
||||
@@ -2444,7 +2472,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
progress_text.set("See the activity log for details.")
|
||||
|
||||
def poll_events() -> None:
|
||||
nonlocal game_window_active, launcher_update_check, runtime_compatibility
|
||||
nonlocal game_window_active, launcher_update_check, launcher_release_history
|
||||
nonlocal runtime_compatibility
|
||||
processed = 0
|
||||
try:
|
||||
while processed < EVENT_BATCH_SIZE:
|
||||
@@ -2657,16 +2686,31 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
"version",
|
||||
"Game version check complete",
|
||||
)
|
||||
elif kind == "launcher_update_check" and isinstance(
|
||||
value,
|
||||
LauncherUpdateCheck,
|
||||
elif (
|
||||
kind == "launcher_update_check"
|
||||
and isinstance(value, tuple)
|
||||
and len(value) == 2
|
||||
and isinstance(value[0], str)
|
||||
and isinstance(value[1], LauncherUpdateCheck)
|
||||
):
|
||||
show_launcher_update_check(value)
|
||||
requested_channel, update_check = value
|
||||
if requested_channel != selected_update_channel:
|
||||
continue
|
||||
show_launcher_update_check(update_check)
|
||||
mark_startup_component_ready(
|
||||
"launcher_update",
|
||||
"Launcher update status ready",
|
||||
)
|
||||
elif kind == "launcher_update_error" and isinstance(value, str):
|
||||
elif (
|
||||
kind == "launcher_update_error"
|
||||
and isinstance(value, tuple)
|
||||
and len(value) == 2
|
||||
and isinstance(value[0], str)
|
||||
and isinstance(value[1], str)
|
||||
):
|
||||
requested_channel, update_error = value
|
||||
if requested_channel != selected_update_channel:
|
||||
continue
|
||||
if launcher_update_check is None:
|
||||
launcher_update_button.configure(
|
||||
text="CHECK LAUNCHER",
|
||||
@@ -2678,13 +2722,22 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
),
|
||||
)
|
||||
launcher_update_badge.pack_forget()
|
||||
append_log(f"Launcher update check unavailable: {value}")
|
||||
append_log(f"Launcher update check unavailable: {update_error}")
|
||||
if launcher_update_dialog is not None:
|
||||
launcher_update_dialog.show_error(value)
|
||||
launcher_update_dialog.show_error(update_error)
|
||||
mark_startup_component_ready(
|
||||
"launcher_update",
|
||||
"Launcher update check complete",
|
||||
)
|
||||
elif kind == "launcher_release_history" and isinstance(
|
||||
value,
|
||||
LauncherRemoteHistory,
|
||||
):
|
||||
launcher_release_history = value
|
||||
if launcher_update_dialog is not None:
|
||||
launcher_update_dialog.refresh()
|
||||
elif kind == "launcher_history_error" and isinstance(value, str):
|
||||
append_log(f"Launcher release history unavailable: {value}")
|
||||
except queue.Empty:
|
||||
pass
|
||||
except Exception as event_error:
|
||||
@@ -2725,7 +2778,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
def rollback_launcher_and_restart(target_version: str) -> None:
|
||||
run_operation(
|
||||
f"Rolling back launcher to {target_version}…",
|
||||
command_for("launcher-update", "--rollback"),
|
||||
command_for("launcher-update", "--rollback", target_version),
|
||||
restart_on_success=True,
|
||||
)
|
||||
|
||||
@@ -2737,6 +2790,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
window_icon=window_icon,
|
||||
apply_window_icon=apply_window_icon,
|
||||
get_check=lambda: launcher_update_check,
|
||||
get_remote_history=lambda: launcher_release_history,
|
||||
get_selected_channel=lambda: selected_update_channel,
|
||||
is_busy=lambda: operation_active or bool(game_clients.running_count),
|
||||
request_check=refresh_launcher_update_check,
|
||||
install_and_restart=install_launcher_and_restart,
|
||||
@@ -2838,6 +2893,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
)
|
||||
|
||||
def apply_saved_settings(updated_settings: Settings) -> None:
|
||||
nonlocal selected_update_channel, launcher_update_check
|
||||
previous_update_channel = selected_update_channel
|
||||
selected_update_channel = updated_settings.update_channel
|
||||
set_shared_telemetry_enabled(updated_settings.telemetry_enabled)
|
||||
saved_limit = (
|
||||
f"{updated_settings.download_speed_limit_mbps:g} MB/s"
|
||||
@@ -2850,6 +2908,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
f"proton_sync={updated_settings.proton_sync}, "
|
||||
f"download_limit={saved_limit}, "
|
||||
f"low_bandwidth={updated_settings.low_bandwidth_mode}, "
|
||||
f"update_channel={updated_settings.update_channel}, "
|
||||
f"telemetry={updated_settings.telemetry_enabled}"
|
||||
)
|
||||
status.set("Settings saved.")
|
||||
@@ -2860,6 +2919,13 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
if show_dashboard_debug:
|
||||
refresh_dashboard_debug_disk_state()
|
||||
update_dashboard_debug_view(force=True)
|
||||
if previous_update_channel != selected_update_channel:
|
||||
launcher_update_check = None
|
||||
append_log(
|
||||
f"Launcher update channel changed from {previous_update_channel} "
|
||||
f"to {selected_update_channel}; checking eligible releases."
|
||||
)
|
||||
refresh_launcher_update_check()
|
||||
|
||||
def open_settings() -> None:
|
||||
nonlocal settings_window
|
||||
@@ -3017,10 +3083,14 @@ def run_dashboard(paths: AppPaths) -> None:
|
||||
show_news(read_cached_news(paths.news_cache))
|
||||
show_patch_notes(read_cached_patch_notes(paths.patches_cache))
|
||||
cached_launcher_update = read_cached_launcher_update_check(
|
||||
paths.launcher_update_check_cache
|
||||
paths.launcher_update_check_cache,
|
||||
channel=selected_update_channel,
|
||||
)
|
||||
if cached_launcher_update is not None:
|
||||
show_launcher_update_check(cached_launcher_update, log_reason=False)
|
||||
launcher_release_history = read_cached_launcher_release_history(
|
||||
paths.launcher_release_history_cache,
|
||||
)
|
||||
refresh_runtime_compatibility()
|
||||
mark_startup_component_ready(
|
||||
"cached_content",
|
||||
|
||||
@@ -36,6 +36,10 @@ PROTON_SYNC_LABELS: dict[str, str] = {
|
||||
"Fsync": "fsync",
|
||||
"Esync": "esync",
|
||||
}
|
||||
UPDATE_CHANNEL_LABELS: dict[str, str] = {
|
||||
"Stable — Recommended for normal use": "stable",
|
||||
"Prerelease — Beta and release candidate builds": "prerelease",
|
||||
}
|
||||
|
||||
SETTINGS_STACKED_LAYOUT_WIDTH = 720
|
||||
SETTINGS_STACKED_FOOTER_WIDTH = 640
|
||||
@@ -207,6 +211,9 @@ def open_settings_dialog(
|
||||
low_bandwidth_value = tk.BooleanVar(
|
||||
value=current_settings.low_bandwidth_mode
|
||||
)
|
||||
update_channel_value = tk.StringVar(
|
||||
value=setting_label(current_settings.update_channel, UPDATE_CHANNEL_LABELS)
|
||||
)
|
||||
|
||||
def add_choice(
|
||||
parent: ttk.LabelFrame,
|
||||
@@ -325,6 +332,10 @@ def open_settings_dialog(
|
||||
game_source=game_source_value.get().strip() or None,
|
||||
download_speed_limit_mbps=download_speed_limit,
|
||||
low_bandwidth_mode=low_bandwidth_value.get(),
|
||||
update_channel=setting_value(
|
||||
update_channel_value.get(),
|
||||
UPDATE_CHANNEL_LABELS,
|
||||
),
|
||||
)
|
||||
except (OSError, ValueError) as save_error:
|
||||
show_themed_dialog(
|
||||
@@ -526,6 +537,42 @@ def open_settings_dialog(
|
||||
)
|
||||
update_custom_download_state()
|
||||
|
||||
updates_card = ttk.LabelFrame(
|
||||
content,
|
||||
text="LAUNCHER UPDATES",
|
||||
style="SettingsCard.TLabelframe",
|
||||
padding=14,
|
||||
)
|
||||
updates_card.grid(
|
||||
row=2,
|
||||
column=0,
|
||||
columnspan=2,
|
||||
sticky="ew",
|
||||
pady=(12, 0),
|
||||
)
|
||||
updates_card.columnconfigure(0, weight=1)
|
||||
updates_card.columnconfigure(1, weight=1)
|
||||
add_choice(
|
||||
updates_card,
|
||||
0,
|
||||
"settings_selector_update_channel",
|
||||
"Update channel",
|
||||
"Existing installations default to Stable",
|
||||
update_channel_value,
|
||||
tuple(UPDATE_CHANNEL_LABELS),
|
||||
)
|
||||
updates_hint = ttk.Label(
|
||||
updates_card,
|
||||
text=(
|
||||
"Stable receives production releases. Prerelease also receives beta and "
|
||||
"release candidate builds. Returning to Stable never downgrades the "
|
||||
"installed launcher automatically."
|
||||
),
|
||||
style="SettingsHint.TLabel",
|
||||
wraplength=720,
|
||||
)
|
||||
updates_hint.grid(row=2, column=0, columnspan=2, sticky="w")
|
||||
|
||||
diagnostics_card = ttk.LabelFrame(
|
||||
content,
|
||||
text="PRIVACY & DIAGNOSTICS",
|
||||
@@ -533,7 +580,7 @@ def open_settings_dialog(
|
||||
padding=14,
|
||||
)
|
||||
diagnostics_card.grid(
|
||||
row=2,
|
||||
row=3,
|
||||
column=0,
|
||||
columnspan=2,
|
||||
sticky="ew",
|
||||
@@ -568,7 +615,7 @@ def open_settings_dialog(
|
||||
style="SettingsCard.TLabelframe",
|
||||
padding=14,
|
||||
)
|
||||
overrides_card.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(12, 0))
|
||||
overrides_card.grid(row=4, column=0, columnspan=2, sticky="ew", pady=(12, 0))
|
||||
overrides_card.columnconfigure(0, weight=1)
|
||||
ttk.Label(
|
||||
overrides_card,
|
||||
@@ -652,6 +699,9 @@ def open_settings_dialog(
|
||||
card_wraplength = max(140, card_width - round(42 * launcher_ui_scale))
|
||||
display_hint.configure(wraplength=card_wraplength)
|
||||
rendering_hint.configure(wraplength=card_wraplength)
|
||||
updates_hint.configure(
|
||||
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
|
||||
)
|
||||
diagnostics_hint.configure(
|
||||
wraplength=max(140, viewport_width - round(42 * launcher_ui_scale))
|
||||
)
|
||||
@@ -685,8 +735,9 @@ def open_settings_dialog(
|
||||
pady=(12, 0),
|
||||
)
|
||||
downloads_card.grid_configure(row=2, pady=(12, 0))
|
||||
diagnostics_card.grid_configure(row=3, pady=(12, 0))
|
||||
overrides_card.grid_configure(row=4, pady=(12, 0))
|
||||
updates_card.grid_configure(row=3, pady=(12, 0))
|
||||
diagnostics_card.grid_configure(row=4, pady=(12, 0))
|
||||
overrides_card.grid_configure(row=5, pady=(12, 0))
|
||||
else:
|
||||
display_card.grid_configure(
|
||||
row=0,
|
||||
@@ -705,8 +756,9 @@ def open_settings_dialog(
|
||||
pady=0,
|
||||
)
|
||||
downloads_card.grid_configure(row=1, pady=(12, 0))
|
||||
diagnostics_card.grid_configure(row=2, pady=(12, 0))
|
||||
overrides_card.grid_configure(row=3, pady=(12, 0))
|
||||
updates_card.grid_configure(row=2, pady=(12, 0))
|
||||
diagnostics_card.grid_configure(row=3, pady=(12, 0))
|
||||
overrides_card.grid_configure(row=4, pady=(12, 0))
|
||||
|
||||
if footer_stacked:
|
||||
footer_note.grid_configure(row=0, column=0, columnspan=2)
|
||||
|
||||
Reference in New Issue
Block a user