From cddfedf1a0de79215905d693a758bb038c403b59 Mon Sep 17 00:00:00 2001 From: Christoffer Martinsson Date: Sat, 13 Dec 2025 11:51:32 +0100 Subject: [PATCH] Add live file counter to library refresh Show real-time progress during library scanning with an atomic counter that updates every 100ms. The refresh popup displays the number of media files found as they are discovered, providing immediate feedback without slowing down the scan operation. --- Cargo.toml | 2 +- src/main.rs | 30 +++++++++++++++++++++++--- src/scanner/mod.rs | 12 +++++++---- src/state/mod.rs | 2 ++ src/ui/mod.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 557c0d7..b3d1664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-player" -version = "0.1.35" +version = "0.1.36" edition = "2021" [dependencies] diff --git a/src/main.rs b/src/main.rs index 6543f40..0d287a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,10 @@ use ratatui::{backend::CrosstermBackend, Terminal}; use state::{AppState, PlayerState}; use std::io::{self, BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread; +use std::time::Duration; use tracing_subscriber; // UI update intervals and thresholds @@ -677,7 +681,8 @@ fn handle_key_event(terminal: &mut Terminal, st KeyCode::Char('y') | KeyCode::Char('Y') => { state.show_refresh_confirm = false; state.is_refreshing = true; - terminal.draw(|f| { let _ = ui::render(f, state, player); })?; // Show "Refreshing library..." immediately + state.refresh_file_count = 0; + terminal.draw(|f| { let _ = ui::render(f, state, player); })?; let cache_dir = cache::get_cache_dir()?; @@ -685,8 +690,26 @@ fn handle_key_event(terminal: &mut Terminal, st 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)?; + // Create atomic counter for file count + let counter = Arc::new(AtomicUsize::new(0)); + let counter_clone = Arc::clone(&counter); + + // Spawn background thread to perform scan + let paths = state.config.scan_paths.paths.clone(); + let scan_thread = thread::spawn(move || { + scanner::scan_paths(&paths, &counter_clone) + }); + + // Poll counter and update UI while scanning + while !scan_thread.is_finished() { + state.refresh_file_count = counter.load(Ordering::Relaxed); + terminal.draw(|f| { let _ = ui::render(f, state, player); })?; + thread::sleep(Duration::from_millis(100)); + } + + // Get the result + let new_cache = scan_thread.join().map_err(|_| anyhow::anyhow!("Scan thread panicked"))??; + new_cache.save(&cache_dir)?; // Replace old cache completely @@ -701,6 +724,7 @@ fn handle_key_event(terminal: &mut Terminal, st } state.is_refreshing = false; + state.refresh_file_count = 0; } KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { state.show_refresh_confirm = false; diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 5769461..6647ffa 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -1,6 +1,7 @@ use crate::cache::{Cache, FileMetadata, FileTreeNode}; use anyhow::Result; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; use walkdir::WalkDir; const AUDIO_EXTENSIONS: &[&str] = &["mp3", "flac", "wav", "ogg", "m4a", "aac", "opus", "wma"]; @@ -33,7 +34,7 @@ pub fn is_video_file(path: &Path) -> bool { } } -pub fn scan_directory(root_path: &Path) -> Result { +fn scan_directory_internal(root_path: &Path, counter: &AtomicUsize) -> Result { let name = root_path .file_name() .unwrap_or_default() @@ -62,13 +63,16 @@ pub fn scan_directory(root_path: &Path) -> Result { if entry.file_type().is_dir() { // Recursively scan subdirectories - if let Ok(child_node) = scan_directory(path) { + if let Ok(child_node) = scan_directory_internal(path, counter) { // 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) { + // Increment counter for each media file found + counter.fetch_add(1, Ordering::Relaxed); + // Add media file let file_name = path .file_name() @@ -100,13 +104,13 @@ pub fn scan_directory(root_path: &Path) -> Result { Ok(node) } -pub fn scan_paths(paths: &[PathBuf]) -> Result { +pub fn scan_paths(paths: &[PathBuf], counter: &AtomicUsize) -> Result { let mut cache = Cache::new(); for path in paths { if path.exists() { tracing::info!("Scanning path: {:?}", path); - let tree_node = scan_directory(path)?; + let tree_node = scan_directory_internal(path, counter)?; // Collect all metadata from the tree collect_metadata(&tree_node, &mut cache); diff --git a/src/state/mod.rs b/src/state/mod.rs index 9983353..bfab6d4 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -89,6 +89,7 @@ pub struct AppState { pub context_menu: Option, pub play_mode: PlayMode, pub last_error: Option, + pub refresh_file_count: usize, } #[derive(Debug, Clone)] @@ -143,6 +144,7 @@ impl AppState { context_menu: None, play_mode: PlayMode::Normal, last_error: None, + refresh_file_count: 0, } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 074a470..7b6c00d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -79,7 +79,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &mut Player) -> ( // Show refreshing popup if scanning if state.is_refreshing { - render_info_popup(frame, "Refreshing library..."); + render_refresh_popup(frame, state.refresh_file_count); } // Show confirmation popup if needed @@ -755,6 +755,56 @@ fn render_info_popup(frame: &mut Frame, message: &str) { frame.render_widget(message_widget, inner); } +fn render_refresh_popup(frame: &mut Frame, file_count: usize) { + // Create centered popup area - bigger for two lines + let area = frame.area(); + let popup_width = 50; + let popup_height = 5; + + 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); + + // Build two-line message + let lines = if file_count > 0 { + vec![ + Line::from("Refreshing library..."), + Line::from(""), + Line::from(format!("{} files found", file_count)) + .style(Style::default().fg(Theme::highlight())), + ] + } else { + vec![ + Line::from("Refreshing library..."), + ] + }; + + // Render message centered + let message_widget = Paragraph::new(lines) + .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();