Files
StarPilot/selfdrive/ui/mici/layouts/settings/driving_model.py
T
firestar5683 d43b7d0d3f UI
2026-04-11 23:49:51 -05:00

756 lines
26 KiB
Python

from __future__ import annotations
import re
import threading
import time
from dataclasses import dataclass
from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.starpilot.assets.model_manager import (
CANCEL_DOWNLOAD_PARAM,
DOWNLOAD_PROGRESS_PARAM,
ModelManager,
TINYGRAD_VERSIONS,
)
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigDialogBase, BigMultiOptionDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.application import gui_app, FontWeight
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import gui_label
import pyray as rl
MANIFEST_STALE_SECONDS = 60 * 60
_PROGRESS_HOLD_SECONDS = 2.5
_DOWNLOAD_DIALOG_CLOSE_SECONDS = 1.0
_TERMINAL_PROGRESS_PATTERNS = (
"downloaded",
"cancelled",
"failed",
"offline",
"verification failed",
"server error",
"download invalid",
)
_SORT_MODE_PARAM = "ModelSortMode"
_SORT_MODE_ALPHABETICAL = "alphabetical"
_SORT_MODE_DATE_NEWEST = "date"
_SORT_MODE_DATE_OLDEST = "date_oldest"
_SORT_MODE_FAVORITES = "favorites"
_SORT_MODES = (
_SORT_MODE_ALPHABETICAL,
_SORT_MODE_DATE_NEWEST,
_SORT_MODE_DATE_OLDEST,
_SORT_MODE_FAVORITES,
)
_SORT_MODE_LABELS = {
_SORT_MODE_ALPHABETICAL: "alphabetical",
_SORT_MODE_DATE_NEWEST: "newest",
_SORT_MODE_DATE_OLDEST: "oldest",
_SORT_MODE_FAVORITES: "favorites",
}
_LABEL_TO_SORT_MODE = {label: mode for mode, label in _SORT_MODE_LABELS.items()}
@dataclass
class ModelEntry:
key: str
name: str
series: str
version: str
released: str
community_favorite: bool
def _clean_model_name(name: str) -> str:
cleaned = re.sub(r"[🗺️👀📡]", "", str(name or "")).replace("(Default)", "")
return cleaned.strip()
def _split_param(param_value: str | None) -> list[str]:
return [item.strip() for item in (param_value or "").split(",") if item.strip()]
class DownloadProgressDialog(BigDialogBase):
def __init__(self, params_memory: Params, is_downloading: Callable[[], bool], cancel_callback: Callable[[], None],
is_terminal_progress: Callable[[str], bool], on_close: Callable[[], None] | None = None):
super().__init__()
self._params_memory = params_memory
self._is_downloading = is_downloading
self._cancel_callback = cancel_callback
self._is_terminal_progress = is_terminal_progress
self._on_close = on_close
self._progress = 0.0
self._status = "Downloading..."
self._terminal_progress_since = 0.0
self._downloading = False
self._dismissed = False
self._cancel_btn = DownloadActionButton("cancel download")
self._cancel_btn.set_click_callback(self._cancel_callback)
def show_event(self):
super().show_event()
self._cancel_btn.show_event()
def hide_event(self):
super().hide_event()
self._cancel_btn.hide_event()
@staticmethod
def _parse_progress(progress: str) -> float | None:
match = re.search(r"(\d{1,3})\s*%", progress)
if match:
return max(0.0, min(float(match.group(1)) / 100.0, 1.0))
lowered = progress.lower()
if "verifying" in lowered:
return 1.0
if "downloaded" in lowered:
return 1.0
return None
def _update_state(self):
super()._update_state()
progress = self._params_memory.get(DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or ""
is_downloading = self._is_downloading()
self._downloading = is_downloading
parsed_progress = self._parse_progress(progress)
if parsed_progress is not None:
self._progress = parsed_progress
if progress:
self._status = progress
elif is_downloading:
self._status = "Downloading..."
else:
self._status = "Downloaded!"
self._progress = max(self._progress, 1.0)
self._cancel_btn.set_enabled(is_downloading)
terminal = (not is_downloading) and (
not progress or
self._is_terminal_progress(progress) or
(parsed_progress is not None and parsed_progress >= 1.0)
)
if terminal:
if self._terminal_progress_since == 0.0:
self._terminal_progress_since = time.monotonic()
elif time.monotonic() - self._terminal_progress_since >= _DOWNLOAD_DIALOG_CLOSE_SECONDS:
if not self._dismissed:
self._dismissed = True
self.dismiss(self._on_close)
else:
self._terminal_progress_since = 0.0
def _render(self, _):
super()._render(_)
width = int(self._rect.width)
height = int(self._rect.height)
rl.draw_rectangle(0, 0, width, height, rl.Color(0, 0, 0, 170))
panel_margin_x = 40
panel_margin_y = 28
panel_width = int(min(width - panel_margin_x * 2, 980))
panel_height = int(min(height - panel_margin_y * 2, 300))
panel_x = int((width - panel_width) / 2)
panel_y = int((height - panel_height) / 2)
panel_rect = rl.Rectangle(panel_x, panel_y, panel_width, panel_height)
rl.draw_rectangle_rounded(panel_rect, 0.08, 24, rl.Color(16, 16, 16, 245))
rl.draw_rectangle_rounded_lines_ex(panel_rect, 0.08, 24, 2, rl.Color(255, 255, 255, 32))
cancel_w = int(self._cancel_btn.rect.width)
cancel_h = int(self._cancel_btn.rect.height)
cancel_x = int(panel_x + (panel_width - cancel_w) / 2)
top_padding = 22
side_padding = 36
gap = 16
bar_h = 34
status_h = 44
bottom_padding = 24
bar_y = int(panel_y + top_padding)
status_y = int(bar_y + bar_h + gap)
cancel_y = int(status_y + status_h + gap)
max_cancel_y = int(panel_y + panel_height - cancel_h - bottom_padding)
if cancel_y > max_cancel_y:
cancel_y = max_cancel_y
status_rect = rl.Rectangle(panel_x + side_padding, status_y, panel_width - side_padding * 2, status_h)
gui_label(
status_rect,
self._status,
32,
font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
bar_width = int(min(panel_width - side_padding * 2, 760))
bar_x = int(panel_x + (panel_width - bar_width) / 2)
self._cancel_btn.render(rl.Rectangle(cancel_x, cancel_y, cancel_w, cancel_h))
# Draw the bar last so it cannot get hidden by other dialog graphics.
bar_rect = rl.Rectangle(bar_x, bar_y, bar_width, bar_h)
rl.draw_rectangle_rounded(bar_rect, 0.35, 16, rl.Color(34, 34, 34, 255))
rl.draw_rectangle_rounded_lines_ex(bar_rect, 0.35, 16, 2, rl.Color(255, 255, 255, 95))
fill_padding = 4
fill_h = bar_h - fill_padding * 2
bar_inner_w = max(0, bar_width - fill_padding * 2)
clamped_progress = min(max(self._progress, 0.0), 1.0)
if self._downloading and clamped_progress <= 0.001:
# If backend reports status text but no percentage yet, show animated activity.
segment_w = max(70.0, bar_inner_w * 0.22)
max_offset = max(1.0, bar_inner_w - segment_w)
phase = (time.monotonic() * 1.35) % 1.0
fill_x = bar_x + fill_padding + (phase * max_offset)
rl.draw_rectangle_rounded(
rl.Rectangle(fill_x, bar_y + fill_padding, segment_w, fill_h),
0.35,
16,
rl.Color(70, 91, 234, 255),
)
else:
fill_width = max(0.0, bar_inner_w * clamped_progress)
if fill_width > 0:
rl.draw_rectangle_rounded(
rl.Rectangle(bar_x + fill_padding, bar_y + fill_padding, fill_width, fill_h),
0.35,
16,
rl.Color(70, 91, 234, 255),
)
if clamped_progress > 0.0:
gui_label(
bar_rect,
f"{int(clamped_progress * 100)}%",
24,
color=rl.Color(255, 255, 255, 210),
font_weight=FontWeight.BOLD,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
class DownloadActionButton(Widget):
def __init__(self, label: str):
super().__init__()
self._label = label
self.set_rect(rl.Rectangle(0, 0, 380, 86))
def set_label(self, label: str):
self._label = label
def _render(self, _):
if not self.enabled:
bg = rl.Color(48, 48, 48, 255)
border = rl.Color(255, 255, 255, 45)
label_alpha = 145
elif self.is_pressed:
bg = rl.Color(72, 86, 170, 255)
border = rl.Color(255, 255, 255, 85)
label_alpha = 235
else:
bg = rl.Color(58, 70, 146, 255)
border = rl.Color(255, 255, 255, 70)
label_alpha = 225
rl.draw_rectangle_rounded(self._rect, 0.35, 14, bg)
rl.draw_rectangle_rounded_lines_ex(self._rect, 0.35, 14, 2, border)
gui_label(
self._rect,
self._label,
34,
color=rl.Color(255, 255, 255, label_alpha),
font_weight=FontWeight.MEDIUM,
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
)
class DrivingModelBigButton(BigButton):
def __init__(self):
super().__init__("driving model", "", gui_app.texture("icons_mici/settings/device/lkas.png", 72, 56))
self._params = Params()
self._params_memory = Params(memory=True)
self._model_manager = ModelManager(self._params, self._params_memory)
self._worker_thread: threading.Thread | None = None
self._active_job = ""
self._manifest_last_refresh_mono = 0.0
self._terminal_progress_since = 0.0
self._sub_label.set_font_size(32)
self._sub_label._scroll = True
self._sub_label._elide = False
self._sub_label._wrap_text = False
self.set_click_callback(self._open_manager_menu)
self.refresh()
def show_event(self):
super().show_event()
self._sub_label.reset_scroll()
self.refresh()
# Always fetch manifest once when this settings pane opens.
self._maybe_refresh_manifest(force=(self._manifest_last_refresh_mono == 0.0))
def refresh(self):
self._update_button_value()
def set_value(self, value: str):
super().set_value(value)
self._sub_label.reset_scroll()
def _get_label_font_size(self):
return 42
def _width_hint(self) -> int:
icon_width = self._txt_icon.width + 16 if self._txt_icon else 0
return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - icon_width)
def _show_option_dialog(self, title: str, options: list[str], current: str | None, on_selected: Callable[[str], None],
back_callback: Callable[[], None] | None = None):
dialog_holder: dict[str, BigMultiOptionDialog] = {}
def on_confirm():
on_selected(dialog_holder["dialog"].get_selected_option())
default_option = current if current in options else None
dialog = BigMultiOptionDialog(options=options, default=default_option, right_btn_callback=on_confirm)
if back_callback is not None:
dialog.set_back_callback(back_callback)
dialog_holder["dialog"] = dialog
gui_app.push_widget(dialog)
def _update_state(self):
super()._update_state()
self._process_terminal_progress()
self._update_button_value()
def _open_manager_menu(self):
options = [
"set sort mode",
"switch model",
"download model",
"download all missing",
"refresh manifest",
]
if not options:
return
def on_selected(value: str):
if value == "set sort mode":
self._open_sort_mode_dialog()
elif value == "switch model":
self._open_switch_dialog()
elif value == "download model":
self._open_download_dialog()
elif value == "download all missing":
self._download_all_missing()
elif value == "refresh manifest":
self._maybe_refresh_manifest(force=True)
self._show_option_dialog("driving model", options, None, on_selected)
def _open_switch_dialog(self):
self._maybe_refresh_manifest(force=False)
entries = self._load_model_entries()
if not entries:
message = "Refreshing model list..." if self._active_job == "refresh" else "Refresh manifest and try again."
self._show_message("Model list unavailable", message, return_to_manager=True)
return
installed = [entry for entry in entries if self._is_model_installed(entry.key, entry.version)]
if not installed:
self._show_message("No downloaded models", "Download a model first.", return_to_manager=True)
return
current_key = self._get_current_model_key()
self._show_model_dialog("Select Driving Model", installed, current_key, self._switch_model,
back_callback=self._open_manager_menu)
def _open_download_dialog(self):
if ui_state.started:
self._show_message("Downloads blocked while driving", "Try again offroad.", return_to_manager=True)
return
self._maybe_refresh_manifest(force=False)
entries = self._load_model_entries()
if not entries:
message = "Refreshing model list..." if self._active_job == "refresh" else "Refresh manifest and try again."
self._show_message("Model list unavailable", message, return_to_manager=True)
return
missing = [entry for entry in entries if not self._is_model_installed(entry.key, entry.version)]
if not missing:
self._show_message("All models downloaded", "No additional models are available.", return_to_manager=True)
return
self._show_model_dialog("Download Driving Model", missing, "", self._start_model_download,
back_callback=self._open_manager_menu)
def _download_all_missing(self):
if ui_state.started:
self._show_message("Downloads blocked while driving", "Try again offroad.", return_to_manager=True)
return
self._maybe_refresh_manifest(force=False)
entries = self._load_model_entries()
if not entries:
self._show_message("Model list unavailable", "Refresh manifest and try again.", return_to_manager=True)
return
missing_exists = any(not self._is_model_installed(entry.key, entry.version) for entry in entries)
if not missing_exists:
self._show_message("All models downloaded", "No additional models are available.", return_to_manager=True)
return
if not self._start_worker("download_all", self._run_download_all):
self._show_message("Model manager busy", "Please wait for the current task.", return_to_manager=True)
return
self._show_download_progress_dialog()
def _cancel_download(self):
if not self._is_download_job_running():
return
self._params_memory.put_bool(CANCEL_DOWNLOAD_PARAM, True)
def _open_sort_mode_dialog(self):
options = [_SORT_MODE_LABELS[mode] for mode in _SORT_MODES]
current_mode = self._get_sort_mode()
def on_selected(selected_label: str):
selected_mode = _LABEL_TO_SORT_MODE.get(selected_label, _SORT_MODE_ALPHABETICAL)
self._params.put(_SORT_MODE_PARAM, selected_mode)
self._open_manager_menu()
self._show_option_dialog("sort mode", options, _SORT_MODE_LABELS[current_mode], on_selected,
back_callback=self._open_manager_menu)
def _get_sort_mode(self) -> str:
mode = (self._params.get(_SORT_MODE_PARAM, encoding="utf-8") or "").strip()
return mode if mode in _SORT_MODES else _SORT_MODE_ALPHABETICAL
def _show_model_dialog(self, title: str, entries: list[ModelEntry], current_key: str,
on_selected: Callable[[str], None], back_callback: Callable[[], None] | None = None):
options, option_to_key, key_to_option = self._build_model_options(entries)
if not options:
self._show_message("No models available", "Refresh manifest and try again.", return_to_manager=True)
return
default_option = key_to_option.get(current_key, options[0])
def on_dialog_selected(selected_option: str):
model_key = option_to_key.get(selected_option)
if model_key:
on_selected(model_key)
self._show_option_dialog(title, options, default_option, on_dialog_selected, back_callback=back_callback)
def _build_model_options(self, entries: list[ModelEntry]) -> tuple[list[str], dict[str, str], dict[str, str]]:
# Ensure display names are unique before applying status text (date/favorite).
display_names: dict[str, str] = {}
for entry in entries:
name = entry.name
if name in display_names and display_names[name] != entry.key:
name = f"{entry.name} [{entry.series}]"
if name in display_names and display_names[name] != entry.key:
name = f"{entry.name} [{entry.key}]"
display_names[name] = entry.key
key_to_display = {key: name for name, key in display_names.items()}
sorted_entries = self._sort_entries(entries)
options: list[str] = []
option_to_key: dict[str, str] = {}
key_to_option: dict[str, str] = {}
for entry in sorted_entries:
display_name = key_to_display.get(entry.key, entry.name)
favorite_prefix = "" if entry.community_favorite else ""
option = f"{favorite_prefix}{display_name}"
if option in option_to_key:
option = f"{option} [{entry.key}]"
options.append(option)
option_to_key[option] = entry.key
key_to_option[entry.key] = option
return options, option_to_key, key_to_option
def _sort_entries(self, entries: list[ModelEntry]) -> list[ModelEntry]:
sort_mode = self._get_sort_mode()
entry_list = sorted(entries, key=lambda entry: (entry.series.lower(), entry.name.lower()))
def normalized_release(entry: ModelEntry) -> str:
return entry.released if entry.released else "0000-00-00"
if sort_mode == _SORT_MODE_DATE_NEWEST:
return sorted(entry_list, key=normalized_release, reverse=True)
if sort_mode == _SORT_MODE_DATE_OLDEST:
return sorted(entry_list, key=normalized_release)
if sort_mode == _SORT_MODE_FAVORITES:
return sorted(entry_list, key=lambda entry: (
0 if entry.community_favorite else 1,
entry.series.lower(),
entry.name.lower(),
))
return entry_list
def _start_model_download(self, model_key: str):
if not self._start_worker("download", self._run_download_one, model_key):
self._show_message("Model manager busy", "Please wait for the current task.", return_to_manager=True)
return
self._show_download_progress_dialog()
def _run_download_one(self, model_key: str):
self._params_memory.put_bool(CANCEL_DOWNLOAD_PARAM, False)
self._model_manager.download_model(model_key)
entries = {entry.key: entry for entry in self._load_model_entries()}
entry = entries.get(model_key)
model_version = entry.version if entry else ""
if not self._is_model_installed(model_key, model_version):
self._params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Verification failed...")
def _run_download_all(self):
self._params_memory.put_bool(CANCEL_DOWNLOAD_PARAM, False)
self._model_manager.download_all_models()
def _run_manifest_refresh(self):
self._model_manager.update_models()
def _switch_model(self, model_key: str):
entries = {entry.key: entry for entry in self._load_model_entries()}
entry = entries.get(model_key)
if entry is None:
self._show_message("Model unavailable", "Refresh manifest and try again.", return_to_manager=True)
return
if not self._is_model_installed(entry.key, entry.version):
self._show_message("Model not downloaded", "Download this model first.", return_to_manager=True)
return
self._params.put("Model", entry.key)
self._params.put("DrivingModel", entry.key)
self._params.put("DrivingModelName", entry.name)
version = entry.version.strip()
if version:
self._params.put("ModelVersion", version)
self._params.put("DrivingModelVersion", version)
if ui_state.started:
self._params.put_bool("OnroadCycleRequested", True)
self._show_message("Model switched", "Drive-cycle requested for immediate apply.", return_to_manager=True)
self.refresh()
def _start_worker(self, job: str, target, *args) -> bool:
if self._worker_thread is not None and self._worker_thread.is_alive():
return False
self._active_job = job
def run_job():
try:
target(*args)
finally:
if job == "refresh":
self._manifest_last_refresh_mono = time.monotonic()
self._active_job = ""
self._worker_thread = threading.Thread(target=run_job, daemon=True)
self._worker_thread.start()
return True
def _maybe_refresh_manifest(self, force: bool):
if ui_state.started:
return
now = time.monotonic()
has_entries = bool(self._load_model_entries())
stale = (now - self._manifest_last_refresh_mono) > MANIFEST_STALE_SECONDS
if force or not has_entries or stale:
self._start_worker("refresh", self._run_manifest_refresh)
def _is_download_job_running(self) -> bool:
return self._active_job in {"download", "download_all"}
def _process_terminal_progress(self):
if self._is_download_job_running():
self._terminal_progress_since = 0.0
return
progress = self._params_memory.get(DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or ""
if not progress:
self._terminal_progress_since = 0.0
return
if not self._is_terminal_progress(progress):
return
if self._terminal_progress_since == 0.0:
self._terminal_progress_since = time.monotonic()
return
if time.monotonic() - self._terminal_progress_since >= _PROGRESS_HOLD_SECONDS:
self._params_memory.remove(CANCEL_DOWNLOAD_PARAM)
self._params_memory.remove(DOWNLOAD_PROGRESS_PARAM)
self._terminal_progress_since = 0.0
def _update_button_value(self):
if self._active_job == "refresh":
value = "refreshing"
elif self._is_download_job_running():
value = self._params_memory.get(DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or "downloading..."
else:
progress = self._params_memory.get(DOWNLOAD_PROGRESS_PARAM, encoding="utf-8") or ""
if progress and self._is_terminal_progress(progress):
value = progress
else:
value = self._get_current_model_name()
if value != self.get_value():
self.set_value(value)
def _load_model_entries(self) -> list[ModelEntry]:
available_models = _split_param(self._params.get("AvailableModels", encoding="utf-8"))
model_names = _split_param(self._params.get("AvailableModelNames", encoding="utf-8"))
model_series = [item.strip() for item in (self._params.get("AvailableModelSeries", encoding="utf-8") or "").split(",")]
model_versions = [item.strip() for item in (self._params.get("ModelVersions", encoding="utf-8") or "").split(",")]
released_dates = [item.strip() for item in (self._params.get("ModelReleasedDates", encoding="utf-8") or "").split(",")]
community_favs = set(_split_param(self._params.get("CommunityFavorites", encoding="utf-8")))
size = min(len(available_models), len(model_names))
entries: list[ModelEntry] = []
for i in range(size):
key = available_models[i].strip()
name = _clean_model_name(model_names[i])
if not key or not name:
continue
series = model_series[i].strip() if i < len(model_series) and model_series[i].strip() else "Custom Series"
version = model_versions[i].strip() if i < len(model_versions) else ""
released = released_dates[i].strip() if i < len(released_dates) else ""
entries.append(ModelEntry(
key=key,
name=name,
series=series,
version=version,
released=released,
community_favorite=(key in community_favs),
))
return entries
def _get_current_model_key(self) -> str:
model_key = self._params.get("Model", encoding="utf-8") or self._params.get("DrivingModel", encoding="utf-8") or ""
if model_key:
return model_key
default_key = self._params.get_default_value("Model") or self._params.get_default_value("DrivingModel")
if isinstance(default_key, bytes):
return default_key.decode("utf-8", errors="ignore").strip()
return str(default_key or "").strip()
def _get_current_model_name(self) -> str:
current_name = _clean_model_name(self._params.get("DrivingModelName", encoding="utf-8") or "")
if current_name:
return current_name
current_key = self._get_current_model_key()
for entry in self._load_model_entries():
if entry.key == current_key:
return entry.name
return "default"
def _is_model_installed(self, key: str, version: str) -> bool:
if not key:
return False
if self._is_builtin_default_model(key):
return True
if (MODELS_PATH / f"{key}.thneed").is_file():
return True
required_files = self._required_files_for_version(key, version)
if not required_files:
return False
return all((MODELS_PATH / filename).is_file() for filename in required_files)
def _is_builtin_default_model(self, key: str) -> bool:
default_key = self._params.get_default_value("DrivingModel") or self._params.get_default_value("Model")
if isinstance(default_key, bytes):
default_key = default_key.decode("utf-8", errors="ignore")
default_key = str(default_key or "").strip()
if not default_key:
default_key = "sc"
# Manifest can expose legacy IDs like "sc2" while default remains "sc".
if key == default_key:
return True
if default_key.endswith("2") and key == default_key[:-1]:
return True
if not default_key.endswith("2") and key == f"{default_key}2":
return True
return False
def _required_files_for_version(self, key: str, version: str) -> list[str]:
if version not in TINYGRAD_VERSIONS:
return []
files = [
f"{key}_driving_policy_tinygrad.pkl",
f"{key}_driving_vision_tinygrad.pkl",
f"{key}_driving_policy_metadata.pkl",
f"{key}_driving_vision_metadata.pkl",
]
if version == "v12":
files.extend([
f"{key}_driving_off_policy_tinygrad.pkl",
f"{key}_driving_off_policy_metadata.pkl",
])
return files
@staticmethod
def _is_terminal_progress(progress: str) -> bool:
lower = progress.lower()
return any(pattern in lower for pattern in _TERMINAL_PROGRESS_PATTERNS)
def _show_download_progress_dialog(self):
dialog = DownloadProgressDialog(
params_memory=self._params_memory,
is_downloading=self._is_download_job_running,
cancel_callback=self._cancel_download,
is_terminal_progress=self._is_terminal_progress,
on_close=self._open_manager_menu,
)
gui_app.push_widget(dialog)
def _show_message(self, title: str, description: str, return_to_manager: bool = False):
dialog = BigDialog(title, description)
if return_to_manager:
dialog.set_back_callback(self._open_manager_menu)
gui_app.push_widget(dialog)