cm-player/src/ui/mod.rs
Christoffer Martinsson 8104f54887 Add vim bindings and directory expand/collapse
- Replace arrow keys with j/k for navigation
- Add h/l for collapse/expand directories
- Remove emoji icons, use clean text markers
- Show directories with [-]/[+] expand markers
- Track expanded state per directory path
- Add directory suffix (/) for clarity
- Update help text with vim bindings
2025-12-06 12:39:11 +01:00

151 lines
5.1 KiB
Rust

use crate::state::{AppState, PlayerState};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
Frame,
};
pub fn render(frame: &mut Frame, state: &AppState) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.area());
render_file_panel(frame, state, chunks[0]);
render_status_panel(frame, state, chunks[1]);
}
fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) {
let items: Vec<ListItem> = state
.flattened_items
.iter()
.enumerate()
.map(|(idx, item)| {
let indent = " ".repeat(item.depth);
let expand_marker = if item.node.is_dir {
if item.is_expanded { "[-] " } else { "[+] " }
} else {
" "
};
let suffix = if item.node.is_dir { "/" } else { "" };
let text = format!("{}{}{}{}", indent, expand_marker, item.node.name, suffix);
let style = if idx == state.selected_index {
Style::default().fg(Color::Black).bg(Color::Cyan)
} else if item.node.is_dir {
Style::default().fg(Color::Blue)
} else {
Style::default()
};
ListItem::new(text).style(style)
})
.collect();
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title("Media Files (Cached)")
.style(Style::default().fg(Color::White)),
);
frame.render_widget(list, area);
}
fn render_status_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),
])
.split(area);
// Player state
let state_text = match state.player_state {
PlayerState::Stopped => "⏹ Stopped",
PlayerState::Playing => "▶ Playing",
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
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 file_widget = Paragraph::new(current_file)
.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}",
position_mins, position_secs, duration_mins, duration_secs
)
} else {
"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"))
.style(Style::default().fg(Color::White));
frame.render_widget(volume_widget, chunks[3]);
// Help
let help_text = vec![
Line::from(""),
Line::from(vec![
Span::styled("j/k", Style::default().fg(Color::Cyan)),
Span::raw(" Navigate down/up"),
]),
Line::from(vec![
Span::styled("h/l", Style::default().fg(Color::Cyan)),
Span::raw(" Collapse/expand dir"),
]),
Line::from(vec![
Span::styled("Enter", Style::default().fg(Color::Cyan)),
Span::raw(" Play file"),
]),
Line::from(vec![
Span::styled("Space", Style::default().fg(Color::Cyan)),
Span::raw(" Pause/Resume"),
]),
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]);
}