diff --git a/selfdrive/ui/layouts/settings/starpilot/scribble.py b/selfdrive/ui/layouts/settings/starpilot/scribble.py index d3b8adeb3..95296c712 100644 --- a/selfdrive/ui/layouts/settings/starpilot/scribble.py +++ b/selfdrive/ui/layouts/settings/starpilot/scribble.py @@ -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) + diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 7a8a67d94..b62828209 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -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(