Krolyxon
Commit 4993431

rmptuisrcui.rs

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