Add live file counter to library refresh
All checks were successful
Build and Release / build-and-release (push) Successful in 1m20s
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:
30
src/main.rs
30
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<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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user