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)
This commit is contained in:
x3
2026-09-06 08:36:41 +00:00
parent 9f84722233
commit 68f6e0ef84
2 changed files with 139 additions and 50 deletions
+3
View File
@@ -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
+135 -49
View File
@@ -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:
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: