use tracing::{error, warn}; use super::*; impl BrowserState { /// Database ID of the currently active VFS, or `None` if the list is empty. pub fn current_vfs_id(&self) -> Option { self.vfs_list.get(self.current_vfs_idx).map(|v| v.id) } /// Reload the child node list and apply current sort/search. pub fn refresh_contents(&mut self) { // In similarity mode, contents are managed by find_similar() — skip normal refresh. if self.similarity_search_hash.is_some() { return; } let vfs_id = match self.current_vfs_id() { Some(id) => id, None => { self.contents = Arc::new(Vec::new()); return; } }; if self.search_filter.is_active() || !self.search_query.is_empty() { let mut filter = self.search_filter.clone(); filter.text_query = self.search_query.clone(); match filter.scope { audiofiles_core::search::SearchScope::CurrentFolder => { match self.backend.search_in_folder(&filter, vfs_id, self.current_dir) { Ok(results) => self.contents = Arc::new(results), Err(e) => { error!("Search failed: {e}"); self.status = "Search error".to_string(); self.contents = Arc::new(Vec::new()); } } } audiofiles_core::search::SearchScope::Global => { match self.backend.search_global(&filter) { Ok(results) => self.contents = Arc::new(results), Err(e) => { error!("Global search failed: {e}"); self.status = "Search error".to_string(); self.contents = Arc::new(Vec::new()); } } } } } else { match self.backend.list_children_enriched(vfs_id, self.current_dir) { Ok(nodes) => self.contents = Arc::new(nodes), Err(e) => { error!("Failed to list directory: {e}"); self.status = "Failed to load contents".to_string(); self.contents = Arc::new(Vec::new()); } } } self.sort_contents(); self.refresh_selected_tags(); self.mark_mirror_dirty(); } /// Apply current search query and filters. pub fn apply_search(&mut self) { self.selection.clear(); self.refresh_contents(); } /// Sort contents by the current sort column and direction. pub fn sort_contents(&mut self) { // Directories always first Arc::make_mut(&mut self.contents).sort_by(|a, b| { let a_is_dir = a.node.node_type == NodeType::Directory; let b_is_dir = b.node.node_type == NodeType::Directory; if a_is_dir != b_is_dir { return b_is_dir.cmp(&a_is_dir); } let cmp = match self.sort_column { SortColumn::Name => a.node.name.to_lowercase().cmp(&b.node.name.to_lowercase()), SortColumn::Bpm => a.bpm.partial_cmp(&b.bpm).unwrap_or(std::cmp::Ordering::Equal), SortColumn::Key => a.musical_key.cmp(&b.musical_key), SortColumn::Duration => a .duration .partial_cmp(&b.duration) .unwrap_or(std::cmp::Ordering::Equal), SortColumn::Classification => a.classification.cmp(&b.classification), }; match self.sort_direction { SortDirection::Ascending => cmp, SortDirection::Descending => cmp.reverse(), } }); } /// Cycle sort for a column: ascending -> descending -> default(name asc). pub fn toggle_sort(&mut self, column: SortColumn) { if self.sort_column == column { match self.sort_direction { SortDirection::Ascending => self.sort_direction = SortDirection::Descending, SortDirection::Descending => { self.sort_column = SortColumn::Name; self.sort_direction = SortDirection::Ascending; } } } else { self.sort_column = column; self.sort_direction = SortDirection::Ascending; } self.sort_contents(); } /// Reload the tag list for the currently focused sample (shown in the detail panel). pub fn refresh_selected_tags(&mut self) { self.selected_tags = Arc::new(Vec::new()); if let Some(node) = self.selected_node() && let Some(hash) = &node.node.sample_hash { self.selected_tags = Arc::new(self.backend.get_sample_tags(hash).unwrap_or_else(|e| { warn!("Failed to load tags: {e}"); Vec::new() })); } } /// Refresh the detail panel (analysis + waveform) for the currently selected sample. pub fn refresh_selected_detail(&mut self) { self.selected_analysis = None; self.selected_waveform = None; if let Some(node) = self.selected_node() && let Some(hash) = &node.node.sample_hash { self.selected_analysis = self.backend.get_analysis(hash) .unwrap_or(None); self.selected_waveform = self.backend.get_waveform(hash) .unwrap_or(None); } } /// Whether the file list currently shows a ".." parent-directory entry. fn has_parent_entry(&self) -> bool { self.current_dir.is_some() } /// Total number of visible rows: contents + optional ".." parent entry. pub fn visible_len(&self) -> usize { self.contents.len() + if self.has_parent_entry() { 1 } else { 0 } } /// Return the VfsNodeWithAnalysis at the current selection focus, or `None` if ".." is selected. pub fn selected_node(&self) -> Option { let focus = self.selection.focus; if self.has_parent_entry() { if focus == 0 { return None; // ".." selected } self.contents.get(focus - 1).cloned() } else { self.contents.get(focus).cloned() } } /// Move selection focus down by one row (Down arrow). pub fn select_next(&mut self) { let len = self.visible_len(); if len > 0 && self.selection.focus < len - 1 { let next = self.selection.focus + 1; self.selection.set_single(next); self.scroll_to_row = Some(next); self.refresh_selected_tags(); self.refresh_selected_detail(); } } /// Move selection focus up by one row (Up arrow). pub fn select_prev(&mut self) { if self.selection.focus > 0 { let prev = self.selection.focus - 1; self.selection.set_single(prev); self.scroll_to_row = Some(prev); self.refresh_selected_tags(); self.refresh_selected_detail(); } } /// Navigate into the selected directory, or go up if ".." is selected. pub fn enter_directory(&mut self) { self.similarity_search_hash = None; self.similarity_source_name = None; if self.has_parent_entry() && self.selection.focus == 0 { self.go_up(); return; } if let Some(node) = self.selected_node() && node.node.node_type == NodeType::Directory { self.current_dir = Some(node.node.id); self.breadcrumb = self.backend.get_breadcrumb(node.node.id).unwrap_or_else(|e| { warn!("Breadcrumb failed: {e}"); Vec::new() }); self.selection.clear(); self.refresh_contents(); } } /// Navigate to the parent directory, or do nothing if already at root. /// If a collection is active, exits collection view first. pub fn go_up(&mut self) { self.similarity_search_hash = None; self.similarity_source_name = None; if self.active_collection.is_some() { self.deactivate_collection(); return; } if let Some(current) = self.current_dir { if let Ok(node) = self.backend.get_node(current) { self.current_dir = node.parent_id; if let Some(pid) = node.parent_id { self.breadcrumb = self.backend.get_breadcrumb(pid).unwrap_or_else(|e| { warn!("Breadcrumb failed: {e}"); Vec::new() }); } else { self.breadcrumb.clear(); } } else { self.current_dir = None; self.breadcrumb.clear(); } self.selection.clear(); self.refresh_contents(); } } /// Switch to a different VFS by index, resetting navigation to its root. pub fn select_vfs(&mut self, idx: usize) { if idx < self.vfs_list.len() && idx != self.current_vfs_idx { self.current_vfs_idx = idx; self.current_dir = None; self.breadcrumb.clear(); self.selection.clear(); self.similarity_search_hash = None; self.similarity_source_name = None; // Persist the selection by VFS id (not index — indices shift when // vaults are added or removed) so it restores on next launch. let _ = self.backend.set_config( "current_vfs_id", &self.vfs_list[self.current_vfs_idx].id.as_i64().to_string(), ); self.refresh_contents(); self.refresh_collections(); self.status = format!("Switched to: {}", self.vfs_list[self.current_vfs_idx].name); } } /// Toggle the left sidebar and persist the choice across restarts. pub fn toggle_sidebar(&mut self) { self.sidebar_visible = !self.sidebar_visible; let _ = self.backend.set_config( "sidebar_visible", if self.sidebar_visible { "1" } else { "0" }, ); } /// Toggle the right detail panel and persist the choice across restarts. pub fn toggle_detail(&mut self) { self.set_detail_visible(!self.detail_visible); } /// Set detail-panel visibility and persist it (used by both the explicit /// toggle and the auto-show on selection so the stored state never drifts). pub fn set_detail_visible(&mut self, visible: bool) { if self.detail_visible != visible { self.detail_visible = visible; let _ = self .backend .set_config("detail_visible", if visible { "1" } else { "0" }); } } /// Toggle the filter panel and persist the choice across restarts. pub fn toggle_filter_panel(&mut self) { self.filter_panel_open = !self.filter_panel_open; let _ = self.backend.set_config( "filter_panel_open", if self.filter_panel_open { "1" } else { "0" }, ); } }