This repository has been archived on 2026-01-13. You can view files and clone it, but cannot push or open issues or pull requests.
rmptui/src/ui.rs

452 lines
14 KiB
Rust
Executable File

use std::time::Duration;
use crate::app::{App, SelectedTab};
use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
#[derive(Debug, PartialEq)]
pub enum InputMode {
Editing,
Normal,
PlaylistRename,
}
/// Renders the user interface widgets
pub fn render(app: &mut App, frame: &mut Frame) {
// Layout
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(93), Constraint::Min(3)])
.split(frame.size());
match app.selected_tab {
SelectedTab::Queue => draw_queue(frame, app, layout[0]),
SelectedTab::Playlists => draw_playlist_viewer(frame, app, layout[0]),
SelectedTab::DirectoryBrowser => draw_directory_browser(frame, app, layout[0]),
}
match app.inputmode {
InputMode::Normal => {
draw_progress_bar(frame, app, layout[1]);
}
InputMode::Editing => {
draw_search_bar(frame, app, layout[1]);
}
InputMode::PlaylistRename => {
draw_rename_playlist(frame, app, layout[1]);
}
}
if app.playlist_popup {
draw_add_to_playlist(frame, app, layout[0]);
}
}
/// Draws the directory
fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
let total_songs = app.conn.stats.songs.to_string();
let rows = app.browser.filetree.iter().enumerate().map(|(i, (t, s))| {
if t == "file" {
let song = app.browser.songs.get(i).unwrap().clone();
// metadata
let title = song.clone().title.unwrap_or_else(|| song.clone().file);
let artist = song.clone().artist.unwrap_or_default().cyan();
let album = song
.tags
.iter()
.filter(|(x, _)| x == "Album")
.map(|(_, l)| l.clone())
.collect::<Vec<String>>()
.join("");
let track = song
.tags
.iter()
.filter(|(x, _)| x == "Track")
.map(|(_, l)| l.clone())
.collect::<Vec<String>>()
.join("");
let time =
App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
let mut status: bool = false;
for sn in app.queue_list.list.iter() {
if sn.file.contains(s) {
status = true;
}
}
let row = Row::new(vec![
Cell::from(artist),
Cell::from(track.green()),
Cell::from(title),
Cell::from(album.cyan()),
Cell::from(time.to_string().magenta()),
]);
if status {
row.magenta().bold()
} else {
row
}
} else {
let row = Row::new(vec![Cell::from(format!("[{}]", *s))]);
row
}
});
let mut state = TableState::new();
let header = ["Artist", "Track", "Title", "Album", "Time"]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.bold()
.height(1);
let table = Table::new(
rows,
[
Constraint::Percentage(34),
Constraint::Percentage(3),
Constraint::Min(30),
Constraint::Percentage(30),
Constraint::Percentage(3),
],
)
.block(
Block::default()
.title(format!("Song Browser: {}", app.browser.path.clone()).bold())
.title(
Title::from(format!("Total Songs: {}", total_songs).green())
.alignment(Alignment::Center),
)
.title(
Title::from(format!("Volume: {}%", app.conn.volume).green())
.alignment(Alignment::Right),
)
.borders(Borders::ALL),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(Color::Cyan)
.bg(Color::Black),
)
.header(header)
.flex(layout::Flex::Legacy);
state.select(Some(app.browser.selected));
frame.render_stateful_widget(table, size, &mut state);
}
/// draws playing queue
fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
let rows = app.queue_list.list.iter().enumerate().map(|(i, song)| {
// metadata
let title = song.clone().title.unwrap_or_else(|| song.clone().file);
let artist = song.clone().artist.unwrap_or_default().cyan();
let album = song
.tags
.iter()
.filter(|(x, _)| x == "Album")
.map(|(_, l)| l.clone())
.collect::<Vec<String>>()
.join("");
let track = song
.tags
.iter()
.filter(|(x, _)| x == "Track")
.map(|(_, l)| l.clone())
.collect::<Vec<String>>()
.join("");
let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
let row = Row::new(vec![
Cell::from(artist),
Cell::from(track.green()),
Cell::from(title),
Cell::from(album.cyan()),
Cell::from(time.to_string().magenta()),
]);
let place = app.conn.current_song.place;
if let Some(pos) = place {
if i == pos.pos as usize {
row.magenta().bold()
} else {
row
}
} else {
row
}
});
let mut state = TableState::new();
let header = ["Artist", "Track", "Title", "Album", "Time"]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.bold()
.height(1);
let table = Table::new(
rows,
[
Constraint::Percentage(34),
Constraint::Percentage(3),
Constraint::Min(30),
Constraint::Percentage(30),
Constraint::Percentage(3),
],
)
.block(
Block::default()
.title(Title::from("Play Queue".green().bold()))
.title(Title::from(
format!("({} items)", app.queue_list.list.len()).bold(),
))
.title(
Title::from(format!("Volume: {}%", app.conn.volume).green())
.alignment(Alignment::Right),
)
.borders(Borders::ALL),
)
.highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(Color::Cyan)
.bg(Color::Black),
)
.header(header)
.flex(layout::Flex::Legacy);
state.select(Some(app.queue_list.index));
frame.render_stateful_widget(table, size, &mut state);
}
// Draw search bar
fn draw_search_bar(frame: &mut Frame, app: &mut App, size: Rect) {
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
// rendering
#[allow(clippy::cast_possible_truncation)]
frame.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
size.x + app.search_cursor_pos as u16 + 2,
// Move one line down, from the border to the input line
size.y + 1,
);
let input = Paragraph::new("/".to_string() + &app.search_input)
.style(Style::default())
.block(
Block::default()
.borders(Borders::ALL)
.title("Search Forward: ".bold().green()),
);
frame.render_widget(input, size);
}
/// Draws Progress Bar
fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
// Get the current playing song
let song = app
.conn
.now_playing()
.unwrap()
.unwrap_or_else(|| "No Title Found".to_string());
// Get the current playing state
let mut state: String = String::new();
if !app.queue_list.list.is_empty() {
state = app.conn.state.clone();
state.push(':');
}
// Get the current modes
let mut modes_bottom: String = String::new();
// we do this to check if at least one mode is enabled so we can push "[]"
if app.conn.repeat | app.conn.random {
modes_bottom.push('r');
}
if !modes_bottom.is_empty() {
modes_bottom.clear();
modes_bottom.push('[');
if app.conn.repeat {
modes_bottom.push('r');
}
if app.conn.random {
modes_bottom.push('z');
}
modes_bottom.push(']');
};
// get the duration
let duration = if app.conn.total_duration.as_secs() != 0 {
format!(
"[{}/{}]",
App::format_time(app.conn.elapsed),
App::format_time(app.conn.total_duration)
)
} else {
"".to_string()
};
// Define the title
let title = Block::default()
.title(Title::from(state.red().bold()))
.title(Title::from(song.green()))
.title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
.title(Title::from(modes_bottom).position(block::Position::Bottom))
.borders(Borders::ALL);
let progress_bar = LineGauge::default()
.block(title.borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Blue)
.bg(Color::Black)
.add_modifier(Modifier::BOLD),
)
.line_set(symbols::line::THICK)
.ratio(app.conn.get_progress_ratio());
frame.render_widget(progress_bar, size);
}
fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
let layouts = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(40), Constraint::Percentage(60)])
.split(area);
// Draw list of playlists
let mut state = ListState::default();
let title = Block::default().title(Title::from("Playlist".green().bold()));
let list = List::new(app.pl_list.list.clone())
.block(title.borders(Borders::ALL))
.highlight_style(
Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
)
.repeat_highlight_symbol(true);
state.select(Some(app.pl_list.index));
frame.render_stateful_widget(list, layouts[0], &mut state);
// Playlist viewer
// Handle if there are no playlists in the mpd database
if app.pl_list.list.is_empty() {
let title = Block::default()
.title(Title::from("No Playlists Found".red().bold()))
.title_alignment(Alignment::Center)
.borders(Borders::ALL);
frame.render_widget(Clear, area); //this clears out the background
frame.render_widget(title, area);
return;
}
let pl_name = app.pl_list.get_item_at_current_index();
let songs = app.conn.conn.playlist(pl_name).unwrap();
let rows = songs.iter().map(|song| {
let title = song.clone().title.unwrap_or_default().cyan();
let artist = song.clone().artist.unwrap_or_else(|| song.clone().file);
let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
let row = Row::new(vec![
Cell::from(artist),
Cell::from(title),
Cell::from(time.to_string().green()),
]);
row
});
let title = Block::default()
.title(format!("Content: ({} items)", songs.len()).bold())
.borders(Borders::ALL);
let table = Table::new(
rows,
vec![
Constraint::Min(48),
Constraint::Percentage(48),
Constraint::Percentage(4),
],
)
.block(title)
.highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(Color::Cyan)
.bg(Color::Black),
)
.flex(layout::Flex::SpaceBetween);
frame.render_widget(table, layouts[1]);
}
fn draw_add_to_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
let area = centered_rect(40, 50, area);
let mut state = ListState::default();
let title = Block::default()
.title(Title::from("Add Selected Item to: "))
.title(Title::from("<Esc> to Cancel".green().bold()).alignment(Alignment::Right));
let list = List::new(app.append_list.list.clone())
.block(title.borders(Borders::ALL))
.highlight_style(
Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
)
.repeat_highlight_symbol(true);
state.select(Some(app.append_list.index));
frame.render_widget(Clear, area); //this clears out the background
frame.render_stateful_widget(list, area, &mut state);
}
fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
#[allow(clippy::cast_possible_truncation)]
frame.set_cursor(
// Draw the cursor at the current position in the input field.
// This position is can be controlled via the left and right arrow key
area.x + app.pl_cursor_pos as u16 + 2,
// Move one line down, from the border to the input line
area.y + 1,
);
let input = Paragraph::new("/".to_string() + &app.pl_newname_input)
.style(Style::default())
.block(
Block::default()
.borders(Borders::ALL)
.title("Enter New Name: ".bold().green()),
);
frame.render_widget(input, area);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
let popup_layout = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}