ommp.git

ommp

~ Oh My Music Player — a standalone terminal music player built with Rust and ratatui
git clone https://github.com/debuman/ommp.git

src/ui/widgets/playlist_modal.rs (6049B)

      1 use ratatui::layout::{Constraint, Direction, Layout, Rect};
      2 use ratatui::style::{Color, Modifier, Style};
      3 use ratatui::text::{Line, Span};
      4 use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph};
      5 use ratatui::Frame;
      6 
      7 use crate::app::App;
      8 use crate::ui::theme::Theme;
      9 use crate::ui::text::centered_rect;
     10 
     11 #[derive(Debug, Clone, PartialEq)]
     12 pub enum PlaylistModalMode {
     13     List,
     14     Create,
     15     Rename,
     16 }
     17 
     18 pub fn render_playlist_modal(
     19     frame: &mut Frame,
     20     area: Rect,
     21     selected: usize,
     22     mode: &PlaylistModalMode,
     23     input: &str,
     24     app: &App,
     25     theme: &Theme,
     26 ) {
     27     let modal = centered_rect(45, 45, area);
     28 
     29     frame.render_widget(Clear, modal);
     30 
     31     let title = match mode {
     32         PlaylistModalMode::List => " Playlist ",
     33         PlaylistModalMode::Create => " New Playlist ",
     34         PlaylistModalMode::Rename => " Rename Playlist ",
     35     };
     36 
     37     let block = Block::default()
     38         .borders(Borders::ALL)
     39         .border_style(Style::default().fg(Color::Yellow))
     40         .title(title)
     41         .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD));
     42 
     43     let inner = block.inner(modal);
     44     frame.render_widget(block, modal);
     45 
     46     match mode {
     47         PlaylistModalMode::Create | PlaylistModalMode::Rename => {
     48             let chunks = Layout::default()
     49                 .direction(Direction::Vertical)
     50                 .constraints([
     51                     Constraint::Length(2),
     52                     Constraint::Length(1),
     53                     Constraint::Min(1),
     54                 ])
     55                 .split(inner);
     56 
     57             let prompt = if *mode == PlaylistModalMode::Create { "Name:" } else { "New name:" };
     58             let input_line = Line::from(vec![
     59                 Span::styled(
     60                     format!(" {} ", prompt),
     61                     Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
     62                 ),
     63                 Span::styled(input, Style::default().fg(Color::White)),
     64                 Span::styled("_", Style::default().fg(Color::Yellow).add_modifier(Modifier::SLOW_BLINK)),
     65             ]);
     66             frame.render_widget(Paragraph::new(input_line), chunks[0]);
     67 
     68             let hint = Line::from(Span::styled(
     69                 " Enter: confirm  Esc: cancel",
     70                 Style::default().fg(Color::DarkGray),
     71             ));
     72             frame.render_widget(Paragraph::new(hint), chunks[2]);
     73         }
     74         PlaylistModalMode::List => {
     75             if app.playlists.is_empty() {
     76                 let chunks = Layout::default()
     77                     .direction(Direction::Vertical)
     78                     .constraints([Constraint::Min(1), Constraint::Length(1)])
     79                     .split(inner);
     80 
     81                 frame.render_widget(
     82                     Paragraph::new(Line::from(Span::styled(
     83                         "  No playlists. Press 'a' to create.",
     84                         Style::default().fg(Color::DarkGray),
     85                     ))),
     86                     chunks[0],
     87                 );
     88                 let hint = Line::from(Span::styled(
     89                     " a: add  Esc: close ",
     90                     Style::default().fg(Color::DarkGray),
     91                 ));
     92                 frame.render_widget(Paragraph::new(hint), chunks[1]);
     93                 return;
     94             }
     95 
     96             let current_track_idx = app.queue.current_index
     97                 .and_then(|qi| app.queue.tracks.get(qi).copied());
     98 
     99             // Scroll the selection into view. Without this the list was drawn
    100             // from the top and simply clipped, so on a short terminal `j` walked
    101             // the highlight off the bottom and Enter toggled a playlist the user
    102             // could not see.
    103             let viewport = (inner.height as usize).saturating_sub(1).max(1);
    104             let scroll = selected.saturating_sub(viewport.saturating_sub(1));
    105 
    106             let items: Vec<ListItem> = app
    107                 .playlists
    108                 .iter()
    109                 .enumerate()
    110                 .skip(scroll)
    111                 .take(viewport)
    112                 .map(|(i, pl)| {
    113                     let is_selected = i == selected;
    114                     let already_in = current_track_idx
    115                         .is_some_and(|ti| pl.tracks.contains(&ti));
    116 
    117                     let check = if already_in { "\u{F00C} " } else { "  " };
    118                     let icon = "\u{F005} ";
    119 
    120                     let style = if is_selected {
    121                         Style::default()
    122                             .bg(theme.highlight_bg)
    123                             .fg(theme.highlight_fg)
    124                             .add_modifier(Modifier::BOLD)
    125                     } else {
    126                         Style::default().fg(theme.fg)
    127                     };
    128 
    129                     let check_style = if is_selected {
    130                         style
    131                     } else if already_in {
    132                         Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
    133                     } else {
    134                         Style::default().fg(Color::DarkGray)
    135                     };
    136 
    137                     let count_style = if is_selected {
    138                         style
    139                     } else {
    140                         Style::default().fg(Color::DarkGray)
    141                     };
    142 
    143                     ListItem::new(Line::from(vec![
    144                         Span::styled(check, check_style),
    145                         Span::styled(icon, if is_selected { style } else { Style::default().fg(Color::Yellow) }),
    146                         Span::styled(&pl.name, style),
    147                         Span::styled(format!(" ({})", pl.tracks.len()), count_style),
    148                     ]))
    149                 })
    150                 .collect();
    151 
    152             let hint = Line::from(Span::styled(
    153                 " Enter: toggle  a: add  d: delete  r: rename  Esc: close ",
    154                 Style::default().fg(Color::DarkGray),
    155             ));
    156 
    157             let list = List::new(items).block(
    158                 Block::default().title_bottom(hint)
    159             );
    160             frame.render_widget(list, inner);
    161         }
    162     }
    163 }
    164