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:
+136
-50
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user