ferrosonic 0.8.2

A terminal-based Subsonic music client with bit-perfect audio playback
//! Queue page showing current play queue

use ratatui::{
    layout::Rect,
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
    Frame,
};

use crate::app::state::{format_duration, AppState, RenderMutations};

/// Render the queue page.
///
/// Scroll offsets are written to `mutations` so they can be applied under a
/// write lock after the render pass.
pub fn render(frame: &mut Frame, area: Rect, state: &AppState, mutations: &mut RenderMutations) {
    let colors = *state.settings_state.theme_colors();

    // Sum durations of the current and upcoming tracks (already-played tracks
    // are excluded) so the title can show how much listening time is left.
    let start = state.queue_position.unwrap_or(0);
    let remaining_secs: i64 = state
        .queue
        .iter()
        .skip(start)
        .filter_map(|song| song.duration)
        .map(i64::from)
        .sum();

    let title = if remaining_secs > 0 {
        format!(
            " Queue ({}) [{} left] ",
            state.queue.len(),
            format_duration(remaining_secs as f64)
        )
    } else {
        format!(" Queue ({}) ", state.queue.len())
    };

    let block = Block::default()
        .borders(Borders::ALL)
        .title(title)
        .border_style(Style::default().fg(colors.border_focused));

    if state.queue.is_empty() {
        let hint = Paragraph::new("Queue is empty. Add songs from Artists or Playlists.")
            .style(Style::default().fg(colors.muted))
            .block(block);
        frame.render_widget(hint, area);
        return;
    }

    let items: Vec<ListItem> = state
        .queue
        .iter()
        .enumerate()
        .map(|(i, song)| {
            // TODO: update this code to use styled_lines
            let is_current = state.queue_position == Some(i);
            let is_selected = state.queue_state.selected == Some(i);
            let is_played = state.queue_position.map(|pos| i < pos).unwrap_or(false);
            let is_starred = song.starred.is_some();

            let indicator = if is_current { "" } else { "  " };
            let star_indicator = if is_starred { "" } else { "  " };

            let artist = song.artist.clone().unwrap_or_default();
            let duration = song.format_duration();
            // Show disc.track for songs with disc info
            let track_info = match (song.disc_number, song.track) {
                (Some(d), Some(t)) if d > 1 => format!(" [{}.{}]", d, t),
                (_, Some(t)) => format!(" [#{}]", t),
                _ => String::new(),
            };

            // Color scheme: played = muted, current = playing color, upcoming = song color
            let (title_style, artist_style, number_style) = if is_current {
                (
                    Style::default()
                        .fg(colors.playing)
                        .add_modifier(Modifier::BOLD),
                    Style::default().fg(colors.playing),
                    Style::default().fg(colors.playing),
                )
            } else if is_played {
                (
                    if is_selected {
                        Style::default()
                            .fg(colors.played)
                            .add_modifier(Modifier::BOLD)
                    } else {
                        Style::default().fg(colors.played)
                    },
                    Style::default().fg(colors.muted),
                    Style::default().fg(colors.muted),
                )
            } else if is_selected {
                (
                    Style::default()
                        .fg(colors.primary)
                        .add_modifier(Modifier::BOLD),
                    Style::default().fg(colors.muted),
                    Style::default().fg(colors.muted),
                )
            } else {
                (
                    Style::default().fg(colors.song),
                    Style::default().fg(colors.muted),
                    Style::default().fg(colors.muted),
                )
            };

            let line = Line::from(vec![
                Span::styled(format!("{:3}. ", i + 1), number_style),
                Span::styled(indicator, Style::default().fg(colors.playing)),
                Span::styled(
                    star_indicator.to_string(),
                    Style::default().fg(colors.playing),
                ),
                Span::styled(song.title.clone(), title_style),
                Span::styled(track_info, Style::default().fg(colors.muted)),
                if !artist.is_empty() {
                    Span::styled(format!(" - {}", artist), artist_style)
                } else {
                    Span::raw("")
                },
                Span::styled(
                    format!(" [{}]", duration),
                    Style::default().fg(colors.muted),
                ),
            ]);

            ListItem::new(line)
        })
        .collect();

    let list = List::new(items)
        .block(block)
        .highlight_style(Style::default().bg(colors.highlight_bg))
        .highlight_symbol("");

    let mut list_state = ListState::default();
    list_state.select(state.queue_state.selected);

    frame.render_stateful_widget(list, area, &mut list_state);
    mutations.queue_scroll_offset = Some(list_state.offset());
}