Added tabs for news / patches, integrated new patches api from website.
This commit is contained in:
@@ -166,6 +166,12 @@ class AppPaths:
|
|||||||
def news_cache(self) -> Path:
|
def news_cache(self) -> Path:
|
||||||
return self.cache / "news.json"
|
return self.cache / "news.json"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patches_cache(self) -> Path:
|
||||||
|
"""Cached official patch changelog shown beside the news feed."""
|
||||||
|
|
||||||
|
return self.cache / "patches.json"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def broadcast_cache(self) -> Path:
|
def broadcast_cache(self) -> Path:
|
||||||
"""Cached latest announcement from the broadcast service."""
|
"""Cached latest announcement from the broadcast service."""
|
||||||
|
|||||||
349
ptlinux/ui.py
349
ptlinux/ui.py
@@ -17,6 +17,13 @@ import webbrowser
|
|||||||
from typing import Callable, Mapping
|
from typing import Callable, Mapping
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
from .changelog import (
|
||||||
|
MAX_PATCHES as MAX_PATCH_NOTES,
|
||||||
|
PatchNote,
|
||||||
|
fetch_patch_notes,
|
||||||
|
patch_change_tag,
|
||||||
|
read_cached_patch_notes,
|
||||||
|
)
|
||||||
from .depot import (
|
from .depot import (
|
||||||
DEFAULT_DEPOT_START_OPTION,
|
DEFAULT_DEPOT_START_OPTION,
|
||||||
DepotClient,
|
DepotClient,
|
||||||
@@ -69,6 +76,8 @@ WINDOW_SIZE = "1000x750"
|
|||||||
NEWS_CARD_HEIGHT = 242
|
NEWS_CARD_HEIGHT = 242
|
||||||
NEWS_IMAGE_WIDTH = 292
|
NEWS_IMAGE_WIDTH = 292
|
||||||
NEWS_IMAGE_HEIGHT = 118
|
NEWS_IMAGE_HEIGHT = 118
|
||||||
|
PATCH_CARD_WRAP_LENGTH = 900
|
||||||
|
PATCH_CHANGE_WRAP_LENGTH = 790
|
||||||
LAUNCH_STAGE_TOTAL = 4
|
LAUNCH_STAGE_TOTAL = 4
|
||||||
EVENT_BATCH_SIZE = 48
|
EVENT_BATCH_SIZE = 48
|
||||||
POLL_INTERVAL_MILLISECONDS = 33
|
POLL_INTERVAL_MILLISECONDS = 33
|
||||||
@@ -93,6 +102,7 @@ DASHBOARD_STARTUP_COMPONENTS = frozenset(
|
|||||||
"logo",
|
"logo",
|
||||||
"news",
|
"news",
|
||||||
"news_artwork",
|
"news_artwork",
|
||||||
|
"patches",
|
||||||
"server_status",
|
"server_status",
|
||||||
"version",
|
"version",
|
||||||
}
|
}
|
||||||
@@ -235,6 +245,20 @@ def server_status_presentation(status: RealmServerStatus) -> ServerStatusPresent
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_change_colors(type_key: str) -> tuple[str, str]:
|
||||||
|
"""Return a restrained card-background palette for one patch change tag."""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"fixed": ("#321c1a", "#e88879"),
|
||||||
|
"hotfix": ("#352018", "#e6a06a"),
|
||||||
|
"new-feature": ("#192a20", "#77c795"),
|
||||||
|
"improvement": ("#19252c", "#82b8d2"),
|
||||||
|
"security": ("#302719", "#ddbb69"),
|
||||||
|
"server": ("#282035", "#b8a0d8"),
|
||||||
|
"game": ("#242b1b", "#a8ca75"),
|
||||||
|
}.get(type_key.casefold(), ("#2a201c", "#d8aa61"))
|
||||||
|
|
||||||
|
|
||||||
def launcher_update_presentation(
|
def launcher_update_presentation(
|
||||||
check: LauncherUpdateCheck,
|
check: LauncherUpdateCheck,
|
||||||
) -> LauncherUpdatePresentation:
|
) -> LauncherUpdatePresentation:
|
||||||
@@ -650,7 +674,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
bordercolor="#4a3528",
|
bordercolor="#4a3528",
|
||||||
lightcolor="#4a3528",
|
lightcolor="#4a3528",
|
||||||
darkcolor="#4a3528",
|
darkcolor="#4a3528",
|
||||||
font=("Sans", 10, "bold"),
|
font=("Sans", 9, "bold"),
|
||||||
padding=(10, 7),
|
padding=(10, 7),
|
||||||
relief="flat",
|
relief="flat",
|
||||||
)
|
)
|
||||||
@@ -684,7 +708,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
root.option_add("*realm_selector*Listbox.foreground", "#fff0d2")
|
root.option_add("*realm_selector*Listbox.foreground", "#fff0d2")
|
||||||
root.option_add("*realm_selector*Listbox.selectBackground", "#2a201c")
|
root.option_add("*realm_selector*Listbox.selectBackground", "#2a201c")
|
||||||
root.option_add("*realm_selector*Listbox.selectForeground", "#f0d7a2")
|
root.option_add("*realm_selector*Listbox.selectForeground", "#f0d7a2")
|
||||||
root.option_add("*realm_selector*Listbox.font", "Sans 10 bold")
|
root.option_add("*realm_selector*Listbox.font", "Sans 9 bold")
|
||||||
root.option_add("*realm_selector*Listbox.justify", "center")
|
root.option_add("*realm_selector*Listbox.justify", "center")
|
||||||
root.option_add("*realm_selector*Listbox.selectBorderWidth", 4)
|
root.option_add("*realm_selector*Listbox.selectBorderWidth", 4)
|
||||||
root.option_add("*realm_selector*Listbox.borderWidth", 1)
|
root.option_add("*realm_selector*Listbox.borderWidth", 1)
|
||||||
@@ -732,6 +756,46 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
foreground="#c9baaa",
|
foreground="#c9baaa",
|
||||||
font=("Sans", 8),
|
font=("Sans", 8),
|
||||||
)
|
)
|
||||||
|
style.configure(
|
||||||
|
"NewsTab.TButton",
|
||||||
|
background="#191311",
|
||||||
|
foreground="#9f8d7c",
|
||||||
|
font=("Sans", 8, "bold"),
|
||||||
|
padding=(10, 4),
|
||||||
|
bordercolor="#32251f",
|
||||||
|
borderwidth=1,
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"NewsTab.TButton",
|
||||||
|
background=[("active", "#2a201c")],
|
||||||
|
foreground=[("active", "#f0d7a2")],
|
||||||
|
bordercolor=[("active", "#4a3528")],
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"NewsTabActive.TButton",
|
||||||
|
background="#2a201c",
|
||||||
|
foreground="#f0d7a2",
|
||||||
|
font=("Sans", 8, "bold"),
|
||||||
|
padding=(10, 4),
|
||||||
|
bordercolor="#6a4d38",
|
||||||
|
borderwidth=1,
|
||||||
|
relief="flat",
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"NewsTabActive.TButton",
|
||||||
|
background=[("active", "#32251f")],
|
||||||
|
foreground=[("active", "#fff0d2")],
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"News.Vertical.TScrollbar",
|
||||||
|
background="#4a3528",
|
||||||
|
troughcolor="#191311",
|
||||||
|
arrowcolor="#d8aa61",
|
||||||
|
bordercolor="#191311",
|
||||||
|
lightcolor="#4a3528",
|
||||||
|
darkcolor="#32251f",
|
||||||
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
"Play.TButton",
|
"Play.TButton",
|
||||||
background="#d98927",
|
background="#d98927",
|
||||||
@@ -1134,11 +1198,11 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
values=(DEFAULT_DEPOT_START_OPTION.name,),
|
values=(DEFAULT_DEPOT_START_OPTION.name,),
|
||||||
state="readonly",
|
state="readonly",
|
||||||
style="Realm.TCombobox",
|
style="Realm.TCombobox",
|
||||||
font=("Sans", 10, "bold"),
|
font=("Sans", 9, "bold"),
|
||||||
justify="center",
|
justify="center",
|
||||||
width=20,
|
width=20,
|
||||||
)
|
)
|
||||||
realm_selector.pack(fill="x", pady=(5, 0))
|
realm_selector.pack(fill="x", pady=(6, 5))
|
||||||
activity_copy = ttk.Frame(activity_panel, style="Surface.TFrame")
|
activity_copy = ttk.Frame(activity_panel, style="Surface.TFrame")
|
||||||
activity_copy.pack(side="left", fill="both", expand=True)
|
activity_copy.pack(side="left", fill="both", expand=True)
|
||||||
status = tk.StringVar(value="Ready")
|
status = tk.StringVar(value="Ready")
|
||||||
@@ -1200,21 +1264,58 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
news_heading = ttk.Frame(news_panel, style="Surface.TFrame")
|
news_heading = ttk.Frame(news_panel, style="Surface.TFrame")
|
||||||
news_heading.pack(fill="x")
|
news_heading.pack(fill="x")
|
||||||
ttk.Label(news_heading, text="LATEST NEWS", style="Section.TLabel").pack(side="left")
|
ttk.Label(news_heading, text="LATEST NEWS", style="Section.TLabel").pack(side="left")
|
||||||
|
news_tabs = ttk.Frame(news_heading, style="Surface.TFrame")
|
||||||
|
news_tabs.pack(side="left", padx=(14, 0))
|
||||||
|
news_tab_button = ttk.Button(
|
||||||
|
news_tabs,
|
||||||
|
text="NEWS",
|
||||||
|
style="NewsTabActive.TButton",
|
||||||
|
)
|
||||||
|
news_tab_button.pack(side="left")
|
||||||
|
patches_tab_button = ttk.Button(
|
||||||
|
news_tabs,
|
||||||
|
text="PATCHES",
|
||||||
|
style="NewsTab.TButton",
|
||||||
|
)
|
||||||
|
patches_tab_button.pack(side="left", padx=(5, 0))
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
news_heading,
|
news_heading,
|
||||||
text="OFFICIAL FEED",
|
text="OFFICIAL FEED",
|
||||||
style="Muted.TLabel",
|
style="Muted.TLabel",
|
||||||
).pack(side="right")
|
).pack(side="right")
|
||||||
|
news_viewport = ttk.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.Canvas(
|
news_canvas = tk.Canvas(
|
||||||
news_panel,
|
news_viewport,
|
||||||
background="#191311",
|
background="#191311",
|
||||||
highlightthickness=0,
|
highlightthickness=0,
|
||||||
borderwidth=0,
|
borderwidth=0,
|
||||||
height=252,
|
height=252,
|
||||||
)
|
)
|
||||||
news_canvas.pack(fill="both", expand=True, pady=(6, 0))
|
news_canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
news_content = ttk.Frame(news_canvas, style="Surface.TFrame")
|
news_scrollbar = ttk.Scrollbar(
|
||||||
news_window = news_canvas.create_window((0, 0), window=news_content, anchor="nw")
|
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.Frame(news_canvas, style="Surface.TFrame")
|
||||||
|
news_content_host.grid_columnconfigure(0, weight=1)
|
||||||
|
news_content = ttk.Frame(news_content_host, style="Surface.TFrame")
|
||||||
|
patches_content = ttk.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",
|
||||||
|
)
|
||||||
|
|
||||||
broadcast_panel = ttk.Frame(frame, style="Surface.TFrame", padding=(10, 8))
|
broadcast_panel = ttk.Frame(frame, style="Surface.TFrame", padding=(10, 8))
|
||||||
broadcast_panel.pack(fill="x", pady=(0, 8), before=news_panel)
|
broadcast_panel.pack(fill="x", pady=(0, 8), before=news_panel)
|
||||||
@@ -1269,6 +1370,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
news_image_targets: dict[int, tk.Canvas] = {}
|
news_image_targets: dict[int, tk.Canvas] = {}
|
||||||
news_image_references: dict[int, tk.PhotoImage] = {}
|
news_image_references: dict[int, tk.PhotoImage] = {}
|
||||||
startup_news_images_pending: set[int] = set()
|
startup_news_images_pending: set[int] = set()
|
||||||
|
active_news_tab = "news"
|
||||||
|
rendered_news_articles: tuple[NewsArticle, ...] | None = None
|
||||||
|
rendered_patch_notes: tuple[PatchNote, ...] | None = None
|
||||||
logo_image_references: list[tk.PhotoImage] = []
|
logo_image_references: list[tk.PhotoImage] = []
|
||||||
preloader: tk.Toplevel | None = None
|
preloader: tk.Toplevel | None = None
|
||||||
preloader_status: tk.StringVar | None = None
|
preloader_status: tk.StringVar | None = None
|
||||||
@@ -1632,12 +1736,37 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def open_article(url: str) -> None:
|
def open_article(url: str) -> None:
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
|
|
||||||
def make_news_card_clickable(card: tk.Frame, url: str) -> None:
|
def scroll_news_view(event: object) -> str | None:
|
||||||
|
if active_news_tab != "patches":
|
||||||
|
return None
|
||||||
|
delta = getattr(event, "delta", 0)
|
||||||
|
button = getattr(event, "num", None)
|
||||||
|
if isinstance(delta, int) and delta:
|
||||||
|
direction = -1 if delta > 0 else 1
|
||||||
|
elif button == 4:
|
||||||
|
direction = -1
|
||||||
|
elif button == 5:
|
||||||
|
direction = 1
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
news_canvas.yview_scroll(direction * 3, "units")
|
||||||
|
return "break"
|
||||||
|
|
||||||
|
def make_news_card_clickable(
|
||||||
|
card: tk.Frame,
|
||||||
|
url: str,
|
||||||
|
*,
|
||||||
|
border_only: bool = False,
|
||||||
|
) -> None:
|
||||||
def enter(_event: object) -> None:
|
def enter(_event: object) -> None:
|
||||||
card.configure(background="#2a1e17", highlightbackground="#e4a34c", highlightthickness=1)
|
card.configure(highlightbackground="#e4a34c", highlightthickness=1)
|
||||||
|
if not border_only:
|
||||||
|
card.configure(background="#2a1e17")
|
||||||
|
|
||||||
def leave(_event: object) -> None:
|
def leave(_event: object) -> None:
|
||||||
card.configure(background="#211916", highlightbackground="#4a3528", highlightthickness=1)
|
card.configure(highlightbackground="#4a3528", highlightthickness=1)
|
||||||
|
if not border_only:
|
||||||
|
card.configure(background="#211916")
|
||||||
|
|
||||||
def open_card(_event: object) -> None:
|
def open_card(_event: object) -> None:
|
||||||
open_article(url)
|
open_article(url)
|
||||||
@@ -1648,6 +1777,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
bind("<Enter>", enter)
|
bind("<Enter>", enter)
|
||||||
bind("<Leave>", leave)
|
bind("<Leave>", leave)
|
||||||
bind("<Button-1>", open_card)
|
bind("<Button-1>", open_card)
|
||||||
|
bind("<MouseWheel>", scroll_news_view, "+")
|
||||||
|
bind("<Button-4>", scroll_news_view, "+")
|
||||||
|
bind("<Button-5>", scroll_news_view, "+")
|
||||||
configure(cursor="hand2")
|
configure(cursor="hand2")
|
||||||
for child in getattr(widget, "winfo_children")():
|
for child in getattr(widget, "winfo_children")():
|
||||||
bind_widget(child)
|
bind_widget(child)
|
||||||
@@ -1695,12 +1827,18 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
).pack(anchor="w", fill="x", pady=(3, 0))
|
).pack(anchor="w", fill="x", pady=(3, 0))
|
||||||
make_news_card_clickable(card, broadcast.url)
|
make_news_card_clickable(card, broadcast.url)
|
||||||
|
|
||||||
def show_news(articles: tuple[NewsArticle, ...]) -> None:
|
def render_news(articles: tuple[NewsArticle, ...]) -> None:
|
||||||
|
nonlocal rendered_news_articles
|
||||||
|
visible_articles = articles[:MAX_NEWS_CARDS]
|
||||||
|
if rendered_news_articles == visible_articles:
|
||||||
|
return
|
||||||
news_image_targets.clear()
|
news_image_targets.clear()
|
||||||
news_image_references.clear()
|
news_image_references.clear()
|
||||||
for child in news_content.winfo_children():
|
for child in news_content.winfo_children():
|
||||||
child.destroy()
|
child.destroy()
|
||||||
if not articles:
|
for column in range(MAX_NEWS_CARDS):
|
||||||
|
news_content.grid_columnconfigure(column, weight=0, uniform="")
|
||||||
|
if not visible_articles:
|
||||||
ttk.Label(
|
ttk.Label(
|
||||||
news_content,
|
news_content,
|
||||||
text="Official news is temporarily unavailable. Your cached game can still play.",
|
text="Official news is temporarily unavailable. Your cached game can still play.",
|
||||||
@@ -1711,8 +1849,8 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
"news_artwork",
|
"news_artwork",
|
||||||
"News layout ready",
|
"News layout ready",
|
||||||
)
|
)
|
||||||
|
rendered_news_articles = visible_articles
|
||||||
return
|
return
|
||||||
visible_articles = articles[:MAX_NEWS_CARDS]
|
|
||||||
for column in range(len(visible_articles)):
|
for column in range(len(visible_articles)):
|
||||||
news_content.grid_columnconfigure(column, weight=1, uniform="news-card")
|
news_content.grid_columnconfigure(column, weight=1, uniform="news-card")
|
||||||
news_content.grid_rowconfigure(0, weight=1)
|
news_content.grid_rowconfigure(0, weight=1)
|
||||||
@@ -1775,6 +1913,163 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
).pack(anchor="w", fill="x")
|
).pack(anchor="w", fill="x")
|
||||||
make_news_card_clickable(card, article.url)
|
make_news_card_clickable(card, article.url)
|
||||||
load_news_images(visible_articles)
|
load_news_images(visible_articles)
|
||||||
|
rendered_news_articles = visible_articles
|
||||||
|
|
||||||
|
def show_news(articles: tuple[NewsArticle, ...]) -> None:
|
||||||
|
render_news(articles)
|
||||||
|
|
||||||
|
def render_patch_notes(patches: tuple[PatchNote, ...]) -> None:
|
||||||
|
nonlocal rendered_patch_notes
|
||||||
|
visible_patches = patches[:MAX_PATCH_NOTES]
|
||||||
|
if rendered_patch_notes == visible_patches:
|
||||||
|
return
|
||||||
|
for child in patches_content.winfo_children():
|
||||||
|
child.destroy()
|
||||||
|
if not visible_patches:
|
||||||
|
ttk.Label(
|
||||||
|
patches_content,
|
||||||
|
text=(
|
||||||
|
"Official patch notes are temporarily unavailable. "
|
||||||
|
"Your cached game can still play."
|
||||||
|
),
|
||||||
|
style="Muted.TLabel",
|
||||||
|
).pack(anchor="w", padx=8, pady=8)
|
||||||
|
rendered_patch_notes = visible_patches
|
||||||
|
return
|
||||||
|
for patch in visible_patches:
|
||||||
|
card = tk.Frame(
|
||||||
|
patches_content,
|
||||||
|
background="#211916",
|
||||||
|
highlightbackground="#4a3528",
|
||||||
|
highlightthickness=1,
|
||||||
|
padx=16,
|
||||||
|
pady=13,
|
||||||
|
)
|
||||||
|
card.pack(fill="x", padx=4, pady=(4, 9))
|
||||||
|
tk.Label(
|
||||||
|
card,
|
||||||
|
text=patch.title,
|
||||||
|
background="#211916",
|
||||||
|
foreground="#fff0d2",
|
||||||
|
font=("Sans", 12, "bold"),
|
||||||
|
wraplength=PATCH_CARD_WRAP_LENGTH,
|
||||||
|
justify="left",
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w", fill="x")
|
||||||
|
metadata = tk.Frame(card, background="#211916")
|
||||||
|
metadata.pack(anchor="w", fill="x", pady=(7, 0))
|
||||||
|
tk.Label(
|
||||||
|
metadata,
|
||||||
|
text=f"VERSION {patch.version or '—'}",
|
||||||
|
background="#30231d",
|
||||||
|
foreground="#d8aa61",
|
||||||
|
font=("Sans", 8, "bold"),
|
||||||
|
padx=6,
|
||||||
|
pady=2,
|
||||||
|
).pack(side="left", padx=(0, 7))
|
||||||
|
for tag in patch.tags or ("general",):
|
||||||
|
tk.Label(
|
||||||
|
metadata,
|
||||||
|
text=tag.upper(),
|
||||||
|
background="#2a201c",
|
||||||
|
foreground="#d8aa61",
|
||||||
|
font=("Sans", 7, "bold"),
|
||||||
|
padx=6,
|
||||||
|
pady=2,
|
||||||
|
).pack(side="left", padx=(0, 7))
|
||||||
|
if patch.published_at:
|
||||||
|
tk.Label(
|
||||||
|
metadata,
|
||||||
|
text=patch.published_at,
|
||||||
|
background="#211916",
|
||||||
|
foreground="#c9baaa",
|
||||||
|
font=("Sans", 8),
|
||||||
|
).pack(side="right")
|
||||||
|
tk.Label(
|
||||||
|
card,
|
||||||
|
text=patch.summary or "No summary provided.",
|
||||||
|
background="#211916",
|
||||||
|
foreground="#ded1c4",
|
||||||
|
font=("Sans", 10),
|
||||||
|
wraplength=PATCH_CARD_WRAP_LENGTH,
|
||||||
|
justify="left",
|
||||||
|
anchor="w",
|
||||||
|
).pack(anchor="w", fill="x", pady=(9, 3))
|
||||||
|
if not patch.sections:
|
||||||
|
tk.Label(
|
||||||
|
card,
|
||||||
|
text="No detailed changes were published for this patch.",
|
||||||
|
background="#211916",
|
||||||
|
foreground="#ded1c4",
|
||||||
|
font=("Sans", 9),
|
||||||
|
).pack(anchor="w", pady=(8, 0))
|
||||||
|
for section in patch.sections:
|
||||||
|
tk.Label(
|
||||||
|
card,
|
||||||
|
text=section.title.upper(),
|
||||||
|
background="#211916",
|
||||||
|
foreground="#d8aa61",
|
||||||
|
font=("Sans", 9, "bold"),
|
||||||
|
).pack(anchor="w", pady=(12, 5))
|
||||||
|
for change in section.items:
|
||||||
|
change_row = tk.Frame(card, background="#211916")
|
||||||
|
change_row.pack(anchor="w", fill="x", pady=3)
|
||||||
|
badge_background, badge_foreground = patch_change_colors(
|
||||||
|
change.type_key
|
||||||
|
)
|
||||||
|
tk.Label(
|
||||||
|
change_row,
|
||||||
|
text=patch_change_tag(change.type_key),
|
||||||
|
background=badge_background,
|
||||||
|
foreground=badge_foreground,
|
||||||
|
font=("Sans", 8, "bold"),
|
||||||
|
width=14,
|
||||||
|
anchor="center",
|
||||||
|
padx=5,
|
||||||
|
pady=2,
|
||||||
|
).pack(side="left", anchor="n", padx=(0, 8))
|
||||||
|
tk.Label(
|
||||||
|
change_row,
|
||||||
|
text=change.text,
|
||||||
|
background="#211916",
|
||||||
|
foreground="#ded1c4",
|
||||||
|
font=("Sans", 10),
|
||||||
|
wraplength=PATCH_CHANGE_WRAP_LENGTH,
|
||||||
|
justify="left",
|
||||||
|
anchor="w",
|
||||||
|
).pack(side="left", anchor="w", fill="x", expand=True)
|
||||||
|
make_news_card_clickable(card, patch.url, border_only=True)
|
||||||
|
rendered_patch_notes = visible_patches
|
||||||
|
|
||||||
|
def show_patch_notes(patches: tuple[PatchNote, ...]) -> None:
|
||||||
|
render_patch_notes(patches)
|
||||||
|
|
||||||
|
def switch_news_tab(tab: str) -> None:
|
||||||
|
nonlocal active_news_tab
|
||||||
|
if tab not in {"news", "patches"} or tab == active_news_tab:
|
||||||
|
return
|
||||||
|
active_news_tab = tab
|
||||||
|
news_tab_button.configure(
|
||||||
|
style="NewsTabActive.TButton" if tab == "news" else "NewsTab.TButton"
|
||||||
|
)
|
||||||
|
patches_tab_button.configure(
|
||||||
|
style=(
|
||||||
|
"NewsTabActive.TButton" if tab == "patches" else "NewsTab.TButton"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
news_canvas.yview_moveto(0.0)
|
||||||
|
if tab == "patches":
|
||||||
|
news_content.grid_remove()
|
||||||
|
patches_content.grid()
|
||||||
|
news_scrollbar.grid()
|
||||||
|
else:
|
||||||
|
patches_content.grid_remove()
|
||||||
|
news_content.grid()
|
||||||
|
news_scrollbar.grid_remove()
|
||||||
|
root.after_idle(update_news_viewport)
|
||||||
|
|
||||||
|
news_tab_button.configure(command=lambda: switch_news_tab("news"))
|
||||||
|
patches_tab_button.configure(command=lambda: switch_news_tab("patches"))
|
||||||
|
|
||||||
def load_news_images(articles: tuple[NewsArticle, ...]) -> None:
|
def load_news_images(articles: tuple[NewsArticle, ...]) -> None:
|
||||||
image_articles = tuple(
|
image_articles = tuple(
|
||||||
@@ -2144,6 +2439,9 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
def news_worker() -> None:
|
def news_worker() -> None:
|
||||||
events.put(("news", fetch_news(paths.news_cache)))
|
events.put(("news", fetch_news(paths.news_cache)))
|
||||||
|
|
||||||
|
def patches_worker() -> None:
|
||||||
|
events.put(("patches", fetch_patch_notes(paths.patches_cache)))
|
||||||
|
|
||||||
def server_status_worker() -> None:
|
def server_status_worker() -> None:
|
||||||
try:
|
try:
|
||||||
statuses = fetch_server_status()
|
statuses = fetch_server_status()
|
||||||
@@ -2171,6 +2469,7 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
|
|
||||||
threading.Thread(target=broadcast_worker, name="ptlinux-broadcast", daemon=True).start()
|
threading.Thread(target=broadcast_worker, name="ptlinux-broadcast", daemon=True).start()
|
||||||
threading.Thread(target=news_worker, name="ptlinux-news", daemon=True).start()
|
threading.Thread(target=news_worker, name="ptlinux-news", daemon=True).start()
|
||||||
|
threading.Thread(target=patches_worker, name="ptlinux-patches", daemon=True).start()
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=server_status_worker,
|
target=server_status_worker,
|
||||||
name="ptlinux-server-status",
|
name="ptlinux-server-status",
|
||||||
@@ -2355,6 +2654,16 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
"news",
|
"news",
|
||||||
"News cards ready",
|
"News cards ready",
|
||||||
)
|
)
|
||||||
|
elif (
|
||||||
|
kind == "patches"
|
||||||
|
and isinstance(value, tuple)
|
||||||
|
and all(isinstance(item, PatchNote) for item in value)
|
||||||
|
):
|
||||||
|
show_patch_notes(value)
|
||||||
|
mark_startup_component_ready(
|
||||||
|
"patches",
|
||||||
|
"Patch notes ready",
|
||||||
|
)
|
||||||
elif kind == "news_image":
|
elif kind == "news_image":
|
||||||
complete_news_image(value)
|
complete_news_image(value)
|
||||||
elif kind == "logo_image":
|
elif kind == "logo_image":
|
||||||
@@ -2909,13 +3218,25 @@ def run_dashboard(paths: AppPaths) -> None:
|
|||||||
if isinstance(width, int) and width > 0:
|
if isinstance(width, int) and width > 0:
|
||||||
news_canvas.itemconfigure(news_window, width=width)
|
news_canvas.itemconfigure(news_window, width=width)
|
||||||
|
|
||||||
|
news_content_host.bind("<Configure>", update_news_viewport)
|
||||||
news_content.bind("<Configure>", update_news_viewport)
|
news_content.bind("<Configure>", update_news_viewport)
|
||||||
|
patches_content.bind("<Configure>", update_news_viewport)
|
||||||
news_canvas.bind("<Configure>", resize_news_content)
|
news_canvas.bind("<Configure>", resize_news_content)
|
||||||
|
for scroll_widget in (
|
||||||
|
news_canvas,
|
||||||
|
news_content_host,
|
||||||
|
news_content,
|
||||||
|
patches_content,
|
||||||
|
):
|
||||||
|
scroll_widget.bind("<MouseWheel>", scroll_news_view, add="+")
|
||||||
|
scroll_widget.bind("<Button-4>", scroll_news_view, add="+")
|
||||||
|
scroll_widget.bind("<Button-5>", scroll_news_view, add="+")
|
||||||
startup_overlay.lift()
|
startup_overlay.lift()
|
||||||
startup_status.set("Restoring dashboard content…")
|
startup_status.set("Restoring dashboard content…")
|
||||||
startup_overlay.update_idletasks()
|
startup_overlay.update_idletasks()
|
||||||
show_broadcast(read_cached_broadcast(paths.broadcast_cache))
|
show_broadcast(read_cached_broadcast(paths.broadcast_cache))
|
||||||
show_news(read_cached_news(paths.news_cache))
|
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(
|
cached_launcher_update = read_cached_launcher_update_check(
|
||||||
paths.launcher_update_check_cache
|
paths.launcher_update_check_cache
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user