Krolyxon
Commit 7eac9cf

rmptuisrcui.rs

300 lines · 9.6 KB · Rust

1use crate::app::{App, SelectedTab};
2use ratatui::{
3 prelude::*,
4 widgets::{block::Title, *},
5};
6
7#[derive(Debug, PartialEq)]
8pub enum InputMode {
9 Editing,
10 Normal,
11}
12
13impl InputMode {
14 pub fn toggle_editing_states(state: &InputMode) -> InputMode {
15 match state {
16 InputMode::Editing => return InputMode::Normal,
17 InputMode::Normal => return InputMode::Editing,
18 };
19 }
20}
21
22/// Renders the user interface widgets
23pub fn render(app: &mut App, frame: &mut Frame) {
24 // Layout
25 let layout = Layout::default()
26 .direction(Direction::Vertical)
27 .constraints(vec![Constraint::Percentage(93), Constraint::Min(3)])
28 .split(frame.size());
29
30 match app.selected_tab {
31 SelectedTab::Queue => draw_queue(frame, app, layout[0]),
32 SelectedTab::Playlists => draw_playlists(frame, app, layout[0]),
33 SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
34 }
35
36 match app.inputmode {
37 InputMode::Normal => {
38 draw_progress_bar(frame, app, layout[1]);
39 }
40 InputMode::Editing => {
41 draw_search_bar(frame, app, layout[1]);
42 }
43 }
44
45 if app.playlist_popup {
46 draw_add_to_playlist(frame, app, layout[0]);
47 }
48}
49
50/// Draws the file tree browser
51fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
52 let mut song_state = ListState::default();
53 let total_songs = app.conn.conn.stats().unwrap().songs.to_string();
54 let mut list: Vec<ListItem> = vec![];
55 for (t, s) in app.browser.filetree.iter() {
56 if t == "file" {
57 let mut status: bool = false;
58 for sn in app.queue_list.list.iter() {
59 if sn.contains(s) {
60 status = true;
61 }
62 }
63 if status {
64 list.push(ListItem::new(s.clone().magenta().bold()));
65 } else {
66 list.push(ListItem::new(s.clone()));
67 }
68 } else {
69 list.push(ListItem::new(Line::styled(
70 format!("[{}]", *s),
71 Style::default(),
72 )));
73 }
74 }
75 let list = List::new(list)
76 .block(
77 Block::default()
78 .title(format!("File Browser: {}", app.browser.path.clone()).bold())
79 .title(
80 Title::from(format!("Total Songs: {}", total_songs).bold().green())
81 .alignment(Alignment::Center),
82 )
83 .title(
84 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
85 .alignment(Alignment::Right),
86 )
87 .borders(Borders::ALL),
88 )
89 .highlight_style(
90 Style::new()
91 .fg(Color::Cyan)
92 .bg(Color::Black)
93 .add_modifier(Modifier::BOLD)
94 .add_modifier(Modifier::REVERSED),
95 )
96 .highlight_symbol(">>")
97 .repeat_highlight_symbol(true)
98 .scroll_padding(20);
99
100 song_state.select(Some(app.browser.selected));
101 frame.render_stateful_widget(list, size, &mut song_state);
102}
103
104/// draws playing queue
105fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
106 let mut queue_state = ListState::default();
107 let title = Block::default()
108 .title(Title::from("Play Queue".green().bold()))
109 .title(
110 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
111 .alignment(Alignment::Right),
112 );
113
114 let mut items: Vec<ListItem> = vec![];
115 for item in app.queue_list.list.iter() {
116 if item.contains(&app.conn.current_song.file) {
117 items.push(ListItem::new(item.clone().magenta().bold()))
118 } else {
119 items.push(ListItem::new(item.clone()));
120 }
121 }
122 let list = List::new(items)
123 .block(title.borders(Borders::ALL))
124 .highlight_style(
125 Style::new()
126 .fg(Color::Cyan)
127 .bg(Color::Black)
128 .add_modifier(Modifier::BOLD)
129 .add_modifier(Modifier::REVERSED),
130 )
131 .highlight_symbol(">>");
132
133 queue_state.select(Some(app.queue_list.index));
134 frame.render_stateful_widget(list, size, &mut queue_state);
135}
136
137/// draws all playlists
138fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
139 let mut state = ListState::default();
140
141 let title = Block::default()
142 .title(Title::from("Playlist".green().bold()))
143 .title(
144 Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
145 .alignment(Alignment::Right),
146 );
147
148 let list = List::new(app.pl_list.list.clone())
149 .block(title.borders(Borders::ALL))
150 .highlight_style(
151 Style::new()
152 .fg(Color::Cyan)
153 .bg(Color::Black)
154 .add_modifier(Modifier::BOLD)
155 .add_modifier(Modifier::REVERSED),
156 )
157 .highlight_symbol(">>")
158 .repeat_highlight_symbol(true);
159
160 state.select(Some(app.pl_list.index));
161 frame.render_stateful_widget(list, size, &mut state);
162}
163
164// Draw search bar
165fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
166 match app.inputmode {
167 InputMode::Normal =>
168 // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
169 {}
170
171 InputMode::Editing => {
172 // Make the cursor visible and ask ratatui to put it at the specified coordinates after
173 // rendering
174 #[allow(clippy::cast_possible_truncation)]
175 frame.set_cursor(
176 // Draw the cursor at the current position in the input field.
177 // This position is can be controlled via the left and right arrow key
178 size.x + app.cursor_position as u16 + 2,
179 // Move one line down, from the border to the input line
180 size.y + 1,
181 );
182 }
183 }
184
185 let input = Paragraph::new("/".to_string() + &app.search_input)
186 .style(Style::default())
187 .block(
188 Block::default()
189 .borders(Borders::ALL)
190 .title("Search Forward: ".bold().green()),
191 );
192 frame.render_widget(input, size);
193}
194
195/// Draws Progress Bar
196fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
197 // Get the current playing song
198 let song = app
199 .conn
200 .now_playing()
201 .unwrap()
202 .unwrap_or_else(|| "No Title Found".to_string());
203
204 // Get the current playing state
205 let mut state: String = String::new();
206 if !app.queue_list.list.is_empty() {
207 state = app.conn.state.clone();
208 state.push(':');
209 }
210
211 // Get the current modes
212 let mut modes_bottom: String = String::new();
213 // we do this to check if at least one mode is enabled so we can push "[]"
214 if app.conn.repeat | app.conn.random {
215 modes_bottom.push('r');
216 }
217
218 if !modes_bottom.is_empty() {
219 modes_bottom.clear();
220 modes_bottom.push('[');
221 if app.conn.repeat {
222 modes_bottom.push('r');
223 }
224 if app.conn.random {
225 modes_bottom.push('z');
226 }
227 modes_bottom.push(']');
228 };
229
230 // get the duration
231 let duration = if app.conn.total_duration.as_secs() != 0 {
232 format!(
233 "[{}/{}]",
234 humantime::format_duration(app.conn.elapsed),
235 humantime::format_duration(app.conn.total_duration)
236 )
237 } else {
238 "".to_string()
239 };
240
241 // Define the title
242 let title = Block::default()
243 .title(Title::from(format!("{}", state).red().bold()))
244 .title(Title::from(song.green().bold()))
245 .title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
246 .title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom))
247 .borders(Borders::ALL);
248
249 let progress_bar = LineGauge::default()
250 .block(title.borders(Borders::ALL))
251 .gauge_style(
252 Style::default()
253 .fg(Color::Blue)
254 .bg(Color::Black)
255 .add_modifier(Modifier::BOLD),
256 )
257 .line_set(symbols::line::THICK)
258 .ratio(app.conn.get_progress_ratio());
259
260 frame.render_widget(progress_bar, size);
261}
262
263fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
264 let area = centered_rect(40, 50, area);
265 let mut state = ListState::default();
266 let title = Block::default()
267 .title(Title::from("Add Selected Item to: "))
268 .title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right));
269 let list = List::new(app.append_list.list.clone())
270 .block(title.borders(Borders::ALL))
271 .highlight_style(
272 Style::new()
273 .fg(Color::Cyan)
274 .bg(Color::Black)
275 .add_modifier(Modifier::BOLD)
276 .add_modifier(Modifier::REVERSED),
277 )
278 .highlight_symbol(">>")
279 .repeat_highlight_symbol(true);
280
281 state.select(Some(app.append_list.index));
282 frame.render_widget(Clear, area); //this clears out the background
283 frame.render_stateful_widget(list, area, &mut state);
284}
285
286fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
287 let popup_layout = Layout::vertical([
288 Constraint::Percentage((100 - percent_y) / 2),
289 Constraint::Percentage(percent_y),
290 Constraint::Percentage((100 - percent_y) / 2),
291 ])
292 .split(r);
293
294 Layout::horizontal([
295 Constraint::Percentage((100 - percent_x) / 2),
296 Constraint::Percentage(percent_x),
297 Constraint::Percentage((100 - percent_x) / 2),
298 ])
299 .split(popup_layout[1])[1]
300}