Krolyxon
Commit 32a3c60

rmptuisrcui.rs

461 lines · 14.5 KB · Rust

1use std::time::Duration;
2
3use crate::app::{App, SelectedTab};
4use mpd::Song;
5use ratatui::{
6 prelude::*,
7 widgets::{block::Title, *},
8};
9
10#[derive(Debug, PartialEq)]
11pub enum InputMode {
12 Editing,
13 Normal,
14 PlaylistRename,
15}
16
17/// Renders the user interface widgets
18pub fn render(app: &mut App, frame: &mut Frame) {
19 // Layout
20 let layout = Layout::default()
21 .direction(Direction::Vertical)
22 .constraints(vec![Constraint::Percentage(93), Constraint::Min(3)])
23 .split(frame.size());
24
25 match app.selected_tab {
26 SelectedTab::Queue => draw_queue(frame, app, layout[0]),
27 SelectedTab::Playlists => draw_playlist_viewer(frame, app, layout[0]),
28 SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
29 }
30
31 match app.inputmode {
32 InputMode::Normal => {
33 draw_progress_bar(frame, app, layout[1]);
34 }
35 InputMode::Editing => {
36 draw_search_bar(frame, app, layout[1]);
37 }
38 InputMode::PlaylistRename => {
39 draw_rename_playlist(frame, app, layout[1]);
40 }
41 }
42
43 if app.playlist_popup {
44 draw_add_to_playlist(frame, app, layout[0]);
45 }
46}
47
48/// Draws the directory
49fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
50 let total_songs = app.conn.conn.stats().unwrap().songs.to_string();
51
52 let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| {
53 if t == "file" {
54 let song = app.browser.songs.get(i).unwrap().clone();
55
56 // metadata
57 let title = song.clone().title.unwrap_or_else(|| song.clone().file);
58 let artist = song.clone().artist.unwrap_or_default().cyan();
59 let album = song
60 .tags
61 .iter()
62 .filter(|(x, _)| x == "Album")
63 .map(|(_, l)| l.clone())
64 .collect::<Vec<String>>()
65 .join("");
66
67 let track = song
68 .tags
69 .iter()
70 .filter(|(x, _)| x == "Track")
71 .map(|(_, l)| l.clone())
72 .collect::<Vec<String>>()
73 .join("");
74
75 let time =
76 App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
77
78 let mut status: bool = false;
79 for sn in app.queue_list.list.iter() {
80 if sn.contains(s) {
81 status = true;
82 }
83 }
84
85 if status {
86 let row = Row::new(vec![
87 Cell::from(artist),
88 Cell::from(track.green()),
89 Cell::from(title),
90 Cell::from(album),
91 Cell::from(time.to_string().magenta()),
92 ]);
93 row.magenta().bold()
94 } else {
95 let row = Row::new(vec![
96 Cell::from(artist),
97 Cell::from(track.green()),
98 Cell::from(title),
99 Cell::from(album),
100 Cell::from(time.to_string().magenta()),
101 ]);
102 row
103 }
104 } else {
105 let row = Row::new(vec![Cell::from(format!("[{}]", *s))]);
106 row
107 }
108 });
109
110 let mut state = TableState::new();
111 let header = ["Artist", "Track", "Title", "Album", "Time"]
112 .into_iter()
113 .map(Cell::from)
114 .collect::<Row>()
115 .bold()
116 .height(1);
117 let table = Table::new(
118 rows,
119 [
120 Constraint::Percentage(34),
121 Constraint::Percentage(3),
122 Constraint::Min(30),
123 Constraint::Percentage(30),
124 Constraint::Percentage(3),
125 ],
126 )
127 .block(
128 Block::default()
129 .title(format!("Song Browser: {}", app.browser.path.clone()).bold())
130 .title(
131 Title::from(format!("Total Songs: {}", total_songs).bold().green())
132 .alignment(Alignment::Center),
133 )
134 .title(
135 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
136 .alignment(Alignment::Right),
137 )
138 .borders(Borders::ALL),
139 )
140 .highlight_style(
141 Style::default()
142 .add_modifier(Modifier::REVERSED)
143 .fg(Color::Cyan)
144 .bg(Color::Black),
145 )
146 .highlight_symbol(">>")
147 .header(header)
148 .flex(layout::Flex::Legacy);
149
150 state.select(Some(app.browser.selected));
151 frame.render_stateful_widget(table, size, &mut state);
152}
153
154/// draws playing queue
155fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
156 let rows = app.queue_list.list.iter().map(|s| {
157 // metadata
158 let song = app
159 .conn
160 .conn
161 .lsinfo(&Song {
162 file: app.conn.get_full_path(&s).unwrap_or_default(),
163 ..Default::default()
164 })
165 .unwrap_or_default();
166 let song = song.get(0).unwrap();
167 let title = song.clone().title.unwrap_or_else(|| song.clone().file);
168 let artist = song.clone().artist.unwrap_or_default().cyan();
169 let album = song
170 .tags
171 .iter()
172 .filter(|(x, _)| x == "Album")
173 .map(|(_, l)| l.clone())
174 .collect::<Vec<String>>()
175 .join("");
176
177 let track = song
178 .tags
179 .iter()
180 .filter(|(x, _)| x == "Track")
181 .map(|(_, l)| l.clone())
182 .collect::<Vec<String>>()
183 .join("");
184
185 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
186
187 if s.contains(&app.conn.current_song.file) {
188 let row = Row::new(vec![
189 Cell::from(artist),
190 Cell::from(track.green()),
191 Cell::from(title),
192 Cell::from(album),
193 Cell::from(time.to_string().magenta()),
194 ]);
195 row.magenta().bold()
196 } else {
197 let row = Row::new(vec![
198 Cell::from(artist),
199 Cell::from(track.green()),
200 Cell::from(title),
201 Cell::from(album),
202 Cell::from(time.to_string().magenta()),
203 ]);
204 row
205 }
206 });
207
208 let mut state = TableState::new();
209 let header = ["Artist", "Track", "Title", "Album", "Time"]
210 .into_iter()
211 .map(Cell::from)
212 .collect::<Row>()
213 .bold()
214 .height(1);
215 let table = Table::new(
216 rows,
217 [
218 Constraint::Percentage(34),
219 Constraint::Percentage(3),
220 Constraint::Min(30),
221 Constraint::Percentage(30),
222 Constraint::Percentage(3),
223 ],
224 )
225 .block(
226 Block::default()
227 .title(Title::from("Play Queue".green().bold()))
228 .title(Title::from(
229 format!("({} items)", app.queue_list.list.len()).bold(),
230 ))
231 .title(
232 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
233 .alignment(Alignment::Right),
234 )
235 .borders(Borders::ALL),
236 )
237 .highlight_style(
238 Style::default()
239 .add_modifier(Modifier::REVERSED)
240 .fg(Color::Cyan)
241 .bg(Color::Black),
242 )
243 .highlight_symbol(">>")
244 .header(header)
245 .flex(layout::Flex::Legacy);
246
247 state.select(Some(app.queue_list.index));
248 frame.render_stateful_widget(table, size, &mut state);
249}
250
251// Draw search bar
252fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
253 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
254 // rendering
255 #[allow(clippy::cast_possible_truncation)]
256 frame.set_cursor(
257 // Draw the cursor at the current position in the input field.
258 // This position is can be controlled via the left and right arrow key
259 size.x + app.search_cursor_pos as u16 + 2,
260 // Move one line down, from the border to the input line
261 size.y + 1,
262 );
263
264 let input = Paragraph::new("/".to_string() + &app.search_input)
265 .style(Style::default())
266 .block(
267 Block::default()
268 .borders(Borders::ALL)
269 .title("Search Forward: ".bold().green()),
270 );
271 frame.render_widget(input, size);
272}
273
274/// Draws Progress Bar
275fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
276 // Get the current playing song
277 let song = app
278 .conn
279 .now_playing()
280 .unwrap()
281 .unwrap_or_else(|| "No Title Found".to_string());
282
283 // Get the current playing state
284 let mut state: String = String::new();
285 if !app.queue_list.list.is_empty() {
286 state = app.conn.state.clone();
287 state.push(':');
288 }
289
290 // Get the current modes
291 let mut modes_bottom: String = String::new();
292 // we do this to check if at least one mode is enabled so we can push "[]"
293 if app.conn.repeat | app.conn.random {
294 modes_bottom.push('r');
295 }
296
297 if !modes_bottom.is_empty() {
298 modes_bottom.clear();
299 modes_bottom.push('[');
300 if app.conn.repeat {
301 modes_bottom.push('r');
302 }
303 if app.conn.random {
304 modes_bottom.push('z');
305 }
306 modes_bottom.push(']');
307 };
308
309 // get the duration
310 let duration = if app.conn.total_duration.as_secs() != 0 {
311 format!(
312 "[{}/{}]",
313 App::format_time(app.conn.elapsed),
314 App::format_time(app.conn.total_duration)
315 )
316 } else {
317 "".to_string()
318 };
319
320 // Define the title
321 let title = Block::default()
322 .title(Title::from(format!("{}", state).red().bold()))
323 .title(Title::from(song.green().bold()))
324 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
325 .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom))
326 .borders(Borders::ALL);
327
328 let progress_bar = LineGauge::default()
329 .block(title.borders(Borders::ALL))
330 .gauge_style(
331 Style::default()
332 .fg(Color::Blue)
333 .bg(Color::Black)
334 .add_modifier(Modifier::BOLD),
335 )
336 .line_set(symbols::line::THICK)
337 .ratio(app.conn.get_progress_ratio());
338
339 frame.render_widget(progress_bar, size);
340}
341
342fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
343 let layouts = Layout::default()
344 .direction(Direction::Horizontal)
345 .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(60)])
346 .split(area);
347
348 // Draw list of playlists
349 let mut state = ListState::default();
350 let title = Block::default().title(Title::from("Playlist".green().bold()));
351 let list = List::new(app.pl_list.list.clone())
352 .block(title.borders(Borders::ALL))
353 .highlight_style(
354 Style::new()
355 .fg(Color::Cyan)
356 .bg(Color::Black)
357 .add_modifier(Modifier::BOLD)
358 .add_modifier(Modifier::REVERSED),
359 )
360 .highlight_symbol(">>")
361 .repeat_highlight_symbol(true);
362 state.select(Some(app.pl_list.index));
363 frame.render_stateful_widget(list, layouts[0], &mut state);
364
365 // Playlist viewer
366 let pl_name = app.pl_list.list.get(app.pl_list.index).unwrap();
367 let songs = app.conn.conn.playlist(pl_name).unwrap();
368 let rows = songs.iter().map(|song| {
369 let title = song.clone().title.unwrap_or_default().cyan();
370 let artist = song.clone().artist.unwrap_or_else(|| song.clone().file);
371 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
372
373 let row = Row::new(vec![
374 Cell::from(artist),
375 Cell::from(title),
376 Cell::from(time.to_string().green()),
377 ]);
378 row
379 });
380
381 let title = Block::default()
382 .title(format!("Content: ({} items)", songs.len()).bold())
383 .borders(Borders::ALL);
384 let table = Table::new(
385 rows,
386 vec![
387 Constraint::Min(40),
388 Constraint::Percentage(40),
389 Constraint::Percentage(20),
390 ],
391 )
392 .block(title)
393 .highlight_style(
394 Style::default()
395 .add_modifier(Modifier::REVERSED)
396 .fg(Color::Cyan)
397 .bg(Color::Black),
398 )
399 .highlight_symbol(">>")
400 .flex(layout::Flex::SpaceBetween);
401 frame.render_widget(table, layouts[1]);
402}
403
404fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
405 let area = centered_rect(40, 50, area);
406 let mut state = ListState::default();
407 let title = Block::default()
408 .title(Title::from("Add Selected Item to: "))
409 .title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right));
410 let list = List::new(app.append_list.list.clone())
411 .block(title.borders(Borders::ALL))
412 .highlight_style(
413 Style::new()
414 .fg(Color::Cyan)
415 .bg(Color::Black)
416 .add_modifier(Modifier::BOLD)
417 .add_modifier(Modifier::REVERSED),
418 )
419 .highlight_symbol(">>")
420 .repeat_highlight_symbol(true);
421
422 state.select(Some(app.append_list.index));
423 frame.render_widget(Clear, area); //this clears out the background
424 frame.render_stateful_widget(list, area, &mut state);
425}
426
427fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
428 #[allow(clippy::cast_possible_truncation)]
429 frame.set_cursor(
430 // Draw the cursor at the current position in the input field.
431 // This position is can be controlled via the left and right arrow key
432 area.x + app.pl_cursor_pos as u16 + 2,
433 // Move one line down, from the border to the input line
434 area.y + 1,
435 );
436
437 let input = Paragraph::new("/".to_string() + &app.pl_newname_input)
438 .style(Style::default())
439 .block(
440 Block::default()
441 .borders(Borders::ALL)
442 .title("Enter New Name: ".bold().green()),
443 );
444 frame.render_widget(input, area);
445}
446
447fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
448 let popup_layout = Layout::vertical([
449 Constraint::Percentage((100 - percent_y) / 2),
450 Constraint::Percentage(percent_y),
451 Constraint::Percentage((100 - percent_y) / 2),
452 ])
453 .split(r);
454
455 Layout::horizontal([
456 Constraint::Percentage((100 - percent_x) / 2),
457 Constraint::Percentage(percent_x),
458 Constraint::Percentage((100 - percent_x) / 2),
459 ])
460 .split(popup_layout[1])[1]
461}