From 68f6e0ef8484fb73cbd9879f75ebd2b8b3e09cd4 Mon Sep 17 00:00:00 2001 From: x3 Date: Sun, 6 Sep 2026 08:36:41 +0000 Subject: [PATCH] fix: Tab-Navigation, Status-Updates, Hotkeys ueberarbeitet - Tab wechselt zwischen Nav und Content - Player-Callback triggert UI-Refresh - Nach Aktionen wird refresh() aufgerufen - Key-Handling komplett neu (fokus-basiert) --- README.md | 3 + spotify_cli/ui.py | 186 +++++++++++++++++++++++++++++++++------------- 2 files changed, 139 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 80d7c1d..3bbc677 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ spotify-cli | Taste | Aktion | |-------|--------| +| `Tab` | Zwischen Nav und Content wechseln | | `j`/`k` oder `↑`/`↓` | Navigieren | | `Enter` | Auswaehlen / Abspielen | | `Space` | Play / Pause | @@ -65,6 +66,8 @@ spotify-cli | `L` | Library | | `q` | Beenden | +**Fokus:** Mit `Tab` wechselst du zwischen der Navigation (links) und dem Content-Bereich (rechts). Die Statusleiste zeigt an, welcher Bereich aktiv ist. + ### Views - **Home**: Kuerzlich gespielte Tracks diff --git a/spotify_cli/ui.py b/spotify_cli/ui.py index c564360..77300ed 100644 --- a/spotify_cli/ui.py +++ b/spotify_cli/ui.py @@ -5,6 +5,10 @@ from .client import SpotifyClient, TokenExpired from .player import Player +FOCUS_NAV = "nav" +FOCUS_CONTENT = "content" + + class View: HOME = "home" PLAYLISTS = "playlists" @@ -62,7 +66,8 @@ class UI: self.artist_albums = [] self.artist_top_tracks = [] self.context_uri = None - self.last_key_time = 0 + self.focus = FOCUS_CONTENT + self.force_refresh = False curses.curs_set(0) curses.use_default_colors() @@ -75,10 +80,15 @@ class UI: curses.init_pair(6, curses.COLOR_MAGENTA, -1) curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLUE) curses.init_pair(8, curses.COLOR_GREEN, curses.COLOR_BLUE) - self.stdscr.timeout(500) + curses.init_pair(9, curses.COLOR_BLACK, curses.COLOR_CYAN) + self.stdscr.timeout(250) + self.player.register_callback(self._on_player_update) self.player.start_polling(interval=2) + def _on_player_update(self, playback, track): + self.force_refresh = True + def stop(self): self.running = False self.player.stop_polling() @@ -130,7 +140,10 @@ class UI: def _draw_nav(self, y, x, w, h): try: - self.stdscr.addnstr(y, x, " NAV".ljust(w), w-1, curses.color_pair(7) | curses.A_BOLD) + title_attr = curses.color_pair(7) | curses.A_BOLD + if self.focus == FOCUS_NAV: + title_attr = curses.color_pair(9) | curses.A_BOLD + self.stdscr.addnstr(y, x, " NAV".ljust(w), w-1, title_attr) except curses.error: pass @@ -138,10 +151,14 @@ class UI: if i + 1 >= h: break try: - prefix = "> " if i == self.nav_index else " " - attr = curses.color_pair(8) if i == self.nav_index else curses.color_pair(1) - if i == self.nav_index: - attr |= curses.A_BOLD + is_selected = i == self.nav_index and self.focus == FOCUS_NAV + prefix = "> " if is_selected else " " + if self.focus == FOCUS_NAV and i == self.nav_index: + attr = curses.color_pair(9) | curses.A_BOLD + elif i == self.nav_index: + attr = curses.color_pair(8) | curses.A_BOLD + else: + attr = curses.color_pair(1) line = f"{prefix}{label}" self.stdscr.addnstr(y + 1 + i, x, line.ljust(w), w-1, attr) except curses.error: @@ -149,13 +166,16 @@ class UI: def _draw_content(self, y, x, w, h): try: - self.stdscr.addnstr(y, x, "".ljust(w), w-1, curses.color_pair(7) | curses.A_BOLD) + title_attr = curses.color_pair(7) | curses.A_BOLD + if self.focus == FOCUS_CONTENT: + title_attr = curses.color_pair(9) | curses.A_BOLD + self.stdscr.addnstr(y, x, "".ljust(w), w-1, title_attr) except curses.error: pass header = self._get_view_header() try: - self.stdscr.addnstr(y, x + 1, header[:w-2], w-2, curses.color_pair(7) | curses.A_BOLD) + self.stdscr.addnstr(y, x + 1, header[:w-2], w-2, title_attr) except curses.error: pass @@ -175,7 +195,7 @@ class UI: if row >= y + h: break item = self.content_items[i] - is_selected = i == self.selected_index + is_selected = i == self.selected_index and self.focus == FOCUS_CONTENT try: prefix = "> " if is_selected else " " if isinstance(item, dict): @@ -183,9 +203,10 @@ class UI: else: text = str(item) line = f"{prefix}{text}" - attr = curses.color_pair(2) if is_selected else curses.color_pair(1) if is_selected: - attr |= curses.A_BOLD + attr = curses.color_pair(2) | curses.A_BOLD + else: + attr = curses.color_pair(1) self.stdscr.addnstr(row, x + 1, line[:w-2], w-2, attr) except curses.error: pass @@ -231,9 +252,12 @@ class UI: pass def _draw_bar(self, y, x, w, h): - keys = "j/k:Move Enter:Select Space:Play/Pause n/p:Next/Prev +/-:Vol q:Quit" if self.search_mode: keys = f"Search: {self.search_query}_ Enter:Search Esc:Cancel" + elif self.focus == FOCUS_NAV: + keys = "j/k:Move Enter:Select Tab:Content q:Quit" + else: + keys = "j/k:Move Enter:Select Space:Play n/p:Next +/-:Vol Tab:Nav q:Quit" try: self.stdscr.addnstr(y, x, keys.ljust(w), w-1, curses.color_pair(7)) except curses.error: @@ -328,6 +352,10 @@ class UI: elif kind == "search_playlist": name = item.get("name", "Unknown") return f"[Playlist] {name}" + elif kind == "section": + return item.get("label", "") + elif kind == "nav": + return item.get("label", "") return str(item) # --- Data Loading --- @@ -715,6 +743,8 @@ class UI: def handle_select(self): if not self.content_items: return + if self.selected_index >= len(self.content_items): + return item = self.content_items[self.selected_index] kind = item.get("type", "") @@ -728,18 +758,19 @@ class UI: self.current_view = View.ARTIST_DETAIL self.load_artist_detail(item["id"]) elif kind == "track" or kind == "search_track": - ctx = item.get("context_uri") - if self.context_uri: - ctx = self.context_uri try: self.player.play_tracks([item["uri"]]) - self.show_message(f"Spiele: {item['name']}") + self.show_message(f"Spiele: {item.get('name', '')}") + time.sleep(0.5) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") elif kind == "device": try: self.player.transfer_to(item["id"]) - self.show_message(f"Übertragen zu: {item['name']}") + self.show_message(f"Uebertragen zu: {item['name']}") + time.sleep(0.5) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") elif kind == "nav": @@ -763,7 +794,10 @@ class UI: self.navigate_to(View.HOME) while self.running: - self.draw() + if self.force_refresh: + self.force_refresh = False + self.draw() + key = self.stdscr.getch() if key == -1: @@ -771,9 +805,11 @@ class UI: if self.search_mode: self._handle_search_input(key) + self.draw() continue self._handle_key(key) + self.draw() def _handle_search_input(self, key): if key == 27: @@ -791,13 +827,74 @@ class UI: self.search_query += chr(key) def _handle_key(self, key): - nav_count = len(self.NAV_ITEMS) content_count = len(self.content_items) + nav_count = len(self.NAV_ITEMS) if key == ord("q"): self.running = False - elif key == ord("j") or key == curses.KEY_DOWN: + elif key == 9: + if self.focus == FOCUS_NAV: + self.focus = FOCUS_CONTENT + self.selected_index = min(self.selected_index, max(0, content_count - 1)) + else: + self.focus = FOCUS_NAV + self.nav_index = min(self.nav_index, nav_count - 1) + + elif self.focus == FOCUS_NAV: + self._handle_nav_key(key, nav_count) + else: + self._handle_content_key(key, content_count) + + def _handle_nav_key(self, key, nav_count): + if key == ord("j") or key == curses.KEY_DOWN: + if self.nav_index < nav_count - 1: + self.nav_index += 1 + + elif key == ord("k") or key == curses.KEY_UP: + if self.nav_index > 0: + self.nav_index -= 1 + + elif key == 10 or key == 13: + self.navigate_to(self.NAV_ITEMS[self.nav_index][0]) + + elif key == ord("h") or key == 27: + pass + + elif key == ord("/"): + self.search_mode = True + self.search_query = "" + + elif key == ord("1"): + self.nav_index = 0 + self.navigate_to(self.NAV_ITEMS[0][0]) + elif key == ord("2"): + self.nav_index = 1 + self.navigate_to(self.NAV_ITEMS[1][0]) + elif key == ord("3"): + self.nav_index = 2 + self.navigate_to(self.NAV_ITEMS[2][0]) + elif key == ord("4"): + self.nav_index = 3 + self.navigate_to(self.NAV_ITEMS[3][0]) + elif key == ord("5"): + self.nav_index = 4 + self.navigate_to(self.NAV_ITEMS[4][0]) + elif key == ord("6"): + self.nav_index = 5 + self.navigate_to(self.NAV_ITEMS[5][0]) + elif key == ord("7"): + self.nav_index = 6 + self.navigate_to(self.NAV_ITEMS[6][0]) + elif key == ord("8"): + self.nav_index = 7 + self.navigate_to(self.NAV_ITEMS[7][0]) + + elif key == ord("L"): + self.navigate_to(View.LIBRARY) + + def _handle_content_key(self, key, content_count): + if key == ord("j") or key == curses.KEY_DOWN: if self.selected_index < content_count - 1: self.selected_index += 1 self._adjust_scroll() @@ -830,6 +927,8 @@ class UI: try: self.player.toggle_play() self.show_message("Toggled play/pause") + time.sleep(0.3) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") @@ -837,6 +936,8 @@ class UI: try: self.player.next() self.show_message("Next track") + time.sleep(0.5) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") @@ -844,18 +945,26 @@ class UI: try: self.player.previous() self.show_message("Previous track") + time.sleep(0.5) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") elif key == ord("+") or key == ord("="): try: self.player.volume_up() + self.show_message(f"Volume: {self.player.get_volume()}%") + time.sleep(0.3) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") elif key == ord("-"): try: self.player.volume_down() + self.show_message(f"Volume: {self.player.get_volume()}%") + time.sleep(0.3) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") @@ -863,6 +972,8 @@ class UI: try: self.player.toggle_repeat() self.show_message("Toggle repeat") + time.sleep(0.3) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") @@ -870,6 +981,8 @@ class UI: try: self.player.toggle_shuffle() self.show_message("Toggle shuffle") + time.sleep(0.3) + self.player.refresh() except Exception as e: self.show_message(f"Fehler: {e}") @@ -877,44 +990,17 @@ class UI: self.search_mode = True self.search_query = "" - elif key == ord("a"): - if self.current_view == View.PLAYLISTS and self.content_items: - self._add_to_playlist_prompt() - elif key == ord("h") or key == 27: self.handle_back() - elif key == ord("1"): - self.nav_index = 0 - self.navigate_to(self.NAV_ITEMS[0][0]) - elif key == ord("2"): - self.nav_index = 1 - self.navigate_to(self.NAV_ITEMS[1][0]) - elif key == ord("3"): - self.nav_index = 2 - self.navigate_to(self.NAV_ITEMS[2][0]) - elif key == ord("4"): - self.nav_index = 3 - self.navigate_to(self.NAV_ITEMS[3][0]) - elif key == ord("5"): - self.nav_index = 4 - self.navigate_to(self.NAV_ITEMS[4][0]) - elif key == ord("6"): - self.nav_index = 5 - self.navigate_to(self.NAV_ITEMS[5][0]) - elif key == ord("7"): - self.nav_index = 6 - self.navigate_to(self.NAV_ITEMS[6][0]) - elif key == ord("8"): - self.nav_index = 7 - self.navigate_to(self.NAV_ITEMS[7][0]) - elif key == ord("L"): self.navigate_to(View.LIBRARY) def _adjust_scroll(self): h, w = self.stdscr.getmaxyx() visible = h - 7 + if visible < 1: + visible = 1 if self.selected_index < self.content_scroll: self.content_scroll = self.selected_index elif self.selected_index >= self.content_scroll + visible: