Krolyxon
Commit 08c4f41

rmptuisrcui.rs

457 lines · 14.3 KB · Rust

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