Files
ncmprs/src/ui.rs
T
nicknase27 6fbf09b448
Build / build (push) Canceled after 23m10s
add sample rate support
2026-08-01 18:00:29 +02:00

291 lines
9.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use ratatui::{
buffer::Buffer,
layout::{
Alignment, Constraint,
Direction::{self},
Layout, Rect,
},
style::{Color, Modifier, Style, Stylize},
symbols,
text::Line,
widgets::{
Block, BorderType::Rounded, Clear, LineGauge, List, ListDirection, ListItem, Paragraph,
Row, StatefulWidget, Table, Widget,
},
};
use crate::app::App;
use crate::app::Focus;
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let position = self.player.get_pos().as_secs_f64();
let outer_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(50),
Constraint::Percentage(25),
])
.split(area);
let left_area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Fill(100)])
.split(outer_layout[0]);
let right_area = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(outer_layout[2]);
let center_area = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(8), // Album list
Constraint::Length(6),
])
.split(outer_layout[1]);
let now_playing_block = Block::bordered()
.title(format!(
"Now Playing | Vol: {:.0} | Rate: {}Hz",
self.player.volume() * 100.0,
self.sample_rate
))
.border_type(Rounded);
let inner = now_playing_block.inner(center_area[1]);
Widget::render(now_playing_block, center_area[1], buf);
let player_area = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(4),
Constraint::Fill(1),
])
.split(inner);
let now_playing = Layout::vertical([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(player_area[1]);
let progress = Layout::horizontal([
Constraint::Length(3), // ▶
Constraint::Length(2), // %
Constraint::Fill(1), // bar
Constraint::Length(15), // time
])
.split(now_playing[2]);
// Dynamic coloring based on pane focus status
let artist_border_color = if self.focus == Focus::Artists {
Color::Green
} else {
Color::Gray
};
let album_border_color = if self.focus == Focus::Albums {
Color::Green
} else {
Color::Gray
};
let song_border_color = if self.focus == Focus::Songs {
Color::Green
} else {
Color::Gray
};
let queue_border_color = if self.focus == Focus::Queue {
Color::Green
} else {
Color::Gray
};
// Artists
let artists: Vec<ListItem> = self
.library
.artists
.iter()
.map(|artist| ListItem::new(artist.to_string()))
.collect();
let artist_list = List::new(artists)
.block(
Block::bordered()
.title("Artists")
.border_style(Style::new().fg(artist_border_color))
.border_type(Rounded),
)
.style(Style::new().white())
.highlight_symbol(">>")
.repeat_highlight_symbol(true)
.highlight_style(Style::new().reversed())
.direction(ListDirection::TopToBottom);
StatefulWidget::render(artist_list, left_area[0], buf, &mut self.artist_state);
// Info
let instructions = Line::from(vec![
" Help ".gray().bold(),
"<?> ".blue().bold(),
]);
// Albums
let albums: Vec<ListItem> = self
.library
.albums_for_artist(
&self.library.artists[self.artist_state.selected().expect("None")].clone(),
)
.iter()
.map(|albums| ListItem::new(albums.to_string()))
.collect();
let album_list = List::new(albums)
.block(
Block::bordered()
.title("Albums")
.title_bottom(instructions.right_aligned())
.border_style(Style::new().fg(album_border_color))
.border_type(Rounded),
)
.style(Style::new().white())
.highlight_symbol(">>")
.repeat_highlight_symbol(true)
.highlight_style(Style::new().reversed())
.direction(ListDirection::TopToBottom);
StatefulWidget::render(album_list, center_area[0], buf, &mut self.album_state);
// Songs
let song_indexes: Vec<usize> = self.current_selected_song();
let song_items: Vec<ListItem> = song_indexes
.iter()
.map(|&index| {
ListItem::new(format!(
"{} ({})",
self.library.songs[index].title.clone(),
format_time(self.library.songs[index].duration.unwrap())
))
})
.collect();
let song_list = List::new(song_items)
.block(
Block::bordered()
.title("Songs")
.border_style(Style::new().fg(song_border_color))
.border_type(Rounded),
)
.style(Style::new().white())
.highlight_symbol(">>")
.repeat_highlight_symbol(true)
.highlight_style(Style::new().reversed())
.direction(ListDirection::TopToBottom);
StatefulWidget::render(song_list, right_area[0], buf, &mut self.song_state);
// Queue
let queue_items: Vec<ListItem> = self
.queue
.iter()
.map(|&index| {
ListItem::new(format!(
"{} ({}))",
self.library.songs[index].title.as_str(),
format_time(self.library.songs[index].duration.unwrap())
))
})
.collect();
let queue_list = List::new(queue_items)
.block(
Block::bordered()
.title("Queue")
.border_style(Style::new().fg(queue_border_color))
.border_type(Rounded),
)
.style(Style::new().white())
.highlight_symbol(">>")
.repeat_highlight_symbol(true)
.highlight_style(Style::new())
.direction(ListDirection::TopToBottom);
StatefulWidget::render(queue_list, right_area[1], buf, &mut self.queue_state);
// Help Menu
if self.control_popup {
let popup_block = Block::bordered().title("Controls").border_type(Rounded);
Widget::render(Clear, center_area[0], buf);
let header = Row::new(["", "Key", "Function"])
.style(Style::new().bold())
.bottom_margin(1);
let rows = self.get_controls();
let widths = [Constraint::Percentage(10), Constraint::Percentage(45), Constraint::Percentage(45)];
let help_menu = Table::new(rows, widths)
.header(header)
.column_spacing(1)
.style(Color::White)
.block(popup_block);
Widget::render(help_menu, center_area[0], buf);
}
// Play area
if let Some(song) = self.current_song() {
let duration = song.duration.unwrap_or(1.0);
Paragraph::new(song.title.as_str())
.alignment(Alignment::Center)
.render(now_playing[0], buf);
Paragraph::new(format!(" {}", song.artist))
.alignment(Alignment::Center)
.style(Style::new().fg(Color::DarkGray))
.render(now_playing[1], buf);
let percent = position / duration;
let line_gauge = LineGauge::default()
.filled_style(Style::new().green().add_modifier(Modifier::BOLD))
.unfilled_style(
Style::new()
.gray()
.add_modifier(Modifier::BOLD | Modifier::DIM),
)
.label(format!("{:>3.0}%", percent * 100.0))
.ratio(percent.clamp(0.0, 1.0))
.filled_symbol(symbols::line::HORIZONTAL)
.unfilled_symbol(symbols::line::HORIZONTAL);
Widget::render(line_gauge, progress[2], buf);
Paragraph::new(format!(
" {} / {}",
format_time(position),
format_time(duration),
))
.render(progress[3], buf);
} else {
Paragraph::new("Nothing is playing").render(now_playing[0], buf);
let line_gauge = LineGauge::default()
.ratio(0.0)
.filled_symbol(symbols::line::HORIZONTAL)
.unfilled_symbol(symbols::line::HORIZONTAL);
Widget::render(line_gauge, progress[2], buf);
}
Paragraph::new(if self.player.is_paused() { "||" } else { ">" }).render(progress[0], buf);
}
}
fn format_time(seconds: f64) -> String {
let secs = seconds as u64;
format!("{}:{:02}", secs / 60, secs % 60)
}