Krolyxon
Tag v0.1.5

rmptuisrcui.rs

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