Reworked UI for easier workflow.
This commit is contained in:
1
ptlinux/ui/components/__init__.py
Normal file
1
ptlinux/ui/components/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Reusable Tk dashboard components."""
|
||||
119
ptlinux/ui/components/action_panel.py
Normal file
119
ptlinux/ui/components/action_panel.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Primary game and launcher action controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActionPanel:
|
||||
container: Any
|
||||
game_actions: Any
|
||||
system_actions: Any
|
||||
update_button: Any
|
||||
repair_button: Any
|
||||
import_button: Any
|
||||
doctor_button: Any
|
||||
settings_button: Any
|
||||
launcher_update_button: Any
|
||||
launcher_update_badge: Any
|
||||
|
||||
@property
|
||||
def operation_buttons(self) -> tuple[Any, ...]:
|
||||
return (
|
||||
self.update_button,
|
||||
self.repair_button,
|
||||
self.doctor_button,
|
||||
self.settings_button,
|
||||
self.import_button,
|
||||
self.launcher_update_button,
|
||||
)
|
||||
|
||||
@property
|
||||
def mutation_buttons(self) -> tuple[Any, ...]:
|
||||
return (
|
||||
self.update_button,
|
||||
self.repair_button,
|
||||
self.import_button,
|
||||
self.launcher_update_button,
|
||||
)
|
||||
|
||||
|
||||
def build_action_panel(parent: Any, *, ttk: Any, launcher_version: str) -> ActionPanel:
|
||||
controls = ttk.Frame(parent, style="Root.TFrame")
|
||||
controls.pack(fill="x", pady=(8, 8))
|
||||
controls.columnconfigure(0, weight=1, uniform="actions")
|
||||
controls.columnconfigure(1, weight=1, uniform="actions")
|
||||
game_actions = ttk.Frame(controls, style="Surface.TFrame", padding=12)
|
||||
game_actions.grid(row=0, column=0, sticky="nsew", padx=(0, 4))
|
||||
ttk.Label(game_actions, text="GAME FILES", style="Section.TLabel").pack(anchor="w")
|
||||
game_button_row = ttk.Frame(game_actions, style="Surface.TFrame")
|
||||
game_button_row.pack(anchor="w", fill="x", pady=(6, 0))
|
||||
update_button = ttk.Button(
|
||||
game_button_row,
|
||||
text="UPDATE",
|
||||
style="Secondary.TButton",
|
||||
underline=0,
|
||||
)
|
||||
update_button.pack(side="left")
|
||||
repair_button = ttk.Button(
|
||||
game_button_row,
|
||||
text="REPAIR",
|
||||
style="Secondary.TButton",
|
||||
underline=0,
|
||||
)
|
||||
repair_button.pack(side="left", padx=(8, 0))
|
||||
import_button = ttk.Button(
|
||||
game_button_row,
|
||||
text="IMPORT GAME",
|
||||
style="Secondary.TButton",
|
||||
underline=0,
|
||||
)
|
||||
import_button.pack(side="left", padx=(8, 0))
|
||||
|
||||
system_actions = ttk.Frame(controls, style="Surface.TFrame", padding=12)
|
||||
system_actions.grid(row=0, column=1, sticky="nsew", padx=(4, 0))
|
||||
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",
|
||||
underline=7,
|
||||
)
|
||||
doctor_button.pack(side="left")
|
||||
settings_button = ttk.Button(
|
||||
system_button_row,
|
||||
text="OPEN SETTINGS",
|
||||
style="Secondary.TButton",
|
||||
underline=5,
|
||||
)
|
||||
settings_button.pack(side="left", padx=(8, 0))
|
||||
launcher_update_button = ttk.Button(
|
||||
system_button_row,
|
||||
text=f"LAUNCHER {launcher_version}",
|
||||
style="Secondary.TButton",
|
||||
underline=0,
|
||||
)
|
||||
launcher_update_button.pack(side="left", padx=(8, 0))
|
||||
return ActionPanel(
|
||||
container=controls,
|
||||
game_actions=game_actions,
|
||||
system_actions=system_actions,
|
||||
update_button=update_button,
|
||||
repair_button=repair_button,
|
||||
import_button=import_button,
|
||||
doctor_button=doctor_button,
|
||||
settings_button=settings_button,
|
||||
launcher_update_button=launcher_update_button,
|
||||
launcher_update_badge=launcher_update_badge,
|
||||
)
|
||||
33
ptlinux/ui/components/broadcasts_panel.py
Normal file
33
ptlinux/ui/components/broadcasts_panel.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Latest official broadcast panel construction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BroadcastsPanel:
|
||||
container: Any
|
||||
content: Any
|
||||
|
||||
|
||||
def build_broadcasts_panel(
|
||||
parent: Any,
|
||||
*,
|
||||
ttk: Any,
|
||||
before: Any,
|
||||
) -> BroadcastsPanel:
|
||||
panel = ttk.Frame(parent, style="Surface.TFrame", padding=(10, 8))
|
||||
panel.pack(fill="x", pady=(0, 8), before=before)
|
||||
heading = ttk.Frame(panel, style="Surface.TFrame")
|
||||
heading.pack(fill="x")
|
||||
ttk.Label(heading, text="LATEST BROADCAST", style="Section.TLabel").pack(side="left")
|
||||
ttk.Label(
|
||||
heading,
|
||||
text="OFFICIAL DISCORD",
|
||||
style="Muted.TLabel",
|
||||
).pack(side="right")
|
||||
content = ttk.Frame(panel, style="Surface.TFrame")
|
||||
content.pack(fill="x", pady=(6, 0))
|
||||
return BroadcastsPanel(container=panel, content=content)
|
||||
74
ptlinux/ui/components/header.py
Normal file
74
ptlinux/ui/components/header.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Dashboard header construction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ..viewmodels.dashboard import preloader_logo_subsample_factor
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DashboardHeader:
|
||||
frame: Any
|
||||
version: Any
|
||||
logo: Any
|
||||
|
||||
|
||||
def build_dashboard_header(
|
||||
parent: Any,
|
||||
*,
|
||||
widgets: Any,
|
||||
tk_module: Any,
|
||||
launcher_version: str,
|
||||
window_icon: Any | None,
|
||||
) -> DashboardHeader:
|
||||
"""Build the shared content frame and branded dashboard heading."""
|
||||
|
||||
frame = widgets.Frame(parent, style="Root.TFrame", padding=12)
|
||||
frame.pack(fill="both", expand=True)
|
||||
header = widgets.Frame(frame, style="Root.TFrame")
|
||||
header.pack(fill="x", pady=(0, 8))
|
||||
logo = tk_module.Canvas(
|
||||
header,
|
||||
width=42,
|
||||
height=42,
|
||||
background="#0c0908",
|
||||
highlightthickness=0,
|
||||
borderwidth=0,
|
||||
)
|
||||
dashboard_header_icon = None
|
||||
icon: Any = window_icon
|
||||
if icon is not None:
|
||||
try:
|
||||
header_icon_factor = preloader_logo_subsample_factor(
|
||||
icon.width(),
|
||||
icon.height(),
|
||||
max_dimension=40,
|
||||
)
|
||||
dashboard_header_icon = icon.subsample(
|
||||
header_icon_factor,
|
||||
header_icon_factor,
|
||||
)
|
||||
except tk_module.TclError:
|
||||
dashboard_header_icon = None
|
||||
if dashboard_header_icon is not None:
|
||||
logo.create_image(21, 21, image=dashboard_header_icon)
|
||||
logo.image = dashboard_header_icon
|
||||
logo.pack(side="left", padx=(0, 9))
|
||||
brand = widgets.Frame(header, style="Root.TFrame")
|
||||
brand.pack(side="left", fill="x", expand=True)
|
||||
widgets.Label(brand, text="PRISTONTALE EU", style="Brand.TLabel").pack(anchor="w")
|
||||
widgets.Label(
|
||||
brand,
|
||||
text="PRISTONTALE EU · NATIVE LINUX LAUNCHER",
|
||||
style="Tagline.TLabel",
|
||||
).pack(anchor="w", pady=(1, 0))
|
||||
version = tk_module.StringVar(
|
||||
value=f"Launcher {launcher_version} · checking official game build…"
|
||||
)
|
||||
widgets.Label(header, textvariable=version, style="Tagline.TLabel").pack(
|
||||
side="right",
|
||||
anchor="e",
|
||||
)
|
||||
return DashboardHeader(frame=frame, version=version, logo=logo)
|
||||
108
ptlinux/ui/components/news_panel.py
Normal file
108
ptlinux/ui/components/news_panel.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Official news and patch-note viewport construction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
MAX_NEWS_CARDS = 3
|
||||
NEWS_CARD_HEIGHT = 242
|
||||
NEWS_IMAGE_WIDTH = 292
|
||||
NEWS_IMAGE_HEIGHT = 118
|
||||
PATCH_CARD_WRAP_LENGTH = 900
|
||||
PATCH_CHANGE_WRAP_LENGTH = 790
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewsPanel:
|
||||
container: Any
|
||||
news_tab_button: Any
|
||||
patches_tab_button: Any
|
||||
canvas: Any
|
||||
scrollbar: Any
|
||||
content_host: Any
|
||||
news_content: Any
|
||||
patches_content: Any
|
||||
canvas_window: int
|
||||
|
||||
|
||||
def build_news_panel(
|
||||
parent: Any,
|
||||
*,
|
||||
ttk_module: Any,
|
||||
tk_module: Any,
|
||||
controls: Any,
|
||||
ui_scale: float,
|
||||
) -> NewsPanel:
|
||||
news_panel = ttk_module.Frame(parent, style="Surface.TFrame", padding=(10, 8))
|
||||
news_panel.pack(fill="both", expand=True, pady=(0, 8), before=controls)
|
||||
news_heading = ttk_module.Frame(news_panel, style="Surface.TFrame")
|
||||
news_heading.pack(fill="x")
|
||||
ttk_module.Label(news_heading, text="LATEST NEWS", style="Section.TLabel").pack(side="left")
|
||||
news_tabs = ttk_module.Frame(news_heading, style="Surface.TFrame")
|
||||
news_tabs.pack(side="left", padx=(14, 0))
|
||||
news_tab_button = ttk_module.Button(
|
||||
news_tabs,
|
||||
text="NEWS",
|
||||
style="NewsTabActive.TButton",
|
||||
underline=0,
|
||||
)
|
||||
news_tab_button.pack(side="left")
|
||||
patches_tab_button = ttk_module.Button(
|
||||
news_tabs,
|
||||
text="PATCHES",
|
||||
style="NewsTab.TButton",
|
||||
underline=0,
|
||||
)
|
||||
patches_tab_button.pack(side="left", padx=(5, 0))
|
||||
ttk_module.Label(
|
||||
news_heading,
|
||||
text="OFFICIAL FEED",
|
||||
style="Muted.TLabel",
|
||||
).pack(side="right")
|
||||
news_viewport = ttk_module.Frame(news_panel, style="Surface.TFrame")
|
||||
news_viewport.pack(fill="both", expand=True, pady=(6, 0))
|
||||
news_viewport.grid_columnconfigure(0, weight=1)
|
||||
news_viewport.grid_rowconfigure(0, weight=1)
|
||||
news_canvas = tk_module.Canvas(
|
||||
news_viewport,
|
||||
background="#191311",
|
||||
highlightthickness=0,
|
||||
borderwidth=0,
|
||||
height=round(252 * ui_scale),
|
||||
takefocus=True,
|
||||
)
|
||||
news_canvas.grid(row=0, column=0, sticky="nsew")
|
||||
news_scrollbar = ttk_module.Scrollbar(
|
||||
news_viewport,
|
||||
orient="vertical",
|
||||
style="News.Vertical.TScrollbar",
|
||||
command=news_canvas.yview,
|
||||
)
|
||||
news_scrollbar.grid(row=0, column=1, sticky="ns", padx=(5, 0))
|
||||
news_scrollbar.grid_remove()
|
||||
news_canvas.configure(yscrollcommand=news_scrollbar.set)
|
||||
news_content_host = ttk_module.Frame(news_canvas, style="Surface.TFrame")
|
||||
news_content_host.grid_columnconfigure(0, weight=1)
|
||||
news_content = ttk_module.Frame(news_content_host, style="Surface.TFrame")
|
||||
patches_content = ttk_module.Frame(news_content_host, style="Surface.TFrame")
|
||||
news_content.grid(row=0, column=0, sticky="nsew")
|
||||
patches_content.grid(row=0, column=0, sticky="nsew")
|
||||
patches_content.grid_remove()
|
||||
news_window = news_canvas.create_window(
|
||||
(0, 0),
|
||||
window=news_content_host,
|
||||
anchor="nw",
|
||||
)
|
||||
return NewsPanel(
|
||||
container=news_panel,
|
||||
news_tab_button=news_tab_button,
|
||||
patches_tab_button=patches_tab_button,
|
||||
canvas=news_canvas,
|
||||
scrollbar=news_scrollbar,
|
||||
content_host=news_content_host,
|
||||
news_content=news_content,
|
||||
patches_content=patches_content,
|
||||
canvas_window=news_window,
|
||||
)
|
||||
163
ptlinux/ui/components/progress_panel.py
Normal file
163
ptlinux/ui/components/progress_panel.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""Operation progress, realm choice, and server-summary controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProgressPanel:
|
||||
container: Any
|
||||
realm_value: Any
|
||||
realm_selector: Any
|
||||
play_button: Any
|
||||
status: Any
|
||||
progress_text: Any
|
||||
speed_text: Any
|
||||
client_activity_text: Any
|
||||
download_context_text: Any
|
||||
activity_meta_text: Any
|
||||
progress_bar: Any
|
||||
server_status_content: Any
|
||||
server_status_debug_text: Any
|
||||
log_toggle: Any
|
||||
|
||||
def refresh_activity_metadata(self) -> None:
|
||||
self.activity_meta_text.set(
|
||||
self.download_context_text.get() or self.client_activity_text.get()
|
||||
)
|
||||
|
||||
def set_download_context(self, value: str) -> None:
|
||||
self.download_context_text.set(value)
|
||||
self.refresh_activity_metadata()
|
||||
|
||||
|
||||
def build_progress_panel(
|
||||
parent: Any,
|
||||
*,
|
||||
widgets: Any,
|
||||
tk_module: Any,
|
||||
default_realm: str,
|
||||
) -> ProgressPanel:
|
||||
activity_panel = widgets.Frame(parent, style="Surface.TFrame", padding=(10, 7))
|
||||
activity_panel.pack(fill="x", pady=(0, 8))
|
||||
realm_value = tk_module.StringVar(value=default_realm)
|
||||
activity_action = widgets.Frame(activity_panel, style="Surface.TFrame")
|
||||
activity_action.pack(side="right", fill="y", padx=(12, 0))
|
||||
play_button = widgets.Button(
|
||||
activity_action,
|
||||
text="PLAY",
|
||||
style="Play.TButton",
|
||||
underline=0,
|
||||
)
|
||||
play_button.pack(fill="both", expand=True)
|
||||
realm_selector = widgets.Combobox(
|
||||
activity_action,
|
||||
name="realm_selector",
|
||||
textvariable=realm_value,
|
||||
values=(default_realm,),
|
||||
state="readonly",
|
||||
style="Realm.TCombobox",
|
||||
font=("Sans", 9, "bold"),
|
||||
justify="center",
|
||||
width=20,
|
||||
)
|
||||
realm_selector.pack(fill="x", pady=(6, 5))
|
||||
activity_copy = widgets.Frame(activity_panel, style="Surface.TFrame")
|
||||
activity_copy.pack(side="left", fill="both", expand=True)
|
||||
status = tk_module.StringVar(value="Ready")
|
||||
progress_text = tk_module.StringVar(value="Click Play to start your journey")
|
||||
speed_text = tk_module.StringVar(value="")
|
||||
client_activity_text = tk_module.StringVar(value="")
|
||||
download_context_text = tk_module.StringVar(value="")
|
||||
activity_meta_text = tk_module.StringVar(value="")
|
||||
activity_heading = widgets.Frame(activity_copy, style="Surface.TFrame")
|
||||
activity_heading.pack(fill="x")
|
||||
widgets.Label(
|
||||
activity_heading,
|
||||
textvariable=status,
|
||||
style="Status.TLabel",
|
||||
).pack(side="left", anchor="w")
|
||||
widgets.Label(
|
||||
activity_heading,
|
||||
textvariable=activity_meta_text,
|
||||
style="Muted.TLabel",
|
||||
).pack(side="right", anchor="e")
|
||||
progress_bar = widgets.Progressbar(
|
||||
activity_copy,
|
||||
style="Depot.Horizontal.TProgressbar",
|
||||
mode="determinate",
|
||||
maximum=1,
|
||||
value=0,
|
||||
)
|
||||
progress_bar.pack(fill="x", pady=(5, 2))
|
||||
progress_details = widgets.Frame(activity_copy, style="Surface.TFrame")
|
||||
progress_details.pack(fill="x")
|
||||
widgets.Label(
|
||||
progress_details,
|
||||
textvariable=speed_text,
|
||||
style="Speed.TLabel",
|
||||
width=28,
|
||||
anchor="e",
|
||||
).pack(side="right")
|
||||
widgets.Label(
|
||||
progress_details,
|
||||
textvariable=progress_text,
|
||||
style="Muted.TLabel",
|
||||
anchor="w",
|
||||
).pack(side="left", fill="x", expand=True)
|
||||
server_status_row = widgets.Frame(activity_copy, style="Surface.TFrame")
|
||||
server_status_row.pack(fill="x", pady=(5, 1))
|
||||
widgets.Label(
|
||||
server_status_row,
|
||||
text="SERVERS",
|
||||
style="ServerHeading.TLabel",
|
||||
).pack(side="left", fill="y", padx=(0, 8))
|
||||
server_status_content = widgets.Frame(server_status_row, style="Surface.TFrame")
|
||||
server_status_content.pack(side="left", fill="y", anchor="w")
|
||||
log_toggle = tk_module.Button(
|
||||
server_status_row,
|
||||
text=">_",
|
||||
background="#191311",
|
||||
foreground="#f8fafc",
|
||||
activebackground="#24170f",
|
||||
activeforeground="#ffffff",
|
||||
font=("Monospace", 8, "bold"),
|
||||
padx=3,
|
||||
pady=1,
|
||||
borderwidth=0,
|
||||
highlightthickness=1,
|
||||
highlightbackground="#191311",
|
||||
highlightcolor="#f4bd61",
|
||||
relief="flat",
|
||||
takefocus=True,
|
||||
)
|
||||
log_toggle.pack(side="right", anchor="e", padx=(8, 0))
|
||||
server_status_debug_text = tk_module.StringVar(value="")
|
||||
widgets.Label(
|
||||
server_status_row,
|
||||
textvariable=server_status_debug_text,
|
||||
style="Muted.TLabel",
|
||||
).pack(side="right", anchor="e", padx=(8, 0))
|
||||
widgets.Label(
|
||||
server_status_content,
|
||||
text="Checking official server status…",
|
||||
style="ServerUnknown.TLabel",
|
||||
).pack(side="left", anchor="w")
|
||||
return ProgressPanel(
|
||||
container=activity_panel,
|
||||
realm_value=realm_value,
|
||||
realm_selector=realm_selector,
|
||||
play_button=play_button,
|
||||
status=status,
|
||||
progress_text=progress_text,
|
||||
speed_text=speed_text,
|
||||
client_activity_text=client_activity_text,
|
||||
download_context_text=download_context_text,
|
||||
activity_meta_text=activity_meta_text,
|
||||
progress_bar=progress_bar,
|
||||
server_status_content=server_status_content,
|
||||
server_status_debug_text=server_status_debug_text,
|
||||
log_toggle=log_toggle,
|
||||
)
|
||||
58
ptlinux/ui/components/server_status.py
Normal file
58
ptlinux/ui/components/server_status.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Rendering for the compact official-realm status strip."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from ...depot import DepotStartOption
|
||||
from ...server_status import RealmServerStatus
|
||||
from ..viewmodels.health import server_status_presentation
|
||||
|
||||
|
||||
def render_server_status(
|
||||
container: Any,
|
||||
*,
|
||||
ttk: Any,
|
||||
statuses: tuple[RealmServerStatus, ...] | None,
|
||||
start_options: Iterable[DepotStartOption],
|
||||
placeholder: str,
|
||||
show_debug_details: bool,
|
||||
) -> None:
|
||||
"""Replace the status strip contents from one immutable state snapshot."""
|
||||
|
||||
for child in container.winfo_children():
|
||||
child.destroy()
|
||||
if statuses is None:
|
||||
ttk.Label(
|
||||
container,
|
||||
text=placeholder,
|
||||
style="ServerUnknown.TLabel",
|
||||
).pack(side="left", anchor="w")
|
||||
return
|
||||
statuses_by_name = {item.name.casefold(): item for item in statuses}
|
||||
visible = [
|
||||
statuses_by_name[option.name.casefold()]
|
||||
for option in start_options
|
||||
if option.name.casefold() in statuses_by_name
|
||||
]
|
||||
if not visible:
|
||||
ttk.Label(
|
||||
container,
|
||||
text="Official status is unavailable for this realm.",
|
||||
style="ServerUnknown.TLabel",
|
||||
).pack(side="left", anchor="w")
|
||||
return
|
||||
for index, server in enumerate(visible):
|
||||
presentation = server_status_presentation(
|
||||
server,
|
||||
show_debug_details=show_debug_details,
|
||||
)
|
||||
ttk.Label(
|
||||
container,
|
||||
text=presentation.text,
|
||||
style=presentation.style,
|
||||
).pack(
|
||||
side="left",
|
||||
anchor="w",
|
||||
padx=(0 if index == 0 else 6, 0),
|
||||
)
|
||||
459
ptlinux/ui/components/theme.py
Normal file
459
ptlinux/ui/components/theme.py
Normal file
@@ -0,0 +1,459 @@
|
||||
"""Central Tk style configuration for the native dashboard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def configure_dashboard_style(root: Any, ttk: Any, launcher_ui_scale: float) -> None:
|
||||
style = ttk.Style(root)
|
||||
style.theme_use("clam")
|
||||
style.configure("Root.TFrame", background="#0c0908")
|
||||
style.configure("Surface.TFrame", background="#191311")
|
||||
style.configure("Hero.TFrame", background="#24170f")
|
||||
style.configure("Card.TFrame", background="#211916")
|
||||
style.configure(
|
||||
"Brand.TLabel",
|
||||
background="#0c0908",
|
||||
foreground="#f5d18b",
|
||||
font=("Georgia", 21, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"Tagline.TLabel",
|
||||
background="#0c0908",
|
||||
foreground="#a8998a",
|
||||
font=("Sans", 8),
|
||||
)
|
||||
style.configure(
|
||||
"Section.TLabel",
|
||||
background="#191311",
|
||||
foreground="#f0d7a2",
|
||||
font=("Sans", 9, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"HeroTitle.TLabel",
|
||||
background="#24170f",
|
||||
foreground="#fff2d3",
|
||||
font=("Georgia", 17, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"HeroText.TLabel",
|
||||
background="#24170f",
|
||||
foreground="#d6c6b3",
|
||||
font=("Sans", 10),
|
||||
)
|
||||
style.configure(
|
||||
"Status.TLabel",
|
||||
background="#191311",
|
||||
foreground="#dfd1c1",
|
||||
font=("Sans", 9),
|
||||
)
|
||||
style.configure(
|
||||
"Muted.TLabel",
|
||||
background="#191311",
|
||||
foreground="#aa9a8b",
|
||||
font=("Sans", 8),
|
||||
)
|
||||
style.configure(
|
||||
"ServerHeading.TLabel",
|
||||
background="#191311",
|
||||
foreground="#d8aa61",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(0, 3),
|
||||
)
|
||||
style.configure(
|
||||
"ServerOnline.TLabel",
|
||||
background="#102719",
|
||||
foreground="#58e27a",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(8, 3),
|
||||
anchor="w",
|
||||
)
|
||||
style.configure(
|
||||
"ServerOffline.TLabel",
|
||||
background="#301514",
|
||||
foreground="#ff6258",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(8, 3),
|
||||
anchor="w",
|
||||
)
|
||||
style.configure(
|
||||
"ServerUnknown.TLabel",
|
||||
background="#211916",
|
||||
foreground="#b8a99a",
|
||||
font=("Sans", 8),
|
||||
padding=(8, 3),
|
||||
anchor="w",
|
||||
)
|
||||
style.configure(
|
||||
"Speed.TLabel",
|
||||
background="#191311",
|
||||
foreground="#e5b66c",
|
||||
font=("Monospace", 8, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"SettingsTitle.TLabel",
|
||||
background="#191311",
|
||||
foreground="#fff0d2",
|
||||
font=("Georgia", 16, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"SettingsText.TLabel",
|
||||
background="#191311",
|
||||
foreground="#d8cbbd",
|
||||
font=("Sans", 9),
|
||||
)
|
||||
style.configure(
|
||||
"SettingsHint.TLabel",
|
||||
background="#191311",
|
||||
foreground="#aa9a8b",
|
||||
font=("Sans", 8),
|
||||
)
|
||||
style.configure(
|
||||
"SettingsCard.TLabelframe",
|
||||
background="#191311",
|
||||
bordercolor="#4a3528",
|
||||
lightcolor="#4a3528",
|
||||
darkcolor="#120e0c",
|
||||
borderwidth=1,
|
||||
relief="solid",
|
||||
)
|
||||
style.configure(
|
||||
"SettingsCard.TLabelframe.Label",
|
||||
background="#191311",
|
||||
foreground="#f0d7a2",
|
||||
font=("Sans", 9, "bold"),
|
||||
)
|
||||
style.configure("SettingsCard.TFrame", background="#191311")
|
||||
style.configure(
|
||||
"Update.Treeview",
|
||||
background="#100d0c",
|
||||
fieldbackground="#100d0c",
|
||||
foreground="#ded1c4",
|
||||
rowheight=max(24, round(24 * launcher_ui_scale)),
|
||||
bordercolor="#4a3528",
|
||||
)
|
||||
style.configure(
|
||||
"Update.Treeview.Heading",
|
||||
background="#2a201c",
|
||||
foreground="#f0d7a2",
|
||||
font=("Sans", 8, "bold"),
|
||||
bordercolor="#4a3528",
|
||||
lightcolor="#4a3528",
|
||||
darkcolor="#120e0c",
|
||||
relief="flat",
|
||||
)
|
||||
style.map(
|
||||
"Update.Treeview.Heading",
|
||||
background=[("active", "#2a201c"), ("pressed", "#2a201c")],
|
||||
foreground=[("active", "#f0d7a2"), ("pressed", "#f0d7a2")],
|
||||
relief=[("active", "flat"), ("pressed", "flat")],
|
||||
)
|
||||
style.map(
|
||||
"Update.Treeview",
|
||||
background=[("selected", "#6a482d")],
|
||||
foreground=[("selected", "#ffffff")],
|
||||
)
|
||||
style.configure("SettingsFooter.TFrame", background="#100d0c")
|
||||
style.configure(
|
||||
"SettingsFooter.TLabel",
|
||||
background="#100d0c",
|
||||
foreground="#aa9a8b",
|
||||
font=("Sans", 8),
|
||||
)
|
||||
style.configure(
|
||||
"Settings.TEntry",
|
||||
fieldbackground="#100d0c",
|
||||
foreground="#f8ead2",
|
||||
insertcolor="#f8fafc",
|
||||
bordercolor="#4a3528",
|
||||
lightcolor="#4a3528",
|
||||
darkcolor="#4a3528",
|
||||
focuscolor="#f4bd61",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map("Settings.TEntry", bordercolor=[("focus", "#f4bd61")])
|
||||
style.configure(
|
||||
"Settings.TCombobox",
|
||||
fieldbackground="#100d0c",
|
||||
background="#211916",
|
||||
foreground="#f8ead2",
|
||||
arrowcolor="#e5b66c",
|
||||
bordercolor="#4a3528",
|
||||
focuscolor="#f4bd61",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"Settings.TCombobox",
|
||||
fieldbackground=[("readonly", "#100d0c")],
|
||||
foreground=[("readonly", "#f8ead2")],
|
||||
bordercolor=[("focus", "#f4bd61")],
|
||||
)
|
||||
style.configure(
|
||||
"Realm.TCombobox",
|
||||
fieldbackground="#211916",
|
||||
background="#211916",
|
||||
foreground="#fff0d2",
|
||||
arrowcolor="#d8aa61",
|
||||
bordercolor="#4a3528",
|
||||
lightcolor="#4a3528",
|
||||
darkcolor="#4a3528",
|
||||
font=("Sans", 9, "bold"),
|
||||
padding=(10, 7),
|
||||
relief="flat",
|
||||
)
|
||||
style.map(
|
||||
"Realm.TCombobox",
|
||||
fieldbackground=[
|
||||
("disabled", "#171412"),
|
||||
("readonly", "#211916"),
|
||||
],
|
||||
background=[
|
||||
("disabled", "#171412"),
|
||||
("active", "#2a201c"),
|
||||
("readonly", "#211916"),
|
||||
],
|
||||
foreground=[
|
||||
("disabled", "#887a70"),
|
||||
("readonly", "#fff0d2"),
|
||||
],
|
||||
arrowcolor=[
|
||||
("disabled", "#887a70"),
|
||||
("active", "#f0d7a2"),
|
||||
("readonly", "#d8aa61"),
|
||||
],
|
||||
bordercolor=[
|
||||
("disabled", "#4a3528"),
|
||||
("focus", "#6a4d38"),
|
||||
("readonly", "#4a3528"),
|
||||
],
|
||||
)
|
||||
root.option_add("*realm_selector*Listbox.background", "#211916")
|
||||
root.option_add("*realm_selector*Listbox.foreground", "#fff0d2")
|
||||
root.option_add("*realm_selector*Listbox.selectBackground", "#2a201c")
|
||||
root.option_add("*realm_selector*Listbox.selectForeground", "#f0d7a2")
|
||||
root.option_add("*realm_selector*Listbox.font", "Sans 9 bold")
|
||||
root.option_add("*realm_selector*Listbox.justify", "center")
|
||||
root.option_add("*realm_selector*Listbox.selectBorderWidth", 4)
|
||||
root.option_add("*realm_selector*Listbox.borderWidth", 1)
|
||||
root.option_add("*realm_selector*Listbox.highlightThickness", 0)
|
||||
root.option_add("*realm_selector*Listbox.relief", "flat")
|
||||
root.option_add("*realm_selector*Listbox.activeStyle", "none")
|
||||
style.configure(
|
||||
"Settings.TCheckbutton",
|
||||
background="#191311",
|
||||
foreground="#d8cbbd",
|
||||
font=("Sans", 9),
|
||||
focuscolor="#f4bd61",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"Settings.TCheckbutton",
|
||||
background=[("active", "#191311")],
|
||||
foreground=[("active", "#fff0d2")],
|
||||
)
|
||||
style.configure(
|
||||
"PreloaderStatus.TLabel",
|
||||
background="#0c0908",
|
||||
foreground="#dfd1c1",
|
||||
font=("Sans", 10),
|
||||
)
|
||||
style.configure(
|
||||
"PreloaderMuted.TLabel",
|
||||
background="#0c0908",
|
||||
foreground="#aa9a8b",
|
||||
font=("Sans", 9),
|
||||
)
|
||||
style.configure(
|
||||
"NewsTitle.TLabel",
|
||||
background="#211916",
|
||||
foreground="#fff0d2",
|
||||
font=("Sans", 9, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"NewsMeta.TLabel",
|
||||
background="#211916",
|
||||
foreground="#d8aa61",
|
||||
font=("Sans", 9, "bold"),
|
||||
)
|
||||
style.configure(
|
||||
"NewsBody.TLabel",
|
||||
background="#211916",
|
||||
foreground="#c9baaa",
|
||||
font=("Sans", 8),
|
||||
)
|
||||
style.configure(
|
||||
"NewsTab.TButton",
|
||||
background="#191311",
|
||||
foreground="#9f8d7c",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(10, 4),
|
||||
bordercolor="#32251f",
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
focusthickness=0,
|
||||
)
|
||||
style.map(
|
||||
"NewsTab.TButton",
|
||||
background=[("active", "#2a201c")],
|
||||
foreground=[("active", "#f0d7a2")],
|
||||
bordercolor=[("focus", "#f4bd61"), ("active", "#4a3528")],
|
||||
)
|
||||
style.configure(
|
||||
"NewsTabActive.TButton",
|
||||
background="#2a201c",
|
||||
foreground="#f0d7a2",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(10, 4),
|
||||
bordercolor="#6a4d38",
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
focusthickness=0,
|
||||
)
|
||||
style.map(
|
||||
"NewsTabActive.TButton",
|
||||
background=[("active", "#32251f")],
|
||||
foreground=[("active", "#fff0d2")],
|
||||
bordercolor=[("focus", "#f4bd61")],
|
||||
)
|
||||
style.configure(
|
||||
"News.Vertical.TScrollbar",
|
||||
background="#4a3528",
|
||||
troughcolor="#191311",
|
||||
arrowcolor="#d8aa61",
|
||||
bordercolor="#191311",
|
||||
lightcolor="#4a3528",
|
||||
darkcolor="#32251f",
|
||||
)
|
||||
style.map(
|
||||
"News.Vertical.TScrollbar",
|
||||
background=[
|
||||
("pressed", "#765034"),
|
||||
("active", "#62452f"),
|
||||
("!disabled", "#4a3528"),
|
||||
],
|
||||
arrowcolor=[
|
||||
("pressed", "#fff0c2"),
|
||||
("active", "#f0c77f"),
|
||||
("!disabled", "#d8aa61"),
|
||||
],
|
||||
lightcolor=[
|
||||
("pressed", "#765034"),
|
||||
("active", "#62452f"),
|
||||
("!disabled", "#4a3528"),
|
||||
],
|
||||
darkcolor=[
|
||||
("pressed", "#4b3021"),
|
||||
("active", "#3d2a21"),
|
||||
("!disabled", "#32251f"),
|
||||
],
|
||||
)
|
||||
style.configure(
|
||||
"Play.TButton",
|
||||
background="#d98927",
|
||||
foreground="#fff8e9",
|
||||
font=("Sans", 10, "bold"),
|
||||
padding=(20, 10),
|
||||
bordercolor="#f4bd61",
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
focuscolor="#fff0c2",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"Play.TButton",
|
||||
background=[("active", "#efaa4c"), ("disabled", "#6c4826")],
|
||||
foreground=[("disabled", "#bca889")],
|
||||
bordercolor=[("focus", "#fff0c2")],
|
||||
)
|
||||
style.configure(
|
||||
"Secondary.TButton",
|
||||
background="#0b0a09",
|
||||
foreground="#f2b657",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(12, 8),
|
||||
bordercolor="#d89332",
|
||||
borderwidth=1,
|
||||
relief="flat",
|
||||
focuscolor="#f4bd61",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"Secondary.TButton",
|
||||
background=[("active", "#271b10"), ("disabled", "#171412")],
|
||||
foreground=[("active", "#ffd17c"), ("disabled", "#887a70")],
|
||||
bordercolor=[("focus", "#f4bd61")],
|
||||
)
|
||||
style.configure(
|
||||
"LauncherUpdate.TButton",
|
||||
background="#f4bd61",
|
||||
foreground="#24170f",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(12, 8),
|
||||
bordercolor="#ffe3a3",
|
||||
borderwidth=2,
|
||||
relief="flat",
|
||||
focuscolor="#24170f",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"LauncherUpdate.TButton",
|
||||
background=[("active", "#ffd47f"), ("disabled", "#7b5d37")],
|
||||
foreground=[("active", "#120b06"), ("disabled", "#d0bea2")],
|
||||
bordercolor=[("focus", "#ffffff")],
|
||||
)
|
||||
style.configure(
|
||||
"LauncherManualUpdate.TButton",
|
||||
background="#a63f2f",
|
||||
foreground="#fff4e9",
|
||||
font=("Sans", 8, "bold"),
|
||||
padding=(12, 8),
|
||||
bordercolor="#e98769",
|
||||
borderwidth=2,
|
||||
relief="flat",
|
||||
focuscolor="#ffffff",
|
||||
focusthickness=2,
|
||||
)
|
||||
style.map(
|
||||
"LauncherManualUpdate.TButton",
|
||||
background=[("active", "#c95842"), ("disabled", "#60352e")],
|
||||
foreground=[("active", "#ffffff"), ("disabled", "#c9ada5")],
|
||||
bordercolor=[("focus", "#ffffff")],
|
||||
)
|
||||
style.configure(
|
||||
"UpdateBadge.TLabel",
|
||||
background="#d98927",
|
||||
foreground="#1a1008",
|
||||
font=("Sans", 7, "bold"),
|
||||
padding=(7, 3),
|
||||
)
|
||||
style.configure(
|
||||
"Activity.TButton",
|
||||
background="#0c0908",
|
||||
foreground="#f8fafc",
|
||||
font=("Monospace", 8, "bold"),
|
||||
padding=(3, 2),
|
||||
borderwidth=0,
|
||||
relief="flat",
|
||||
)
|
||||
style.map(
|
||||
"Activity.TButton",
|
||||
background=[("active", "#24170f")],
|
||||
foreground=[("active", "#ffffff")],
|
||||
)
|
||||
style.configure(
|
||||
"News.TButton",
|
||||
background="#211916",
|
||||
foreground="#e5b66c",
|
||||
font=("Sans", 9, "bold"),
|
||||
padding=(0, 3),
|
||||
)
|
||||
style.map("News.TButton", foreground=[("active", "#ffe0a0")])
|
||||
style.configure(
|
||||
"Depot.Horizontal.TProgressbar",
|
||||
background="#d68c34",
|
||||
troughcolor="#34251f",
|
||||
bordercolor="#34251f",
|
||||
lightcolor="#e4a34c",
|
||||
darkcolor="#b66c25",
|
||||
)
|
||||
434
ptlinux/ui/components/update_dialog.py
Normal file
434
ptlinux/ui/components/update_dialog.py
Normal file
@@ -0,0 +1,434 @@
|
||||
"""Launcher update history, install, and rollback dialog."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from ...paths import AppPaths
|
||||
from ...self_update import (
|
||||
LauncherUpdateCheck,
|
||||
LauncherUpdateHistory,
|
||||
inspect_launcher_update_history,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
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],
|
||||
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._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._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 STABLE 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="INSTALLED 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)
|
||||
self._history = ttk.Treeview(
|
||||
history_card,
|
||||
columns=("version", "state", "installed"),
|
||||
show="headings",
|
||||
height=6,
|
||||
style="Update.Treeview",
|
||||
selectmode="browse",
|
||||
)
|
||||
self._history.heading("version", text="VERSION", anchor="w")
|
||||
self._history.heading("state", text="STATE", anchor="w")
|
||||
self._history.heading("installed", text="INSTALLED", 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(
|
||||
"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=0, column=0, sticky="nsew")
|
||||
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))
|
||||
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}")
|
||||
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._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._release_notes_button.configure(state="disabled")
|
||||
return
|
||||
status_prefix = (
|
||||
f"Compatibility update required: {self._required_reason}\n"
|
||||
if self._required_reason is not None
|
||||
else ""
|
||||
)
|
||||
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 · "
|
||||
f"{format_bytes(check.manifest.archive_size)} · "
|
||||
f"published {check.manifest.published_at} · "
|
||||
f"checked {check.checked_at or 'time unavailable'}"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
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 stable manifest and signature.")
|
||||
self._check_button.configure(state="disabled")
|
||||
self._install_button.configure(state="disabled")
|
||||
self._request_check()
|
||||
|
||||
def _view_release_notes(self) -> None:
|
||||
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"({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
|
||||
if history is None or history.previous_version is None:
|
||||
return
|
||||
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"
|
||||
"The current release remains available as the next rollback target."
|
||||
),
|
||||
kind="warning",
|
||||
confirm=True,
|
||||
accept_text="ROLL BACK & RESTART",
|
||||
window_icon=self._window_icon,
|
||||
):
|
||||
return
|
||||
target_version = history.previous_version
|
||||
self.close()
|
||||
self._rollback_and_restart(target_version)
|
||||
Reference in New Issue
Block a user