727 lines
22 KiB
Python
727 lines
22 KiB
Python
"""Launcher update history, install, and rollback dialog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
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,
|
|
show_themed_dialog,
|
|
)
|
|
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."""
|
|
|
|
def __init__(
|
|
self,
|
|
root: Any,
|
|
*,
|
|
paths: AppPaths,
|
|
launcher_version: str,
|
|
ui_scale: float,
|
|
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],
|
|
rollback_and_restart: Callable[[str], None],
|
|
open_article: Callable[[str], None],
|
|
append_log: Callable[[str], None],
|
|
) -> None:
|
|
self._root = root
|
|
self._paths = paths
|
|
self._launcher_version = launcher_version
|
|
self._ui_scale = ui_scale
|
|
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
|
|
self._rollback_and_restart = rollback_and_restart
|
|
self._open_article = open_article
|
|
self._append_log = append_log
|
|
self._window: Any = None
|
|
self._required_reason: str | None = None
|
|
self._installed_history: LauncherUpdateHistory | None = None
|
|
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
|
|
self._install_button: Any = None
|
|
|
|
def open(self, required_reason: str | None = None) -> None:
|
|
if self._window is not None and self._window.winfo_exists():
|
|
if required_reason is not None:
|
|
self._required_reason = required_reason
|
|
self._window.deiconify()
|
|
self._window.lift()
|
|
self._window.focus_set()
|
|
self.refresh()
|
|
return
|
|
self._required_reason = required_reason
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
dialog = tk.Toplevel(self._root)
|
|
dialog.withdraw()
|
|
self._window = dialog
|
|
dialog.title("PristonTale EU — Launcher Updates")
|
|
dialog.resizable(False, False)
|
|
dialog.configure(background="#0c0908")
|
|
dialog.transient(self._root)
|
|
self._apply_window_icon(dialog)
|
|
updates_chrome = create_window_chrome(
|
|
dialog,
|
|
title="PRISTONTALE EU",
|
|
subtitle="LAUNCHER UPDATES",
|
|
window_icon=self._window_icon,
|
|
show_close=True,
|
|
show_minimize=False,
|
|
allow_close=True,
|
|
)
|
|
|
|
updates_header = tk.Frame(updates_chrome.content, background="#24170f")
|
|
updates_header.pack(fill="x")
|
|
tk_accent = tk.Frame(updates_header, background="#d98927", width=5)
|
|
tk_accent.pack(side="left", fill="y")
|
|
header_copy = tk.Frame(updates_header, background="#24170f")
|
|
header_copy.pack(side="left", fill="both", expand=True, padx=20, pady=14)
|
|
tk.Label(
|
|
header_copy,
|
|
text="SIGNED RELEASE CHANNEL",
|
|
background="#24170f",
|
|
foreground="#d8aa61",
|
|
font=("Sans", 8, "bold"),
|
|
).pack(anchor="w")
|
|
tk.Label(
|
|
header_copy,
|
|
text="Launcher Updates",
|
|
background="#24170f",
|
|
foreground="#fff0d2",
|
|
font=("Georgia", 18, "bold"),
|
|
).pack(anchor="w", pady=(2, 0))
|
|
|
|
content = ttk.Frame(
|
|
updates_chrome.content,
|
|
style="Root.TFrame",
|
|
padding=(16, 14, 16, 16),
|
|
)
|
|
content.pack(fill="both", expand=True)
|
|
content.columnconfigure(0, weight=1)
|
|
content.rowconfigure(1, weight=1)
|
|
self._status_value = tk.StringVar(value="Checking the signed update channel…")
|
|
self._details_value = tk.StringVar(value="")
|
|
overview = ttk.LabelFrame(
|
|
content,
|
|
text="UPDATE STATUS",
|
|
style="SettingsCard.TLabelframe",
|
|
padding=14,
|
|
)
|
|
overview.grid(row=0, column=0, sticky="ew")
|
|
overview.columnconfigure(0, weight=1)
|
|
ttk.Label(
|
|
overview,
|
|
textvariable=self._status_value,
|
|
style="SettingsText.TLabel",
|
|
wraplength=round(480 * self._ui_scale),
|
|
justify="left",
|
|
).grid(row=0, column=0, sticky="w")
|
|
ttk.Label(
|
|
overview,
|
|
textvariable=self._details_value,
|
|
style="SettingsHint.TLabel",
|
|
wraplength=round(480 * self._ui_scale),
|
|
justify="left",
|
|
).grid(row=1, column=0, sticky="w", pady=(6, 0))
|
|
self._release_notes_button = ttk.Button(
|
|
overview,
|
|
text="VIEW RELEASE NOTES",
|
|
style="Secondary.TButton",
|
|
state="disabled",
|
|
command=self._view_release_notes,
|
|
)
|
|
self._release_notes_button.grid(
|
|
row=0,
|
|
column=1,
|
|
rowspan=2,
|
|
sticky="e",
|
|
padx=(12, 0),
|
|
)
|
|
|
|
history_card = ttk.LabelFrame(
|
|
content,
|
|
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(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", "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="DATE", anchor="w")
|
|
self._history.column(
|
|
"version",
|
|
width=round(120 * self._ui_scale),
|
|
minwidth=round(80 * self._ui_scale),
|
|
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),
|
|
minwidth=round(100 * self._ui_scale),
|
|
stretch=False,
|
|
anchor="w",
|
|
)
|
|
self._history.column(
|
|
"installed",
|
|
width=round(360 * self._ui_scale),
|
|
minwidth=round(180 * self._ui_scale),
|
|
stretch=True,
|
|
anchor="w",
|
|
)
|
|
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=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))
|
|
footer.grid(row=2, column=0, sticky="ew", pady=(12, 0))
|
|
footer.columnconfigure(0, weight=1)
|
|
self._check_button = ttk.Button(
|
|
footer,
|
|
text="CHECK AGAIN",
|
|
style="Secondary.TButton",
|
|
command=self._check_again,
|
|
)
|
|
self._check_button.grid(row=0, column=0, sticky="w")
|
|
self._rollback_button = ttk.Button(
|
|
footer,
|
|
text="ROLL BACK & RESTART",
|
|
style="Secondary.TButton",
|
|
state="disabled",
|
|
command=self._rollback_update,
|
|
)
|
|
self._rollback_button.grid(row=0, column=1, sticky="e", padx=(8, 0))
|
|
self._install_button = ttk.Button(
|
|
footer,
|
|
text="INSTALL & RESTART",
|
|
style="Play.TButton",
|
|
state="disabled",
|
|
command=self._install_update,
|
|
)
|
|
self._install_button.grid(row=0, column=2, sticky="e", padx=(8, 0))
|
|
close_button = ttk.Button(
|
|
footer,
|
|
text="CLOSE",
|
|
style="Secondary.TButton",
|
|
command=self.close,
|
|
)
|
|
close_button.grid(row=0, column=3, sticky="e", padx=(8, 0))
|
|
|
|
updates_chrome.set_close_command(self.close)
|
|
dialog.protocol("WM_DELETE_WINDOW", self.close)
|
|
dialog.bind("<Escape>", lambda _event: self.close())
|
|
self.refresh()
|
|
if self._get_check() is None:
|
|
self._check_again()
|
|
dialog.update_idletasks()
|
|
updates_width, updates_height = fitted_window_size(
|
|
760,
|
|
620,
|
|
scale=self._ui_scale,
|
|
screen_width=dialog.winfo_screenwidth(),
|
|
screen_height=dialog.winfo_screenheight(),
|
|
)
|
|
dialog.geometry(
|
|
centered_window_geometry(
|
|
updates_width,
|
|
updates_height,
|
|
parent_x=self._root.winfo_rootx(),
|
|
parent_y=self._root.winfo_rooty(),
|
|
parent_width=self._root.winfo_width(),
|
|
parent_height=self._root.winfo_height(),
|
|
)
|
|
)
|
|
dialog.deiconify()
|
|
dialog.lift()
|
|
self._history.focus_set()
|
|
|
|
def close(self) -> None:
|
|
window = self._window
|
|
self._window = None
|
|
if window is not None and window.winfo_exists():
|
|
window.destroy()
|
|
|
|
def refresh(self) -> None:
|
|
if self._window is None or not self._window.winfo_exists():
|
|
return
|
|
assert self._status_value is not None
|
|
assert self._details_value is not None
|
|
assert self._history 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
|
|
assert self._install_button is not None
|
|
check = self._get_check()
|
|
try:
|
|
self._installed_history = inspect_launcher_update_history(self._paths)
|
|
except RuntimeError as error:
|
|
self._installed_history = None
|
|
self._append_log(f"Could not inspect launcher update history: {error}")
|
|
busy = self._is_busy()
|
|
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…")
|
|
self._details_value.set(
|
|
"Installed launcher "
|
|
+ self._launcher_version
|
|
+ " · release information pending"
|
|
)
|
|
self._install_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)
|
|
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"
|
|
if check.available and check.supported and not busy
|
|
else "disabled"
|
|
)
|
|
)
|
|
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():
|
|
return
|
|
assert self._status_value is not None
|
|
assert self._details_value is not None
|
|
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._check_button.configure(state="normal")
|
|
|
|
def _check_again(self) -> None:
|
|
assert self._status_value is not None
|
|
assert self._details_value is not None
|
|
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 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)
|
|
|
|
def _install_update(self) -> None:
|
|
check = self._get_check()
|
|
if check is None or not check.available or not check.supported:
|
|
return
|
|
if not show_themed_dialog(
|
|
self._window,
|
|
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",
|
|
confirm=True,
|
|
accept_text="INSTALL & RESTART",
|
|
window_icon=self._window_icon,
|
|
):
|
|
return
|
|
version_to_install = check.latest_version
|
|
self.close()
|
|
self._install_and_restart(version_to_install)
|
|
|
|
def _rollback_update(self) -> None:
|
|
history = self._installed_history
|
|
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 "
|
|
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,
|
|
accept_text="ROLL BACK & RESTART",
|
|
window_icon=self._window_icon,
|
|
):
|
|
return
|
|
self.close()
|
|
self._rollback_and_restart(target_version)
|