Add live file counter to library refresh
All checks were successful
Build and Release / build-and-release (push) Successful in 1m20s

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.
This commit is contained in:
2025-12-13 11:51:32 +01:00
parent 821a844fe0
commit cddfedf1a0
5 changed files with 89 additions and 9 deletions

View File

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

View File

@@ -16,6 +16,10 @@ use ratatui::{backend::CrosstermBackend, Terminal};
use state::{AppState, PlayerState}; use state::{AppState, PlayerState};
use std::io::{self, BufRead, BufReader, Write}; use std::io::{self, BufRead, BufReader, Write};
use std::os::unix::net::UnixStream; 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; use tracing_subscriber;
// UI update intervals and thresholds // UI update intervals and thresholds
@@ -677,7 +681,8 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
KeyCode::Char('y') | KeyCode::Char('Y') => { KeyCode::Char('y') | KeyCode::Char('Y') => {
state.show_refresh_confirm = false; state.show_refresh_confirm = false;
state.is_refreshing = true; 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()?; let cache_dir = cache::get_cache_dir()?;
@@ -685,8 +690,26 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
let _ = std::fs::remove_file(cache_dir.join("file_tree.json")); let _ = std::fs::remove_file(cache_dir.join("file_tree.json"));
let _ = std::fs::remove_file(cache_dir.join("metadata.json")); let _ = std::fs::remove_file(cache_dir.join("metadata.json"));
// Perform fresh scan // Create atomic counter for file count
let new_cache = scanner::scan_paths(&state.config.scan_paths.paths)?; 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)?; new_cache.save(&cache_dir)?;
// Replace old cache completely // Replace old cache completely
@@ -701,6 +724,7 @@ fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
} }
state.is_refreshing = false; state.is_refreshing = false;
state.refresh_file_count = 0;
} }
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.show_refresh_confirm = false; state.show_refresh_confirm = false;

View File

@@ -1,6 +1,7 @@
use crate::cache::{Cache, FileMetadata, FileTreeNode}; use crate::cache::{Cache, FileMetadata, FileTreeNode};
use anyhow::Result; use anyhow::Result;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use walkdir::WalkDir; use walkdir::WalkDir;
const AUDIO_EXTENSIONS: &[&str] = &["mp3", "flac", "wav", "ogg", "m4a", "aac", "opus", "wma"]; 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<FileTreeNode> { fn scan_directory_internal(root_path: &Path, counter: &AtomicUsize) -> Result<FileTreeNode> {
let name = root_path let name = root_path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
@@ -62,13 +63,16 @@ pub fn scan_directory(root_path: &Path) -> Result<FileTreeNode> {
if entry.file_type().is_dir() { if entry.file_type().is_dir() {
// Recursively scan subdirectories // 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 // Only add directory if it contains media files or non-empty subdirectories
if !child_node.children.is_empty() { if !child_node.children.is_empty() {
node.children.push(child_node); node.children.push(child_node);
} }
} }
} else if is_media_file(path) { } else if is_media_file(path) {
// Increment counter for each media file found
counter.fetch_add(1, Ordering::Relaxed);
// Add media file // Add media file
let file_name = path let file_name = path
.file_name() .file_name()
@@ -100,13 +104,13 @@ pub fn scan_directory(root_path: &Path) -> Result<FileTreeNode> {
Ok(node) Ok(node)
} }
pub fn scan_paths(paths: &[PathBuf]) -> Result<Cache> { pub fn scan_paths(paths: &[PathBuf], counter: &AtomicUsize) -> Result<Cache> {
let mut cache = Cache::new(); let mut cache = Cache::new();
for path in paths { for path in paths {
if path.exists() { if path.exists() {
tracing::info!("Scanning path: {:?}", path); 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 all metadata from the tree
collect_metadata(&tree_node, &mut cache); collect_metadata(&tree_node, &mut cache);

View File

@@ -89,6 +89,7 @@ pub struct AppState {
pub context_menu: Option<ContextMenu>, pub context_menu: Option<ContextMenu>,
pub play_mode: PlayMode, pub play_mode: PlayMode,
pub last_error: Option<String>, pub last_error: Option<String>,
pub refresh_file_count: usize,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -143,6 +144,7 @@ impl AppState {
context_menu: None, context_menu: None,
play_mode: PlayMode::Normal, play_mode: PlayMode::Normal,
last_error: None, last_error: None,
refresh_file_count: 0,
} }
} }

View File

@@ -79,7 +79,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &mut Player) -> (
// Show refreshing popup if scanning // Show refreshing popup if scanning
if state.is_refreshing { if state.is_refreshing {
render_info_popup(frame, "Refreshing library..."); render_refresh_popup(frame, state.refresh_file_count);
} }
// Show confirmation popup if needed // Show confirmation popup if needed
@@ -755,6 +755,56 @@ fn render_info_popup(frame: &mut Frame, message: &str) {
frame.render_widget(message_widget, inner); 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) { fn render_confirm_popup(frame: &mut Frame, title: &str, message: &str) {
// Create centered popup area // Create centered popup area
let area = frame.area(); let area = frame.area();