2 Commits

Author SHA1 Message Date
ea72368841 Remove buffer mode feature and relocate cache metrics
All checks were successful
Build and Release / build-and-release (push) Successful in 56s
- Remove buffer mode toggle (Normal/Large/Huge) as demuxer settings
  do not significantly impact local file playback
- Move cache duration metric from title bar to bottom status bar
- Display cache alongside codec, bitrate, and sample rate info
- Remove 'b' key binding and Buffer context menu option
- Update version to 0.1.15
2025-12-09 11:51:51 +01:00
ed6765039c Add nerd font icons and UI polish
All checks were successful
Build and Release / build-and-release (push) Successful in 1m18s
- Add nerd font file type icons:
  - Folder icons: closed/open folders with visual state
  - Music files: icon in green (mp3, flac, wav, ogg, etc.)
  - Video files: icon in yellow (mp4, mkv, avi, mov, etc.)
- Add spacing after icons for better readability

- Add "Refresh" option to title bar right-click menu
- Make all context menus more compact (13 chars wide)

- Change panel titles to lowercase:
  - "Media Files" → "files"
  - "Playlist" → "playlist"
- Remove bold styling from focused panel titles

- All icons show as bold black on selection bar
- Folders show in blue, music in green, video in yellow
2025-12-08 23:07:28 +01:00
4 changed files with 55 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "cm-player"
version = "0.1.13"
version = "0.1.15"
edition = "2021"
[dependencies]

View File

