mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-06 00:36:25 +08:00
BigUI WIP: Simple Download Manager Holdover
This commit is contained in:
@@ -27,6 +27,8 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
PLATE_TAU,
|
||||
AetherListColors,
|
||||
)
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.simple_download_manager import SimpleDownloadManager
|
||||
from openpilot.starpilot.common.starpilot_variables import THEME_SAVE_PATH
|
||||
|
||||
PANEL_STYLE = DEFAULT_PANEL_STYLE
|
||||
|
||||
@@ -445,7 +447,7 @@ class StarPilotAppearanceLayout(_SettingsPage):
|
||||
SettingRow("BootLogo", "value", tr_noop("Boot Logo"),
|
||||
subtitle="",
|
||||
get_value=lambda: self._get_theme_value("BootLogo"),
|
||||
on_click=lambda: self._show_theme_selector("BootLogo")),
|
||||
on_click=self._show_boot_logo_manager),
|
||||
SettingRow("StartupAlert", "value", tr_noop("Startup Alert"),
|
||||
subtitle="",
|
||||
get_value=self._get_startup_alert_display,
|
||||
@@ -703,3 +705,21 @@ class StarPilotAppearanceLayout(_SettingsPage):
|
||||
|
||||
dialog = MultiOptionDialog(tr("Startup Alert"), options, current, callback=on_select)
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
# ── Boot logo manager ──
|
||||
|
||||
def _show_boot_logo_manager(self):
|
||||
def on_close(res, val):
|
||||
pass
|
||||
|
||||
gui_app.push_widget(SimpleDownloadManager(
|
||||
title=tr("Boot Logo"),
|
||||
asset_type="boot logo",
|
||||
directory=THEME_SAVE_PATH / "bootlogos",
|
||||
asset_param="BootLogo",
|
||||
download_param="BootLogoToDownload",
|
||||
downloadable_list_param="DownloadableBootLogos",
|
||||
params=self._params,
|
||||
params_memory=self._params_memory,
|
||||
on_close=on_close,
|
||||
))
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
AetherListColors, AetherListMetrics, AetherScrollbar,
|
||||
draw_hud_background, draw_soft_card, draw_action_pill, draw_selection_list_row,
|
||||
draw_empty_state_card, draw_busy_ring, draw_list_scroll_fades,
|
||||
draw_rounded_fill, draw_rounded_stroke, draw_text_fit_common,
|
||||
draw_download_icon,
|
||||
with_alpha, hex_to_color, snap_rect, point_hits,
|
||||
aether_begin_scissor_mode, aether_end_scissor_mode,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ──
|
||||
|
||||
def _theme_display_name(value: str) -> str:
|
||||
if not value:
|
||||
return "Stock"
|
||||
if value.lower() == "stock":
|
||||
return "Stock"
|
||||
if value.lower() == "none":
|
||||
return "None"
|
||||
base, creator = (value.split("~", 1) + [""])[:2] if "~" in value else (value, "")
|
||||
user_created = False
|
||||
for suffix in ("-user_created", "_user_created", "-user-created", "_user-created"):
|
||||
if base.endswith(suffix):
|
||||
base = base[: -len(suffix)]
|
||||
user_created = True
|
||||
break
|
||||
parts = [part for part in re.split(r"[-_]+", base) if part]
|
||||
display = " ".join(part[:1].upper() + part[1:] for part in parts) if parts else value
|
||||
if user_created:
|
||||
display += " (User Created)"
|
||||
if creator:
|
||||
display += f" - by: {creator}"
|
||||
return display
|
||||
|
||||
|
||||
def _display_to_slug(display: str) -> str:
|
||||
slug = display.lower().strip()
|
||||
slug = slug.replace("(user created)", "").strip()
|
||||
if " - by: " in slug:
|
||||
slug = slug.split(" - by: ")[0].strip()
|
||||
slug = slug.replace("(", "").replace(")", "").replace("'", "").replace(".", "").replace("\u2728", "").strip()
|
||||
slug = slug.replace(" ", "_")
|
||||
slug = re.sub(r"_+", "_", slug)
|
||||
slug = slug.strip("_")
|
||||
return slug
|
||||
|
||||
|
||||
def _delete_asset_file(directory: Path, display_name: str) -> bool:
|
||||
base = display_name.lower()
|
||||
base = base.replace("(", "-").replace(")", "").replace(" ", "-")
|
||||
base = re.sub(r"[^a-z0-9\-]", "", base)
|
||||
while base.endswith("-"):
|
||||
base = base[:-1]
|
||||
base_underscore = base.replace("-", "_")
|
||||
candidates = {base, base + "-user-created", base_underscore, base_underscore + "-user_created"}
|
||||
|
||||
for f in sorted(directory.iterdir()):
|
||||
if f.is_file():
|
||||
normalized = f.stem.lower().replace("_", "-")
|
||||
normalized = re.sub(r"[^a-z0-9\-~]", "", normalized)
|
||||
if normalized in candidates:
|
||||
f.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _add_to_downloadable_list(params, param_key: str, display_name: str):
|
||||
current = params.get(param_key, encoding="utf-8") or ""
|
||||
assets = [a.strip() for a in current.split(",") if a.strip()]
|
||||
slug = _display_to_slug(display_name)
|
||||
if slug and slug not in assets:
|
||||
assets.append(slug)
|
||||
assets.sort()
|
||||
params.put(param_key, ",".join(assets))
|
||||
|
||||
|
||||
def _is_online() -> bool:
|
||||
try:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from cereal import log
|
||||
nt = ui_state.sm["deviceState"].networkType if ui_state.sm.valid.get("deviceState", False) else log.NetworkType.none
|
||||
return nt != log.NetworkType.none
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def _is_parked() -> bool:
|
||||
try:
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
if ui_state.is_offroad():
|
||||
return True
|
||||
if ui_state.sm.valid.get("starpilotCarState", False):
|
||||
return bool(ui_state.sm["starpilotCarState"].isParked)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
class SimpleDownloadManager(Widget):
|
||||
"""Generic fullscreen dialog for managing downloadable assets."""
|
||||
|
||||
MODE_SELECT = 0
|
||||
MODE_DELETE = 1
|
||||
MODE_DOWNLOAD = 2
|
||||
|
||||
MODE_LABELS = ["SELECT", "DELETE", "DOWNLOAD"]
|
||||
ITEM_HEIGHT = 88
|
||||
PILL_HEIGHT = 50
|
||||
PILL_GAP = 10
|
||||
OUTER_MARGIN = 60
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
asset_type: str,
|
||||
directory: str | Path,
|
||||
asset_param: str,
|
||||
download_param: str,
|
||||
downloadable_list_param: str,
|
||||
params,
|
||||
params_memory,
|
||||
on_close: Callable,
|
||||
*,
|
||||
get_is_online: Callable[[], bool] | None = None,
|
||||
get_is_parked: Callable[[], bool] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.title = title
|
||||
self.asset_type = asset_type
|
||||
self.directory = Path(directory)
|
||||
self.asset_param = asset_param
|
||||
self.download_param = download_param
|
||||
self.downloadable_list_param = downloadable_list_param
|
||||
self.params = params
|
||||
self.params_memory = params_memory
|
||||
self._on_close = on_close
|
||||
|
||||
self._get_is_online = get_is_online or (lambda: True)
|
||||
self._get_is_parked = get_is_parked or (lambda: True)
|
||||
|
||||
self._active_mode = self.MODE_SELECT
|
||||
self._downloading = False
|
||||
self._download_progress = ""
|
||||
self._finalizing_until = 0.0
|
||||
self._pressed_target: str | None = None
|
||||
self._pressed_time = 0.0
|
||||
|
||||
self._scroll_panel = GuiScrollPanel2(horizontal=False)
|
||||
self._scrollbar = AetherScrollbar()
|
||||
self._scroll_offset = 0.0
|
||||
self._content_height = 0.0
|
||||
|
||||
self._list_items: list[str] = []
|
||||
self._item_rects: dict[str, rl.Rectangle] = {}
|
||||
self._pill_rects: list[rl.Rectangle] = []
|
||||
self._confirm_target: str | None = None
|
||||
self._confirm_yes_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._confirm_no_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._close_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._cancel_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
self._info_message = ""
|
||||
self._info_message_until = 0.0
|
||||
|
||||
self._refresh_list()
|
||||
self.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
|
||||
|
||||
# ── List data ──
|
||||
|
||||
def _refresh_list(self):
|
||||
if self._active_mode in (self.MODE_SELECT, self.MODE_DELETE):
|
||||
self._list_items = self._list_downloaded()
|
||||
elif self._active_mode == self.MODE_DOWNLOAD:
|
||||
downloadable = self._list_downloadable()
|
||||
downloaded_stems = set(s.lower() for s in self._list_downloaded_stems())
|
||||
self._list_items = [
|
||||
item for item in downloadable
|
||||
if _display_to_slug(item).lower() not in downloaded_stems
|
||||
]
|
||||
|
||||
def _list_downloaded(self) -> list[str]:
|
||||
if not self.directory.is_dir():
|
||||
return []
|
||||
items: list[str] = []
|
||||
for f in sorted(self.directory.iterdir()):
|
||||
if f.is_file():
|
||||
items.append(_theme_display_name(f.stem))
|
||||
return items
|
||||
|
||||
def _list_downloaded_stems(self) -> list[str]:
|
||||
if not self.directory.is_dir():
|
||||
return []
|
||||
return [f.stem for f in self.directory.iterdir() if f.is_file()]
|
||||
|
||||
def _list_downloadable(self) -> list[str]:
|
||||
raw = self.params.get(self.downloadable_list_param, encoding="utf-8") or ""
|
||||
if not raw.strip():
|
||||
return []
|
||||
names = [s.strip() for s in raw.split(",") if s.strip()]
|
||||
return [_theme_display_name(n) for n in names]
|
||||
|
||||
def _current_value(self) -> str:
|
||||
return self.params.get(self.asset_param, encoding="utf-8") or ""
|
||||
|
||||
# ── Actions ──
|
||||
|
||||
def _select_asset(self, display_name: str):
|
||||
slug = _display_to_slug(display_name)
|
||||
if slug:
|
||||
self.params.put(self.asset_param, slug)
|
||||
self._close(DialogResult.CONFIRM, slug)
|
||||
|
||||
def _delete_asset(self, display_name: str):
|
||||
_delete_asset_file(self.directory, display_name)
|
||||
_add_to_downloadable_list(self.params, self.downloadable_list_param, display_name)
|
||||
self._info_message = f"Deleted \"{display_name}\""
|
||||
self._info_message_until = time.monotonic() + 2.5
|
||||
self._confirm_target = None
|
||||
self._refresh_list()
|
||||
|
||||
def _start_download(self, display_name: str):
|
||||
if self._downloading:
|
||||
return
|
||||
slug = _display_to_slug(display_name)
|
||||
if not slug:
|
||||
return
|
||||
self._downloading = True
|
||||
self._download_progress = "Downloading..."
|
||||
self.params_memory.put("ThemeDownloadProgress", "Downloading...")
|
||||
self.params_memory.put(self.download_param, slug)
|
||||
|
||||
def _cancel_download(self):
|
||||
self.params_memory.put_bool("CancelThemeDownload", True)
|
||||
self._info_message = tr("Cancelling...")
|
||||
self._info_message_until = time.monotonic() + 5.0
|
||||
|
||||
def _close(self, result: DialogResult, value=None):
|
||||
gui_app.pop_widget()
|
||||
if self._on_close:
|
||||
self._on_close(result, value)
|
||||
|
||||
# ── Mouse handling ──
|
||||
|
||||
def _hit_target(self, pos: MousePos) -> str | None:
|
||||
if rl.check_collision_point_rec(pos, self._close_rect):
|
||||
return "close"
|
||||
for i, prect in enumerate(self._pill_rects):
|
||||
if rl.check_collision_point_rec(pos, prect):
|
||||
return f"mode:{i}"
|
||||
if self._confirm_target is not None:
|
||||
if rl.check_collision_point_rec(pos, self._confirm_yes_rect):
|
||||
return "confirm_yes"
|
||||
if rl.check_collision_point_rec(pos, self._confirm_no_rect):
|
||||
return "confirm_no"
|
||||
return None
|
||||
if self._downloading:
|
||||
if rl.check_collision_point_rec(pos, self._cancel_rect):
|
||||
return "cancel_download"
|
||||
return None
|
||||
for key, irect in self._item_rects.items():
|
||||
if rl.check_collision_point_rec(pos, irect):
|
||||
return key
|
||||
return None
|
||||
|
||||
def _handle_mouse_press(self, pos: MousePos):
|
||||
target = self._hit_target(pos)
|
||||
if target:
|
||||
self._pressed_target = target
|
||||
self._pressed_time = rl.get_time()
|
||||
|
||||
def _handle_mouse_release(self, pos: MousePos):
|
||||
target = self._hit_target(pos)
|
||||
if target and target == self._pressed_target:
|
||||
self._on_target(target)
|
||||
self._pressed_target = None
|
||||
|
||||
def _on_target(self, target: str):
|
||||
if target == "close":
|
||||
self._close(DialogResult.CANCEL)
|
||||
elif target.startswith("mode:"):
|
||||
mode = int(target.split(":")[1])
|
||||
if mode != self._active_mode:
|
||||
self._active_mode = mode
|
||||
self._scroll_offset = 0.0
|
||||
self._confirm_target = None
|
||||
self._refresh_list()
|
||||
elif target.startswith("item:"):
|
||||
idx = int(target.split(":")[1])
|
||||
if 0 <= idx < len(self._list_items):
|
||||
item = self._list_items[idx]
|
||||
if self._active_mode == self.MODE_SELECT:
|
||||
self._select_asset(item)
|
||||
elif self._active_mode == self.MODE_DELETE:
|
||||
self._confirm_target = item
|
||||
elif self._active_mode == self.MODE_DOWNLOAD:
|
||||
self._start_download(item)
|
||||
elif target == "confirm_yes" and self._confirm_target is not None:
|
||||
self._delete_asset(self._confirm_target)
|
||||
elif target == "confirm_no":
|
||||
self._confirm_target = None
|
||||
elif target == "cancel_download":
|
||||
self._cancel_download()
|
||||
|
||||
# ── State update ──
|
||||
|
||||
def _update_state(self):
|
||||
now = time.monotonic()
|
||||
if self._pressed_target is not None and not self.is_pressed:
|
||||
self._pressed_target = None
|
||||
if self._info_message_until > 0 and now >= self._info_message_until:
|
||||
self._info_message = ""
|
||||
self._info_message_until = 0.0
|
||||
if self._finalizing_until > 0 and now >= self._finalizing_until:
|
||||
self._finalizing_until = 0.0
|
||||
self._downloading = False
|
||||
self._download_progress = ""
|
||||
self._refresh_list()
|
||||
if self._downloading:
|
||||
progress = self.params_memory.get("ThemeDownloadProgress", encoding="utf-8") or ""
|
||||
if progress != self._download_progress:
|
||||
self._download_progress = progress
|
||||
if progress == "Downloaded!" or ("cancelled" in progress.lower() or "failed" in progress.lower() or "offline" in progress.lower() or "unavailable" in progress.lower()):
|
||||
self._finalizing_until = now + 2.5
|
||||
|
||||
# ── keepScreenOn support ──
|
||||
|
||||
@property
|
||||
def should_keep_screen_on(self) -> bool:
|
||||
return self._downloading
|
||||
|
||||
# ── Rendering ──
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> bool | int | None:
|
||||
self.set_rect(rect)
|
||||
|
||||
# Full-screen HUD background
|
||||
draw_hud_background(rect, AetherListColors.PRIMARY, glow=0.5, radius_px=200,
|
||||
bg_color=rl.Color(3, 3, 5, 240))
|
||||
|
||||
# Dialog card
|
||||
margin_x = self.OUTER_MARGIN
|
||||
margin_y = self.OUTER_MARGIN
|
||||
card_w = min(rect.width - margin_x * 2, 680)
|
||||
card_h = min(rect.height - margin_y * 2, 660)
|
||||
card_x = rect.x + (rect.width - card_w) / 2
|
||||
card_y = rect.y + (rect.height - card_h) / 2
|
||||
card_rect = rl.Rectangle(card_x, card_y, card_w, card_h)
|
||||
draw_soft_card(card_rect, rl.Color(14, 13, 18, 255), with_alpha(AetherListColors.PANEL_BORDER, 180),
|
||||
radius=0.022, segments=20)
|
||||
|
||||
cx = int(card_x + 24)
|
||||
cw = int(card_w - 48)
|
||||
y = int(card_y + 20)
|
||||
|
||||
# ── Title + close ──
|
||||
title_font = gui_app.font(FontWeight.BOLD)
|
||||
close_size = 36
|
||||
close_x = int(card_x + card_w - 24 - close_size)
|
||||
self._close_rect = rl.Rectangle(close_x, y + 2, close_size, close_size)
|
||||
|
||||
draw_text_fit_common(title_font, self.title, rl.Vector2(float(cx), float(y)),
|
||||
float(cw - close_size - 16), 32, color=AetherListColors.HEADER)
|
||||
|
||||
close_hovered = self._pressed_target == "close"
|
||||
rl.draw_rectangle_rounded(self._close_rect, 0.5, 8,
|
||||
with_alpha(rl.Color(255, 255, 255, 28 if close_hovered else 14), 255))
|
||||
cx_c = self._close_rect.x + self._close_rect.width / 2
|
||||
cy_c = self._close_rect.y + self._close_rect.height / 2
|
||||
s = close_size * 0.3
|
||||
clr = AetherListColors.MUTED
|
||||
rl.draw_line_ex(rl.Vector2(cx_c - s, cy_c - s), rl.Vector2(cx_c + s, cy_c + s), 2.5, clr)
|
||||
rl.draw_line_ex(rl.Vector2(cx_c + s, cy_c - s), rl.Vector2(cx_c - s, cy_c + s), 2.5, clr)
|
||||
|
||||
y += 48
|
||||
|
||||
# ── Current selection ──
|
||||
current_val = self._current_value()
|
||||
current_display = _theme_display_name(current_val) if current_val else "Stock"
|
||||
info_font = gui_app.font(FontWeight.NORMAL)
|
||||
rl.draw_text_ex(info_font, f"{tr('Current')}: {current_display}",
|
||||
rl.Vector2(float(cx), float(y)), 20, 0, AetherListColors.SUBTEXT)
|
||||
y += 32
|
||||
|
||||
# ── Info message ──
|
||||
if self._info_message and time.monotonic() < self._info_message_until:
|
||||
rl.draw_text_ex(info_font, self._info_message,
|
||||
rl.Vector2(float(cx), float(y)), 18, 0, AetherListColors.SUCCESS)
|
||||
y += 26
|
||||
|
||||
# ── Mode pills ──
|
||||
pill_w = int((cw - self.PILL_GAP * 2) / 3)
|
||||
self._pill_rects = []
|
||||
active_fill = AetherListColors.PRIMARY
|
||||
active_border = with_alpha(AetherListColors.PRIMARY, 180)
|
||||
active_text = AetherListColors.HEADER
|
||||
inactive_fill = with_alpha(rl.Color(255, 255, 255, 8), 255)
|
||||
inactive_border = with_alpha(rl.Color(255, 255, 255, 36), 255)
|
||||
inactive_text = AetherListColors.MUTED
|
||||
|
||||
for i, label in enumerate(self.MODE_LABELS):
|
||||
px = cx + i * (pill_w + self.PILL_GAP)
|
||||
prect = snap_rect(rl.Rectangle(float(px), float(y), float(pill_w), float(self.PILL_HEIGHT)))
|
||||
self._pill_rects.append(prect)
|
||||
is_active = i == self._active_mode
|
||||
is_pressed = self._pressed_target == f"mode:{i}"
|
||||
hovered = is_pressed
|
||||
draw_action_pill(
|
||||
prect,
|
||||
tr(label),
|
||||
active_fill if is_active else inactive_fill,
|
||||
active_border if is_active else inactive_border,
|
||||
active_text if is_active else inactive_text,
|
||||
font_size=20,
|
||||
)
|
||||
if is_pressed:
|
||||
rl.draw_rectangle_rounded(prect, 0.35, 12, with_alpha(rl.Color(255, 255, 255, 10), 255))
|
||||
|
||||
y += self.PILL_HEIGHT + 16
|
||||
|
||||
# ── Content area ──
|
||||
scroll_y = y
|
||||
scroll_h = int(card_rect.y + card_rect.height - scroll_y - 16)
|
||||
if scroll_h < 40:
|
||||
return True
|
||||
scroll_rect = rl.Rectangle(float(cx), float(scroll_y), float(cw), float(scroll_h))
|
||||
|
||||
# Measure content
|
||||
n_items = len(self._list_items)
|
||||
content_height = float(n_items * self.ITEM_HEIGHT)
|
||||
|
||||
self._scroll_panel.set_enabled(True)
|
||||
self._scroll_offset = self._scroll_panel.update(scroll_rect, max(content_height, scroll_rect.height))
|
||||
|
||||
aether_begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y),
|
||||
int(scroll_rect.width), int(scroll_rect.height))
|
||||
|
||||
if n_items == 0 and not self._downloading:
|
||||
empty_msg = self._empty_state_text()
|
||||
draw_empty_state_card(
|
||||
scroll_rect,
|
||||
tr(empty_msg["title"]),
|
||||
tr(empty_msg["body"]),
|
||||
title_size=26, body_size=20,
|
||||
fill=rl.Color(0, 0, 0, 0), border=rl.Color(0, 0, 0, 0),
|
||||
)
|
||||
else:
|
||||
self._item_rects.clear()
|
||||
item_y = scroll_rect.y + self._scroll_offset
|
||||
normal_font = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
if self._downloading:
|
||||
# Download progress display
|
||||
prog_h = 120
|
||||
prog_rect = rl.Rectangle(scroll_rect.x + 20, scroll_rect.y + self._scroll_offset + 20,
|
||||
scroll_rect.width - 40, prog_h)
|
||||
draw_rounded_fill(prog_rect, with_alpha(rl.Color(255, 255, 255, 6), 255), radius_px=12)
|
||||
|
||||
# Busy ring
|
||||
ring_center = rl.Vector2(prog_rect.x + 50, prog_rect.y + prog_rect.height / 2)
|
||||
draw_busy_ring(ring_center, rl.get_time() * 4, AetherListColors.PRIMARY,
|
||||
track_color=with_alpha(rl.Color(255, 255, 255, 18), 255))
|
||||
|
||||
# Progress text
|
||||
progress_text = self._download_progress or "Downloading..."
|
||||
draw_text_fit_common(
|
||||
normal_font, progress_text,
|
||||
rl.Vector2(prog_rect.x + 90, prog_rect.y + 24),
|
||||
prog_rect.width - 110, 24,
|
||||
color=AetherListColors.HEADER,
|
||||
)
|
||||
|
||||
# Cancel button
|
||||
cancel_w, cancel_h = 160, 44
|
||||
cancel_y = prog_rect.y + prog_h - cancel_h - 16
|
||||
self._cancel_rect = snap_rect(rl.Rectangle(
|
||||
prog_rect.x + 90, cancel_y, float(cancel_w), float(cancel_h)))
|
||||
cancel_hovered = self._pressed_target == "cancel_download"
|
||||
draw_action_pill(
|
||||
self._cancel_rect, tr("CANCEL"),
|
||||
with_alpha(AetherListColors.DANGER, 40 if cancel_hovered else 24),
|
||||
with_alpha(AetherListColors.DANGER, 80),
|
||||
AetherListColors.HEADER, font_size=18,
|
||||
)
|
||||
|
||||
item_y += prog_h + 40
|
||||
|
||||
if self._confirm_target is not None:
|
||||
# Confirm overlay
|
||||
confirm_w = min(380, scroll_rect.width - 60)
|
||||
confirm_h = 160
|
||||
confirm_rect = rl.Rectangle(
|
||||
scroll_rect.x + (scroll_rect.width - confirm_w) / 2,
|
||||
scroll_rect.y + self._scroll_offset + (scroll_rect.height - confirm_h) / 2,
|
||||
confirm_w, confirm_h,
|
||||
)
|
||||
draw_soft_card(confirm_rect, rl.Color(20, 19, 25, 255), with_alpha(AetherListColors.PANEL_BORDER, 120),
|
||||
radius=0.03, segments=16)
|
||||
|
||||
confirm_font = gui_app.font(FontWeight.MEDIUM)
|
||||
confirm_msg = tr("Delete \"{name}\"?").format(name=self._confirm_target)
|
||||
draw_text_fit_common(
|
||||
confirm_font, confirm_msg,
|
||||
rl.Vector2(confirm_rect.x + 20, confirm_rect.y + 24),
|
||||
confirm_rect.width - 40, 24,
|
||||
align_center=True, color=AetherListColors.HEADER,
|
||||
)
|
||||
|
||||
btn_w = (confirm_rect.width - 50) / 2
|
||||
btn_y = confirm_rect.y + confirm_rect.height - 56
|
||||
btn_h = 40
|
||||
|
||||
self._confirm_no_rect = snap_rect(rl.Rectangle(confirm_rect.x + 16, btn_y, btn_w, btn_h))
|
||||
self._confirm_yes_rect = snap_rect(rl.Rectangle(confirm_rect.x + confirm_rect.width - btn_w - 16, btn_y, btn_w, btn_h))
|
||||
|
||||
no_hovered = self._pressed_target == "confirm_no"
|
||||
yes_hovered = self._pressed_target == "confirm_yes"
|
||||
|
||||
draw_action_pill(self._confirm_no_rect, tr("CANCEL"),
|
||||
with_alpha(rl.Color(255, 255, 255, 10 if no_hovered else 6), 255),
|
||||
with_alpha(rl.Color(255, 255, 255, 36), 255),
|
||||
AetherListColors.SUBTEXT, font_size=18)
|
||||
draw_action_pill(self._confirm_yes_rect, tr("DELETE"),
|
||||
with_alpha(AetherListColors.DANGER, 50 if yes_hovered else 30),
|
||||
with_alpha(AetherListColors.DANGER, 100),
|
||||
AetherListColors.HEADER, font_size=18)
|
||||
|
||||
item_y += confirm_h + 20
|
||||
|
||||
# Draw list items
|
||||
for idx, item in enumerate(self._list_items):
|
||||
irec = rl.Rectangle(scroll_rect.x, item_y, scroll_rect.width, float(self.ITEM_HEIGHT))
|
||||
key = f"item:{idx}"
|
||||
self._item_rects[key] = irec
|
||||
|
||||
is_last = idx == n_items - 1
|
||||
is_current = (
|
||||
self._active_mode == self.MODE_SELECT
|
||||
and _display_to_slug(item) == self._current_value()
|
||||
)
|
||||
is_pressed = self._pressed_target == key
|
||||
is_hovered = is_pressed
|
||||
|
||||
is_deleting = self._active_mode == self.MODE_DELETE
|
||||
is_downloading_mode = self._active_mode == self.MODE_DOWNLOAD
|
||||
|
||||
action_text = ""
|
||||
action_fill = AetherListColors.CURRENT_BG
|
||||
action_border = AetherListColors.CURRENT_BORDER
|
||||
action_text_color = AetherListColors.HEADER
|
||||
row_bg = AetherListColors.ROW_BG
|
||||
row_border = AetherListColors.ROW_BORDER
|
||||
|
||||
if is_current:
|
||||
action_text = tr("Current")
|
||||
action_fill = AetherListColors.CURRENT_BG
|
||||
action_border = AetherListColors.CURRENT_BORDER
|
||||
action_text_color = AetherListColors.HEADER
|
||||
|
||||
if is_deleting:
|
||||
action_text = tr("DELETE")
|
||||
action_fill = with_alpha(AetherListColors.DANGER, 30)
|
||||
action_border = with_alpha(AetherListColors.DANGER, 80)
|
||||
action_text_color = AetherListColors.DANGER
|
||||
|
||||
if is_downloading_mode:
|
||||
action_text = tr("DOWNLOAD")
|
||||
action_fill = with_alpha(AetherListColors.SUCCESS, 30)
|
||||
action_border = with_alpha(AetherListColors.SUCCESS, 80)
|
||||
action_text_color = AetherListColors.SUCCESS
|
||||
|
||||
draw_selection_list_row(
|
||||
irec,
|
||||
title=item,
|
||||
subtitle="",
|
||||
action_text=action_text,
|
||||
current=is_current and self._active_mode == self.MODE_SELECT,
|
||||
hovered=is_hovered,
|
||||
pressed=is_pressed and is_deleting,
|
||||
is_last=is_last,
|
||||
alpha=255,
|
||||
action_pill=True,
|
||||
action_pill_height=38,
|
||||
title_size=26,
|
||||
title_color=AetherListColors.HEADER,
|
||||
subtitle_color=AetherListColors.SUBTEXT,
|
||||
action_fill=action_fill,
|
||||
action_border=action_border,
|
||||
action_text_color=action_text_color,
|
||||
row_bg=row_bg,
|
||||
row_border=row_border,
|
||||
row_separator=AetherListColors.ROW_SEPARATOR,
|
||||
row_hover=AetherListColors.ROW_HOVER,
|
||||
current_bg=AetherListColors.CURRENT_BG,
|
||||
current_border=AetherListColors.CURRENT_BORDER,
|
||||
)
|
||||
|
||||
item_y += self.ITEM_HEIGHT
|
||||
|
||||
aether_end_scissor_mode()
|
||||
|
||||
# Scrollbar
|
||||
if content_height > scroll_rect.height:
|
||||
self._scrollbar.render(scroll_rect, content_height, self._scroll_offset)
|
||||
|
||||
# Fades
|
||||
draw_list_scroll_fades(scroll_rect, content_height, self._scroll_offset, rl.Color(14, 13, 18, 255))
|
||||
|
||||
return True
|
||||
|
||||
def _empty_state_text(self) -> dict:
|
||||
at = self.asset_type
|
||||
if self._active_mode == self.MODE_SELECT:
|
||||
return {"title": tr("No {name}s").format(name=at.title()), "body": tr("Download {name}s to see them here.").format(name=at)}
|
||||
if self._active_mode == self.MODE_DELETE:
|
||||
return {"title": tr("Nothing to Delete"), "body": tr("No {name}s are currently downloaded.").format(name=at)}
|
||||
if self._active_mode == self.MODE_DOWNLOAD:
|
||||
if self._downloading:
|
||||
return {"title": "", "body": ""}
|
||||
return {"title": tr("All Available"), "body": tr("You have all available {name}s.").format(name=at)}
|
||||
return {"title": "", "body": ""}
|
||||
Reference in New Issue
Block a user