Krolyxon
Commit d0b76c3

rmptuisrcui.rs

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