@@ -348,7 +348,7 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
let max_items = match menu.menu_type {
ContextMenuType::FilePanel => 2,
ContextMenuType::Playlist => 2,
ContextMenuType::TitleBar => 3,
ContextMenuType::TitleBar => 4,
};
if menu.selected_index < max_items - 1 {
menu.selected_index += 1;
@@ -700,7 +700,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
let items = match menu.menu_type {
ContextMenuType::FilePanel => 2,
ContextMenuType::Playlist => 2,
ContextMenuType::TitleBar => 3,
ContextMenuType::TitleBar => 4,
};
let popup_width = 13;
let popup_height = items as u16 + 2; // +2 for borders

View File

@@ -21,6 +21,7 @@ pub struct Player {
pub audio_codec: Option<String>,
pub audio_bitrate: Option<f64>,
pub sample_rate: Option<i64>,
pub cache_duration: Option<f64>,
}
impl Player {
@@ -62,6 +63,7 @@ impl Player {
audio_codec: None,
audio_bitrate: None,
sample_rate: None,
cache_duration: None,
})
}
@@ -119,6 +121,7 @@ impl Player {
self.audio_codec = None;
self.audio_bitrate = None;
self.sample_rate = None;
self.cache_duration = None;
// Wait for socket to be created and mpv to be ready
std::thread::sleep(Duration::from_millis(800));
@@ -246,6 +249,13 @@ impl Player {
self.is_idle = idle;
}
}
// Update cache duration (how many seconds are buffered ahead)
if let Some(val) = self.get_property("demuxer-cache-duration") {
self.cache_duration = val.as_f64();
} else {
self.cache_duration = None;
}
}
pub fn update_metadata(&mut self) {

View File

@@ -34,7 +34,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &Player) -> (Rect
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(main_chunks[1]);
render_title_bar(frame, state, main_chunks[0]);
render_title_bar(frame, state, player, main_chunks[0]);
render_file_panel(frame, state, content_chunks[0]);
render_right_panel(frame, state, content_chunks[1]);
render_status_bar(frame, state, player, main_chunks[2]);
@@ -147,16 +147,40 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
// Only show selection bar when file panel has focus
let is_selected = !state.focus_playlist && idx == state.selected_index;
// Add folder icon for directories
// Add icon for directories and files
let icon = if item.node.is_dir {
let is_expanded = state.expanded_dirs.contains(&item.node.path);
// Nerd font folder icons: \u{eaf7} = open, \u{ea83} = closed
let icon_char = if is_expanded { "\u{eaf7} " } else { "\u{ea83} " };
// Bold black icon on selection bar, blue otherwise
if is_selected {
Span::styled("", Style::default().fg(Theme::background()).add_modifier(Modifier::BOLD))
Span::styled(icon_char, Style::default().fg(Theme::background()).add_modifier(Modifier::BOLD))
} else {
Span::styled("", Style::default().fg(Theme::highlight()))
Span::styled(icon_char, Style::default().fg(Theme::highlight()))
}
} else {
Span::raw(" ")
// File icons based on extension
let extension = item.node.path.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
let (icon_char, color) = match extension.as_str() {
// Audio files - music note icon
"mp3" | "flac" | "wav" | "ogg" | "m4a" | "aac" | "wma" | "opus" =>
("\u{f0e2a} ", Theme::success()), //
// Video files - film icon
"mp4" | "mkv" | "avi" | "mov" | "webm" | "flv" | "wmv" | "m4v" =>
("\u{f1c8} ", Theme::warning()), //
_ => (" ", Theme::foreground()),
};
if is_selected {
Span::styled(icon_char, Style::default().fg(Theme::background()).add_modifier(Modifier::BOLD))
} else {
Span::styled(icon_char, Style::default().fg(color))
}
};
let name_spans = if in_search && !search_query.is_empty() {
highlight_search_matches(&item.node.name, &search_query, is_selected)
@@ -201,20 +225,13 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
items.push(more_item);
}
let title_style = if !state.focus_playlist {
// File panel has focus - bold title
Theme::title_style().add_modifier(Modifier::BOLD)
} else {
Theme::title_style()
};
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.title("Media Files")
.title("files")
.style(Theme::widget_border_style())
.title_style(title_style),
.title_style(Theme::title_style()),
);
let mut list_state = ListState::default();
@@ -311,16 +328,9 @@ fn render_right_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
}
let playlist_title = if !state.playlist.is_empty() {
format!("Playlist [{}/{}]", state.playlist_index + 1, state.playlist.len())
format!("playlist [{}/{}]", state.playlist_index + 1, state.playlist.len())
} else {
"Playlist (empty)".to_string()
};
let playlist_title_style = if state.focus_playlist {
// Playlist has focus - bold title
Theme::title_style().add_modifier(Modifier::BOLD)
} else {
Theme::title_style()
"playlist (empty)".to_string()
};
let playlist_widget = List::new(playlist_items)
@@ -329,7 +339,7 @@ fn render_right_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
.borders(Borders::ALL)
.title(playlist_title)
.style(Theme::widget_border_style())
.title_style(playlist_title_style),
.title_style(Theme::title_style()),
);
let mut playlist_state = ListState::default();
@@ -339,7 +349,7 @@ fn render_right_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
frame.render_stateful_widget(playlist_widget, area, &mut playlist_state);
}
fn render_title_bar(frame: &mut Frame, state: &AppState, area: Rect) {
fn render_title_bar(frame: &mut Frame, state: &AppState, _player: &Player, area: Rect) {
let background_color = match state.player_state {
PlayerState::Playing => Theme::success(), // Green for playing
PlayerState::Paused => Theme::highlight(), // Blue for paused
@@ -511,7 +521,7 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &Player, area:
left_parts.push(title.clone());
}
// Right side: Bitrate | Codec | Sample rate
// Right side: Bitrate | Codec | Sample rate | Cache
if let Some(bitrate) = player.audio_bitrate {
right_parts.push(format!("{:.0} kbps", bitrate));
}
@@ -524,6 +534,12 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &Player, area:
right_parts.push(format!("{} Hz", samplerate));
}
if let Some(cache_dur) = player.cache_duration {
if cache_dur > 0.0 {
right_parts.push(format!("Cache:{:.1}s", cache_dur));
}
}
// Create layout for left and right sections
let chunks = Layout::default()
.direction(Direction::Horizontal)