BigUI WIP: System Settings Cleanup Pass

This commit is contained in:
firestarsdog
2026-06-01 00:36:35 -04:00
parent 2132a5ceb0
commit 594f525c0c
2 changed files with 343 additions and 281 deletions
@@ -218,3 +218,28 @@ def draw_custom_icon(key: str, x: float, y: float, s: float, color: rl.Color):
# Window cutout details
rl.draw_line_ex(rl.Vector2(x + 18.0 * s, y + 25.0 * s), rl.Vector2(x + 36.0 * s, y + 25.0 * s), 1.5 * s, color)
rl.draw_line_ex(rl.Vector2(x + 36.0 * s, y + 25.0 * s), rl.Vector2(x + 34.0 * s, y + 20.0 * s), 1.5 * s, color)
elif key == "first_aid":
# First Aid Kit Symbol
x_c = x + 30.0 * s
y_c = y + 30.0 * s
# Handle on top of the kit
draw_ellipse_arc(x_c, y_c - 10.0 * s, 6.0 * s, 4.0 * s, 0.0, 180.0, 360.0, 2.5 * s)
# Box outline with rounded corners
draw_ellipse_arc(x_c - 15.0 * s, y_c - 7.0 * s, 3.0 * s, 3.0 * s, 0.0, 180.0, 270.0, 2.5 * s)
draw_ellipse_arc(x_c + 15.0 * s, y_c - 7.0 * s, 3.0 * s, 3.0 * s, 0.0, 270.0, 360.0, 2.5 * s)
draw_ellipse_arc(x_c + 15.0 * s, y_c + 12.0 * s, 3.0 * s, 3.0 * s, 0.0, 0.0, 90.0, 2.5 * s)
draw_ellipse_arc(x_c - 15.0 * s, y_c + 12.0 * s, 3.0 * s, 3.0 * s, 0.0, 90.0, 180.0, 2.5 * s)
# Connecting lines for the box outline
rl.draw_line_ex(rl.Vector2(x_c - 15.0 * s, y_c - 10.0 * s), rl.Vector2(x_c + 15.0 * s, y_c - 10.0 * s), 2.5 * s, color)
rl.draw_line_ex(rl.Vector2(x_c - 15.0 * s, y_c + 15.0 * s), rl.Vector2(x_c + 15.0 * s, y_c + 15.0 * s), 2.5 * s, color)
rl.draw_line_ex(rl.Vector2(x_c - 18.0 * s, y_c - 7.0 * s), rl.Vector2(x_c - 18.0 * s, y_c + 12.0 * s), 2.5 * s, color)
rl.draw_line_ex(rl.Vector2(x_c + 18.0 * s, y_c - 7.0 * s), rl.Vector2(x_c + 18.0 * s, y_c + 12.0 * s), 2.5 * s, color)
# Cross in the center (centered at y_c + 2.5)
rl.draw_line_ex(rl.Vector2(x_c, y_c - 4.0 * s), rl.Vector2(x_c, y_c + 9.0 * s), 3.5 * s, color)
rl.draw_line_ex(rl.Vector2(x_c - 6.5 * s, y_c + 2.5 * s), rl.Vector2(x_c + 6.5 * s, y_c + 2.5 * s), 3.5 * s, color)
@@ -24,6 +24,7 @@ from openpilot.system.ui.widgets.label import gui_label
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage
from openpilot.selfdrive.ui.layouts.settings.starpilot.scribble import draw_custom_icon
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherAdjustorRow,
@@ -31,17 +32,25 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AetherScrollbar,
AetherSegmentedControl,
AetherListColors,
TileGrid,
ToggleTile,
panel_style_from_color,
init_list_panel,
draw_list_group_shell,
draw_list_scroll_fades,
draw_section_header,
draw_selection_list_row,
draw_standard_toggle_row,
draw_settings_panel_header,
draw_soft_card,
draw_tab_bar,
AetherSliderDialog,
_mix_colors,
_with_alpha,
_snap_rect,
_draw_rounded_fill,
_draw_rounded_stroke,
_point_hits,
_draw_text_fit_common,
)
from openpilot.starpilot.common.connect_server import prepare_konik_server_switch
@@ -106,7 +115,6 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
self._scrollbar = AetherScrollbar()
self._content_height = 0.0
self._scroll_offset = 0.0
self._active_tab_key = "basics"
self._adjustor_rows: dict[str, AetherAdjustorRow] = {}
self._display_slider_keys = ["ScreenBrightness", "ScreenBrightnessOnroad", "ScreenTimeout", "ScreenTimeoutOnroad"]
self._power_slider_keys = ["DeviceShutdown", "LowVoltageShutdown"]
@@ -288,63 +296,22 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
},
]
self._support_rows = [
{
"id": "ReportIssue",
"title": tr("Report Issue"),
"subtitle": tr("Send feedback with your Discord handle."),
"action": tr("Open"),
},
{
"id": "FlashPanda",
"title": tr("Flash Panda"),
"subtitle": tr("Reflash Panda firmware from this panel."),
"action": tr("Flash"),
},
]
self._danger_rows = [
{
"id": "Storage",
"title": tr("Clear Driving Data"),
"subtitle": tr("Delete recorded driving data and footage."),
"action": tr("Delete"),
},
{
"id": "ErrorLogs",
"title": tr("Clear Error Logs"),
"subtitle": tr("Remove saved crash logs and diagnostics."),
"action": tr("Delete"),
},
{
"id": "ResetDefaults",
"title": tr("Reset Toggles"),
"subtitle": tr("Restore StarPilot defaults for all toggles."),
"action": tr("Reset"),
},
{
"id": "ResetStock",
"title": tr("Reset To Stock"),
"subtitle": tr("Restore stock openpilot toggle values."),
"action": tr("Reset"),
},
]
self._toggle_groups = [
{
"id": "device_controls",
"title": tr("Device Controls"),
"toggle_ids": ["StandbyMode", "UseKonikServer", "DebugMode", "ShowFPS"],
},
{
"id": "uploads_logging",
"title": tr("Uploads & Logging"),
"toggle_ids": ["NoUploads", "DisableOnroadUploads", "NoLogging", "HigherBitrate"],
},
]
self._tab_defs = [
{"id": "basics", "title": tr("Display & Power")},
{"id": "connectivity", "title": tr("Connectivity")},
{"id": "care", "title": tr("Backups & Care")},
]
self._basics_tile_grid_h = 0.0
self._connectivity_tile_grid = TileGrid(columns=2, padding=12)
for toggle_def in self._toggle_defs:
tile = ToggleTile(
title=toggle_def["title"],
get_state=toggle_def["get"],
set_state=toggle_def["set"],
bg_color=PANEL_STYLE.accent,
desc=toggle_def["subtitle"],
is_enabled=toggle_def.get("is_enabled"),
disabled_label=toggle_def.get("disabled_label", ""),
)
self._connectivity_tile_grid.add_tile(tile)
self._connectivity_tile_grid.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid())
self._child(self._connectivity_tile_grid)
self._drive_mode_control = self._child(
AetherSegmentedControl(
@@ -371,17 +338,11 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
def _column_width(self, width: float) -> float:
return (width - self.COLUMN_GAP) / 2 if self._uses_two_columns(width) else width
def _lookup_toggle(self, toggle_id: str):
return next((toggle for toggle in self._toggle_defs if toggle["id"] == toggle_id), None)
def _toggle_defs_for_group(self, group: dict) -> list[dict]:
return [toggle for toggle_id in group["toggle_ids"] if (toggle := self._lookup_toggle(toggle_id)) is not None]
def _tab_subtitle(self, tab_id: str) -> str:
if tab_id == "basics":
return tr("{} controls").format(len(self._display_slider_keys) + len(self._power_slider_keys))
if tab_id == "connectivity":
return tr("{} toggles").format(len(self._toggle_defs))
return tr("{} controls + {} toggles").format(
len(self._display_slider_keys) + len(self._power_slider_keys),
len(self._toggle_defs))
return self._controller.backup_status_text()
def _format_slider_value(self, key: str) -> str:
@@ -464,30 +425,32 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
super().hide_event()
self._clear_ephemeral_state()
def _interactive_state(self, target_id: str, rect: rl.Rectangle, *, pad_y: float = 0) -> tuple[bool, bool]:
self._interactive_rects[target_id] = rect
parent_rect = None if target_id.startswith("static:") else self._scroll_rect
hovered = _point_hits(gui_app.last_mouse_event.pos, rect, parent_rect, pad_x=6, pad_y=pad_y)
return hovered, self._pressed_target == target_id
def _target_at(self, mouse_pos) -> str | None:
for target_id, rect in self._interactive_rects.items():
if target_id.startswith("static:"):
if _point_hits(mouse_pos, rect, None, pad_x=6, pad_y=0):
return target_id
for target_id, rect in self._interactive_rects.items():
if not target_id.startswith("static:"):
if _point_hits(mouse_pos, rect, self._scroll_rect, pad_x=6, pad_y=0):
return target_id
return None
def _activate_target(self, target_id: str | None):
if not target_id:
return
prefix, _, value = target_id.partition(":")
if prefix == "tab":
self._active_tab_key = value
for adjustor in self._adjustor_rows.values():
adjustor.reset_interaction()
return
if prefix == "toggle":
toggle_def = self._lookup_toggle(value)
if toggle_def is None:
return
is_enabled = toggle_def.get("is_enabled", lambda: True)
if is_enabled():
toggle_def["set"](not toggle_def["get"]())
return
if prefix == "backup":
self._controller.open_backup_manager(value)
return
if prefix == "action":
self._controller.handle_action(value)
if target_id == "static:first_aid":
gui_app.push_widget(AetherBackupsCareDialog(self._controller))
def _render(self, rect: rl.Rectangle):
self._interactive_rects.clear()
self.set_rect(rect)
frame, scroll_rect, content_width = init_list_panel(rect, PANEL_STYLE)
@@ -514,6 +477,32 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
tr("Manage display, backups, connectivity, and device maintenance from one touch-first panel."),
max_title_width=0.60, max_subtitle_width=0.62)
# First Aid Button in top right
btn_w, btn_h = 68.0, 68.0
btn_x = rect.x + rect.width - btn_w
btn_y = rect.y + 4.0
btn_rect = rl.Rectangle(btn_x, btn_y, btn_w, btn_h)
hovered, pressed = self._interactive_state("static:first_aid", btn_rect)
if pressed:
fill = rl.Color(255, 255, 255, 30)
border = PANEL_STYLE.accent
elif hovered:
fill = rl.Color(255, 255, 255, 18)
border = PANEL_STYLE.accent
else:
fill = rl.Color(255, 255, 255, 8)
border = rl.Color(255, 255, 255, 20)
draw_soft_card(btn_rect, fill, border)
s = 48.0 / 60.0
icon_x = btn_x + (btn_w - 60.0 * s) / 2.0
icon_y = btn_y + (btn_h - 60.0 * s) / 2.0
icon_color = PANEL_STYLE.accent if (hovered or pressed) else AetherListColors.HEADER
draw_custom_icon("first_aid", icon_x, icon_y, s, icon_color)
summary_y = rect.y + 48 + self.HEADER_SUBTITLE_HEIGHT + self.HEADER_SUMMARY_GAP
summary_rect = rl.Rectangle(rect.x, summary_y, rect.width, min(self.HEADER_CARD_HEIGHT, rect.y + rect.height - summary_y))
self._draw_summary_card(summary_rect)
@@ -554,28 +543,20 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
self._drive_mode_control.render(control_rect)
def _measure_content_height(self, width: float) -> float:
content_height = self._measure_active_tab_height(width)
return self.TAB_HEIGHT + self.TAB_BOTTOM_GAP + content_height
def _measure_active_tab_height(self, width: float) -> float:
display_h = self._section_block_height(self._slider_section_height(self._display_slider_keys, width))
power_h = self._section_block_height(self._slider_section_height(self._power_slider_keys, width))
backups_h = self._section_block_height(self._section_height(2, ROW_HEIGHT))
maintenance_h = self._section_block_height(self._maintenance_section_content_height())
if self._active_tab_key == "basics":
if self._uses_two_columns(width):
return max(display_h, power_h)
return self._stacked_section_height([display_h, power_h])
if self._active_tab_key == "connectivity":
group_heights = [self._section_block_height(self._section_height(len(self._toggle_defs_for_group(group)), ROW_HEIGHT)) for group in self._toggle_groups]
if self._uses_two_columns(width):
return max(group_heights)
return self._stacked_section_height(group_heights)
if self._uses_two_columns(width):
return max(backups_h, maintenance_h)
return self._stacked_section_height([backups_h, maintenance_h])
left_col = display_h + SECTION_GAP + power_h
self._basics_tile_grid_h = left_col
return left_col
else:
tile_rows = self._connectivity_tile_grid.get_row_count(
len(self._connectivity_tile_grid.tiles), available_width=width)
tile_gaps = self._connectivity_tile_grid.get_internal_gap_height(
len(self._connectivity_tile_grid.tiles), available_width=width)
tiles_content_h = tile_rows * 96 + tile_gaps
connectivity_h = tiles_content_h
return self._stacked_section_height([display_h, power_h, connectivity_h])
def _section_block_height(self, content_height: float) -> float:
return SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP + content_height
@@ -587,61 +568,39 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
total += adjustor.measure_height(width)
return total
def _maintenance_section_content_height(self) -> float:
support_h = self._section_height(len(self._support_rows), ROW_HEIGHT)
danger_h = self._section_height(len(self._danger_rows), ROW_HEIGHT)
return support_h + 12 + 30 + danger_h
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
self._interactive_rects.clear()
y = rect.y + self._scroll_offset
self._draw_tabs(rl.Rectangle(rect.x, y, width, self.TAB_HEIGHT))
y += self.TAB_HEIGHT + self.TAB_BOTTOM_GAP
if self._active_tab_key == "basics":
self._draw_basics_tab(y, rect.x, width)
elif self._active_tab_key == "connectivity":
self._draw_connectivity_tab(y, rect.x, width)
else:
self._draw_care_tab(y, rect.x, width)
def _draw_tabs(self, rect: rl.Rectangle):
draw_tab_bar(
rect, self._tab_defs, self._active_tab_key, self._interactive_state,
subtitle_fn=self._tab_subtitle, style=PANEL_STYLE,
)
self._draw_basics_tab(y, rect.x, width)
def _draw_basics_tab(self, y: float, x: float, width: float):
if self._uses_two_columns(width):
column_w = self._column_width(width)
self._draw_slider_section(y, x, column_w, tr("Display"), self._display_slider_keys)
self._draw_slider_section(y, x + column_w + self.COLUMN_GAP, column_w, tr("Power"), self._power_slider_keys)
y2 = self._draw_slider_section(y, x, column_w, tr("Display"), self._display_slider_keys)
y2 += SECTION_GAP
self._draw_slider_section(y2, x, column_w, tr("Power"), self._power_slider_keys)
self._draw_connectivity_tiles_column(y, x + column_w + self.COLUMN_GAP, column_w)
return
y = self._draw_slider_section(y, x, width, tr("Display"), self._display_slider_keys)
y += SECTION_GAP
self._draw_slider_section(y, x, width, tr("Power"), self._power_slider_keys)
def _draw_connectivity_tab(self, y: float, x: float, width: float):
if self._uses_two_columns(width):
column_w = self._column_width(width)
self._draw_toggle_group_section(y, x, column_w, self._toggle_groups[0])
self._draw_toggle_group_section(y, x + column_w + self.COLUMN_GAP, column_w, self._toggle_groups[1])
return
current_y = y
for i, group in enumerate(self._toggle_groups):
current_y = self._draw_toggle_group_section(current_y, x, width, group)
if i < len(self._toggle_groups) - 1:
current_y += SECTION_GAP
def _draw_care_tab(self, y: float, x: float, width: float):
if self._uses_two_columns(width):
column_w = self._column_width(width)
self._draw_backups_section(y, x, column_w)
self._draw_maintenance_section(y, x + column_w + self.COLUMN_GAP, column_w)
return
y = self._draw_backups_section(y, x, width)
y = self._draw_slider_section(y, x, width, tr("Power"), self._power_slider_keys)
y += SECTION_GAP
self._draw_maintenance_section(y, x, width)
self._draw_connectivity_tiles_section(y, x, width)
def _draw_connectivity_tiles_column(self, y: float, x: float, width: float):
tile_h = self._basics_tile_grid_h
shell_rect = rl.Rectangle(x - 12, y - 12, width + 24, tile_h + 24)
draw_list_group_shell(shell_rect, style=PANEL_STYLE)
self._connectivity_tile_grid.set_parent_rect(self._scroll_rect)
self._connectivity_tile_grid.render(rl.Rectangle(x, y, width, tile_h))
def _draw_connectivity_tiles_section(self, y: float, x: float, width: float):
tile_rows = self._connectivity_tile_grid.get_row_count(len(self._connectivity_tile_grid.tiles), available_width=width)
tile_gaps = self._connectivity_tile_grid.get_internal_gap_height(len(self._connectivity_tile_grid.tiles), available_width=width)
tile_h = tile_rows * 96 + tile_gaps
group_rect = rl.Rectangle(x, y, width, tile_h)
draw_list_group_shell(group_rect, style=PANEL_STYLE)
self._connectivity_tile_grid.set_parent_rect(self._scroll_rect)
self._connectivity_tile_grid.render(rl.Rectangle(x, y, width, tile_h))
def _draw_slider_section(self, y: float, x: float, width: float, title: str, keys: list[str]) -> float:
draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), title, style=PANEL_STYLE)
@@ -662,124 +621,180 @@ class SystemSettingsManagerView(AetherInteractiveMixin, Widget):
adjustor.render(row_rect)
return rect.y + row_h
def _draw_toggle_group_section(self, y: float, x: float, width: float, group: dict) -> float:
toggles = self._toggle_defs_for_group(group)
trailing_text = tr("{} toggles").format(len(toggles))
draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), group["title"], trailing_text=trailing_text, style=PANEL_STYLE)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
toggle_rect = rl.Rectangle(x, y, width, self._section_height(len(toggles), ROW_HEIGHT))
draw_list_group_shell(toggle_rect, style=PANEL_STYLE)
for index, toggle_def in enumerate(toggles):
self._draw_toggle_row(
rl.Rectangle(toggle_rect.x, toggle_rect.y + index * ROW_HEIGHT, toggle_rect.width, ROW_HEIGHT),
toggle_def,
is_last=index == len(toggles) - 1,
class AetherBackupsCareDialog(Widget):
def __init__(self, controller: StarPilotSystemLayout):
super().__init__()
self._controller = controller
self._color = PANEL_STYLE.accent
self._font_title = gui_app.font(FontWeight.BOLD)
self._font_btn = gui_app.font(FontWeight.SEMI_BOLD)
self._pressed_btn_id: str | None = None
self._buttons = [
{"id": "system_backups", "text": tr("System Backups"), "danger": False},
{"id": "toggle_snapshots", "text": tr("Toggle Snapshots"), "danger": False},
{"id": "report_issue", "text": tr("Report Issue"), "danger": False},
{"id": "flash_panda", "text": tr("Flash Panda"), "danger": False},
{"id": "clear_data", "text": tr("Clear Driving Data"), "danger": True},
{"id": "clear_logs", "text": tr("Clear Error Logs"), "danger": True},
{"id": "reset_toggles", "text": tr("Reset Toggles"), "danger": True},
{"id": "reset_stock", "text": tr("Reset To Stock"), "danger": True},
]
self._button_rects: dict[str, rl.Rectangle] = {}
self._close_rect = rl.Rectangle(0, 0, 0, 0)
def _handle_mouse_press(self, mouse_pos):
for btn in self._buttons:
btn_id = btn["id"]
rect = self._button_rects.get(btn_id)
if rect and rl.check_collision_point_rec(mouse_pos, rect):
self._pressed_btn_id = btn_id
return
if rl.check_collision_point_rec(mouse_pos, self._close_rect):
self._pressed_btn_id = "close"
def _handle_mouse_release(self, mouse_pos):
if not self._pressed_btn_id:
return
released_btn = self._pressed_btn_id
self._pressed_btn_id = None
if released_btn == "close":
if rl.check_collision_point_rec(mouse_pos, self._close_rect):
gui_app.pop_widget()
return
for btn in self._buttons:
btn_id = btn["id"]
if btn_id == released_btn:
rect = self._button_rects.get(btn_id)
if rect and rl.check_collision_point_rec(mouse_pos, rect):
self._trigger_action(btn_id)
break
def _trigger_action(self, btn_id: str):
if btn_id == "system_backups":
self._controller.open_backup_manager("system")
elif btn_id == "toggle_snapshots":
self._controller.open_backup_manager("toggle")
elif btn_id == "report_issue":
self._controller.handle_action("ReportIssue")
elif btn_id == "flash_panda":
self._controller.handle_action("FlashPanda")
elif btn_id == "clear_data":
self._controller.handle_action("Storage")
elif btn_id == "clear_logs":
self._controller.handle_action("ErrorLogs")
elif btn_id == "reset_toggles":
self._controller.handle_action("ResetDefaults")
elif btn_id == "reset_stock":
self._controller.handle_action("ResetStock")
def _render(self, rect: rl.Rectangle):
rl.draw_rectangle(0, 0, gui_app.width, gui_app.height, rl.Color(0, 0, 0, 160))
dialog_w = 880
dialog_h = 600
dx = rect.x + (rect.width - dialog_w) / 2
dy = rect.y + (rect.height - dialog_h) / 2
d_rect = _snap_rect(rl.Rectangle(dx, dy, dialog_w, dialog_h))
_draw_rounded_fill(d_rect, rl.Color(10, 12, 16, 255), radius_px=24)
_draw_rounded_stroke(d_rect, rl.Color(255, 255, 255, 16), radius_px=24)
rl.draw_rectangle_rec(rl.Rectangle(d_rect.x, d_rect.y, d_rect.width, 4), self._color)
title_text = tr("Maintenance")
title_size = 32
ts = measure_text_cached(self._font_title, title_text, title_size)
rl.draw_text_ex(self._font_title, title_text, rl.Vector2(round(dx + (dialog_w - ts.x) / 2), round(dy + 32)), title_size, 0, rl.WHITE)
mouse_pos = gui_app.last_mouse_event.pos
self._button_rects.clear()
for i, btn in enumerate(self._buttons):
btn_id = btn["id"]
row = i % 4
col = i // 4
bx = dx + 48 + col * (368 + 48)
by = dy + 104 + row * (68 + 16)
btn_rect = _snap_rect(rl.Rectangle(bx, by, 368, 68))
self._button_rects[btn_id] = btn_rect
hovered = rl.check_collision_point_rec(mouse_pos, btn_rect)
pressed = self._pressed_btn_id == btn_id
if btn["danger"]:
if pressed:
fill = rl.Color(173, 78, 90, 120)
border = PANEL_STYLE.danger_text
elif hovered:
fill = rl.Color(173, 78, 90, 80)
border = PANEL_STYLE.danger_text
else:
fill = PANEL_STYLE.danger_fill
border = PANEL_STYLE.danger_border
text_color = PANEL_STYLE.danger_text
else:
if pressed:
fill = rl.Color(255, 255, 255, 30)
border = self._color
elif hovered:
fill = rl.Color(255, 255, 255, 18)
border = self._color
else:
fill = rl.Color(255, 255, 255, 8)
border = rl.Color(255, 255, 255, 20)
text_color = AetherListColors.HEADER
_draw_rounded_fill(btn_rect, fill, radius_px=14)
_draw_rounded_stroke(btn_rect, border, radius_px=14)
font_size = 20
_draw_text_fit_common(
self._font_btn,
btn["text"],
rl.Vector2(btn_rect.x + 12, btn_rect.y + (btn_rect.height - font_size) / 2),
btn_rect.width - 24,
font_size,
align_center=True,
color=text_color,
)
return y + toggle_rect.height
def _draw_toggle_row(self, rect: rl.Rectangle, toggle_def: dict, is_last: bool):
target_id = f"toggle:{toggle_def['id']}"
hovered, pressed = self._interactive_state(target_id, rect)
is_enabled = toggle_def.get("is_enabled", lambda: True)()
subtitle = toggle_def.get("disabled_label", "") if not is_enabled and toggle_def.get("disabled_label") else toggle_def["subtitle"]
draw_standard_toggle_row(
rect, toggle_def["title"], subtitle, toggle_def["get"](),
enabled=is_enabled, hovered=hovered, pressed=pressed,
is_last=is_last, style=PANEL_STYLE,
)
cx = dx + (dialog_w - 320) / 2
cy = dy + 464
self._close_rect = _snap_rect(rl.Rectangle(cx, cy, 320, 72))
def _draw_backups_section(self, y: float, x: float, width: float) -> float:
draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), tr("Backups"), trailing_text=self._controller.backup_status_text(), style=PANEL_STYLE)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
close_hovered = rl.check_collision_point_rec(mouse_pos, self._close_rect)
close_pressed = self._pressed_btn_id == "close"
summary_rect = rl.Rectangle(x, y, width, ROW_HEIGHT * 2)
draw_list_group_shell(summary_rect, style=PANEL_STYLE)
self._draw_backup_manager_row(rl.Rectangle(summary_rect.x, summary_rect.y, summary_rect.width, ROW_HEIGHT), "system", is_last=False)
self._draw_backup_manager_row(rl.Rectangle(summary_rect.x, summary_rect.y + ROW_HEIGHT, summary_rect.width, ROW_HEIGHT), "toggle", is_last=True)
return y + summary_rect.height
def _draw_backup_manager_row(self, rect: rl.Rectangle, backup_kind: str, is_last: bool):
target_id = f"backup:{backup_kind}"
hovered, pressed = self._interactive_state(target_id, rect)
if backup_kind == "system":
title = tr("System Backups")
subtitle = self._controller.latest_backup_summary()
count = self._controller.backup_count()
if close_pressed:
close_fill = _mix_colors(self._color, rl.Color(0, 0, 0, 255), 0.2)
close_border = self._color
elif close_hovered:
close_fill = self._color
close_border = self._color
else:
title = tr("Toggle Snapshots")
subtitle = self._controller.latest_toggle_backup_summary()
count = self._controller.toggle_backup_count()
action_text = tr("Create") if count == 0 else tr("Manage")
draw_selection_list_row(
rect,
title=title,
subtitle=subtitle,
action_text=action_text,
hovered=hovered,
pressed=pressed,
is_last=is_last,
action_width=188,
action_pill=True,
action_pill_height=44,
action_pill_width=108 if count == 0 else self.ACTION_PILL_WIDTH,
title_size=34,
subtitle_size=22,
action_text_size=18,
row_separator=PANEL_STYLE.divider_color,
action_fill=AetherListColors.CURRENT_BG,
action_border=PANEL_STYLE.current_border,
action_text_color=AetherListColors.HEADER,
close_fill = rl.Color(255, 255, 255, 14)
close_border = rl.Color(255, 255, 255, 28)
_draw_rounded_fill(self._close_rect, close_fill, radius_px=16)
_draw_rounded_stroke(self._close_rect, close_border, radius_px=16)
close_text = tr("Close")
close_size = 22
cts = measure_text_cached(self._font_btn, close_text, close_size)
rl.draw_text_ex(
self._font_btn,
close_text,
rl.Vector2(round(self._close_rect.x + (self._close_rect.width - cts.x) / 2), round(self._close_rect.y + (self._close_rect.height - cts.y) / 2)),
close_size,
0,
rl.WHITE,
)
def _draw_maintenance_section(self, y: float, x: float, width: float):
draw_section_header(rl.Rectangle(x, y, width, SECTION_HEADER_HEIGHT), tr("Support & Maintenance"), style=PANEL_STYLE)
y += SECTION_HEADER_HEIGHT + SECTION_HEADER_GAP
support_rect = rl.Rectangle(x, y, width, self._section_height(len(self._support_rows), ROW_HEIGHT))
draw_list_group_shell(support_rect, style=PANEL_STYLE)
for index, row in enumerate(self._support_rows):
row_rect = rl.Rectangle(support_rect.x, support_rect.y + index * ROW_HEIGHT, support_rect.width, ROW_HEIGHT)
self._draw_action_row(row_rect, row, is_last=index == len(self._support_rows) - 1)
y += support_rect.height + 12
danger_title_rect = rl.Rectangle(x, y, width, 22)
gui_label(danger_title_rect, tr("Danger Zone"), 20, PANEL_STYLE.danger_text, FontWeight.MEDIUM)
y += 30
danger_rect = rl.Rectangle(x, y, width, self._section_height(len(self._danger_rows), ROW_HEIGHT))
draw_list_group_shell(danger_rect, fill=PANEL_STYLE.danger_fill, border=PANEL_STYLE.danger_border, style=PANEL_STYLE)
for index, row in enumerate(self._danger_rows):
row_rect = rl.Rectangle(danger_rect.x, danger_rect.y + index * ROW_HEIGHT, danger_rect.width, ROW_HEIGHT)
self._draw_action_row(row_rect, row, is_last=index == len(self._danger_rows) - 1, danger=True)
def _draw_action_row(self, rect: rl.Rectangle, row: dict, is_last: bool, *, danger: bool = False):
target_id = f"action:{row['id']}"
hovered, pressed = self._interactive_state(target_id, rect)
action_fill = PANEL_STYLE.danger_fill if danger else rl.Color(255, 255, 255, 8)
action_border = PANEL_STYLE.danger_border if danger else rl.Color(255, 255, 255, 24)
action_text_color = AetherListColors.HEADER if not danger else PANEL_STYLE.danger_text
draw_selection_list_row(
rect,
title=row["title"],
subtitle=row["subtitle"],
action_text=row["action"],
hovered=hovered,
pressed=pressed,
is_last=is_last,
action_width=188,
action_pill=True,
action_pill_height=44,
action_pill_width=self.DANGER_PILL_WIDTH if danger else self.ACTION_PILL_WIDTH,
title_size=34,
subtitle_size=22,
action_text_size=18,
row_separator=PANEL_STYLE.divider_color,
action_fill=action_fill,
action_border=action_border,
action_text_color=action_text_color,
title_color=AetherListColors.HEADER if not danger else PANEL_STYLE.danger_text,
subtitle_color=AetherListColors.SUBTEXT if not danger else PANEL_STYLE.danger_text,
)
class StarPilotSystemLayout(_SettingsPage):
_BACKUP_NAME_SANITIZE_RE = re.compile(r"[^A-Za-z0-9._-]+")
@@ -949,30 +964,52 @@ class StarPilotSystemLayout(_SettingsPage):
return self._params.get_bool("UseKonikServer")
def _on_konik_toggle(self, state):
prepare_konik_server_switch(state, self._params)
cache_path = Path("/cache/use_konik")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
if ui_state.started:
gui_app.push_widget(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
target = tr("Konik") if state else tr("Comma")
def on_confirm(res):
if res != DialogResult.CONFIRM:
self._params.put_bool("UseKonikServer", not state)
return
prepare_konik_server_switch(state, self._params)
try:
cache_path = Path("/cache/use_konik")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
except OSError:
pass
if ui_state.started:
gui_app.push_widget(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"),
callback=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
)
)
gui_app.push_widget(
ConfirmDialog(
tr("Switch Connect endpoint to {}?").format(target),
tr("Switch"),
tr("Cancel"),
callback=on_confirm
)
)
def _on_higher_bitrate_toggle(self, state):
self._params.put_bool("HigherBitrate", state)
cache_path = Path("/cache/use_HD")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
try:
cache_path = Path("/cache/use_HD")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
except OSError:
pass
if ui_state.started:
gui_app.push_widget(
ConfirmDialog(