202 lines
6.1 KiB
Rust
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();
|
|
}
|