//! Export workflow: collect items from the VFS/selection, configure the //! export, and drive the export/cleanup workers. Owns the export- and //! cleanup-event handling applied in `poll_workers`. use super::{BrowserState, ImportMode, NodeId, NodeType}; impl BrowserState { /// Begin the export workflow: collect items from the current VFS/selection and show config. pub fn start_export_flow(&mut self, node_ids: Option>) { let Some(vfs_id) = self.current_vfs_id() else { self.status = "No VFS available".to_string(); return; }; let mut items = if let Some(ids) = node_ids { // Export specific selected items: collect subtrees for each let mut all_items = Vec::new(); for id in ids { let Ok(node) = self.backend.get_node(id) else { continue; }; match node.node_type { NodeType::Directory => { if let Ok(sub_items) = self.backend.collect_export_items(vfs_id, Some(id)) { all_items.extend(sub_items); } } NodeType::Sample => { if let Some(hash) = &node.sample_hash { let ext = self.backend.sample_extension(hash).unwrap_or_default(); all_items.push(audiofiles_core::export::ExportItem { hash: hash.clone(), ext, relative_path: std::path::PathBuf::from(&node.name), name: node.name.clone(), bpm: None, musical_key: None, classification: None, duration: None, tags: Vec::new(), source_path: None, }); } } } } all_items } else if self.collections_ui.active_collection.is_some() { // Export collection contents self.nav .contents .iter() .filter_map(|node| { let hash = node.node.sample_hash.as_ref()?; let ext = self.backend.sample_extension(hash).unwrap_or_default(); Some(audiofiles_core::export::ExportItem { hash: hash.clone(), ext, relative_path: std::path::PathBuf::from(&node.node.name), name: node.node.name.clone(), bpm: node.bpm, musical_key: node.musical_key.clone(), classification: node.classification.clone(), duration: node.duration, tags: node.tags.clone(), source_path: None, }) }) .collect() } else { // Export entire current VFS subtree self.backend .collect_export_items(vfs_id, self.nav.current_dir) .unwrap_or_default() }; super::log_backend_err( "enrich_export_with_tags", self.backend.enrich_export_with_tags(&mut items), ); if items.is_empty() { self.status = "No samples to export".to_string(); return; } let default_dest = dirs::download_dir() .or_else(dirs::home_dir) .unwrap_or_else(|| std::path::PathBuf::from(".")) .join("audiofiles Export"); let available_profiles = self.backend.list_device_profiles().unwrap_or_default(); self.import_wf.import_mode = ImportMode::ConfigureExport { items, config: audiofiles_core::export::ExportConfig { format: audiofiles_core::export::ExportFormat::Original, sample_rate: None, bit_depth: None, channels: audiofiles_core::export::ExportChannels::Original, naming_pattern: None, flatten: false, metadata_sidecar: false, destination: default_dest, device_profile: None, naming_rules: None, max_file_size_bytes: None, name_overrides: None, }, available_profiles, }; } /// Spawn the export worker and start processing. pub fn run_export( &mut self, items: Vec, config: audiofiles_core::export::ExportConfig, ) { // Stash destination so the cancel-acknowledgement screen can name it // if the user cancels mid-export (C-3). The Exporting variant doesn't // carry destination, it's consumed by the worker, but the path is // still useful in the UI for "files already written remain at ". self.import_wf.last_export_destination = Some(config.destination.clone()); self.import_wf.operation_progress = Some(crate::state::OperationProgress::new()); super::log_backend_err("start_export", self.backend.start_export(items, config)); self.import_wf.import_mode = ImportMode::Exporting { completed: 0, total: 0, current_name: String::new(), }; } /// Cancel the running cleanup and return to idle state. pub fn cancel_cleanup(&mut self) { let _ = self.backend.cancel_cleanup(); self.import_wf.import_mode = ImportMode::None; self.refresh_vfs_list(); self.status = "Cleanup cancelled".to_string(); } /// Cancel the running export. Lands in the acknowledgement screen (C-3) /// when meaningful progress has happened, surfacing the destination folder /// so the user knows where partial files may sit. Falls through to `None` /// when no items have been written yet. pub fn cancel_export(&mut self) { let progress = match &self.import_wf.import_mode { ImportMode::Exporting { completed, total, .. } if *total > 0 => Some((*completed, *total)), _ => None, }; let _ = self.backend.cancel_export(); self.status = "Export cancelled".to_string(); self.import_wf.import_mode = match progress { Some((completed, total)) => ImportMode::OperationCancelled { kind: crate::state::CancelKind::Export, completed, total, destination: self.import_wf.last_export_destination.clone(), }, None => ImportMode::None, }; } /// Update the Exporting screen as items are written. `poll_workers` handler /// for `ExportProgress`. pub(super) fn on_export_progress( &mut self, completed: usize, total: usize, current_name: String, ) { self.import_wf.import_mode = ImportMode::Exporting { completed, total, current_name, }; } /// Land on the export-complete screen, surfacing any per-item errors. /// `poll_workers` handler for `ExportComplete`. pub(super) fn on_export_complete(&mut self, total: usize, errors: Vec<(String, String)>) { let error_count = errors.len(); if error_count == 0 { self.status = format!("Exported {total} files"); } else { self.status = format!("Exported {total} files ({error_count} errors)"); } self.import_wf.import_mode = ImportMode::ExportComplete { total, errors }; } /// Update the Cleaning screen as the library-delete worker progresses. /// `poll_workers` handler for `CleanupProgress`. pub(super) fn on_cleanup_progress( &mut self, completed: usize, total: usize, current_name: String, ) { self.import_wf.import_mode = ImportMode::Cleaning { completed, total, current_name, }; } /// Report the result of a library delete and return to idle. `poll_workers` /// handler for `CleanupComplete`. pub(super) fn on_cleanup_complete(&mut self, removed: usize, errors: usize) { self.refresh_vfs_list(); if errors > 0 { self.status = format!("Library deleted ({removed} samples removed, {errors} errors)"); } else if removed > 0 { self.status = format!("Library deleted ({removed} samples removed)"); } else { self.status = "Library deleted".to_string(); } self.import_wf.import_mode = ImportMode::None; } }