Reorganize UI layout with playlist panel and status bar
- Merge Status, Progress, Volume into one compact top panel - Add Playlist panel showing queue with highlighted current track - Move help to bottom status bar (cm-dashboard style) - Bottom bar shows all keybindings in one line - Right panel now: Status (top) + Playlist (bottom) - Current playing track highlighted in cyan in playlist
This commit is contained in:
parent
71b43d644c
commit
7f5aa7602d
177
src/ui/mod.rs
177
src/ui/mod.rs
@ -8,13 +8,19 @@ use ratatui::{
|
||||
};
|
||||
|
||||
pub fn render(frame: &mut Frame, state: &AppState) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
let main_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||
.split(frame.area());
|
||||
|
||||
render_file_panel(frame, state, chunks[0]);
|
||||
render_status_panel(frame, state, chunks[1]);
|
||||
let top_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(main_chunks[0]);
|
||||
|
||||
render_file_panel(frame, state, top_chunks[0]);
|
||||
render_right_panel(frame, state, top_chunks[1]);
|
||||
render_status_bar(frame, state, main_chunks[1]);
|
||||
}
|
||||
|
||||
fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
||||
@ -53,19 +59,13 @@ fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
|
||||
fn render_status_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
||||
fn render_right_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||
.split(area);
|
||||
|
||||
// Player state or refreshing status
|
||||
// Combined status line: State | Progress | Volume
|
||||
let state_text = if state.is_refreshing {
|
||||
"Refreshing library..."
|
||||
} else {
|
||||
@ -75,97 +75,88 @@ fn render_status_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
||||
PlayerState::Paused => "Paused",
|
||||
}
|
||||
};
|
||||
let state_widget = Paragraph::new(state_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Status"))
|
||||
.style(Style::default().fg(Color::White));
|
||||
frame.render_widget(state_widget, chunks[0]);
|
||||
|
||||
// Current file with playlist position
|
||||
let current_file = state
|
||||
.current_file
|
||||
.as_ref()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "None".to_string());
|
||||
|
||||
let playlist_info = if !state.playlist.is_empty() {
|
||||
format!("{} [{}/{}]", current_file, state.playlist_index + 1, state.playlist.len())
|
||||
} else {
|
||||
current_file
|
||||
};
|
||||
|
||||
let file_widget = Paragraph::new(playlist_info)
|
||||
.block(Block::default().borders(Borders::ALL).title("Current File"))
|
||||
.style(Style::default().fg(Color::White));
|
||||
frame.render_widget(file_widget, chunks[1]);
|
||||
|
||||
// Progress
|
||||
let progress_text = if state.current_duration > 0.0 {
|
||||
let position_mins = (state.current_position / 60.0) as u32;
|
||||
let position_secs = (state.current_position % 60.0) as u32;
|
||||
let duration_mins = (state.current_duration / 60.0) as u32;
|
||||
let duration_secs = (state.current_duration % 60.0) as u32;
|
||||
format!(
|
||||
"{:02}:{:02} / {:02}:{:02}",
|
||||
"{:02}:{:02}/{:02}:{:02}",
|
||||
position_mins, position_secs, duration_mins, duration_secs
|
||||
)
|
||||
} else {
|
||||
"00:00 / 00:00".to_string()
|
||||
"00:00/00:00".to_string()
|
||||
};
|
||||
let progress_widget = Paragraph::new(progress_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Progress"))
|
||||
.style(Style::default().fg(Color::White));
|
||||
frame.render_widget(progress_widget, chunks[2]);
|
||||
|
||||
// Volume
|
||||
let volume_text = format!("{}%", state.volume);
|
||||
let volume_widget = Paragraph::new(volume_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Volume"))
|
||||
let combined_status = format!("{} | {} | Vol: {}%", state_text, progress_text, state.volume);
|
||||
let status_widget = Paragraph::new(combined_status)
|
||||
.block(Block::default().borders(Borders::ALL).title("Status"))
|
||||
.style(Style::default().fg(Color::White));
|
||||
frame.render_widget(volume_widget, chunks[3]);
|
||||
frame.render_widget(status_widget, chunks[0]);
|
||||
|
||||
// Help
|
||||
let help_text = vec![
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Navigate"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("h/l", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Collapse/expand"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("t", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Mark file"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("c", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Clear marks"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Play"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Space", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Pause"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("n/p", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Next/Prev"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("r", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Rescan"),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("q", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Quit"),
|
||||
]),
|
||||
];
|
||||
let help_widget = Paragraph::new(help_text)
|
||||
.block(Block::default().borders(Borders::ALL).title("Help"))
|
||||
.style(Style::default().fg(Color::White));
|
||||
frame.render_widget(help_widget, chunks[4]);
|
||||
// Playlist panel
|
||||
let playlist_items: Vec<ListItem> = state
|
||||
.playlist
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, path)| {
|
||||
let filename = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||
|
||||
let style = if idx == state.playlist_index && state.player_state != PlayerState::Stopped {
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
ListItem::new(filename).style(style)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let playlist_title = if !state.playlist.is_empty() {
|
||||
format!("Playlist [{}/{}]", state.playlist_index + 1, state.playlist.len())
|
||||
} else {
|
||||
"Playlist (empty)".to_string()
|
||||
};
|
||||
|
||||
let playlist_widget = List::new(playlist_items)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(playlist_title)
|
||||
.style(Style::default().fg(Color::White)),
|
||||
);
|
||||
|
||||
frame.render_widget(playlist_widget, chunks[1]);
|
||||
}
|
||||
|
||||
fn render_status_bar(frame: &mut Frame, _state: &AppState, area: Rect) {
|
||||
let help = Line::from(vec![
|
||||
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Nav | "),
|
||||
Span::styled("h/l", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Fold | "),
|
||||
Span::styled("t", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Mark | "),
|
||||
Span::styled("c", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Clear | "),
|
||||
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Play | "),
|
||||
Span::styled("Space", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Pause | "),
|
||||
Span::styled("n/p", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Next/Prev | "),
|
||||
Span::styled("r", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Rescan | "),
|
||||
Span::styled("q", Style::default().fg(Color::Cyan)),
|
||||
Span::raw(" Quit"),
|
||||
]);
|
||||
|
||||
let status_bar = Paragraph::new(help)
|
||||
.style(Style::default().fg(Color::White).bg(Color::DarkGray));
|
||||
|
||||
frame.render_widget(status_bar, area);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user