Implement cm-dashboard style layout

- Add title bar at top: 'cm-player • Playing/Stopped' (cyan bg)
- Three-section vertical layout: title, content, status bar
- Content area: left (files) | right (player + playlist)
- Bottom status bar centered with • separators
- Player state moved to title bar
- Progress and volume in Player panel
- Matches cm-dashboard layout structure
This commit is contained in:
Christoffer Martinsson 2025-12-06 13:11:19 +01:00
parent 7f5aa7602d
commit cc86f8eb55

View File

@ -1,6 +1,6 @@
use crate::state::{AppState, PlayerState}; use crate::state::{AppState, PlayerState};
use ratatui::{ use ratatui::{
layout::{Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph}, widgets::{Block, Borders, List, ListItem, Paragraph},
@ -8,19 +8,26 @@ use ratatui::{
}; };
pub fn render(frame: &mut Frame, state: &AppState) { pub fn render(frame: &mut Frame, state: &AppState) {
// Three-section layout: title bar, main content, statusbar (like cm-dashboard)
let main_chunks = Layout::default() let main_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)]) .constraints([
Constraint::Length(1), // Title bar
Constraint::Min(0), // Main content
Constraint::Length(1), // Status bar
])
.split(frame.area()); .split(frame.area());
let top_chunks = Layout::default() // Main content: left (files) | right (status + playlist)
let content_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_chunks[0]); .split(main_chunks[1]);
render_file_panel(frame, state, top_chunks[0]); render_title_bar(frame, state, main_chunks[0]);
render_right_panel(frame, state, top_chunks[1]); render_file_panel(frame, state, content_chunks[0]);
render_status_bar(frame, state, main_chunks[1]); render_right_panel(frame, state, content_chunks[1]);
render_status_bar(frame, state, main_chunks[2]);
} }
fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) { fn render_file_panel(frame: &mut Frame, state: &AppState, area: Rect) {
@ -65,17 +72,7 @@ fn render_right_panel(frame: &mut Frame, state: &AppState, area: Rect) {
.constraints([Constraint::Length(3), Constraint::Min(0)]) .constraints([Constraint::Length(3), Constraint::Min(0)])
.split(area); .split(area);
// Combined status line: State | Progress | Volume // Combined status line: Progress | Volume
let state_text = if state.is_refreshing {
"Refreshing library..."
} else {
match state.player_state {
PlayerState::Stopped => "Stopped",
PlayerState::Playing => "Playing",
PlayerState::Paused => "Paused",
}
};
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;
@ -89,9 +86,9 @@ fn render_right_panel(frame: &mut Frame, state: &AppState, area: Rect) {
"00:00/00:00".to_string() "00:00/00:00".to_string()
}; };
let combined_status = format!("{} | {} | Vol: {}%", state_text, progress_text, state.volume); let combined_status = format!("{} • Vol: {}%", progress_text, state.volume);
let status_widget = Paragraph::new(combined_status) let status_widget = Paragraph::new(combined_status)
.block(Block::default().borders(Borders::ALL).title("Status")) .block(Block::default().borders(Borders::ALL).title("Player"))
.style(Style::default().fg(Color::White)); .style(Style::default().fg(Color::White));
frame.render_widget(status_widget, chunks[0]); frame.render_widget(status_widget, chunks[0]);
@ -133,30 +130,30 @@ fn render_right_panel(frame: &mut Frame, state: &AppState, area: Rect) {
frame.render_widget(playlist_widget, chunks[1]); frame.render_widget(playlist_widget, chunks[1]);
} }
fn render_status_bar(frame: &mut Frame, _state: &AppState, area: Rect) { fn render_title_bar(frame: &mut Frame, state: &AppState, area: Rect) {
let help = Line::from(vec![ let status_text = if state.is_refreshing {
Span::styled("j/k", Style::default().fg(Color::Cyan)), "Refreshing library..."
Span::raw(" Nav | "), } else {
Span::styled("h/l", Style::default().fg(Color::Cyan)), match state.player_state {
Span::raw(" Fold | "), PlayerState::Stopped => "Stopped",
Span::styled("t", Style::default().fg(Color::Cyan)), PlayerState::Playing => "Playing",
Span::raw(" Mark | "), PlayerState::Paused => "Paused",
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) let title_text = format!("cm-player • {}", status_text);
.style(Style::default().fg(Color::White).bg(Color::DarkGray)); let title = Paragraph::new(title_text)
.style(Style::default().fg(Color::Black).bg(Color::Cyan));
frame.render_widget(title, area);
}
fn render_status_bar(frame: &mut Frame, _state: &AppState, area: Rect) {
let shortcuts = "↑↓/jk: Navigate • h/l: Fold • t: Mark • c: Clear • Enter: Play • Space: Pause • n/p: Next/Prev • r: Rescan • q: Quit";
let status_bar = Paragraph::new(shortcuts)
.style(Style::default().fg(Color::White))
.alignment(Alignment::Center);
frame.render_widget(status_bar, area); frame.render_widget(status_bar, area);
} }