Skip to main content

max / audiofiles

9.6 KB · 259 lines History Blame Raw
1 use tracing::{error, warn};
2
3 use super::*;
4
5 impl BrowserState {
6 /// Database ID of the currently active VFS, or `None` if the list is empty.
7 pub fn current_vfs_id(&self) -> Option<VfsId> {
8 self.vfs_list.get(self.current_vfs_idx).map(|v| v.id)
9 }
10
11 /// Reload the child node list and apply current sort/search.
12 pub fn refresh_contents(&mut self) {
13 // In similarity mode, contents are managed by find_similar() — skip normal refresh.
14 if self.similarity_search_hash.is_some() {
15 return;
16 }
17
18 let vfs_id = match self.current_vfs_id() {
19 Some(id) => id,
20 None => {
21 self.contents = Arc::new(Vec::new());
22 return;
23 }
24 };
25
26 if self.search_filter.is_active() || !self.search_query.is_empty() {
27 let mut filter = self.search_filter.clone();
28 filter.text_query = self.search_query.clone();
29 match filter.scope {
30 audiofiles_core::search::SearchScope::CurrentFolder => {
31 match self.backend.search_in_folder(&filter, vfs_id, self.current_dir) {
32 Ok(results) => self.contents = Arc::new(results),
33 Err(e) => {
34 error!("Search failed: {e}");
35 self.status = "Search error".to_string();
36 self.contents = Arc::new(Vec::new());
37 }
38 }
39 }
40 audiofiles_core::search::SearchScope::Global => {
41 match self.backend.search_global(&filter) {
42 Ok(results) => self.contents = Arc::new(results),
43 Err(e) => {
44 error!("Global search failed: {e}");
45 self.status = "Search error".to_string();
46 self.contents = Arc::new(Vec::new());
47 }
48 }
49 }
50 }
51 } else {
52 match self.backend.list_children_enriched(vfs_id, self.current_dir) {
53 Ok(nodes) => self.contents = Arc::new(nodes),
54 Err(e) => {
55 error!("Failed to list directory: {e}");
56 self.status = "Failed to load contents".to_string();
57 self.contents = Arc::new(Vec::new());
58 }
59 }
60 }
61
62 self.sort_contents();
63 self.refresh_selected_tags();
64 self.mark_mirror_dirty();
65 }
66
67 /// Apply current search query and filters.
68 pub fn apply_search(&mut self) {
69 self.selection.clear();
70 self.refresh_contents();
71 }
72
73 /// Sort contents by the current sort column and direction.
74 pub fn sort_contents(&mut self) {
75 // Directories always first
76 Arc::make_mut(&mut self.contents).sort_by(|a, b| {
77 let a_is_dir = a.node.node_type == NodeType::Directory;
78 let b_is_dir = b.node.node_type == NodeType::Directory;
79 if a_is_dir != b_is_dir {
80 return b_is_dir.cmp(&a_is_dir);
81 }
82
83 let cmp = match self.sort_column {
84 SortColumn::Name => a.node.name.to_lowercase().cmp(&b.node.name.to_lowercase()),
85 SortColumn::Bpm => a.bpm.partial_cmp(&b.bpm).unwrap_or(std::cmp::Ordering::Equal),
86 SortColumn::Key => a.musical_key.cmp(&b.musical_key),
87 SortColumn::Duration => a
88 .duration
89 .partial_cmp(&b.duration)
90 .unwrap_or(std::cmp::Ordering::Equal),
91 SortColumn::Classification => a.classification.cmp(&b.classification),
92 };
93
94 match self.sort_direction {
95 SortDirection::Ascending => cmp,
96 SortDirection::Descending => cmp.reverse(),
97 }
98 });
99 }
100
101 /// Cycle sort for a column: ascending -> descending -> default(name asc).
102 pub fn toggle_sort(&mut self, column: SortColumn) {
103 if self.sort_column == column {
104 match self.sort_direction {
105 SortDirection::Ascending => self.sort_direction = SortDirection::Descending,
106 SortDirection::Descending => {
107 self.sort_column = SortColumn::Name;
108 self.sort_direction = SortDirection::Ascending;
109 }
110 }
111 } else {
112 self.sort_column = column;
113 self.sort_direction = SortDirection::Ascending;
114 }
115 self.sort_contents();
116 }
117
118 /// Reload the tag list for the currently focused sample (shown in the detail panel).
119 pub fn refresh_selected_tags(&mut self) {
120 self.selected_tags = Arc::new(Vec::new());
121 if let Some(node) = self.selected_node() {
122 if let Some(hash) = &node.node.sample_hash {
123 self.selected_tags = Arc::new(self.backend.get_sample_tags(hash).unwrap_or_else(|e| {
124 warn!("Failed to load tags: {e}");
125 Vec::new()
126 }));
127 }
128 }
129 }
130
131 /// Refresh the detail panel (analysis + waveform) for the currently selected sample.
132 pub fn refresh_selected_detail(&mut self) {
133 self.selected_analysis = None;
134 self.selected_waveform = None;
135
136 if let Some(node) = self.selected_node() {
137 if let Some(hash) = &node.node.sample_hash {
138 self.selected_analysis = self.backend.get_analysis(hash)
139 .unwrap_or(None);
140 self.selected_waveform = self.backend.get_waveform(hash)
141 .unwrap_or(None);
142 }
143 }
144 }
145
146 /// Whether the file list currently shows a ".." parent-directory entry.
147 fn has_parent_entry(&self) -> bool {
148 self.current_dir.is_some()
149 }
150
151 /// Total number of visible rows: contents + optional ".." parent entry.
152 pub fn visible_len(&self) -> usize {
153 self.contents.len() + if self.has_parent_entry() { 1 } else { 0 }
154 }
155
156 /// Return the VfsNodeWithAnalysis at the current selection focus, or `None` if ".." is selected.
157 pub fn selected_node(&self) -> Option<VfsNodeWithAnalysis> {
158 let focus = self.selection.focus;
159 if self.has_parent_entry() {
160 if focus == 0 {
161 return None; // ".." selected
162 }
163 self.contents.get(focus - 1).cloned()
164 } else {
165 self.contents.get(focus).cloned()
166 }
167 }
168
169 /// Move selection focus down by one row (Down arrow).
170 pub fn select_next(&mut self) {
171 let len = self.visible_len();
172 if len > 0 && self.selection.focus < len - 1 {
173 let next = self.selection.focus + 1;
174 self.selection.set_single(next);
175 self.scroll_to_row = Some(next);
176 self.refresh_selected_tags();
177 self.refresh_selected_detail();
178 }
179 }
180
181 /// Move selection focus up by one row (Up arrow).
182 pub fn select_prev(&mut self) {
183 if self.selection.focus > 0 {
184 let prev = self.selection.focus - 1;
185 self.selection.set_single(prev);
186 self.scroll_to_row = Some(prev);
187 self.refresh_selected_tags();
188 self.refresh_selected_detail();
189 }
190 }
191
192 /// Navigate into the selected directory, or go up if ".." is selected.
193 pub fn enter_directory(&mut self) {
194 self.similarity_search_hash = None;
195 self.similarity_source_name = None;
196
197 if self.has_parent_entry() && self.selection.focus == 0 {
198 self.go_up();
199 return;
200 }
201
202 if let Some(node) = self.selected_node() {
203 if node.node.node_type == NodeType::Directory {
204 self.current_dir = Some(node.node.id);
205 self.breadcrumb = self.backend.get_breadcrumb(node.node.id).unwrap_or_else(|e| {
206 warn!("Breadcrumb failed: {e}");
207 Vec::new()
208 });
209 self.selection.clear();
210 self.refresh_contents();
211 }
212 }
213 }
214
215 /// Navigate to the parent directory, or do nothing if already at root.
216 /// If a collection is active, exits collection view first.
217 pub fn go_up(&mut self) {
218 self.similarity_search_hash = None;
219 self.similarity_source_name = None;
220 if self.active_collection.is_some() {
221 self.deactivate_collection();
222 return;
223 }
224 if let Some(current) = self.current_dir {
225 if let Ok(node) = self.backend.get_node(current) {
226 self.current_dir = node.parent_id;
227 if let Some(pid) = node.parent_id {
228 self.breadcrumb = self.backend.get_breadcrumb(pid).unwrap_or_else(|e| {
229 warn!("Breadcrumb failed: {e}");
230 Vec::new()
231 });
232 } else {
233 self.breadcrumb.clear();
234 }
235 } else {
236 self.current_dir = None;
237 self.breadcrumb.clear();
238 }
239 self.selection.clear();
240 self.refresh_contents();
241 }
242 }
243
244 /// Switch to a different VFS by index, resetting navigation to its root.
245 pub fn select_vfs(&mut self, idx: usize) {
246 if idx < self.vfs_list.len() && idx != self.current_vfs_idx {
247 self.current_vfs_idx = idx;
248 self.current_dir = None;
249 self.breadcrumb.clear();
250 self.selection.clear();
251 self.similarity_search_hash = None;
252 self.similarity_source_name = None;
253 self.refresh_contents();
254 self.refresh_collections();
255 self.status = format!("Switched to: {}", self.vfs_list[self.current_vfs_idx].name);
256 }
257 }
258 }
259