| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
use std::fs; |
| 7 |
use std::path::{Path, PathBuf}; |
| 8 |
use std::sync::Arc; |
| 9 |
use std::sync::atomic::{AtomicU32, AtomicU64}; |
| 10 |
use std::time::Instant; |
| 11 |
|
| 12 |
use tracing::{error, warn}; |
| 13 |
|
| 14 |
use audiofiles_core::analysis::config::AnalysisConfig; |
| 15 |
use audiofiles_core::analysis::waveform::WaveformData; |
| 16 |
use audiofiles_core::analysis::AnalysisResult; |
| 17 |
use audiofiles_core::db::Database; |
| 18 |
use audiofiles_core::error::CoreError; |
| 19 |
use audiofiles_core::collections::Collection; |
| 20 |
use audiofiles_core::search::SearchFilter; |
| 21 |
use audiofiles_core::store::SampleStore; |
| 22 |
use audiofiles_core::util::split_name_ext; |
| 23 |
use audiofiles_core::vfs::{NodeType, Vfs, VfsNode}; |
| 24 |
use audiofiles_core::{CollectionId, NodeId, VfsId}; |
| 25 |
pub use audiofiles_core::vfs::VfsNodeWithAnalysis; |
| 26 |
use parking_lot::Mutex; |
| 27 |
|
| 28 |
use crate::backend::{Backend, DirectBackend, ImportStrategyDesc}; |
| 29 |
|
| 30 |
use crate::import::{ImportedFolder, ImportStrategy}; |
| 31 |
use crate::instrument::InstrumentPlayback; |
| 32 |
use crate::preview::PreviewPlayback; |
| 33 |
|
| 34 |
mod navigation; |
| 35 |
pub mod import_workflow; |
| 36 |
mod bulk_ops; |
| 37 |
mod forge; |
| 38 |
mod library; |
| 39 |
mod playback; |
| 40 |
mod ui; |
| 41 |
|
| 42 |
#[cfg(test)] |
| 43 |
mod tests; |
| 44 |
|
| 45 |
|
| 46 |
pub use ui::*; |
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
pub struct SharedState { |
| 51 |
|
| 52 |
pub preview: Mutex<PreviewPlayback>, |
| 53 |
|
| 54 |
pub instrument: Mutex<InstrumentPlayback>, |
| 55 |
|
| 56 |
pub device_sample_rate: AtomicU32, |
| 57 |
|
| 58 |
pub midi_recent_notes: Mutex<Vec<MidiNoteEvent>>, |
| 59 |
|
| 60 |
|
| 61 |
pub decode_generation: AtomicU64, |
| 62 |
|
| 63 |
|
| 64 |
|
| 65 |
|
| 66 |
pub preview_device_name: Mutex<Option<String>>, |
| 67 |
} |
| 68 |
|
| 69 |
impl Default for SharedState { |
| 70 |
fn default() -> Self { |
| 71 |
Self { |
| 72 |
preview: Mutex::new(PreviewPlayback::new()), |
| 73 |
instrument: Mutex::new(InstrumentPlayback::new(8)), |
| 74 |
device_sample_rate: AtomicU32::new(44100), |
| 75 |
midi_recent_notes: Mutex::new(Vec::new()), |
| 76 |
decode_generation: AtomicU64::new(0), |
| 77 |
preview_device_name: Mutex::new(None), |
| 78 |
} |
| 79 |
} |
| 80 |
} |
| 81 |
|
| 82 |
impl SharedState { |
| 83 |
|
| 84 |
pub fn new() -> Self { |
| 85 |
Self::default() |
| 86 |
} |
| 87 |
} |
| 88 |
|
| 89 |
|
| 90 |
pub struct BrowserState { |
| 91 |
pub data_dir: PathBuf, |
| 92 |
pub backend: Box<dyn Backend>, |
| 93 |
|
| 94 |
|
| 95 |
pub vfs_list: Arc<Vec<Vfs>>, |
| 96 |
pub current_vfs_idx: usize, |
| 97 |
pub current_dir: Option<NodeId>, |
| 98 |
pub breadcrumb: Vec<VfsNode>, |
| 99 |
pub contents: Arc<Vec<VfsNodeWithAnalysis>>, |
| 100 |
pub selection: Selection, |
| 101 |
pub selected_tags: Arc<Vec<String>>, |
| 102 |
pub status: String, |
| 103 |
|
| 104 |
|
| 105 |
|
| 106 |
|
| 107 |
pub status_set_at: Option<Instant>, |
| 108 |
|
| 109 |
|
| 110 |
pub selected_analysis: Option<AnalysisResult>, |
| 111 |
pub selected_waveform: Option<WaveformData>, |
| 112 |
pub tag_input: String, |
| 113 |
pub detail_visible: bool, |
| 114 |
pub sidebar_visible: bool, |
| 115 |
|
| 116 |
|
| 117 |
pub sort_column: SortColumn, |
| 118 |
pub sort_direction: SortDirection, |
| 119 |
|
| 120 |
|
| 121 |
pub search_query: String, |
| 122 |
pub search_filter: SearchFilter, |
| 123 |
pub filter_panel_open: bool, |
| 124 |
|
| 125 |
|
| 126 |
pub collection_filter_name_input: String, |
| 127 |
|
| 128 |
|
| 129 |
|
| 130 |
|
| 131 |
pub filter_tag_input: String, |
| 132 |
|
| 133 |
|
| 134 |
pub similarity_search_hash: Option<String>, |
| 135 |
|
| 136 |
|
| 137 |
|
| 138 |
pub similarity_source_name: Option<String>, |
| 139 |
|
| 140 |
|
| 141 |
pub all_tags: Arc<Vec<String>>, |
| 142 |
|
| 143 |
pub tag_search: String, |
| 144 |
|
| 145 |
|
| 146 |
pub previewing_hash: Option<String>, |
| 147 |
pub shared: Arc<SharedState>, |
| 148 |
pub sample_rate: f32, |
| 149 |
pub loop_enabled: bool, |
| 150 |
pub autoplay: bool, |
| 151 |
|
| 152 |
|
| 153 |
|
| 154 |
pub forge_auto_trim_overshoot: bool, |
| 155 |
|
| 156 |
|
| 157 |
pub instrument_visible: bool, |
| 158 |
pub instrument_root_note: u8, |
| 159 |
|
| 160 |
pub instrument_locked: bool, |
| 161 |
|
| 162 |
pub piano_held_notes: Vec<u8>, |
| 163 |
|
| 164 |
pub show_midi_window: bool, |
| 165 |
|
| 166 |
|
| 167 |
pub midi_state: MidiUiState, |
| 168 |
|
| 169 |
pub midi_pending_action: Option<MidiAction>, |
| 170 |
|
| 171 |
|
| 172 |
pub show_help: bool, |
| 173 |
|
| 174 |
pub help_tab: u8, |
| 175 |
|
| 176 |
|
| 177 |
|
| 178 |
pub about_requested: bool, |
| 179 |
pub pending_confirm: Option<ConfirmAction>, |
| 180 |
|
| 181 |
|
| 182 |
pub vfs_create_input: String, |
| 183 |
pub vfs_rename_target: Option<(VfsId, String)>, |
| 184 |
pub dir_create_input: String, |
| 185 |
pub show_vfs_create: bool, |
| 186 |
pub show_dir_create: bool, |
| 187 |
pub dir_rename_target: Option<(NodeId, String)>, |
| 188 |
|
| 189 |
|
| 190 |
pub undo_stack: Vec<UndoOp>, |
| 191 |
pub bulk_modal: Option<BulkModal>, |
| 192 |
pub column_config: ColumnConfig, |
| 193 |
|
| 194 |
|
| 195 |
pub import_mode: ImportMode, |
| 196 |
|
| 197 |
pub quick_import: bool, |
| 198 |
|
| 199 |
|
| 200 |
pub pending_import_preflight: Option<crate::state::import_workflow::ImportPreflight>, |
| 201 |
|
| 202 |
|
| 203 |
pub import_preflight_disabled: bool, |
| 204 |
|
| 205 |
|
| 206 |
pub preflight_dont_ask: bool, |
| 207 |
|
| 208 |
pub help_shortcut_search: String, |
| 209 |
|
| 210 |
pub bulk_move_filter: String, |
| 211 |
pub pending_review_items: Vec<ReviewItem>, |
| 212 |
|
| 213 |
|
| 214 |
pub import_file_errors: Vec<ImportFileError>, |
| 215 |
pub analysis_errors: Vec<AnalysisFileError>, |
| 216 |
pub import_errors_expanded: bool, |
| 217 |
|
| 218 |
|
| 219 |
pub last_import_source: Option<PathBuf>, |
| 220 |
|
| 221 |
pub last_analysis_hashes: Vec<(String, String)>, |
| 222 |
pub last_analysis_config: Option<AnalysisConfig>, |
| 223 |
|
| 224 |
|
| 225 |
|
| 226 |
pub last_export_destination: Option<PathBuf>, |
| 227 |
|
| 228 |
|
| 229 |
|
| 230 |
|
| 231 |
#[allow(clippy::type_complexity)] |
| 232 |
pub last_folder_tags: Option<(Vec<crate::state::FolderTagEntry>, Vec<(String, String)>)>, |
| 233 |
|
| 234 |
|
| 235 |
|
| 236 |
pub operation_progress: Option<crate::state::OperationProgress>, |
| 237 |
|
| 238 |
|
| 239 |
|
| 240 |
pub tag_folders_apply_all_input: String, |
| 241 |
|
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
pub name_modal_error: Option<String>, |
| 246 |
|
| 247 |
pub focus_search: bool, |
| 248 |
|
| 249 |
pub focus_tag_input: bool, |
| 250 |
|
| 251 |
|
| 252 |
pub focus_inline_editor: bool, |
| 253 |
|
| 254 |
|
| 255 |
|
| 256 |
pub dismissed_suggestions: std::collections::HashMap<String, Vec<String>>, |
| 257 |
|
| 258 |
|
| 259 |
|
| 260 |
|
| 261 |
pub last_dismissed_suggestion: Option<(String, String, Instant)>, |
| 262 |
|
| 263 |
pub scroll_to_row: Option<usize>, |
| 264 |
|
| 265 |
|
| 266 |
pub current_theme_id: String, |
| 267 |
|
| 268 |
|
| 269 |
pub collections: Vec<Collection>, |
| 270 |
pub active_collection: Option<CollectionId>, |
| 271 |
pub collection_create_input: String, |
| 272 |
pub collection_rename_target: Option<(CollectionId, String)>, |
| 273 |
|
| 274 |
|
| 275 |
pub tag_rename_target: Option<(String, String)>, |
| 276 |
|
| 277 |
|
| 278 |
|
| 279 |
|
| 280 |
pub tag_rename_preview: Option<(usize, Vec<String>)>, |
| 281 |
pub show_collection_create: bool, |
| 282 |
|
| 283 |
|
| 284 |
pub edit: EditUiState, |
| 285 |
|
| 286 |
|
| 287 |
pub forge: ForgeUiState, |
| 288 |
|
| 289 |
|
| 290 |
pub row_height: f32, |
| 291 |
|
| 292 |
|
| 293 |
pub show_vfs_banner: bool, |
| 294 |
|
| 295 |
pub show_first_launch_hint: bool, |
| 296 |
|
| 297 |
|
| 298 |
pub show_sync_intro: bool, |
| 299 |
|
| 300 |
|
| 301 |
|
| 302 |
|
| 303 |
pub os_drag_cooldown: Option<Instant>, |
| 304 |
|
| 305 |
|
| 306 |
pub mirror_enabled: bool, |
| 307 |
pub mirror_path: PathBuf, |
| 308 |
pub mirror_dirty: bool, |
| 309 |
|
| 310 |
|
| 311 |
pub sync: SyncUiState, |
| 312 |
|
| 313 |
|
| 314 |
pub settings: SettingsUiState, |
| 315 |
|
| 316 |
|
| 317 |
|
| 318 |
pub loose_files_missing_count: usize, |
| 319 |
|
| 320 |
pub show_loose_files_warning: bool, |
| 321 |
} |
| 322 |
|
| 323 |
impl BrowserState { |
| 324 |
|
| 325 |
|
| 326 |
pub fn new( |
| 327 |
data_dir: &Path, |
| 328 |
shared: Arc<SharedState>, |
| 329 |
sample_rate: f32, |
| 330 |
vault_name: &str, |
| 331 |
) -> Result<Self, Box<dyn std::error::Error>> { |
| 332 |
std::fs::create_dir_all(data_dir)?; |
| 333 |
|
| 334 |
let db_path = data_dir.join("audiofiles.db"); |
| 335 |
let db = Database::open(&db_path)?; |
| 336 |
|
| 337 |
let store_dir = data_dir.join("samples"); |
| 338 |
let store = SampleStore::new(&store_dir)?; |
| 339 |
|
| 340 |
let backend = Box::new(DirectBackend::new(db, store, data_dir.to_path_buf())); |
| 341 |
Self::new_with_backend(backend, data_dir, shared, sample_rate, vault_name) |
| 342 |
} |
| 343 |
|
| 344 |
|
| 345 |
|
| 346 |
|
| 347 |
|
| 348 |
pub fn new_with_backend( |
| 349 |
backend: Box<dyn Backend>, |
| 350 |
data_dir: &Path, |
| 351 |
shared: Arc<SharedState>, |
| 352 |
sample_rate: f32, |
| 353 |
vault_name: &str, |
| 354 |
) -> Result<Self, Box<dyn std::error::Error>> { |
| 355 |
let mut vfs_list = backend.list_vfs()?; |
| 356 |
if vfs_list.is_empty() { |
| 357 |
backend.create_vfs("Vault")?; |
| 358 |
vfs_list = backend.list_vfs()?; |
| 359 |
} |
| 360 |
|
| 361 |
|
| 362 |
|
| 363 |
let current_vfs_idx = backend |
| 364 |
.get_config("current_vfs_id") |
| 365 |
.ok() |
| 366 |
.flatten() |
| 367 |
.and_then(|s| s.parse::<i64>().ok()) |
| 368 |
.and_then(|id| vfs_list.iter().position(|v| v.id.as_i64() == id)) |
| 369 |
.unwrap_or(0); |
| 370 |
|
| 371 |
let contents = backend.list_children_enriched(vfs_list[current_vfs_idx].id, None) |
| 372 |
.unwrap_or_else(|e| { error!("Failed to load initial contents: {e}"); Vec::new() }); |
| 373 |
let all_tags = backend.list_all_tags() |
| 374 |
.unwrap_or_else(|e| { warn!("Failed to load tags: {e}"); Vec::new() }); |
| 375 |
let collections_list = backend.list_collections() |
| 376 |
.unwrap_or_else(|e| { warn!("Failed to load collections: {e}"); Vec::new() }); |
| 377 |
|
| 378 |
|
| 379 |
let theme_id = backend.get_config("theme") |
| 380 |
.ok() |
| 381 |
.flatten() |
| 382 |
.unwrap_or_else(|| "audiofiles".to_string()); |
| 383 |
crate::ui::theme::init(Some(&theme_id)); |
| 384 |
|
| 385 |
|
| 386 |
let loop_enabled = backend.get_config("preview_loop").ok().flatten().as_deref() == Some("1"); |
| 387 |
let autoplay = backend.get_config("preview_autoplay").ok().flatten().as_deref() == Some("1"); |
| 388 |
let forge_auto_trim_overshoot = backend |
| 389 |
.get_config(crate::backend::FORGE_AUTO_TRIM_OVERSHOOT_KEY) |
| 390 |
.ok() |
| 391 |
.flatten() |
| 392 |
.as_deref() |
| 393 |
== Some("true"); |
| 394 |
|
| 395 |
|
| 396 |
let vfs_explained = backend.get_config("vfs_explained").ok().flatten().as_deref() == Some("1"); |
| 397 |
let hints_dismissed = backend.get_config("hints_dismissed").ok().flatten().as_deref() == Some("1"); |
| 398 |
let sync_intro_dismissed = backend.get_config("sync_intro_dismissed").ok().flatten().as_deref() == Some("1"); |
| 399 |
|
| 400 |
let import_preflight_disabled = backend |
| 401 |
.get_config("import_preflight_disabled") |
| 402 |
.ok() |
| 403 |
.flatten() |
| 404 |
.as_deref() |
| 405 |
== Some("1"); |
| 406 |
|
| 407 |
|
| 408 |
let row_height = backend.get_config("row_height").ok().flatten() |
| 409 |
.and_then(|s| s.parse::<f32>().ok()) |
| 410 |
.unwrap_or(24.0) |
| 411 |
.clamp(20.0, 32.0); |
| 412 |
|
| 413 |
|
| 414 |
let dismissed_suggestions: std::collections::HashMap<String, Vec<String>> = backend |
| 415 |
.get_config("suggestions.dismissed") |
| 416 |
.ok() |
| 417 |
.flatten() |
| 418 |
.and_then(|s| serde_json::from_str(&s).ok()) |
| 419 |
.unwrap_or_default(); |
| 420 |
|
| 421 |
|
| 422 |
let mirror_enabled = backend.get_config("mirror_enabled").ok().flatten().as_deref() == Some("1"); |
| 423 |
let mirror_path = backend |
| 424 |
.get_config("mirror_path") |
| 425 |
.ok() |
| 426 |
.flatten() |
| 427 |
.map(PathBuf::from) |
| 428 |
.unwrap_or_else(|| { |
| 429 |
dirs::home_dir() |
| 430 |
.unwrap_or_else(|| data_dir.to_path_buf()) |
| 431 |
.join("audiofiles") |
| 432 |
}); |
| 433 |
|
| 434 |
|
| 435 |
|
| 436 |
let sidebar_visible = |
| 437 |
backend.get_config("sidebar_visible").ok().flatten().as_deref() != Some("0"); |
| 438 |
let detail_visible = |
| 439 |
backend.get_config("detail_visible").ok().flatten().as_deref() != Some("0"); |
| 440 |
let filter_panel_open = |
| 441 |
backend.get_config("filter_panel_open").ok().flatten().as_deref() == Some("1"); |
| 442 |
|
| 443 |
Ok(Self { |
| 444 |
data_dir: data_dir.to_path_buf(), |
| 445 |
backend, |
| 446 |
vfs_list: Arc::new(vfs_list), |
| 447 |
current_vfs_idx, |
| 448 |
current_dir: None, |
| 449 |
breadcrumb: Vec::new(), |
| 450 |
contents: Arc::new(contents), |
| 451 |
selection: Selection::new(), |
| 452 |
selected_tags: Arc::new(Vec::new()), |
| 453 |
status: String::new(), |
| 454 |
status_set_at: None, |
| 455 |
selected_analysis: None, |
| 456 |
selected_waveform: None, |
| 457 |
tag_input: String::new(), |
| 458 |
detail_visible, |
| 459 |
sidebar_visible, |
| 460 |
sort_column: SortColumn::Name, |
| 461 |
sort_direction: SortDirection::Ascending, |
| 462 |
search_query: String::new(), |
| 463 |
search_filter: SearchFilter::default(), |
| 464 |
filter_panel_open, |
| 465 |
collection_filter_name_input: String::new(), |
| 466 |
filter_tag_input: String::new(), |
| 467 |
similarity_search_hash: None, |
| 468 |
similarity_source_name: None, |
| 469 |
all_tags: Arc::new(all_tags), |
| 470 |
tag_search: String::new(), |
| 471 |
previewing_hash: None, |
| 472 |
shared, |
| 473 |
sample_rate, |
| 474 |
loop_enabled, |
| 475 |
autoplay, |
| 476 |
forge_auto_trim_overshoot, |
| 477 |
instrument_visible: false, |
| 478 |
instrument_root_note: 60, |
| 479 |
instrument_locked: false, |
| 480 |
piano_held_notes: Vec::new(), |
| 481 |
show_midi_window: false, |
| 482 |
midi_state: MidiUiState::default(), |
| 483 |
midi_pending_action: None, |
| 484 |
show_help: false, |
| 485 |
help_tab: 0, |
| 486 |
about_requested: false, |
| 487 |
pending_confirm: None, |
| 488 |
vfs_create_input: String::new(), |
| 489 |
vfs_rename_target: None, |
| 490 |
dir_create_input: String::new(), |
| 491 |
show_vfs_create: false, |
| 492 |
show_dir_create: false, |
| 493 |
dir_rename_target: None, |
| 494 |
undo_stack: Vec::new(), |
| 495 |
bulk_modal: None, |
| 496 |
column_config: ColumnConfig::default(), |
| 497 |
import_mode: ImportMode::None, |
| 498 |
quick_import: false, |
| 499 |
pending_import_preflight: None, |
| 500 |
|
| 501 |
import_preflight_disabled, |
| 502 |
preflight_dont_ask: false, |
| 503 |
|
| 504 |
help_shortcut_search: String::new(), |
| 505 |
|
| 506 |
bulk_move_filter: String::new(), |
| 507 |
pending_review_items: Vec::new(), |
| 508 |
import_file_errors: Vec::new(), |
| 509 |
analysis_errors: Vec::new(), |
| 510 |
import_errors_expanded: false, |
| 511 |
last_import_source: None, |
| 512 |
last_analysis_hashes: Vec::new(), |
| 513 |
last_analysis_config: None, |
| 514 |
last_export_destination: None, |
| 515 |
last_folder_tags: None, |
| 516 |
operation_progress: None, |
| 517 |
tag_folders_apply_all_input: String::new(), |
| 518 |
name_modal_error: None, |
| 519 |
focus_search: false, |
| 520 |
focus_tag_input: false, |
| 521 |
focus_inline_editor: false, |
| 522 |
dismissed_suggestions, |
| 523 |
last_dismissed_suggestion: None, |
| 524 |
scroll_to_row: None, |
| 525 |
current_theme_id: theme_id, |
| 526 |
collections: collections_list, |
| 527 |
active_collection: None, |
| 528 |
collection_create_input: String::new(), |
| 529 |
collection_rename_target: None, |
| 530 |
tag_rename_target: None, |
| 531 |
tag_rename_preview: None, |
| 532 |
show_collection_create: false, |
| 533 |
edit: EditUiState::default(), |
| 534 |
forge: ForgeUiState::default(), |
| 535 |
row_height, |
| 536 |
show_vfs_banner: !vfs_explained, |
| 537 |
show_first_launch_hint: !hints_dismissed, |
| 538 |
|
| 539 |
show_sync_intro: !sync_intro_dismissed, |
| 540 |
os_drag_cooldown: None, |
| 541 |
mirror_enabled, |
| 542 |
mirror_path, |
| 543 |
mirror_dirty: mirror_enabled, |
| 544 |
sync: SyncUiState::default(), |
| 545 |
settings: SettingsUiState { name: vault_name.to_string(), ..Default::default() }, |
| 546 |
loose_files_missing_count: 0, |
| 547 |
show_loose_files_warning: false, |
| 548 |
}) |
| 549 |
} |
| 550 |
|
| 551 |
|
| 552 |
|
| 553 |
|
| 554 |
pub fn post_status(&mut self, msg: impl Into<String>) { |
| 555 |
self.status = msg.into(); |
| 556 |
self.status_set_at = Some(Instant::now()); |
| 557 |
} |
| 558 |
} |
| 559 |
|