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) {
|
pub fn render(frame: &mut Frame, state: &AppState) {
|
||||||
let chunks = Layout::default()
|
let main_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Min(0), Constraint::Length(1)])
|
||||||
.split(frame.area());
|
.split(frame.area());
|
||||||
|
|
||||||
render_file_panel(frame, state, chunks[0]);
|
let top_chunks = Layout::default()
|
||||||
render_status_panel(frame, state, chunks[1]);
|
.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) {
|
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);
|
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()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Length(3),
|
|
||||||
Constraint::Min(0),
|
|
||||||
])
|
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
// Player state or refreshing status
|
// Combined status line: State | Progress | Volume
|
||||||
let state_text = if state.is_refreshing {
|
let state_text = if state.is_refreshing {
|
||||||
"Refreshing library..."
|
"Refreshing library..."
|
||||||
} else {
|
} else {
|
||||||
@ -75,97 +75,88 @@ fn render_status_panel(frame: &mut Frame, state: &AppState, area: Rect) {
|
|||||||
PlayerState::Paused => "Paused",
|
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 progress_text = if state.current_duration > 0.0 {
|
||||||
let position_mins = (state.current_position / 60.0) as u32;
|
let position_mins = (state.current_position / 60.0) as u32;
|
||||||
let position_secs = (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_mins = (state.current_duration / 60.0) as u32;
|
||||||
let duration_secs = (state.current_duration % 60.0) as u32;
|
let duration_secs = (state.current_duration % 60.0) as u32;
|
||||||
format!(
|
format!(
|
||||||
"{:02}:{:02} / {:02}:{:02}",
|
"{:02}:{:02}/{:02}:{:02}",
|
||||||
position_mins, position_secs, duration_mins, duration_secs
|
position_mins, position_secs, duration_mins, duration_secs
|
||||||
)
|
)
|
||||||
} else {
|
} 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 combined_status = format!("{} | {} | Vol: {}%", state_text, progress_text, state.volume);
|
||||||
let volume_text = format!("{}%", state.volume);
|
let status_widget = Paragraph::new(combined_status)
|
||||||
let volume_widget = Paragraph::new(volume_text)
|
.block(Block::default().borders(Borders::ALL).title("Status"))
|
||||||
.block(Block::default().borders(Borders::ALL).title("Volume"))
|
|
||||||
.style(Style::default().fg(Color::White));
|
.style(Style::default().fg(Color::White));
|
||||||
frame.render_widget(volume_widget, chunks[3]);
|
frame.render_widget(status_widget, chunks[0]);
|
||||||
|
|
||||||
// Help
|
// Playlist panel
|
||||||
let help_text = vec![
|
let playlist_items: Vec<ListItem> = state
|
||||||
Line::from(""),
|
.playlist
|
||||||
Line::from(vec![
|
.iter()
|
||||||
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
.enumerate()
|
||||||
Span::raw(" Navigate"),
|
.map(|(idx, path)| {
|
||||||
]),
|
let filename = path
|
||||||
Line::from(vec![
|
.file_name()
|
||||||
Span::styled("h/l", Style::default().fg(Color::Cyan)),
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
Span::raw(" Collapse/expand"),
|
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||||
]),
|
|
||||||
Line::from(vec![
|
let style = if idx == state.playlist_index && state.player_state != PlayerState::Stopped {
|
||||||
Span::styled("t", Style::default().fg(Color::Cyan)),
|
Style::default().fg(Color::Black).bg(Color::Cyan)
|
||||||
Span::raw(" Mark file"),
|
} else {
|
||||||
]),
|
Style::default()
|
||||||
Line::from(vec![
|
};
|
||||||
Span::styled("c", Style::default().fg(Color::Cyan)),
|
|
||||||
Span::raw(" Clear marks"),
|
ListItem::new(filename).style(style)
|
||||||
]),
|
})
|
||||||
Line::from(vec![
|
.collect();
|
||||||
Span::styled("Enter", Style::default().fg(Color::Cyan)),
|
|
||||||
Span::raw(" Play"),
|
let playlist_title = if !state.playlist.is_empty() {
|
||||||
]),
|
format!("Playlist [{}/{}]", state.playlist_index + 1, state.playlist.len())
|
||||||
Line::from(vec![
|
} else {
|
||||||
Span::styled("Space", Style::default().fg(Color::Cyan)),
|
"Playlist (empty)".to_string()
|
||||||
Span::raw(" Pause"),
|
};
|
||||||
]),
|
|
||||||
Line::from(vec![
|
let playlist_widget = List::new(playlist_items)
|
||||||
Span::styled("n/p", Style::default().fg(Color::Cyan)),
|
.block(
|
||||||
Span::raw(" Next/Prev"),
|
Block::default()
|
||||||
]),
|
.borders(Borders::ALL)
|
||||||
Line::from(vec![
|
.title(playlist_title)
|
||||||
Span::styled("r", Style::default().fg(Color::Cyan)),
|
.style(Style::default().fg(Color::White)),
|
||||||
Span::raw(" Rescan"),
|
);
|
||||||
]),
|
|
||||||
Line::from(vec![
|
frame.render_widget(playlist_widget, chunks[1]);
|
||||||
Span::styled("q", Style::default().fg(Color::Cyan)),
|
}
|
||||||
Span::raw(" Quit"),
|
|
||||||
]),
|
fn render_status_bar(frame: &mut Frame, _state: &AppState, area: Rect) {
|
||||||
];
|
let help = Line::from(vec![
|
||||||
let help_widget = Paragraph::new(help_text)
|
Span::styled("j/k", Style::default().fg(Color::Cyan)),
|
||||||
.block(Block::default().borders(Borders::ALL).title("Help"))
|
Span::raw(" Nav | "),
|
||||||
.style(Style::default().fg(Color::White));
|
Span::styled("h/l", Style::default().fg(Color::Cyan)),
|
||||||
frame.render_widget(help_widget, chunks[4]);
|
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