[READ-ONLY] Mirror of https://github.com/probablykasper/ferrum. Music library app for Mac, Linux and Windows ferrum.kasper.space
electron linux macos music music-library music-player napi windows
0

Configure Feed

Select the types of activity you want to include in your feed.

Support multi-word column filtering

Kasper (Sep 18, 2025, 10:02 AM +0200) 53be924a 02a026b0

+47 -9
+1 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 3 ## Next 4 - - Add filtering for specific fields in search, like `artist:apashe`. You can also right-click a column header to add a filter. 4 + - Add filtering for specific fields in search, like `artist:apashe` or `album:"mirai sekai"`. You can also right-click a column header to add a filter. 5 5 - Add genre autocomplete 6 6 - Make macOS media key permission request non-intrusive 7 7 - Improve error and crashing behaviour
+46 -7
src-native/filter.rs
··· 174 174 filtered_tracks 175 175 } 176 176 177 + #[derive(Default, Debug)] 177 178 struct Keyword { 178 179 full_word: String, 179 180 field: Option<String>, 180 181 literal: String, 181 182 } 182 183 impl Keyword { 183 - fn parse(word: &str) -> Keyword { 184 + fn from_word(word: &str) -> Keyword { 184 185 let mut parts = word.splitn(2, ':'); 185 186 let field = parts.next(); 186 187 let literal = parts.next(); ··· 197 198 }, 198 199 } 199 200 } 201 + fn parse_next_keyword(query: &mut String) -> Option<Keyword> { 202 + *query = query.trim_start().to_string(); 203 + if query.is_empty() { 204 + return None; 205 + } 206 + 207 + // get next word 208 + let (word, rest) = match query.find(char::is_whitespace) { 209 + Some(i) => { 210 + let (word, rest) = query.split_at(i); 211 + (word.to_string(), rest.to_string()) 212 + } 213 + None => (query.to_string(), "".to_string()), 214 + }; 215 + *query = rest.to_string(); 216 + println!("word: {}", word); 217 + println!("rest: {}", query); 218 + 219 + let mut keyword = Keyword::from_word(&word); 220 + 221 + // parse field:"literal" 222 + if keyword.field.is_some() && keyword.literal.starts_with('\"') { 223 + keyword.literal = keyword.literal.trim_start_matches('\"').to_string(); 224 + let (literal, rest) = match query.split_once('\"') { 225 + Some((literal, rest)) => (literal.to_string(), rest.to_string()), 226 + None => (query.to_string(), "".to_string()), 227 + }; 228 + keyword.literal.push_str(&literal); 229 + *query = rest; 230 + println!("word: {}", keyword.literal); 231 + println!("rest: {}", query); 232 + } 233 + 234 + Some(keyword) 235 + } 200 236 } 201 237 202 238 pub fn filter(mut item_ids: Vec<ItemId>, query: String, library: &Library) -> Vec<ItemId> { ··· 204 240 if query == "" { 205 241 return item_ids; 206 242 } 207 - let query: String = query.nfc().collect(); 243 + let mut query: String = query.nfc().collect(); 208 244 209 - for word in query.split(' ') { 210 - if word == "" { 211 - continue; 212 - } 213 - let keyword = Keyword::parse(word); 245 + let mut keywords = Vec::new(); 246 + while let Some(keyword) = Keyword::parse_next_keyword(&mut query) { 247 + keywords.push(keyword); 248 + } 249 + 250 + println!("Keywords: {keywords:?}"); 251 + 252 + for keyword in keywords { 214 253 item_ids = filter_keyword(item_ids, keyword, &library); 215 254 } 216 255 println!("Filter: {}ms", now.elapsed().as_millis());
-1
src/components/TrackList.svelte
··· 549 549 }} 550 550 on:contextmenu={() => { 551 551 const column_filter = 'filter' in column ? column.filter : null 552 - console.log('cf', column_filter) 553 552 ipc_renderer.invoke('show_columns_menu', { 554 553 column_filter: typeof column_filter === 'string' ? column_filter : null, 555 554 menu: all_columns.map((col) => {