Krolyxon
Commit b9bbabd

rmptuisrcui.rs

460 lines · 14.4 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 mut state = TableState::new();
105 let header = ["Artist", "Track", "Title", "Album", "Time"]
106 .into_iter()
107 .map(Cell::from)
108 .collect::<Row>()
109 .bold()
110 .height(1);
111 let table = Table::new(
112 rows,
113 [
114 Constraint::Percentage(34),
115 Constraint::Percentage(3),
116 Constraint::Min(30),
117 Constraint::Percentage(30),
118 Constraint::Percentage(3),
119 ],
120 )
121 .block(
122 Block::default()
123 .title(format!("Song Browser: {}", app.browser.path.clone()).bold())
124 .title(
125 Title::from(format!("Total Songs: {}", total_songs).green())
126 .alignment(Alignment::Center),
127 )
128 .title(
129 Title::from(format!("Volume: {}%", app.conn.status.volume).green())
130 .alignment(Alignment::Right),
131 )
132 .borders(Borders::ALL),
133 )
134 .highlight_style(
135 Style::default()
136 .add_modifier(Modifier::REVERSED)
137 .fg(Color::Cyan)
138 .bg(Color::Black),
139 )
140 .header(header)
141 .flex(layout::Flex::Legacy);
142
143 state.select(Some(app.browser.selected));
144 frame.render_stateful_widget(table, size, &mut state);
145}
146
147/// draws playing queue
148fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
149 let rows = app.queue_list.list.iter().enumerate().map(|(i, song)| {
150 // metadata
151 let title = song.clone().title.unwrap_or_else(|| song.clone().file);
152 let artist = song.clone().artist.unwrap_or_default().cyan();
153 let album = song
154 .tags
155 .iter()
156 .filter(|(x, _)| x == "Album")
157 .map(|(_, l)| l.clone())
158 .collect::<Vec<String>>()
159 .join("");
160
161 let track = song
162 .tags
163 .iter()
164 .filter(|(x, _)| x == "Track")
165 .map(|(_, l)| l.clone())
166 .collect::<Vec<String>>()
167 .join("");
168
169 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
170
171 let row = Row::new(vec![
172 Cell::from(artist.clone()),
173 Cell::from(track.clone().green()),
174 Cell::from(title.clone()),
175 Cell::from(album.clone().cyan()),
176 Cell::from(time.to_string().magenta()),
177 ]);
178
179 let place = app.conn.current_song.place;
180 if let Some(pos) = place {
181 if i == pos.pos as usize {
182 let row = Row::new(vec![
183 Cell::from(format!("> {}", artist)),
184 Cell::from(format!(" {}", track).green()),
185 Cell::from(format!(" {}", title)),
186 Cell::from(album.cyan()),
187 Cell::from(time.to_string().magenta()),
188 ]);
189
190 row
191 } else {
192 row
193 }
194 } else {
195 row
196 }
197 });
198
199 let mut state = TableState::new();
200 let header = ["Artist", "Track", "Title", "Album", "Time"]
201 .into_iter()
202 .map(Cell::from)
203 .collect::<Row>()
204 .bold()
205 .height(1);
206 let table = Table::new(
207 rows,
208 [
209 Constraint::Percentage(34),
210 Constraint::Percentage(3),
211 Constraint::Min(30),
212 Constraint::Percentage(30),
213 Constraint::Percentage(3),
214 ],
215 )
216 .block(
217 Block::default()
218 .title(Title::from("Play Queue".green().bold()))
219 .title(Title::from(
220 format!("({} items)", app.queue_list.list.len()).bold(),
221 ))
222 .title(
223 Title::from(format!("Volume: {}%", app.conn.status.volume).green())
224 .alignment(Alignment::Right),
225 )
226 .borders(Borders::ALL),
227 )
228 .highlight_style(
229 Style::default()
230 .add_modifier(Modifier::REVERSED)
231 .fg(Color::Cyan)
232 .bg(Color::Black),
233 )
234 .header(header)
235 .flex(layout::Flex::Legacy);
236
237 state.select(Some(app.queue_list.index));
238 frame.render_stateful_widget(table, size, &mut state);
239}
240
241// Draw search bar
242fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
243 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
244 // rendering
245 #[allow(clippy::cast_possible_truncation)]
246 frame.set_cursor(
247 // Draw the cursor at the current position in the input field.
248 // This position is can be controlled via the left and right arrow key
249 size.x + app.search_cursor_pos as u16 + 2,
250 // Move one line down, from the border to the input line
251 size.y + 1,
252 );
253
254 let input = Paragraph::new("/".to_string() + &app.search_input)
255 .style(Style::default())
256 .block(
257 Block::default()
258 .borders(Borders::ALL)
259 .title("Search Forward: ".bold().green()),
260 );
261 frame.render_widget(input, size);
262}
263
264/// Draws Progress Bar
265fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
266 // Get the current playing song
267 let song = app
268 .conn
269 .now_playing()
270 .unwrap()
271 .unwrap_or_else(|| "No Title Found".to_string());
272
273 // Get the current playing state
274 let mut state: String = String::new();
275 if !app.queue_list.list.is_empty() {
276 state = app.conn.state.clone();
277 state.push(':');
278 }
279
280 // Get the current modes
281 let mut modes_bottom: String = String::new();
282 // we do this to check if at least one mode is enabled so we can push "[]"
283 if app.conn.status.repeat | app.conn.status.random {
284 modes_bottom.push('r');
285 }
286
287 if !modes_bottom.is_empty() {
288 modes_bottom.clear();
289 modes_bottom.push('[');
290 if app.conn.status.repeat {
291 modes_bottom.push('r');
292 }
293 if app.conn.status.random {
294 modes_bottom.push('z');
295 }
296 modes_bottom.push(']');
297 };
298
299 // get the duration
300 let duration = if app.conn.total_duration.as_secs() != 0 {
301 format!(
302 "[{}/{}]",
303 App::format_time(app.conn.elapsed),
304 App::format_time(app.conn.total_duration)
305 )
306 } else {
307 "".to_string()
308 };
309
310 // Define the title
311 let title = Block::default()
312 .title(Title::from(state.red().bold()))
313 .title(Title::from(song.green()))
314 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
315 .title(Title::from(modes_bottom).position(block::Position::Bottom))
316 .borders(Borders::ALL);
317
318 let progress_bar = LineGauge::default()
319 .block(title.borders(Borders::ALL))
320 .gauge_style(
321 Style::default()
322 .fg(Color::Blue)
323 .bg(Color::Black)
324 .add_modifier(Modifier::BOLD),
325 )
326 .line_set(symbols::line::THICK)
327 .ratio(app.conn.get_progress_ratio());
328
329 frame.render_widget(progress_bar, size);
330}
331
332fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
333 let layouts = Layout::default()
334 .direction(Direction::Horizontal)
335 .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(60)])
336 .split(area);
337
338 // Draw list of playlists
339 let mut state = ListState::default();
340 let title = Block::default().title(Title::from("Playlist".green().bold()));
341 let list = List::new(app.pl_list.list.clone())
342 .block(title.borders(Borders::ALL))
343 .highlight_style(
344 Style::new()
345 .fg(Color::Cyan)
346 .bg(Color::Black)
347 .add_modifier(Modifier::BOLD)
348 .add_modifier(Modifier::REVERSED),
349 )
350 .repeat_highlight_symbol(true);
351 state.select(Some(app.pl_list.index));
352 frame.render_stateful_widget(list, layouts[0], &mut state);
353
354 // Playlist viewer
355
356 // Handle if there are no playlists in the mpd database
357 if app.pl_list.list.is_empty() {
358 let title = Block::default()
359 .title(Title::from("No Playlists Found".red().bold()))
360 .title_alignment(Alignment::Center)
361 .borders(Borders::ALL);
362 frame.render_widget(Clear, area); //this clears out the background
363 frame.render_widget(title, area);
364 return;
365 }
366
367 let pl_name = app.pl_list.get_item_at_current_index();
368 let songs = app.conn.conn.playlist(pl_name).unwrap();
369 let rows = songs.iter().map(|song| {
370 let title = song.clone().title.unwrap_or_default().cyan();
371 let artist = song.clone().artist.unwrap_or_else(|| song.clone().file);
372 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
373
374 let row = Row::new(vec![
375 Cell::from(artist),
376 Cell::from(title),
377 Cell::from(time.to_string().green()),
378 ]);
379 row
380 });
381
382 let title = Block::default()
383 .title(format!("Content: ({} items)", songs.len()).bold())
384 .borders(Borders::ALL);
385 let table = Table::new(
386 rows,
387 vec![
388 Constraint::Min(48),
389 Constraint::Percentage(48),
390 Constraint::Percentage(4),
391 ],
392 )
393 .block(title)
394 .highlight_style(
395 Style::default()
396 .add_modifier(Modifier::REVERSED)
397 .fg(Color::Cyan)
398 .bg(Color::Black),
399 )
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 .repeat_highlight_symbol(true);
420
421 state.select(Some(app.append_list.index));
422 frame.render_widget(Clear, area); //this clears out the background
423 frame.render_stateful_widget(list, area, &mut state);
424}
425
426fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
427 #[allow(clippy::cast_possible_truncation)]
428 frame.set_cursor(
429 // Draw the cursor at the current position in the input field.
430 // This position is can be controlled via the left and right arrow key
431 area.x + app.pl_cursor_pos as u16 + 2,
432 // Move one line down, from the border to the input line
433 area.y + 1,
434 );
435
436 let input = Paragraph::new("/".to_string() + &app.pl_newname_input)
437 .style(Style::default())
438 .block(
439 Block::default()
440 .borders(Borders::ALL)
441 .title("Enter New Name: ".bold().green()),
442 );
443 frame.render_widget(input, area);
444}
445
446fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
447 let popup_layout = Layout::vertical([
448 Constraint::Percentage((100 - percent_y) / 2),
449 Constraint::Percentage(percent_y),
450 Constraint::Percentage((100 - percent_y) / 2),
451 ])
452 .split(r);
453
454 Layout::horizontal([
455 Constraint::Percentage((100 - percent_x) / 2),
456 Constraint::Percentage(percent_x),
457 Constraint::Percentage((100 - percent_x) / 2),
458 ])
459 .split(popup_layout[1])[1]
460}