Krolyxon
Tag v0.1.0

rmptuisrcui.rs

439 lines · 13.6 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.conn.stats().unwrap().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 if sn.file.contains(s) {
80 status = true;
81 }
82 }
83
84 let row = Row::new(vec![
85 Cell::from(artist),
86 Cell::from(track.green()),
87 Cell::from(title),
88 Cell::from(album.cyan()),
89 Cell::from(time.to_string().magenta()),
90 ]);
91
92 if status {
93 row.magenta().bold()
94 } else {
95 row
96 }
97 } else {
98 let row = Row::new(vec![Cell::from(format!("[{}]", *s))]);
99 row
100 }
101 });
102
103 let mut state = TableState::new();
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).bold().green())
125 .alignment(Alignment::Center),
126 )
127 .title(
128 Title::from(format!("Volume: {}%", app.conn.volume).bold().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 .highlight_symbol(">>")
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().map(|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),
173 Cell::from(track.green()),
174 Cell::from(title),
175 Cell::from(album.cyan()),
176 Cell::from(time.to_string().magenta()),
177 ]);
178
179 if song.file.contains(&app.conn.current_song.file) {
180 row.magenta().bold()
181 } else {
182 row
183 }
184 });
185
186 let mut state = TableState::new();
187 let header = ["Artist", "Track", "Title", "Album", "Time"]
188 .into_iter()
189 .map(Cell::from)
190 .collect::<Row>()
191 .bold()
192 .height(1);
193 let table = Table::new(
194 rows,
195 [
196 Constraint::Percentage(34),
197 Constraint::Percentage(3),
198 Constraint::Min(30),
199 Constraint::Percentage(30),
200 Constraint::Percentage(3),
201 ],
202 )
203 .block(
204 Block::default()
205 .title(Title::from("Play Queue".green().bold()))
206 .title(Title::from(
207 format!("({} items)", app.queue_list.list.len()).bold(),
208 ))
209 .title(
210 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
211 .alignment(Alignment::Right),
212 )
213 .borders(Borders::ALL),
214 )
215 .highlight_style(
216 Style::default()
217 .add_modifier(Modifier::REVERSED)
218 .fg(Color::Cyan)
219 .bg(Color::Black),
220 )
221 .highlight_symbol(">>")
222 .header(header)
223 .flex(layout::Flex::Legacy);
224
225 state.select(Some(app.queue_list.index));
226 frame.render_stateful_widget(table, size, &mut state);
227}
228
229// Draw search bar
230fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
231 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
232 // rendering
233 #[allow(clippy::cast_possible_truncation)]
234 frame.set_cursor(
235 // Draw the cursor at the current position in the input field.
236 // This position is can be controlled via the left and right arrow key
237 size.x + app.search_cursor_pos as u16 + 2,
238 // Move one line down, from the border to the input line
239 size.y + 1,
240 );
241
242 let input = Paragraph::new("/".to_string() + &app.search_input)
243 .style(Style::default())
244 .block(
245 Block::default()
246 .borders(Borders::ALL)
247 .title("Search Forward: ".bold().green()),
248 );
249 frame.render_widget(input, size);
250}
251
252/// Draws Progress Bar
253fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
254 // Get the current playing song
255 let song = app
256 .conn
257 .now_playing()
258 .unwrap()
259 .unwrap_or_else(|| "No Title Found".to_string());
260
261 // Get the current playing state
262 let mut state: String = String::new();
263 if !app.queue_list.list.is_empty() {
264 state = app.conn.state.clone();
265 state.push(':');
266 }
267
268 // Get the current modes
269 let mut modes_bottom: String = String::new();
270 // we do this to check if at least one mode is enabled so we can push "[]"
271 if app.conn.repeat | app.conn.random {
272 modes_bottom.push('r');
273 }
274
275 if !modes_bottom.is_empty() {
276 modes_bottom.clear();
277 modes_bottom.push('[');
278 if app.conn.repeat {
279 modes_bottom.push('r');
280 }
281 if app.conn.random {
282 modes_bottom.push('z');
283 }
284 modes_bottom.push(']');
285 };
286
287 // get the duration
288 let duration = if app.conn.total_duration.as_secs() != 0 {
289 format!(
290 "[{}/{}]",
291 App::format_time(app.conn.elapsed),
292 App::format_time(app.conn.total_duration)
293 )
294 } else {
295 "".to_string()
296 };
297
298 // Define the title
299 let title = Block::default()
300 .title(Title::from(state.red().bold()))
301 .title(Title::from(song.green().bold()))
302 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
303 .title(Title::from(modes_bottom).position(block::Position::Bottom))
304 .borders(Borders::ALL);
305
306 let progress_bar = LineGauge::default()
307 .block(title.borders(Borders::ALL))
308 .gauge_style(
309 Style::default()
310 .fg(Color::Blue)
311 .bg(Color::Black)
312 .add_modifier(Modifier::BOLD),
313 )
314 .line_set(symbols::line::THICK)
315 .ratio(app.conn.get_progress_ratio());
316
317 frame.render_widget(progress_bar, size);
318}
319
320fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
321 let layouts = Layout::default()
322 .direction(Direction::Horizontal)
323 .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(60)])
324 .split(area);
325
326 // Draw list of playlists
327 let mut state = ListState::default();
328 let title = Block::default().title(Title::from("Playlist".green().bold()));
329 let list = List::new(app.pl_list.list.clone())
330 .block(title.borders(Borders::ALL))
331 .highlight_style(
332 Style::new()
333 .fg(Color::Cyan)
334 .bg(Color::Black)
335 .add_modifier(Modifier::BOLD)
336 .add_modifier(Modifier::REVERSED),
337 )
338 .highlight_symbol(">>")
339 .repeat_highlight_symbol(true);
340 state.select(Some(app.pl_list.index));
341 frame.render_stateful_widget(list, layouts[0], &mut state);
342
343 // Playlist viewer
344 let pl_name = app.pl_list.list.get(app.pl_list.index).unwrap();
345 let songs = app.conn.conn.playlist(pl_name).unwrap();
346 let rows = songs.iter().map(|song| {
347 let title = song.clone().title.unwrap_or_default().cyan();
348 let artist = song.clone().artist.unwrap_or_else(|| song.clone().file);
349 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
350
351 let row = Row::new(vec![
352 Cell::from(artist),
353 Cell::from(title),
354 Cell::from(time.to_string().green()),
355 ]);
356 row
357 });
358
359 let title = Block::default()
360 .title(format!("Content: ({} items)", songs.len()).bold())
361 .borders(Borders::ALL);
362 let table = Table::new(
363 rows,
364 vec![
365 Constraint::Min(40),
366 Constraint::Percentage(40),
367 Constraint::Percentage(20),
368 ],
369 )
370 .block(title)
371 .highlight_style(
372 Style::default()
373 .add_modifier(Modifier::REVERSED)
374 .fg(Color::Cyan)
375 .bg(Color::Black),
376 )
377 .highlight_symbol(">>")
378 .flex(layout::Flex::SpaceBetween);
379 frame.render_widget(table, layouts[1]);
380}
381
382fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
383 let area = centered_rect(40, 50, area);
384 let mut state = ListState::default();
385 let title = Block::default()
386 .title(Title::from("Add Selected Item to: "))
387 .title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right));
388 let list = List::new(app.append_list.list.clone())
389 .block(title.borders(Borders::ALL))
390 .highlight_style(
391 Style::new()
392 .fg(Color::Cyan)
393 .bg(Color::Black)
394 .add_modifier(Modifier::BOLD)
395 .add_modifier(Modifier::REVERSED),
396 )
397 .highlight_symbol(">>")
398 .repeat_highlight_symbol(true);
399
400 state.select(Some(app.append_list.index));
401 frame.render_widget(Clear, area); //this clears out the background
402 frame.render_stateful_widget(list, area, &mut state);
403}
404
405fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
406 #[allow(clippy::cast_possible_truncation)]
407 frame.set_cursor(
408 // Draw the cursor at the current position in the input field.
409 // This position is can be controlled via the left and right arrow key
410 area.x + app.pl_cursor_pos as u16 + 2,
411 // Move one line down, from the border to the input line
412 area.y + 1,
413 );
414
415 let input = Paragraph::new("/".to_string() + &app.pl_newname_input)
416 .style(Style::default())
417 .block(
418 Block::default()
419 .borders(Borders::ALL)
420 .title("Enter New Name: ".bold().green()),
421 );
422 frame.render_widget(input, area);
423}
424
425fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
426 let popup_layout = Layout::vertical([
427 Constraint::Percentage((100 - percent_y) / 2),
428 Constraint::Percentage(percent_y),
429 Constraint::Percentage((100 - percent_y) / 2),
430 ])
431 .split(r);
432
433 Layout::horizontal([
434 Constraint::Percentage((100 - percent_x) / 2),
435 Constraint::Percentage(percent_x),
436 Constraint::Percentage((100 - percent_x) / 2),
437 ])
438 .split(popup_layout[1])[1]
439}