diff --git a/src/core/app.rs b/src/core/app.rs index 066937f8..2fdd8b19 100644 --- a/src/core/app.rs +++ b/src/core/app.rs @@ -231,7 +231,8 @@ pub enum ActiveBlock { TrackTable, Discover, Artists, - BasicView, + LyricsView, + CoverArtView, Dialog(DialogContext), AnnouncementPrompt, @@ -248,7 +249,8 @@ pub enum RouteId { AlbumTracks, AlbumList, Artist, - BasicView, + LyricsView, + CoverArtView, Error, Home, RecentlyPlayed, @@ -3081,10 +3083,17 @@ impl App { value: SettingValue::Key(key_to_string(&self.user_config.keys.audio_analysis)), }, SettingItem { - id: "keys.basic_view".to_string(), - name: "Basic View".to_string(), - description: "Open lyrics/basic view".to_string(), - value: SettingValue::Key(key_to_string(&self.user_config.keys.basic_view)), + id: "keys.lyrics_view".to_string(), + name: "Lyrics View".to_string(), + description: "Open lyrics view".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.lyrics_view)), + }, + #[cfg(feature = "cover-art")] + SettingItem { + id: "keys.cover_art_view".to_string(), + name: "Cover Art View".to_string(), + description: "Open full-screen cover art view".to_string(), + value: SettingValue::Key(key_to_string(&self.user_config.keys.cover_art_view)), }, ], SettingsCategory::Theme => { @@ -3490,10 +3499,18 @@ impl App { } } } - "keys.basic_view" => { + "keys.lyrics_view" => { + if let SettingValue::Key(v) = &setting.value { + if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { + self.user_config.keys.lyrics_view = key; + } + } + } + #[cfg(feature = "cover-art")] + "keys.cover_art_view" => { if let SettingValue::Key(v) = &setting.value { if let Ok(key) = crate::core::user_config::parse_key_public(v.clone()) { - self.user_config.keys.basic_view = key; + self.user_config.keys.cover_art_view = key; } } } diff --git a/src/core/user_config.rs b/src/core/user_config.rs index 924bd471..d8b5998f 100644 --- a/src/core/user_config.rs +++ b/src/core/user_config.rs @@ -582,7 +582,9 @@ pub struct KeyBindingsString { copy_song_url: Option, copy_album_url: Option, audio_analysis: Option, - basic_view: Option, + #[serde(alias = "basic_view")] + lyrics_view: Option, + cover_art_view: Option, add_item_to_queue: Option, show_queue: Option, open_settings: Option, @@ -617,7 +619,8 @@ pub struct KeyBindings { pub copy_song_url: Key, pub copy_album_url: Key, pub audio_analysis: Key, - pub basic_view: Key, + pub lyrics_view: Key, + pub cover_art_view: Key, pub add_item_to_queue: Key, pub show_queue: Key, pub open_settings: Key, @@ -760,7 +763,8 @@ impl UserConfig { copy_song_url: Key::Char('c'), copy_album_url: Key::Char('C'), audio_analysis: Key::Char('v'), - basic_view: Key::Char('B'), + lyrics_view: Key::Char('B'), + cover_art_view: Key::Char('G'), add_item_to_queue: Key::Char('z'), show_queue: Key::Char('Q'), // On macOS, use Ctrl+, for settings since Alt+, produces ≤ on most keyboard layouts @@ -875,7 +879,8 @@ impl UserConfig { to_keys!(copy_song_url); to_keys!(copy_album_url); to_keys!(audio_analysis); - to_keys!(basic_view); + to_keys!(lyrics_view); + to_keys!(cover_art_view); to_keys!(add_item_to_queue); to_keys!(show_queue); to_keys!(open_settings); @@ -1222,7 +1227,8 @@ impl UserConfig { copy_song_url: Some(key_to_config_string(self.keys.copy_song_url)), copy_album_url: Some(key_to_config_string(self.keys.copy_album_url)), audio_analysis: Some(key_to_config_string(self.keys.audio_analysis)), - basic_view: Some(key_to_config_string(self.keys.basic_view)), + lyrics_view: Some(key_to_config_string(self.keys.lyrics_view)), + cover_art_view: Some(key_to_config_string(self.keys.cover_art_view)), add_item_to_queue: Some(key_to_config_string(self.keys.add_item_to_queue)), show_queue: Some(key_to_config_string(self.keys.show_queue)), open_settings: Some(key_to_config_string(self.keys.open_settings)), diff --git a/src/main.rs b/src/main.rs index 2c697ceb..36b55ed7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2522,8 +2522,12 @@ async fn start_ui( ActiveBlock::Analysis => { ui::audio_analysis::draw(f, &app); } - ActiveBlock::BasicView => { - ui::draw_basic_view(f, &app); + ActiveBlock::LyricsView => { + ui::draw_lyrics_view(f, &app); + } + #[cfg(feature = "cover-art")] + ActiveBlock::CoverArtView => { + ui::draw_cover_art_view(f, &app); } ActiveBlock::AnnouncementPrompt => { @@ -2837,7 +2841,9 @@ async fn start_ui( ActiveBlock::Error => ui::draw_error_screen(f, &app), ActiveBlock::SelectDevice => ui::draw_device_list(f, &app), ActiveBlock::Analysis => ui::audio_analysis::draw(f, &app), - ActiveBlock::BasicView => ui::draw_basic_view(f, &app), + ActiveBlock::LyricsView => ui::draw_lyrics_view(f, &app), + #[cfg(feature = "cover-art")] + ActiveBlock::CoverArtView => ui::draw_cover_art_view(f, &app), ActiveBlock::AnnouncementPrompt => ui::draw_announcement_prompt(f, &app), ActiveBlock::ExitPrompt => ui::draw_exit_prompt(f, &app), diff --git a/src/tui/cover_art.rs b/src/tui/cover_art.rs index 55d55704..01f8f0e4 100644 --- a/src/tui/cover_art.rs +++ b/src/tui/cover_art.rs @@ -11,6 +11,9 @@ use std::sync::Mutex; pub struct CoverArt { pub state: Mutex>, + /// Separate protocol state for fullscreen cover art view, avoiding conflicts + /// when the same image is rendered in both the playbar and fullscreen in one frame. + pub fullscreen_state: Mutex>, picker: Picker, } @@ -36,6 +39,7 @@ impl CoverArt { Self { picker, state: Mutex::new(None), + fullscreen_state: Mutex::new(None), } } @@ -81,11 +85,18 @@ impl CoverArt { Err(e) => return Err(anyhow!(e)), }; - let image_protocol = self - .picker - .new_resize_protocol(image::load_from_memory(&file).map_err(|e| anyhow!(e))?); + let img = image::load_from_memory(&file).map_err(|e| anyhow!(e))?; + + // Create two separate protocol instances so the playbar and fullscreen + // views can render independently without conflicting. + let image_protocol = self.picker.new_resize_protocol(img.clone()); + let fullscreen_protocol = self.picker.new_resize_protocol(img); self.set_state(CoverArtState::new(image.url.clone(), image_protocol)); + { + let mut lock = self.fullscreen_state.lock().unwrap(); + *lock = Some(CoverArtState::new(image.url.clone(), fullscreen_protocol)); + } info!("got new cover art: {}", image.url); } else { debug!("skipping image refresh: cover art already downloaded"); @@ -99,7 +110,15 @@ impl CoverArt { } pub fn render(&self, f: &mut Frame, area: Rect) { - let mut lock = self.state.lock().unwrap(); + Self::render_state(&self.state, f, area); + } + + pub fn render_fullscreen(&self, f: &mut Frame, area: Rect) { + Self::render_state(&self.fullscreen_state, f, area); + } + + fn render_state(state: &Mutex>, f: &mut Frame, area: Rect) { + let mut lock = state.lock().unwrap(); if let Some(sp) = lock.as_mut() { f.render_stateful_widget( StatefulImage::new().resize(Resize::Fit(None)), diff --git a/src/tui/handlers/basic_view.rs b/src/tui/handlers/basic_view.rs deleted file mode 100644 index 1c1deecc..00000000 --- a/src/tui/handlers/basic_view.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::core::app::App; -use crate::tui::event::Key; - -pub fn handler(key: Key, app: &mut App) { - if let Key::Char('s') = key { - super::playbar::toggle_like_currently_playing_item(app); - } -} diff --git a/src/tui/handlers/common_key_events.rs b/src/tui/handlers/common_key_events.rs index 725a9809..40c50a8b 100644 --- a/src/tui/handlers/common_key_events.rs +++ b/src/tui/handlers/common_key_events.rs @@ -137,7 +137,8 @@ pub fn handle_right_event(app: &mut App) { RouteId::SelectedDevice => {} RouteId::Error => {} RouteId::Analysis => {} - RouteId::BasicView => {} + RouteId::LyricsView => {} + RouteId::CoverArtView => {} RouteId::Dialog => {} RouteId::AnnouncementPrompt => {} RouteId::ExitPrompt => {} diff --git a/src/tui/handlers/cover_art_view.rs b/src/tui/handlers/cover_art_view.rs new file mode 100644 index 00000000..573c2060 --- /dev/null +++ b/src/tui/handlers/cover_art_view.rs @@ -0,0 +1,14 @@ +use crate::core::app::App; +use crate::tui::event::Key; + +pub fn handler(key: Key, app: &mut App) { + match key { + Key::Char('s') => { + super::playbar::toggle_like_currently_playing_item(app); + } + k if k == app.user_config.keys.back => { + app.pop_navigation_stack(); + } + _ => {} + } +} diff --git a/src/tui/handlers/lyrics_view.rs b/src/tui/handlers/lyrics_view.rs new file mode 100644 index 00000000..573c2060 --- /dev/null +++ b/src/tui/handlers/lyrics_view.rs @@ -0,0 +1,14 @@ +use crate::core::app::App; +use crate::tui::event::Key; + +pub fn handler(key: Key, app: &mut App) { + match key { + Key::Char('s') => { + super::playbar::toggle_like_currently_playing_item(app); + } + k if k == app.user_config.keys.back => { + app.pop_navigation_stack(); + } + _ => {} + } +} diff --git a/src/tui/handlers/mod.rs b/src/tui/handlers/mod.rs index cdcd5c6f..3201a5d1 100644 --- a/src/tui/handlers/mod.rs +++ b/src/tui/handlers/mod.rs @@ -4,8 +4,9 @@ mod analysis; mod announcement_prompt; mod artist; mod artists; -mod basic_view; mod common_key_events; +#[cfg(feature = "cover-art")] +mod cover_art_view; mod dialog; mod discover; mod empty; @@ -15,6 +16,7 @@ mod help_menu; mod home; mod input; mod library; +mod lyrics_view; mod mouse; mod party; mod playbar; @@ -164,8 +166,12 @@ pub fn handle_app(key: Key, app: &mut App) { _ if key == app.user_config.keys.audio_analysis => { app.get_audio_analysis(); } - _ if key == app.user_config.keys.basic_view => { - app.push_navigation_stack(RouteId::BasicView, ActiveBlock::BasicView); + _ if key == app.user_config.keys.lyrics_view => { + app.push_navigation_stack(RouteId::LyricsView, ActiveBlock::LyricsView); + } + #[cfg(feature = "cover-art")] + _ if key == app.user_config.keys.cover_art_view => { + app.push_navigation_stack(RouteId::CoverArtView, ActiveBlock::CoverArtView); } _ if key == app.user_config.keys.listening_party => { app.push_navigation_stack(RouteId::Party, ActiveBlock::Party); @@ -300,8 +306,12 @@ fn handle_block_events(key: Key, app: &mut App) { ActiveBlock::PlayBar => { playbar::handler(key, app); } - ActiveBlock::BasicView => { - basic_view::handler(key, app); + ActiveBlock::LyricsView => { + lyrics_view::handler(key, app); + } + ActiveBlock::CoverArtView => { + #[cfg(feature = "cover-art")] + cover_art_view::handler(key, app); } ActiveBlock::Dialog(_) => { dialog::handler(key, app); @@ -355,6 +365,9 @@ fn handle_escape(app: &mut App) { ActiveBlock::Party => { app.pop_navigation_stack(); } + ActiveBlock::LyricsView | ActiveBlock::CoverArtView => { + app.pop_navigation_stack(); + } // These are global views that have no active/inactive distinction so do nothing ActiveBlock::SelectDevice | ActiveBlock::Analysis => {} diff --git a/src/tui/handlers/mouse.rs b/src/tui/handlers/mouse.rs index 460218d7..a8083810 100644 --- a/src/tui/handlers/mouse.rs +++ b/src/tui/handlers/mouse.rs @@ -5,7 +5,9 @@ use crate::core::app::{ use crate::core::layout::{library_constraints, playbar_constraint, sidebar_constraints}; use crate::tui::event::Key; use crate::tui::ui::player::playbar_control_at; -use crate::tui::ui::util::{get_main_layout_margin, BASIC_VIEW_HEIGHT, SMALL_TERMINAL_WIDTH}; +use crate::tui::ui::util::{ + get_main_layout_margin, FULLSCREEN_VIEW_PLAYBAR_HEIGHT, SMALL_TERMINAL_WIDTH, +}; use crossterm::event::{MouseButton, MouseEvent, MouseEventKind}; use ratatui::layout::{Constraint, Layout, Rect}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -26,8 +28,13 @@ pub fn handler(mouse: MouseEvent, app: &mut App) { return; } - if current_route_id == RouteId::BasicView { - handle_basic_view_mouse(mouse, app); + if current_route_id == RouteId::LyricsView { + handle_fullscreen_view_mouse(ActiveBlock::LyricsView, mouse, app); + return; + } + + if current_route_id == RouteId::CoverArtView { + handle_fullscreen_view_mouse(ActiveBlock::CoverArtView, mouse, app); return; } @@ -162,13 +169,13 @@ fn handle_settings_mouse(mouse: MouseEvent, app: &mut App) { } } -fn handle_basic_view_mouse(mouse: MouseEvent, app: &mut App) { - let Some(playbar_area) = basic_view_playbar_area(app) else { +fn handle_fullscreen_view_mouse(focus_block: ActiveBlock, mouse: MouseEvent, app: &mut App) { + let Some(playbar_area) = fullscreen_view_playbar_area(app) else { return; }; if rect_contains(playbar_area, mouse.column, mouse.row) { - handle_playbar_mouse(mouse, playbar_area, ActiveBlock::BasicView, app); + handle_playbar_mouse(mouse, playbar_area, focus_block, app); } } @@ -338,7 +345,8 @@ fn is_main_layout_mouse_interactive(active_block: ActiveBlock) -> bool { | ActiveBlock::Error | ActiveBlock::SelectDevice | ActiveBlock::Analysis - | ActiveBlock::BasicView + | ActiveBlock::LyricsView + | ActiveBlock::CoverArtView | ActiveBlock::AnnouncementPrompt | ActiveBlock::ExitPrompt | ActiveBlock::Settings @@ -695,7 +703,7 @@ fn settings_unsaved_prompt_areas(app: &App) -> Option Option { +fn fullscreen_view_playbar_area(app: &App) -> Option { if app.size.width == 0 || app.size.height == 0 { return None; } @@ -703,7 +711,7 @@ fn basic_view_playbar_area(app: &App) -> Option { let root = Rect::new(0, 0, app.size.width, app.size.height); let [_lyrics_area, playbar_area] = root.layout(&Layout::vertical([ Constraint::Min(0), - Constraint::Length(BASIC_VIEW_HEIGHT), + Constraint::Length(FULLSCREEN_VIEW_PLAYBAR_HEIGHT), ])); Some(playbar_area) @@ -1064,16 +1072,16 @@ mod tests { } #[test] - fn click_basic_view_playbar_control_triggers_action() { + fn click_lyrics_view_playbar_control_triggers_action() { let mut app = App::default(); app.size = Size { width: 160, height: 50, }; - app.push_navigation_stack(RouteId::BasicView, ActiveBlock::BasicView); + app.push_navigation_stack(RouteId::LyricsView, ActiveBlock::LyricsView); with_playbar_context(&mut app); - let playbar_area = basic_view_playbar_area(&app).expect("basic view playbar area"); + let playbar_area = fullscreen_view_playbar_area(&app).expect("lyrics view playbar area"); let (x, y) = find_playbar_control_click(&app, playbar_area, PlaybarControl::PlayPause); assert!(!app.is_loading); @@ -1084,9 +1092,9 @@ mod tests { assert!(app.is_loading); let route = app.get_current_route(); - assert_eq!(route.id, RouteId::BasicView); - assert_eq!(route.active_block, ActiveBlock::BasicView); - assert_eq!(route.hovered_block, ActiveBlock::BasicView); + assert_eq!(route.id, RouteId::LyricsView); + assert_eq!(route.active_block, ActiveBlock::LyricsView); + assert_eq!(route.hovered_block, ActiveBlock::LyricsView); } #[test] diff --git a/src/tui/ui/help.rs b/src/tui/ui/help.rs index d1fcc7e9..d14c3ba9 100644 --- a/src/tui/ui/help.rs +++ b/src/tui/ui/help.rs @@ -150,7 +150,13 @@ pub fn get_help_docs(app: &App) -> Vec> { ], vec![ String::from("Go to lyrics view"), - key_bindings.basic_view.to_string(), + key_bindings.lyrics_view.to_string(), + String::from("General"), + ], + #[cfg(feature = "cover-art")] + vec![ + String::from("Go to cover art view"), + key_bindings.cover_art_view.to_string(), String::from("General"), ], vec![ diff --git a/src/tui/ui/mod.rs b/src/tui/ui/mod.rs index ac16d0b5..c60c74a8 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/ui/mod.rs @@ -22,7 +22,9 @@ pub use self::artist::draw_artist_albums; pub use self::discover::draw_discover; pub use self::home::draw_home; pub use self::library::draw_user_block; -pub use self::player::{draw_basic_view, draw_device_list, draw_playbar}; +#[cfg(feature = "cover-art")] +pub use self::player::draw_cover_art_view; +pub use self::player::{draw_device_list, draw_lyrics_view, draw_playbar}; pub use self::popups::{ draw_announcement_prompt, draw_dialog, draw_error_screen, draw_exit_prompt, draw_help_menu, draw_party, draw_queue, draw_sort_menu, @@ -124,17 +126,17 @@ pub fn draw_routes(f: &mut Frame<'_>, app: &App, layout_chunk: Rect) { RouteId::Recommendations => { draw_recommendations_table(f, app, content_area); } - RouteId::Error => {} // This is handled as a "full screen" route in main.rs - RouteId::SelectedDevice => {} // This is handled as a "full screen" route in main.rs - RouteId::Analysis => {} // This is handled as a "full screen" route in main.rs - RouteId::BasicView => {} // This is handled as a "full screen" route in main.rs - RouteId::Dialog => {} // This is handled in the draw_dialog function in mod.rs - - RouteId::AnnouncementPrompt => {} // This is handled as a "full screen" route in main.rs - RouteId::ExitPrompt => {} // This is handled as a "full screen" route in main.rs - RouteId::Settings => {} // This is handled as a "full screen" route in main.rs - RouteId::HelpMenu => {} // This is handled as a "full screen" route in main.rs - RouteId::Queue => {} // This is handled as a "full screen" route in main.rs - RouteId::Party => {} // This is handled as a popup overlay in main.rs + RouteId::Error + | RouteId::SelectedDevice + | RouteId::Analysis + | RouteId::LyricsView + | RouteId::CoverArtView + | RouteId::AnnouncementPrompt + | RouteId::ExitPrompt + | RouteId::Settings + | RouteId::HelpMenu + | RouteId::Queue + | RouteId::Party => {} // These are drawn outside the main routed content area. + RouteId::Dialog => {} // This is handled in draw_dialog. }; } diff --git a/src/tui/ui/player.rs b/src/tui/ui/player.rs index 55b2936d..dc43bc41 100644 --- a/src/tui/ui/player.rs +++ b/src/tui/ui/player.rs @@ -15,7 +15,7 @@ use rspotify::prelude::Id; use super::util::{ create_artist_string, display_track_progress, get_color, get_track_progress_percentage, - BASIC_VIEW_HEIGHT, + FULLSCREEN_VIEW_PLAYBAR_HEIGHT, }; const PLAYBAR_CONTROLS: [PlaybarControl; 8] = [ @@ -135,12 +135,12 @@ fn playbar_layout_areas(app: &App, layout_chunk: Rect) -> PlaybarLayoutAreas { let (artist_area, controls_area, progress_area) = split_playbar_rows(other); - return PlaybarLayoutAreas { + PlaybarLayoutAreas { artist_area, controls_area, progress_area, cover_art, - }; + } } #[cfg(not(feature = "cover-art"))] @@ -246,12 +246,12 @@ fn draw_playbar_controls(f: &mut Frame<'_>, app: &App, playbar_area: Rect) { } } -pub fn draw_basic_view(f: &mut Frame<'_>, app: &App) { +pub fn draw_lyrics_view(f: &mut Frame<'_>, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Min(0), // Lyrics Area taking all available space above - Constraint::Length(BASIC_VIEW_HEIGHT), // Playbar at the bottom + Constraint::Length(FULLSCREEN_VIEW_PLAYBAR_HEIGHT), // Playbar at the bottom ]) .split(f.area()); @@ -259,6 +259,146 @@ pub fn draw_basic_view(f: &mut Frame<'_>, app: &App) { draw_playbar(f, app, chunks[1]); } +#[cfg(feature = "cover-art")] +pub fn draw_cover_art_view(f: &mut Frame<'_>, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Min(0), + Constraint::Length(FULLSCREEN_VIEW_PLAYBAR_HEIGHT), + ]) + .split(f.area()); + + draw_cover_art_content(f, app, chunks[0]); + draw_playbar(f, app, chunks[1]); +} + +#[cfg(feature = "cover-art")] +fn draw_cover_art_content(f: &mut Frame<'_>, app: &App, area: Rect) { + use ratatui::widgets::Clear; + + // Clear the area to remove any lingering terminal image protocol artifacts + f.render_widget(Clear, area); + + // Extract track info for display below the cover art + let (track_name, artist_str) = extract_track_info(app); + + if !app.cover_art.available() { + let p = Paragraph::new("No cover art available") + .style(Style::default().fg(Color::Rgb(100, 100, 100))) + .alignment(Alignment::Center); + + let vertical_center = area.y + area.height / 2; + let center_area = Rect { + x: area.x, + y: vertical_center, + width: area.width, + height: 1, + }; + f.render_widget(p, center_area); + return; + } + + // Reserve 3 rows at the bottom for song info (1 blank + 1 title + 1 artist) + let info_height = 3_u16; + let img_area_height = area.height.saturating_sub(info_height); + + // Calculate image dimensions for a square album cover + // Terminal characters are taller than wide, so we use a ratio to get a square. + let char_aspect_ratio = 1.9_f32; + + let max_height = img_area_height.saturating_sub(2); + let max_width = area.width.saturating_sub(2); + + let img_width_from_height = ((max_height as f32) * char_aspect_ratio).ceil() as u16; + + let (img_width, img_height) = if img_width_from_height > max_width { + let h = ((max_width as f32) / char_aspect_ratio).floor() as u16; + (max_width, h) + } else { + (img_width_from_height, max_height) + }; + + // Center the image horizontally, vertically within the image area + let x = area.x + (area.width.saturating_sub(img_width)) / 2; + let y = area.y + (img_area_height.saturating_sub(img_height)) / 2; + + let centered_area = Rect { + x, + y, + width: img_width, + height: img_height, + }; + + app.cover_art.render_fullscreen(f, centered_area); + + // Draw song info below the cover art + if let Some(name) = track_name { + let title_y = y + img_height + 1; + if title_y < area.y + area.height { + let title = Paragraph::new(name) + .style( + Style::default() + .fg(app.user_config.theme.selected) + .add_modifier(Modifier::BOLD), + ) + .alignment(Alignment::Center); + f.render_widget( + title, + Rect { + x: area.x, + y: title_y, + width: area.width, + height: 1, + }, + ); + } + + if let Some(artists) = artist_str { + let artist_y = title_y + 1; + if artist_y < area.y + area.height { + let artist = Paragraph::new(artists) + .style(Style::default().fg(app.user_config.theme.playbar_text)) + .alignment(Alignment::Center); + f.render_widget( + artist, + Rect { + x: area.x, + y: artist_y, + width: area.width, + height: 1, + }, + ); + } + } + } +} + +#[cfg(feature = "cover-art")] +fn extract_track_info(app: &App) -> (Option, Option) { + use rspotify::model::PlayableItem; + + // Prefer native track info (more responsive after skipping tracks) + if let Some(ref native_info) = app.native_track_info { + return ( + Some(native_info.name.clone()), + Some(native_info.artists_display.clone()), + ); + } + + if let Some(ctx) = &app.current_playback_context { + if let Some(track_item) = &ctx.item { + let (name, artists) = match track_item { + PlayableItem::Track(track) => (track.name.clone(), create_artist_string(&track.artists)), + PlayableItem::Episode(episode) => (episode.name.clone(), episode.show.name.clone()), + }; + return (Some(name), Some(artists)); + } + } + + (None, None) +} + fn draw_lyrics(f: &mut Frame<'_>, app: &App, area: Rect) { use crate::core::app::LyricsStatus; diff --git a/src/tui/ui/util.rs b/src/tui/ui/util.rs index 2d5dfb9c..28d5805e 100644 --- a/src/tui/ui/util.rs +++ b/src/tui/ui/util.rs @@ -10,7 +10,7 @@ use ratatui::{ use rspotify::model::artist::SimplifiedArtist; use std::time::Duration; -pub const BASIC_VIEW_HEIGHT: u16 = 6; +pub const FULLSCREEN_VIEW_PLAYBAR_HEIGHT: u16 = 6; pub const SMALL_TERMINAL_WIDTH: u16 = 150; pub const SMALL_TERMINAL_HEIGHT: u16 = 45;