Skip to main content

max / audiofiles

8.7 KB · 223 lines History Blame Raw
1 //! Export workflow: collect items from the VFS/selection, configure the
2 //! export, and drive the export/cleanup workers. Owns the export- and
3 //! cleanup-event handling applied in `poll_workers`.
4
5 use super::{BrowserState, ImportMode, NodeId, NodeType};
6
7 impl BrowserState {
8 /// Begin the export workflow: collect items from the current VFS/selection and show config.
9 pub fn start_export_flow(&mut self, node_ids: Option<Vec<NodeId>>) {
10 let Some(vfs_id) = self.current_vfs_id() else {
11 self.status = "No VFS available".to_string();
12 return;
13 };
14
15 let mut items = if let Some(ids) = node_ids {
16 // Export specific selected items: collect subtrees for each
17 let mut all_items = Vec::new();
18 for id in ids {
19 let Ok(node) = self.backend.get_node(id) else {
20 continue;
21 };
22 match node.node_type {
23 NodeType::Directory => {
24 if let Ok(sub_items) = self.backend.collect_export_items(vfs_id, Some(id)) {
25 all_items.extend(sub_items);
26 }
27 }
28 NodeType::Sample => {
29 if let Some(hash) = &node.sample_hash {
30 let ext = self.backend.sample_extension(hash).unwrap_or_default();
31 all_items.push(audiofiles_core::export::ExportItem {
32 hash: hash.clone(),
33 ext,
34 relative_path: std::path::PathBuf::from(&node.name),
35 name: node.name.clone(),
36 bpm: None,
37 musical_key: None,
38 classification: None,
39 duration: None,
40 tags: Vec::new(),
41 source_path: None,
42 });
43 }
44 }
45 }
46 }
47 all_items
48 } else if self.collections_ui.active_collection.is_some() {
49 // Export collection contents
50 self.nav
51 .contents
52 .iter()
53 .filter_map(|node| {
54 let hash = node.node.sample_hash.as_ref()?;
55 let ext = self.backend.sample_extension(hash).unwrap_or_default();
56 Some(audiofiles_core::export::ExportItem {
57 hash: hash.clone(),
58 ext,
59 relative_path: std::path::PathBuf::from(&node.node.name),
60 name: node.node.name.clone(),
61 bpm: node.bpm,
62 musical_key: node.musical_key.clone(),
63 classification: node.classification.clone(),
64 duration: node.duration,
65 tags: node.tags.clone(),
66 source_path: None,
67 })
68 })
69 .collect()
70 } else {
71 // Export entire current VFS subtree
72 self.backend
73 .collect_export_items(vfs_id, self.nav.current_dir)
74 .unwrap_or_default()
75 };
76
77 super::log_backend_err(
78 "enrich_export_with_tags",
79 self.backend.enrich_export_with_tags(&mut items),
80 );
81
82 if items.is_empty() {
83 self.status = "No samples to export".to_string();
84 return;
85 }
86
87 let default_dest = dirs::download_dir()
88 .or_else(dirs::home_dir)
89 .unwrap_or_else(|| std::path::PathBuf::from("."))
90 .join("audiofiles Export");
91
92 let available_profiles = self.backend.list_device_profiles().unwrap_or_default();
93
94 self.import_wf.import_mode = ImportMode::ConfigureExport {
95 items,
96 config: audiofiles_core::export::ExportConfig {
97 format: audiofiles_core::export::ExportFormat::Original,
98 sample_rate: None,
99 bit_depth: None,
100 channels: audiofiles_core::export::ExportChannels::Original,
101 naming_pattern: None,
102 flatten: false,
103 metadata_sidecar: false,
104 destination: default_dest,
105 device_profile: None,
106 naming_rules: None,
107 max_file_size_bytes: None,
108 name_overrides: None,
109 },
110 available_profiles,
111 };
112 }
113
114 /// Spawn the export worker and start processing.
115 pub fn run_export(
116 &mut self,
117 items: Vec<audiofiles_core::export::ExportItem>,
118 config: audiofiles_core::export::ExportConfig,
119 ) {
120 // Stash destination so the cancel-acknowledgement screen can name it
121 // if the user cancels mid-export (C-3). The Exporting variant doesn't
122 // carry destination, it's consumed by the worker, but the path is
123 // still useful in the UI for "files already written remain at <path>".
124 self.import_wf.last_export_destination = Some(config.destination.clone());
125 self.import_wf.operation_progress = Some(crate::state::OperationProgress::new());
126 super::log_backend_err("start_export", self.backend.start_export(items, config));
127
128 self.import_wf.import_mode = ImportMode::Exporting {
129 completed: 0,
130 total: 0,
131 current_name: String::new(),
132 };
133 }
134
135 /// Cancel the running cleanup and return to idle state.
136 pub fn cancel_cleanup(&mut self) {
137 let _ = self.backend.cancel_cleanup();
138 self.import_wf.import_mode = ImportMode::None;
139 self.refresh_vfs_list();
140 self.status = "Cleanup cancelled".to_string();
141 }
142
143 /// Cancel the running export. Lands in the acknowledgement screen (C-3)
144 /// when meaningful progress has happened, surfacing the destination folder
145 /// so the user knows where partial files may sit. Falls through to `None`
146 /// when no items have been written yet.
147 pub fn cancel_export(&mut self) {
148 let progress = match &self.import_wf.import_mode {
149 ImportMode::Exporting {
150 completed, total, ..
151 } if *total > 0 => Some((*completed, *total)),
152 _ => None,
153 };
154 let _ = self.backend.cancel_export();
155 self.status = "Export cancelled".to_string();
156 self.import_wf.import_mode = match progress {
157 Some((completed, total)) => ImportMode::OperationCancelled {
158 kind: crate::state::CancelKind::Export,
159 completed,
160 total,
161 destination: self.import_wf.last_export_destination.clone(),
162 },
163 None => ImportMode::None,
164 };
165 }
166
167 /// Update the Exporting screen as items are written. `poll_workers` handler
168 /// for `ExportProgress`.
169 pub(super) fn on_export_progress(
170 &mut self,
171 completed: usize,
172 total: usize,
173 current_name: String,
174 ) {
175 self.import_wf.import_mode = ImportMode::Exporting {
176 completed,
177 total,
178 current_name,
179 };
180 }
181
182 /// Land on the export-complete screen, surfacing any per-item errors.
183 /// `poll_workers` handler for `ExportComplete`.
184 pub(super) fn on_export_complete(&mut self, total: usize, errors: Vec<(String, String)>) {
185 let error_count = errors.len();
186 if error_count == 0 {
187 self.status = format!("Exported {total} files");
188 } else {
189 self.status = format!("Exported {total} files ({error_count} errors)");
190 }
191 self.import_wf.import_mode = ImportMode::ExportComplete { total, errors };
192 }
193
194 /// Update the Cleaning screen as the library-delete worker progresses.
195 /// `poll_workers` handler for `CleanupProgress`.
196 pub(super) fn on_cleanup_progress(
197 &mut self,
198 completed: usize,
199 total: usize,
200 current_name: String,
201 ) {
202 self.import_wf.import_mode = ImportMode::Cleaning {
203 completed,
204 total,
205 current_name,
206 };
207 }
208
209 /// Report the result of a library delete and return to idle. `poll_workers`
210 /// handler for `CleanupComplete`.
211 pub(super) fn on_cleanup_complete(&mut self, removed: usize, errors: usize) {
212 self.refresh_vfs_list();
213 if errors > 0 {
214 self.status = format!("Library deleted ({removed} samples removed, {errors} errors)");
215 } else if removed > 0 {
216 self.status = format!("Library deleted ({removed} samples removed)");
217 } else {
218 self.status = "Library deleted".to_string();
219 }
220 self.import_wf.import_mode = ImportMode::None;
221 }
222 }
223