Krolyxon
Commit bf531fa

rmptuisrcui.rs

422 lines · 13.0 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::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
37 }
38
39 match app.inputmode {
40 InputMode::Normal => {
41 draw_progress_bar(frame, app, layout[1]);
42 }
43 InputMode::Editing => {
44 draw_search_bar(frame, app, layout[1]);
45 }
46 }
47
48 if app.playlist_popup {
49 draw_add_to_playlist(frame, app, layout[0]);
50 }
51}
52
53/// Draws the directory
54fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
55 let total_songs = app.conn.conn.stats().unwrap().songs.to_string();
56
57 let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| {
58 if t == "file" {
59 let song = app.browser.songs.get(i).unwrap().clone();
60
61 // metadata
62 let title = song.clone().title.unwrap_or_else(|| song.clone().file);
63 let artist = song.clone().artist.unwrap_or_default().cyan();
64 let album = song
65 .tags
66 .iter()
67 .filter(|(x, _)| x == "Album")
68 .map(|(_, l)| l.clone())
69 .collect::<Vec<String>>()
70 .join("");
71
72 let track = song
73 .tags
74 .iter()
75 .filter(|(x, _)| x == "Track")
76 .map(|(_, l)| l.clone())
77 .collect::<Vec<String>>()
78 .join("");
79
80 let time = humantime::format_duration(
81 song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)),
82 );
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(20),
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 = humantime::format_duration(
192 song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)),
193 );
194
195 if s.contains(&app.conn.current_song.file) {
196 let row = Row::new(vec![
197 Cell::from(artist),
198 Cell::from(track.green()),
199 Cell::from(title),
200 Cell::from(album),
201 Cell::from(time.to_string().magenta()),
202 ]);
203 row.magenta().bold()
204 } else {
205 let row = Row::new(vec![
206 Cell::from(artist),
207 Cell::from(track.green()),
208 Cell::from(title),
209 Cell::from(album),
210 Cell::from(time.to_string().magenta()),
211 ]);
212 row
213 }
214 });
215
216 let mut state = TableState::new();
217 let header = ["Artist", "Track", "Title", "Album", "Time"]
218 .into_iter()
219 .map(Cell::from)
220 .collect::<Row>()
221 .bold()
222 .height(1);
223 let table = Table::new(
224 rows,
225 [
226 Constraint::Percentage(20),
227 Constraint::Percentage(3),
228 Constraint::Min(30),
229 Constraint::Percentage(30),
230 Constraint::Percentage(3),
231 ],
232 )
233 .block(
234 Block::default()
235 .title(Title::from("Play Queue".green().bold()))
236 .title(Title::from(
237 format!("({} items)", app.queue_list.list.len()).bold(),
238 ))
239 .title(
240 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
241 .alignment(Alignment::Right),
242 )
243 .borders(Borders::ALL),
244 )
245 .highlight_style(
246 Style::default()
247 .add_modifier(Modifier::REVERSED)
248 .fg(Color::Cyan)
249 .bg(Color::Black),
250 )
251 .highlight_symbol(">>")
252 .header(header)
253 .flex(layout::Flex::Legacy);
254
255 state.select(Some(app.queue_list.index));
256 frame.render_stateful_widget(table, size, &mut state);
257}
258
259/// draws all playlists
260fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
261 let mut state = ListState::default();
262
263 let title = Block::default()
264 .title(Title::from("Playlist".green().bold()))
265 .title(
266 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
267 .alignment(Alignment::Right),
268 );
269
270 let list = List::new(app.pl_list.list.clone())
271 .block(title.borders(Borders::ALL))
272 .highlight_style(
273 Style::new()
274 .fg(Color::Cyan)
275 .bg(Color::Black)
276 .add_modifier(Modifier::BOLD)
277 .add_modifier(Modifier::REVERSED),
278 )
279 .highlight_symbol(">>")
280 .repeat_highlight_symbol(true);
281
282 state.select(Some(app.pl_list.index));
283 frame.render_stateful_widget(list, size, &mut state);
284}
285
286// Draw search bar
287fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
288 match app.inputmode {
289 InputMode::Normal =>
290 // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
291 {}
292
293 InputMode::Editing => {
294 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
295 // rendering
296 #[allow(clippy::cast_possible_truncation)]
297 frame.set_cursor(
298 // Draw the cursor at the current position in the input field.
299 // This position is can be controlled via the left and right arrow key
300 size.x + app.cursor_position as u16 + 2,
301 // Move one line down, from the border to the input line
302 size.y + 1,
303 );
304 }
305 }
306
307 let input = Paragraph::new("/".to_string() + &app.search_input)
308 .style(Style::default())
309 .block(
310 Block::default()
311 .borders(Borders::ALL)
312 .title("Search Forward: ".bold().green()),
313 );
314 frame.render_widget(input, size);
315}
316
317/// Draws Progress Bar
318fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
319 // Get the current playing song
320 let song = app
321 .conn
322 .now_playing()
323 .unwrap()
324 .unwrap_or_else(|| "No Title Found".to_string());
325
326 // Get the current playing state
327 let mut state: String = String::new();
328 if !app.queue_list.list.is_empty() {
329 state = app.conn.state.clone();
330 state.push(':');
331 }
332
333 // Get the current modes
334 let mut modes_bottom: String = String::new();
335 // we do this to check if at least one mode is enabled so we can push "[]"
336 if app.conn.repeat | app.conn.random {
337 modes_bottom.push('r');
338 }
339
340 if !modes_bottom.is_empty() {
341 modes_bottom.clear();
342 modes_bottom.push('[');
343 if app.conn.repeat {
344 modes_bottom.push('r');
345 }
346 if app.conn.random {
347 modes_bottom.push('z');
348 }
349 modes_bottom.push(']');
350 };
351
352 // get the duration
353 let duration = if app.conn.total_duration.as_secs() != 0 {
354 format!(
355 "[{}/{}]",
356 humantime::format_duration(app.conn.elapsed),
357 humantime::format_duration(app.conn.total_duration)
358 )
359 } else {
360 "".to_string()
361 };
362
363 // Define the title
364 let title = Block::default()
365 .title(Title::from(format!("{}", state).red().bold()))
366 .title(Title::from(song.green().bold()))
367 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
368 .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom))
369 .borders(Borders::ALL);
370
371 let progress_bar = LineGauge::default()
372 .block(title.borders(Borders::ALL))
373 .gauge_style(
374 Style::default()
375 .fg(Color::Blue)
376 .bg(Color::Black)
377 .add_modifier(Modifier::BOLD),
378 )
379 .line_set(symbols::line::THICK)
380 .ratio(app.conn.get_progress_ratio());
381
382 frame.render_widget(progress_bar, size);
383}
384
385fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
386 let area = centered_rect(40, 50, area);
387 let mut state = ListState::default();
388 let title = Block::default()
389 .title(Title::from("Add Selected Item to: "))
390 .title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right));
391 let list = List::new(app.append_list.list.clone())
392 .block(title.borders(Borders::ALL))
393 .highlight_style(
394 Style::new()
395 .fg(Color::Cyan)
396 .bg(Color::Black)
397 .add_modifier(Modifier::BOLD)
398 .add_modifier(Modifier::REVERSED),
399 )
400 .highlight_symbol(">>")
401 .repeat_highlight_symbol(true);
402
403 state.select(Some(app.append_list.index));
404 frame.render_widget(Clear, area); //this clears out the background
405 frame.render_stateful_widget(list, area, &mut state);
406}
407
408fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
409 let popup_layout = Layout::vertical([
410 Constraint::Percentage((100 - percent_y) / 2),
411 Constraint::Percentage(percent_y),
412 Constraint::Percentage((100 - percent_y) / 2),
413 ])
414 .split(r);
415
416 Layout::horizontal([
417 Constraint::Percentage((100 - percent_x) / 2),
418 Constraint::Percentage(percent_x),
419 Constraint::Percentage((100 - percent_x) / 2),
420 ])
421 .split(popup_layout[1])[1]
422}