3 Commits

Author SHA1 Message Date
be9ee8c005 Move refresh status to centered popup
All checks were successful
Build and Release / build-and-release (push) Successful in 54s
Display "Refreshing library..." in a centered popup overlay instead
of showing it in the title bar. This makes the refresh status more
prominent and cleaner.
2025-12-12 15:45:12 +01:00
7c083cfb0e Filter out empty directories during library scan
All checks were successful
Build and Release / build-and-release (push) Successful in 54s
Skip directories that contain no media files or non-empty subdirectories.
This prevents empty folders from appearing in the file list, which can
occur when NFS cache is stale or when directories are emptied.
2025-12-12 15:34:29 +01:00
b438065c23 Use small triangle arrow for playlist playing indicator
All checks were successful
Build and Release / build-and-release (push) Successful in 54s
Replace large arrow with small triangle (▸) to match the style
used for directory indicators before nerd fonts were introduced.
2025-12-12 13:08:00 +01:00
5 changed files with 143 additions and 17 deletions

View File

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

View File

@@ -661,14 +661,29 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
state.show_refresh_confirm = false;
state.is_refreshing = true;
terminal.draw(|f| { let _ = ui::render(f, state, player); })?; // Show "Refreshing library..." immediately
tracing::info!("Rescanning...");
let cache_dir = cache::get_cache_dir()?;
// Delete old cache files to ensure fresh scan
let _ = std::fs::remove_file(cache_dir.join("file_tree.json"));
let _ = std::fs::remove_file(cache_dir.join("metadata.json"));
// Perform fresh scan
let new_cache = scanner::scan_paths(&state.config.scan_paths.paths)?;
new_cache.save(&cache_dir)?;
// Replace old cache completely
state.cache = new_cache;
state.refresh_flattened_items();
state.refresh_flattened_items(); // This also cleans up playlist and expanded_dirs
// If current file was removed, stop playback
if state.current_file.is_none() {
player.stop()?;
state.current_position = 0.0;
state.current_duration = 0.0;
}
state.is_refreshing = false;
tracing::info!("Rescan complete");
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.show_refresh_confirm = false;

View File

