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]
name = "cm-player"
version = "0.1.35"
version = "0.1.36"
edition = "2021"
[dependencies]

View File

@@ -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<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, 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<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("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<B: ratatui::backend::Backend>(terminal: &mut Terminal<B>, st
}
state.is_refreshing = false;
state.refresh_file_count = 0;
}
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
state.show_refresh_confirm = false;

View File

@@ -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<FileTreeNode> {
fn scan_directory_internal(root_path: &Path, counter: &AtomicUsize) -> Result<FileTreeNode> {
let name = root_path
.file_name()
.unwrap_or_default()
@@ -62,13 +63,16 @@ 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) {
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<FileTreeNode> {
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();
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);

View File

@@ -89,6 +89,7 @@ pub struct AppState {
pub context_menu: Option<ContextMenu>,
pub play_mode: PlayMode,
pub last_error: Option<String>,
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,
}
}

View File

@@ -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();