Krolyxon
Commit fde33bc

rmptuisrcui.rs

494 lines · 15.7 KB · Rust

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