Files
ferrosonic/src/ui/pages/playlists.rs
2026-01-27 22:20:34 +00:00

202 lines
6.1 KiB
Rust

//! 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<ListItem> = 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<ListItem> = 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();
}