@@ -63,7 +63,10 @@ pub fn scan_directory(root_path: &Path) -> Result<FileTreeNode> {
if entry.file_type().is_dir() {
// Recursively scan subdirectories
if let Ok(child_node) = scan_directory(path) {
node.children.push(child_node);
// Only add directory if it contains media files or non-empty subdirectories
if !child_node.children.is_empty() {
node.children.push(child_node);
}
}
} else if is_media_file(path) {
// Add media file

View File

@@ -587,8 +587,84 @@ impl AppState {
}
pub fn refresh_flattened_items(&mut self) {
// Keep current expanded state after rescan
// Clean up expanded_dirs - remove paths that no longer exist in new cache
self.cleanup_expanded_dirs();
// Rebuild view with cleaned expanded state
self.rebuild_flattened_items();
// Clean up playlist - remove files that no longer exist in cache
self.cleanup_playlist();
}
fn cleanup_expanded_dirs(&mut self) {
// Build a set of valid directory paths from the cache
let mut valid_dirs = std::collections::HashSet::new();
fn collect_dirs(node: &crate::cache::FileTreeNode, dirs: &mut std::collections::HashSet<std::path::PathBuf>) {
if node.is_dir {
dirs.insert(node.path.clone());
}
for child in &node.children {
collect_dirs(child, dirs);
}
}
for root in &self.cache.file_tree {
collect_dirs(root, &mut valid_dirs);
}
// Remove invalid paths from expanded_dirs
let original_len = self.expanded_dirs.len();
self.expanded_dirs.retain(|path| valid_dirs.contains(path));
if self.expanded_dirs.len() < original_len {
tracing::info!("Cleaned up expanded_dirs: removed {} invalid paths", original_len - self.expanded_dirs.len());
}
}
fn cleanup_playlist(&mut self) {
// Build a set of valid paths from the cache for fast lookup
let mut valid_paths = std::collections::HashSet::new();
fn collect_paths(node: &crate::cache::FileTreeNode, paths: &mut std::collections::HashSet<std::path::PathBuf>) {
if !node.is_dir {
paths.insert(node.path.clone());
}
for child in &node.children {
collect_paths(child, paths);
}
}
for root in &self.cache.file_tree {
collect_paths(root, &mut valid_paths);
}
// Check if current file is invalid
let current_file_invalid = if let Some(ref current) = self.current_file {
!valid_paths.contains(current)
} else {
false
};
if current_file_invalid {
self.current_file = None;
tracing::info!("Current playing file was deleted, cleared current_file");
}
// Remove files from playlist that don't exist in cache
let original_len = self.playlist.len();
self.playlist.retain(|path| valid_paths.contains(path));
// Adjust indices if playlist was modified
if self.playlist.len() < original_len {
// Ensure playlist_index is valid
if self.playlist_index >= self.playlist.len() && !self.playlist.is_empty() {
self.playlist_index = self.playlist.len() - 1;
}
// Ensure selected_playlist_index is valid
if self.selected_playlist_index >= self.playlist.len() && !self.playlist.is_empty() {
self.selected_playlist_index = self.playlist.len() - 1;
}
tracing::info!("Cleaned up playlist: removed {} deleted files", original_len - self.playlist.len());
}
}
pub fn rebuild_flattened_items(&mut self) {

View File

@@ -77,6 +77,11 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &mut Player) -> (
render_status_bar(frame, state, player, main_chunks[2]);
// Show refreshing popup if scanning
if state.is_refreshing {
render_info_popup(frame, "Refreshing library...");
}
// Show confirmation popup if needed
if state.show_refresh_confirm {
render_confirm_popup(frame, "Refresh library?", "This may take a while");
@@ -322,7 +327,7 @@ fn render_right_panel(frame: &mut Frame, state: &mut AppState, area: Rect, _tab_
let is_playing = idx == state.playlist_index;
// Add playing indicator arrow
let indicator = if is_playing { " " } else { " " };
let indicator = if is_playing { " " } else { " " };
// Build line with search highlighting if searching
let mut line_spans = vec![Span::raw(indicator)];
@@ -409,16 +414,7 @@ fn render_title_bar(frame: &mut Frame, state: &AppState, player: &mut Player, ar
// Right side: Status • Progress • Volume • Search (if active)
let mut right_spans = Vec::new();
if state.is_refreshing {
// Show only "Refreshing library..." when refreshing
right_spans.push(Span::styled(
"Refreshing library... ",
Style::default()
.fg(Theme::background())
.bg(background_color)
.add_modifier(Modifier::BOLD)
));
} else {
{
// Status (bold when playing)
let status_text = match player_state {
PlayerState::Stopped => "Stopped",
@@ -724,6 +720,42 @@ fn render_progress_bar(frame: &mut Frame, _state: &AppState, player: &mut Player
frame.render_widget(progress_widget, area);
}
fn render_info_popup(frame: &mut Frame, message: &str) {
// Create centered popup area - smaller than confirm popup
let area = frame.area();
let popup_width = 40;
let popup_height = 3;
let popup_area = Rect {
x: (area.width.saturating_sub(popup_width)) / 2,
y: (area.height.saturating_sub(popup_height)) / 2,
width: popup_width.min(area.width),
height: popup_height.min(area.height),
};
// Use Clear widget to completely erase the background
frame.render_widget(Clear, popup_area);
// Render the popup block with solid background
let block = Block::default()
.borders(Borders::ALL)
.style(Style::default()
.bg(Theme::background())
.fg(Theme::bright_foreground()));
let inner = block.inner(popup_area);
frame.render_widget(block, popup_area);
// Render message centered
let message_widget = Paragraph::new(message)
.alignment(Alignment::Center)
.style(Style::default()
.fg(Theme::bright_foreground())
.bg(Theme::background()));
frame.render_widget(message_widget, inner);
}
fn render_confirm_popup(frame: &mut Frame, title: &str, message: &str) {
// Create centered popup area
let area = frame.area();