5 Commits

Author SHA1 Message Date
ffe7cd0090 Fix time display to update smoothly
All checks were successful
Build and Release / build-and-release (push) Successful in 54s
Change position update logic to only trigger redraw when the
displayed value (rounded to seconds) changes, not when the raw
float value changes. This eliminates jumpy time display and
reduces unnecessary redraws.
2025-12-09 12:33:52 +01:00
907a734be3 Remove Cache prefix from cache duration display
All checks were successful
Build and Release / build-and-release (push) Successful in 55s
Display cache duration as "1.5s" instead of "Cache:1.5s" in
bottom status bar for cleaner presentation alongside other
technical metrics.
2025-12-09 12:23:04 +01:00
135700ce02 Update cache metric refresh rate to match other metadata
All checks were successful
Build and Release / build-and-release (push) Successful in 54s
Move cache duration update from update_properties (~10Hz) to
update_metadata (~0.5Hz) to match the refresh rate of codec,
bitrate, and sample rate. All bottom status bar metrics now
update at the same frequency.
2025-12-09 12:06:59 +01:00
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 59 additions and 31 deletions

View File

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

View File

@@ -183,8 +183,10 @@ async fn run_app<B: ratatui::backend::Backend>(
let new_position = player.get_position().unwrap_or(0.0);
let new_duration = player.get_duration().unwrap_or(0.0);
// Only mark as changed if position moved by at least 0.5 seconds
if (new_position - last_position).abs() >= 0.5 {
// Only update if displayed value (rounded to seconds) changed
let old_display_secs = last_position as u32;
let new_display_secs = new_position as u32;
if new_display_secs != old_display_secs {
state.current_position = new_position;
last_position = new_position;
state_changed = true;
@@ -348,7 +350,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 +702,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));
@@ -303,6 +306,13 @@ impl Player {
if let Some(val) = self.get_property("audio-params/samplerate") {
self.sample_rate = val.as_i64();
}
// 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 get_position(&self) -> Option<f64> {

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!("{:.1}s", cache_dur));
}
}
// Create layout for left and right sections
let chunks = Layout::default()
.direction(Direction::Horizontal)