Krolyxon
Commit 5eafc18

rmptuisrcui.rs

301 lines · 9.6 KB · Rust

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