"""Sessions screen — save/load mixer sessions, setlists, and snapshots. Touch-optimized layout for Raspberry Pi 5"/7" displays. Provides: - Session list with load/delete buttons - Save current session button - Snapshot capture/recall - Setlist browser """ from __future__ import annotations from kivy.properties import ObjectProperty, StringProperty, ListProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.screenmanager import Screen from kivy.uix.scrollview import ScrollView from kivy.uix.textinput import TextInput from kivy.uix.togglebutton import ToggleButton from kivy.metrics import dp from kivy.clock import Clock from ..theme import Colors, Fonts, Sizes class SessionCard(BoxLayout): """A single session/snapshot entry card.""" session_name = StringProperty("") session_info = StringProperty("") is_snapshot = False def __init__(self, name="", info="", is_snapshot=False, on_load=None, on_delete=None, on_recall=None, **kwargs): super().__init__(**kwargs) self.orientation = "horizontal" self.size_hint = (1, None) self.height = dp(52) self.padding = [dp(8), dp(4)] self.spacing = dp(6) self._name = name self._is_snapshot = is_snapshot icon = "📸" if is_snapshot else "💾" # Name + info info_layout = BoxLayout(orientation="vertical", size_hint=(1, 1)) name_label = Label( text=f"{icon} {name}", color=Colors.TEXT, font_size=Fonts.SMALL, halign="left", valign="middle", size_hint=(1, 1), text_size=(None, None), ) name_label.bind(size=name_label.setter("text_size")) info_label = Label( text=info, color=Colors.TEXT_MUTED, font_size=Fonts.XSMALL, halign="left", size_hint=(1, 0.5), text_size=(None, None), ) info_label.bind(size=info_label.setter("text_size")) info_layout.add_widget(name_label) info_layout.add_widget(info_label) # Buttons btn_layout = BoxLayout( orientation="horizontal", size_hint=(None, 1), width=dp(140), spacing=dp(4), ) if is_snapshot: load_btn = Button( text="Recall", font_size=Fonts.XSMALL, size_hint=(1, 1), background_color=Colors.BG_BUTTON, color=Colors.TEXT, ) if on_recall: load_btn.bind(on_press=lambda x: on_recall(name)) else: load_btn = Button( text="Load", font_size=Fonts.XSMALL, size_hint=(1, 1), background_color=Colors.BG_BUTTON, color=Colors.TEXT, ) if on_load: load_btn.bind(on_press=lambda x: on_load(name)) del_btn = Button( text="✕", font_size=Fonts.XSMALL, size_hint=(None, 1), width=dp(40), background_color=Colors.MUTE_ON, color=Colors.TEXT, ) if on_delete: del_btn.bind(on_press=lambda x: on_delete(name)) btn_layout.add_widget(load_btn) btn_layout.add_widget(del_btn) self.add_widget(info_layout) self.add_widget(btn_layout) class SessionsScreen(Screen): """Session management screen for the touch UI.""" client = ObjectProperty(None) def __init__(self, **kwargs): super().__init__(**kwargs) self.name = "sessions" self._sessions_data = [] self._snapshots_data = [] self._current_subtab = "sessions" root = BoxLayout(orientation="vertical") # Header header = BoxLayout( orientation="horizontal", size_hint=(1, None), height=Sizes.HEADER_HEIGHT, padding=[Sizes.PAD_MD, Sizes.PAD_XS], ) header.bg_color = Colors.BG_HEADER title = Label( text="SESSIONS", color=Colors.ACCENT, font_size=Fonts.HEADER, bold=True, size_hint=(None, 1), width=dp(150), halign="left", ) title.bind(size=title.setter("text_size")) # Sub-tab bar sub_tabs = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(40), spacing=0, ) sub_tabs.bg_color = Colors.BG_DARK self._tab_sessions = ToggleButton( text="Sessions", font_size=Fonts.SMALL, group="subtab", state="down", background_color=Colors.BG_HEADER, color=Colors.ACCENT, ) self._tab_setlists = ToggleButton( text="Setlists", font_size=Fonts.SMALL, group="subtab", background_color=Colors.BG_HEADER, color=Colors.TEXT_MUTED, ) self._tab_snapshots = ToggleButton( text="Snapshots", font_size=Fonts.SMALL, group="subtab", background_color=Colors.BG_HEADER, color=Colors.TEXT_MUTED, ) self._tab_sessions.bind(on_press=lambda x: self._switch_subtab("sessions")) self._tab_setlists.bind(on_press=lambda x: self._switch_subtab("setlists")) self._tab_snapshots.bind(on_press=lambda x: self._switch_subtab("snapshots")) sub_tabs.add_widget(self._tab_sessions) sub_tabs.add_widget(self._tab_setlists) sub_tabs.add_widget(self._tab_snapshots) # Save bar save_bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(44), padding=[dp(8), dp(4)], spacing=dp(6), ) self._name_input = TextInput( text="", hint_text="Session name...", font_size=Fonts.SMALL, size_hint=(1, 1), background_color=Colors.BG_INPUT, foreground_color=Colors.TEXT, multiline=False, ) self._notes_input = TextInput( text="", hint_text="Notes...", font_size=Fonts.SMALL, size_hint=(0.6, 1), background_color=Colors.BG_INPUT, foreground_color=Colors.TEXT, multiline=False, ) save_btn = Button( text="Save", font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(80), background_color=Colors.ACCENT, color=Colors.BG_DARK, ) save_btn.bind(on_press=self._on_save) save_bar.add_widget(self._name_input) save_bar.add_widget(self._notes_input) save_bar.add_widget(save_btn) # Snapshot bar (hidden by default) self._snapshot_bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(44), padding=[dp(8), dp(4)], spacing=dp(6), ) self._snap_name_input = TextInput( text="", hint_text="Snapshot name...", font_size=Fonts.SMALL, size_hint=(1, 1), background_color=Colors.BG_INPUT, foreground_color=Colors.TEXT, multiline=False, ) snap_btn = Button( text="Capture", font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(80), background_color=Colors.ACCENT, color=Colors.BG_DARK, ) snap_btn.bind(on_press=self._on_capture_snapshot) next_btn = Button( text="▶", font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(44), background_color=Colors.BG_BUTTON, color=Colors.TEXT, ) next_btn.bind(on_press=self._on_snapshot_next) prev_btn = Button( text="◀", font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(44), background_color=Colors.BG_BUTTON, color=Colors.TEXT, ) prev_btn.bind(on_press=self._on_snapshot_prev) self._snapshot_bar.add_widget(self._snap_name_input) self._snapshot_bar.add_widget(snap_btn) self._snapshot_bar.add_widget(prev_btn) self._snapshot_bar.add_widget(next_btn) # Scrollable list area self._list_scroll = ScrollView(size_hint=(1, 1), do_scroll_x=False) self._list_layout = BoxLayout( orientation="vertical", size_hint=(1, None), spacing=dp(2), padding=[dp(4), dp(4)], ) self._list_layout.bind(minimum_height=self._list_layout.setter("height")) self._list_scroll.add_widget(self._list_layout) # Setlist view (hidden by default) self._setlist_view = BoxLayout( orientation="vertical", size_hint=(1, 1), spacing=dp(4), ) self._setlist_view.opacity = 0 self._setlist_view.disabled = True self._setlist_list = BoxLayout( orientation="vertical", size_hint=(1, 1), spacing=dp(2), padding=[dp(4), dp(4)], ) setlist_bar = BoxLayout( orientation="horizontal", size_hint=(1, None), height=dp(44), padding=[dp(8), dp(4)], spacing=dp(6), ) self._setlist_name_input = TextInput( text="", hint_text="Setlist name...", font_size=Fonts.SMALL, size_hint=(1, 1), background_color=Colors.BG_INPUT, foreground_color=Colors.TEXT, multiline=False, ) new_setlist_btn = Button( text="Create", font_size=Fonts.SMALL, size_hint=(None, 1), width=dp(80), background_color=Colors.ACCENT, color=Colors.BG_DARK, ) new_setlist_btn.bind(on_press=self._on_create_setlist) setlist_bar.add_widget(self._setlist_name_input) setlist_bar.add_widget(new_setlist_btn) self._setlist_view.add_widget(setlist_bar) self._setlist_view.add_widget(self._setlist_list) # Content area content = BoxLayout(orientation="vertical", size_hint=(1, 1)) content.add_widget(self._list_scroll) content.add_widget(self._setlist_view) # Combine header.add_widget(title) root.add_widget(header) root.add_widget(sub_tabs) root.add_widget(save_bar) root.add_widget(self._snapshot_bar) root.add_widget(content) self.add_widget(root) # Refresh periodically Clock.schedule_interval(self._refresh, 10.0) def on_enter(self, *args): """Called when the screen is displayed.""" self._refresh() def _switch_subtab(self, tab: str): """Switch between sessions/setlists/snapshots.""" self._current_subtab = tab # Update toggle states self._tab_sessions.state = "down" if tab == "sessions" else "normal" self._tab_setlists.state = "down" if tab == "setlists" else "normal" self._tab_snapshots.state = "down" if tab == "snapshots" else "normal" # Toggle visibility if tab == "setlists": self._list_scroll.opacity = 0 self._list_scroll.disabled = True self._setlist_view.opacity = 1 self._setlist_view.disabled = False self._snapshot_bar.opacity = 0 self._snapshot_bar.disabled = True elif tab == "snapshots": self._list_scroll.opacity = 1 self._list_scroll.disabled = False self._setlist_view.opacity = 0 self._setlist_view.disabled = True self._snapshot_bar.opacity = 1 self._snapshot_bar.disabled = False else: self._list_scroll.opacity = 1 self._list_scroll.disabled = False self._setlist_view.opacity = 0 self._setlist_view.disabled = True self._snapshot_bar.opacity = 0 self._snapshot_bar.disabled = True self._refresh() def _refresh(self, *args): """Refresh the session list from the REST API.""" if not self.client: return if self._current_subtab in ("sessions", "snapshots"): self._fetch_sessions() if self._current_subtab == "snapshots": self._fetch_snapshots() elif self._current_subtab == "setlists": self._fetch_setlists() def _fetch_sessions(self): """Fetch sessions list from REST API.""" try: self.client.rest_get( "/api/v1/sessions", on_success=self._on_sessions_loaded, on_error=lambda e: None, ) except Exception: pass def _fetch_snapshots(self): """Fetch snapshots list from REST API.""" try: self.client.rest_get( "/api/v1/snapshots", on_success=self._on_snapshots_loaded, on_error=lambda e: None, ) except Exception: pass def _fetch_setlists(self): """Fetch setlists list from REST API.""" try: self.client.rest_get( "/api/v1/setlists", on_success=self._on_setlists_loaded, on_error=lambda e: None, ) except Exception: pass def _on_sessions_loaded(self, data: dict): """Render session cards.""" sessions = data.get("sessions", []) self._sessions_data = sessions self._render_session_list() def _on_snapshots_loaded(self, data: dict): """Render snapshot cards.""" snapshots = data.get("snapshots", []) self._snapshots_data = snapshots self._render_session_list() def _render_session_list(self): """Build the session/snapshot card list.""" self._list_layout.clear_widgets() items = [] for s in (self._sessions_data or []): items.append({ "type": "session", "name": s.get("name", ""), "info": f"{s.get('size_bytes', 0)} bytes", "is_snapshot": False, }) for s in (self._snapshots_data or []): cat = s.get("category", "") cur = " (active)" if s.get("is_current") else "" items.append({ "type": "snapshot", "name": s.get("name", ""), "info": f"{cat}{cur}" if (cat or cur) else "", "is_snapshot": True, }) if not items: self._list_layout.add_widget(Label( text="No sessions saved yet.\nUse the Save button above.", color=Colors.TEXT_MUTED, font_size=Fonts.SMALL, size_hint=(1, None), height=dp(60), halign="center", valign="middle", )) return for item in items: card = SessionCard( name=item["name"], info=item["info"], is_snapshot=item["is_snapshot"], on_load=self._on_load if not item["is_snapshot"] else None, on_delete=self._on_delete, on_recall=self._on_recall if item["is_snapshot"] else None, ) self._list_layout.add_widget(card) def _on_setlists_loaded(self, data: dict): """Render setlist cards.""" self._setlist_list.clear_widgets() setlists = data.get("setlists", []) if not setlists: self._setlist_list.add_widget(Label( text="No setlists. Create one above.", color=Colors.TEXT_MUTED, font_size=Fonts.SMALL, size_hint=(1, None), height=dp(40), halign="center", )) return def _on_load(self, name: str): """Load a session.""" if not self.client: return try: self.client.rest_post( f"/api/v1/sessions/{name}/load", body={}, on_success=lambda d: None, on_error=lambda e: None, ) except Exception: pass def _on_delete(self, name: str): """Delete a session or snapshot.""" if not self.client: return prefix = "snapshots" if self._current_subtab == "snapshots" else "sessions" try: self.client.rest_delete( f"/api/v1/{prefix}/{name}", on_success=lambda d: self._refresh(), on_error=lambda e: None, ) except Exception: pass def _on_recall(self, name: str): """Recall a snapshot.""" if not self.client: return try: self.client.rest_post( f"/api/v1/snapshots/{name}/recall", body={}, on_success=lambda d: None, on_error=lambda e: None, ) except Exception: pass def _on_save(self, instance): """Save current state as a session.""" if not self.client: return name = self._name_input.text.strip() if not name: return notes = self._notes_input.text.strip() try: self.client.rest_post( "/api/v1/sessions/save", body={"name": name, "overwrite": True}, on_success=lambda d: (self._name_input.setter("text")(""), self._refresh()), on_error=lambda e: None, ) except Exception: pass def _on_capture_snapshot(self, instance): """Capture current state as a snapshot.""" if not self.client: return name = self._snap_name_input.text.strip() if not name: return try: self.client.rest_post( "/api/v1/snapshots/capture", body={"name": name, "overwrite": True}, on_success=lambda d: (self._snap_name_input.setter("text")(""), self._refresh()), on_error=lambda e: None, ) except Exception: pass def _on_snapshot_next(self, instance): """Go to next snapshot.""" if not self.client: return try: self.client.rest_post( "/api/v1/snapshots/next", body={}, on_success=lambda d: None, on_error=lambda e: None, ) except Exception: pass def _on_snapshot_prev(self, instance): """Go to previous snapshot.""" if not self.client: return try: self.client.rest_post( "/api/v1/snapshots/previous", body={}, on_success=lambda d: None, on_error=lambda e: None, ) except Exception: pass def _on_create_setlist(self, instance): """Create a new setlist.""" if not self.client: return name = self._setlist_name_input.text.strip() if not name: return try: self.client.rest_post( "/api/v1/setlists/save", body={"name": name, "entries": [], "description": ""}, on_success=lambda d: self._refresh(), on_error=lambda e: None, ) except Exception: pass