//! Playlists page with dual-panel browser use ratatui::{ layout::{Constraint, Layout, Rect}, style::{Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, Frame, }; use crate::app::state::AppState; use crate::ui::theme::ThemeColors; /// Render the playlists page pub fn render(frame: &mut Frame, area: Rect, state: &mut AppState) { let colors = *state.settings_state.theme_colors(); // Split into two panes: [Playlists] [Songs] let chunks = Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]).split(area); render_playlists(frame, chunks[0], state, &colors); render_songs(frame, chunks[1], state, &colors); } /// Render the playlists list fn render_playlists(frame: &mut Frame, area: Rect, state: &mut AppState, colors: &ThemeColors) { let playlists = &state.playlists; let focused = playlists.focus == 0; let border_style = if focused { Style::default().fg(colors.border_focused) } else { Style::default().fg(colors.border_unfocused) }; let block = Block::default() .borders(Borders::ALL) .title(format!(" Playlists ({}) ", playlists.playlists.len())) .border_style(border_style); if playlists.playlists.is_empty() { let hint = Paragraph::new("No playlists found") .style(Style::default().fg(colors.muted)) .block(block); frame.render_widget(hint, area); return; } let items: Vec = playlists .playlists .iter() .enumerate() .map(|(i, playlist)| { let is_selected = playlists.selected_playlist == Some(i); let count = playlist.song_count.unwrap_or(0); let duration = playlist.duration.map(|d| { let mins = d / 60; let secs = d % 60; format!("{}:{:02}", mins, secs) }); let style = if is_selected { Style::default() .fg(colors.primary) .add_modifier(Modifier::BOLD) } else { Style::default().fg(colors.album) }; let mut spans = vec![ Span::styled(&playlist.name, style), Span::styled( format!(" ({} songs)", count), Style::default().fg(colors.muted), ), ]; if let Some(dur) = duration { spans.push(Span::styled( format!(" [{}]", dur), Style::default().fg(colors.muted), )); } ListItem::new(Line::from(spans)) }) .collect(); let mut list = List::new(items).block(block); if focused { list = list .highlight_style( Style::default() .bg(colors.highlight_bg) .add_modifier(Modifier::BOLD), ) .highlight_symbol("▸ "); } let mut list_state = ListState::default(); if focused { list_state.select(playlists.selected_playlist); } frame.render_stateful_widget(list, area, &mut list_state); state.playlists.playlist_scroll_offset = list_state.offset(); } /// Render the songs in selected playlist fn render_songs(frame: &mut Frame, area: Rect, state: &mut AppState, colors: &ThemeColors) { let playlists = &state.playlists; let focused = playlists.focus == 1; let border_style = if focused { Style::default().fg(colors.border_focused) } else { Style::default().fg(colors.border_unfocused) }; let title = if !playlists.songs.is_empty() { format!(" Songs ({}) ", playlists.songs.len()) } else { " Songs ".to_string() }; let block = Block::default() .borders(Borders::ALL) .title(title) .border_style(border_style); if playlists.songs.is_empty() { let hint = Paragraph::new("Select a playlist to view songs") .style(Style::default().fg(colors.muted)) .block(block); frame.render_widget(hint, area); return; } let items: Vec = playlists .songs .iter() .enumerate() .map(|(i, song)| { let is_selected = playlists.selected_song == Some(i); let is_playing = state .current_song() .map(|s| s.id == song.id) .unwrap_or(false); let indicator = if is_playing { "▶ " } else { " " }; let artist = song.artist.clone().unwrap_or_default(); let duration = song.format_duration(); // Colors based on state let (title_color, artist_color, time_color) = if is_selected { ( colors.highlight_fg, colors.highlight_fg, colors.highlight_fg, ) } else if is_playing { (colors.playing, colors.muted, colors.muted) } else { (colors.song, colors.muted, colors.muted) }; let line = Line::from(vec![ Span::styled(indicator, Style::default().fg(colors.playing)), Span::styled(&song.title, Style::default().fg(title_color)), if !artist.is_empty() { Span::styled(format!(" - {}", artist), Style::default().fg(artist_color)) } else { Span::raw("") }, Span::styled(format!(" [{}]", duration), Style::default().fg(time_color)), ]); ListItem::new(line) }) .collect(); let mut list = List::new(items).block(block); if focused { list = list.highlight_style( Style::default() .bg(colors.highlight_bg) .add_modifier(Modifier::BOLD), ); } let mut list_state = ListState::default(); if focused { list_state.select(playlists.selected_song); } frame.render_stateful_widget(list, area, &mut list_state); state.playlists.song_scroll_offset = list_state.offset(); }