Krolyxon
Commit 3a91ab1

rmptuisrcscreensqueue.rs

238 lines · 7.5 KB · Rust

1//! Screen 1: the play queue.
2
3use std::{collections::HashSet, time::Duration};
4
5use crossterm::event::MouseEvent;
6use ratatui::{
7 Frame,
8 layout::{Constraint, Rect},
9 text::Line,
10};
11
12use super::{Ctx, Screen, draw_lyrics};
13use crate::{
14 action::Action,
15 config::Config,
16 mpd::{MpdEvent, Reply, Selection, commands},
17 popups::{AddToPlaylistPopup, InputPopup, InputPurpose},
18 ui::{
19 self,
20 list::{Click, Header, ListView},
21 },
22 util::{format_length, plural},
23};
24
25pub struct QueueScreen {
26 list: ListView,
27 /// Marked songs by queue id.
28 marks: HashSet<u32>,
29 lyrics_pane: bool,
30}
31
32impl QueueScreen {
33 pub fn new(config: &Config) -> Self {
34 Self {
35 list: ListView::default(),
36 marks: HashSet::new(),
37 lyrics_pane: config.ui.lyrics_pane,
38 }
39 }
40
41 pub fn lyrics_pane_visible(&self) -> bool {
42 self.lyrics_pane
43 }
44
45 /// Ids of the marked songs in queue order, or of the selected song.
46 fn targets(&self, ctx: &Ctx) -> Vec<u32> {
47 let queue = &ctx.mpd.queue;
48 if self.marks.is_empty() {
49 self.list
50 .selected(queue.len())
51 .and_then(|i| queue[i].id)
52 .into_iter()
53 .collect()
54 } else {
55 queue
56 .iter()
57 .filter_map(|s| s.id.filter(|id| self.marks.contains(id)))
58 .collect()
59 }
60 }
61
62 fn target_files(&self, ctx: &Ctx) -> Vec<String> {
63 let ids: HashSet<u32> = self.targets(ctx).into_iter().collect();
64 ctx.mpd
65 .queue
66 .iter()
67 .filter(|s| s.id.is_some_and(|id| ids.contains(&id)))
68 .map(|s| s.file.clone())
69 .collect()
70 }
71
72 fn play_selected(&self, ctx: &Ctx) {
73 let queue = &ctx.mpd.queue;
74 if let Some(id) = self.list.selected(queue.len()).and_then(|i| queue[i].id) {
75 ctx.client.exec(commands::play_id(id));
76 }
77 }
78
79 fn move_selected(&mut self, down: bool, ctx: &Ctx) {
80 let len = ctx.mpd.queue.len();
81 let Some(index) = self.list.selected(len) else {
82 return;
83 };
84 let target = if down {
85 index + 1
86 } else {
87 index.wrapping_sub(1)
88 };
89 if target >= len {
90 return;
91 }
92 ctx.client
93 .exec(commands::move_pos(index as u32, target as u32));
94 self.list.select(target, len);
95 }
96}
97
98impl Screen for QueueScreen {
99 fn title(&self, ctx: &Ctx) -> Line<'static> {
100 let queue = &ctx.mpd.queue;
101 let total: Duration = queue.iter().filter_map(|s| s.duration).sum();
102 Line::from(format!(
103 "Playlist ({}, length: {})",
104 plural(queue.len(), "item"),
105 format_length(total)
106 ))
107 }
108
109 fn draw(&mut self, frame: &mut Frame, area: Rect, ctx: &Ctx) {
110 let theme = ctx.theme();
111 let area = if self.lyrics_pane {
112 let width = ctx.config.ui.lyrics_pane_width;
113 let [queue, lyrics] = <[Rect; 2]>::try_from(ui::split_columns(
114 frame,
115 area,
116 &[
117 Constraint::Percentage(100 - width),
118 Constraint::Percentage(width),
119 ],
120 theme,
121 ))
122 .expect("two columns");
123 draw_lyrics(frame, lyrics, ctx);
124 queue
125 } else {
126 area
127 };
128
129 let queue = &ctx.mpd.queue;
130 let columns = &ctx.config.ui.song_columns;
131 let marks = &self.marks;
132 self.list.render(
133 frame,
134 area,
135 Some(Header::Columns(ui::song_header(columns))),
136 &ui::song_widths(columns),
137 queue.len(),
138 true,
139 theme,
140 |i| {
141 let song = &queue[i];
142 let row = ui::song_row(song, columns, ctx.mpd.is_current(song), theme);
143 if song.id.is_some_and(|id| marks.contains(&id)) {
144 row.style(theme.marked())
145 } else {
146 row
147 }
148 },
149 );
150 }
151
152 fn handle_action(&mut self, action: Action, ctx: &mut Ctx) -> bool {
153 let len = ctx.mpd.queue.len();
154 if self.list.navigate(action, len) {
155 return true;
156 }
157 match action {
158 Action::Activate | Action::Right => self.play_selected(ctx),
159 Action::Delete | Action::AddToQueue if self.marks.is_empty() => {
160 // By position, so that repeated presses delete successive rows
161 // even before the queue has been refreshed.
162 if let Some(index) = self.list.selected(len) {
163 ctx.client.exec(commands::delete_pos(index as u32));
164 }
165 }
166 Action::Delete | Action::AddToQueue => {
167 let ids = self.targets(ctx);
168 ctx.client
169 .exec_all(ids.into_iter().map(commands::delete_id).collect());
170 self.marks.clear();
171 }
172 Action::MoveItemUp => self.move_selected(false, ctx),
173 Action::MoveItemDown => self.move_selected(true, ctx),
174 Action::ShuffleQueue => ctx.client.exec(commands::shuffle()),
175 Action::SaveQueue if len > 0 => {
176 ctx.popup(InputPopup::new(
177 "Save queue as",
178 "",
179 InputPurpose::SaveQueue,
180 ));
181 }
182 Action::JumpToCurrent => match ctx.mpd.status.song {
183 Some(pos) => self.list.select(pos as usize, len),
184 None => ctx.notify("Nothing is playing"),
185 },
186 Action::ToggleLyricsPane => self.lyrics_pane = !self.lyrics_pane,
187 Action::ToggleMark => {
188 if let Some(id) = self.list.selected(len).and_then(|i| ctx.mpd.queue[i].id) {
189 if !self.marks.remove(&id) {
190 self.marks.insert(id);
191 }
192 self.list.navigate(Action::Down, len);
193 }
194 }
195 Action::ClearMarks => self.marks.clear(),
196 Action::AddToPlaylist => {
197 let files = self.target_files(ctx);
198 if !files.is_empty() {
199 let popup = AddToPlaylistPopup::new(Selection::Uris(files), ctx);
200 ctx.popup(popup);
201 }
202 }
203 _ => return false,
204 }
205 true
206 }
207
208 fn on_mpd(&mut self, event: &MpdEvent, ctx: &mut Ctx) {
209 if let MpdEvent::Reply(Reply::QueueChanges { .. }) = event
210 && !self.marks.is_empty()
211 {
212 let ids: HashSet<u32> = ctx.mpd.queue.iter().filter_map(|s| s.id).collect();
213 self.marks.retain(|id| ids.contains(id));
214 }
215 }
216
217 fn on_enter(&mut self, ctx: &mut Ctx) {
218 // Start on the playing song the first time.
219 if self.list.selected(ctx.mpd.queue.len()) == Some(0)
220 && let Some(pos) = ctx.mpd.status.song
221 {
222 self.list.select(pos as usize, ctx.mpd.queue.len());
223 }
224 }
225
226 fn on_mouse(&mut self, event: MouseEvent, ctx: &mut Ctx) {
227 if let Some(Click::Activate(_)) = self.list.mouse(event, ctx.mpd.queue.len()) {
228 self.play_selected(ctx);
229 }
230 }
231
232 fn search(&mut self, query: &str, ctx: &Ctx) -> bool {
233 let texts: Vec<String> = ctx.mpd.queue.iter().map(ui::song_search_text).collect();
234 self.list
235 .search(query, texts.iter().map(String::as_str), texts.len());
236 true
237 }
238}