Krolyxon
Commit c047f63

rmptuisrcui.rs

419 lines · 12.9 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 =
81 App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
82
83 let mut status: bool = false;
84 for sn in app.queue_list.list.iter() {
85 if sn.contains(s) {
86 status = true;
87 }
88 }
89
90 if status {
91 let row = Row::new(vec![
92 Cell::from(artist),
93 Cell::from(track.green()),
94 Cell::from(title),
95 Cell::from(album),
96 Cell::from(time.to_string().magenta()),
97 ]);
98 row.magenta().bold()
99 } else {
100 let row = Row::new(vec![
101 Cell::from(artist),
102 Cell::from(track.green()),
103 Cell::from(title),
104 Cell::from(album),
105 Cell::from(time.to_string().magenta()),
106 ]);
107 row
108 }
109 } else {
110 let row = Row::new(vec![Cell::from(format!("[{}]", *s))]);
111 row
112 }
113 });
114
115 let mut state = TableState::new();
116 let header = ["Artist", "Track", "Title", "Album", "Time"]
117 .into_iter()
118 .map(Cell::from)
119 .collect::<Row>()
120 .bold()
121 .height(1);
122 let table = Table::new(
123 rows,
124 [
125 Constraint::Percentage(20),
126 Constraint::Percentage(3),
127 Constraint::Min(30),
128 Constraint::Percentage(30),
129 Constraint::Percentage(3),
130 ],
131 )
132 .block(
133 Block::default()
134 .title(format!("Song Browser: {}", app.browser.path.clone()).bold())
135 .title(
136 Title::from(format!("Total Songs: {}", total_songs).bold().green())
137 .alignment(Alignment::Center),
138 )
139 .title(
140 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
141 .alignment(Alignment::Right),
142 )
143 .borders(Borders::ALL),
144 )
145 .highlight_style(
146 Style::default()
147 .add_modifier(Modifier::REVERSED)
148 .fg(Color::Cyan)
149 .bg(Color::Black),
150 )
151 .highlight_symbol(">>")
152 .header(header)
153 .flex(layout::Flex::Legacy);
154
155 state.select(Some(app.browser.selected));
156 frame.render_stateful_widget(table, size, &mut state);
157}
158
159/// draws playing queue
160fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
161 let rows = app.queue_list.list.iter().map(|s| {
162 // metadata
163 let song = app
164 .conn
165 .conn
166 .lsinfo(&Song {
167 file: app.conn.get_full_path(&s).unwrap_or_default(),
168 ..Default::default()
169 })
170 .unwrap_or_default();
171 let song = song.get(0).unwrap();
172 let title = song.clone().title.unwrap_or_else(|| song.clone().file);
173 let artist = song.clone().artist.unwrap_or_default().cyan();
174 let album = song
175 .tags
176 .iter()
177 .filter(|(x, _)| x == "Album")
178 .map(|(_, l)| l.clone())
179 .collect::<Vec<String>>()
180 .join("");
181
182 let track = song
183 .tags
184 .iter()
185 .filter(|(x, _)| x == "Track")
186 .map(|(_, l)| l.clone())
187 .collect::<Vec<String>>()
188 .join("");
189
190 let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
191
192 if s.contains(&app.conn.current_song.file) {
193 let row = Row::new(vec![
194 Cell::from(artist),
195 Cell::from(track.green()),
196 Cell::from(title),
197 Cell::from(album),
198 Cell::from(time.to_string().magenta()),
199 ]);
200 row.magenta().bold()
201 } else {
202 let row = Row::new(vec![
203 Cell::from(artist),
204 Cell::from(track.green()),
205 Cell::from(title),
206 Cell::from(album),
207 Cell::from(time.to_string().magenta()),
208 ]);
209 row
210 }
211 });
212
213 let mut state = TableState::new();
214 let header = ["Artist", "Track", "Title", "Album", "Time"]
215 .into_iter()
216 .map(Cell::from)
217 .collect::<Row>()
218 .bold()
219 .height(1);
220 let table = Table::new(
221 rows,
222 [
223 Constraint::Percentage(20),
224 Constraint::Percentage(3),
225 Constraint::Min(30),
226 Constraint::Percentage(30),
227 Constraint::Percentage(3),
228 ],
229 )
230 .block(
231 Block::default()
232 .title(Title::from("Play Queue".green().bold()))
233 .title(Title::from(
234 format!("({} items)", app.queue_list.list.len()).bold(),
235 ))
236 .title(
237 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
238 .alignment(Alignment::Right),
239 )
240 .borders(Borders::ALL),
241 )
242 .highlight_style(
243 Style::default()
244 .add_modifier(Modifier::REVERSED)
245 .fg(Color::Cyan)
246 .bg(Color::Black),
247 )
248 .highlight_symbol(">>")
249 .header(header)
250 .flex(layout::Flex::Legacy);
251
252 state.select(Some(app.queue_list.index));
253 frame.render_stateful_widget(table, size, &mut state);
254}
255
256/// draws all playlists
257fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
258 let mut state = ListState::default();
259
260 let title = Block::default()
261 .title(Title::from("Playlist".green().bold()))
262 .title(
263 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
264 .alignment(Alignment::Right),
265 );
266
267 let list = List::new(app.pl_list.list.clone())
268 .block(title.borders(Borders::ALL))
269 .highlight_style(
270 Style::new()
271 .fg(Color::Cyan)
272 .bg(Color::Black)
273 .add_modifier(Modifier::BOLD)
274 .add_modifier(Modifier::REVERSED),
275 )
276 .highlight_symbol(">>")
277 .repeat_highlight_symbol(true);
278
279 state.select(Some(app.pl_list.index));
280 frame.render_stateful_widget(list, size, &mut state);
281}
282
283// Draw search bar
284fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
285 match app.inputmode {
286 InputMode::Normal =>
287 // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
288 {}
289
290 InputMode::Editing => {
291 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
292 // rendering
293 #[allow(clippy::cast_possible_truncation)]
294 frame.set_cursor(
295 // Draw the cursor at the current position in the input field.
296 // This position is can be controlled via the left and right arrow key
297 size.x + app.cursor_position as u16 + 2,
298 // Move one line down, from the border to the input line
299 size.y + 1,
300 );
301 }
302 }
303
304 let input = Paragraph::new("/".to_string() + &app.search_input)
305 .style(Style::default())
306 .block(
307 Block::default()
308 .borders(Borders::ALL)
309 .title("Search Forward: ".bold().green()),
310 );
311 frame.render_widget(input, size);
312}
313
314/// Draws Progress Bar
315fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
316 // Get the current playing song
317 let song = app
318 .conn
319 .now_playing()
320 .unwrap()
321 .unwrap_or_else(|| "No Title Found".to_string());
322
323 // Get the current playing state
324 let mut state: String = String::new();
325 if !app.queue_list.list.is_empty() {
326 state = app.conn.state.clone();
327 state.push(':');
328 }
329
330 // Get the current modes
331 let mut modes_bottom: String = String::new();
332 // we do this to check if at least one mode is enabled so we can push "[]"
333 if app.conn.repeat | app.conn.random {
334 modes_bottom.push('r');
335 }
336
337 if !modes_bottom.is_empty() {
338 modes_bottom.clear();
339 modes_bottom.push('[');
340 if app.conn.repeat {
341 modes_bottom.push('r');
342 }
343 if app.conn.random {
344 modes_bottom.push('z');
345 }
346 modes_bottom.push(']');
347 };
348
349 // get the duration
350 let duration = if app.conn.total_duration.as_secs() != 0 {
351 format!(
352 "[{}/{}]",
353 App::format_time(app.conn.elapsed),
354 App::format_time(app.conn.total_duration)
355 )
356 } else {
357 "".to_string()
358 };
359
360 // Define the title
361 let title = Block::default()
362 .title(Title::from(format!("{}", state).red().bold()))
363 .title(Title::from(song.green().bold()))
364 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
365 .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom))
366 .borders(Borders::ALL);
367
368 let progress_bar = LineGauge::default()
369 .block(title.borders(Borders::ALL))
370 .gauge_style(
371 Style::default()
372 .fg(Color::Blue)
373 .bg(Color::Black)
374 .add_modifier(Modifier::BOLD),
375 )
376 .line_set(symbols::line::THICK)
377 .ratio(app.conn.get_progress_ratio());
378
379 frame.render_widget(progress_bar, size);
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 centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
406 let popup_layout = Layout::vertical([
407 Constraint::Percentage((100 - percent_y) / 2),
408 Constraint::Percentage(percent_y),
409 Constraint::Percentage((100 - percent_y) / 2),
410 ])
411 .split(r);
412
413 Layout::horizontal([
414 Constraint::Percentage((100 - percent_x) / 2),
415 Constraint::Percentage(percent_x),
416 Constraint::Percentage((100 - percent_x) / 2),
417 ])
418 .split(popup_layout[1])[1]
419}