···11+CREATE UNIQUE INDEX idx_file_path ON tracks(file_path);
+44
src/db/migrations/0009_remove_unique_track_id.sql
···11+DROP VIEW IF EXISTS albums;
22+33+CREATE TABLE tracks_new (
44+ id INTEGER PRIMARY KEY NOT NULL,
55+ track_id TEXT NOT NULL,
66+ title TEXT NOT NULL,
77+ artist TEXT NOT NULL,
88+ album TEXT NOT NULL,
99+ genre TEXT NOT NULL,
1010+ track_len INTEGER NOT NULL,
1111+ year INTEGER NOT NULL,
1212+ number INTEGER NOT NULL,
1313+ disc_number INTEGER NOT NULL,
1414+ disc_total INTEGER NOT NULL,
1515+ file_state INTEGER NOT NULL,
1616+ file_path TEXT NOT NULL,
1717+ extension TEXT NOT NULL,
1818+ artwork_path TEXT,
1919+ kind INTEGER NOT NULL,
2020+ description TEXT,
2121+ artwork_bytes BLOB
2222+);
2323+2424+INSERT INTO tracks_new SELECT id, track_id, title, artist, album, genre, track_len, year, number, disc_number, disc_total, file_state, file_path, extension, artwork_path, kind, description, artwork_bytes FROM tracks;
2525+2626+DROP TABLE tracks;
2727+2828+ALTER TABLE tracks_new RENAME TO tracks;
2929+3030+CREATE UNIQUE INDEX idx_file_path ON tracks(file_path);
3131+3232+CREATE TRIGGER track_fts_ai_insert AFTER INSERT ON tracks BEGIN
3333+ INSERT INTO track_fts(rowid, track_id, title, album, artist, extension) VALUES (new.id, new.track_id, new.title, new.album, new.artist, new.extension);
3434+END;
3535+3636+CREATE TRIGGER track_fts_ai_delete AFTER DELETE ON tracks BEGIN
3737+ INSERT INTO track_fts(track_fts, rowid, track_id, title, album, artist, extension) VALUES('delete', old.id, old.track_id, old.title, old.album, old.artist, old.extension);
3838+END;
3939+4040+CREATE VIEW IF NOT EXISTS albums (
4141+ title,
4242+ artist,
4343+ format
4444+) AS SELECT DISTINCT album, artist, extension FROM tracks;
+68-5
src/gtkgui/gtkgui.rs
···44use anyhow::Result;
55use glib::BoxedAnyObject;
66use gtk::{
77- ColumnView, ColumnViewColumn, CustomSorter, FileDialog, Label, ListItem, MenuButton, Ordering,
88- PolicyType, ScrolledWindow, SignalListItemFactory, SortListModel, SortType, StringList, gio,
77+ ColumnView, ColumnViewColumn, CustomFilter, CustomSorter, FileDialog, FilterChange,
88+ FilterListModel, Label, ListItem, MenuButton, Ordering, PolicyType, ScrolledWindow,
99+ SearchEntry, SignalListItemFactory, SortListModel, SortType, StringList, gio,
910};
1011use gtk4 as gtk;
1112use libadwaita as adw;
···4647 // Setup Actions
4748 setup_actions(app, db_state.clone(), store.clone());
48495050+ let search_query = Rc::new(RefCell::new(String::new()));
5151+ let search_query_clone = search_query.clone();
5252+5353+ let filter = CustomFilter::new(move |obj| {
5454+ let item = obj.downcast_ref::<BoxedAnyObject>().unwrap();
5555+ let track = item.borrow::<Track>();
5656+ let query = search_query_clone.borrow();
5757+5858+ if query.is_empty() {
5959+ return true;
6060+ }
6161+6262+ let q = query.to_lowercase();
6363+ track.title.to_lowercase().contains(&q)
6464+ || track.artist.to_lowercase().contains(&q)
6565+ || track.album.to_lowercase().contains(&q)
6666+ });
6767+6868+ let filter_model = FilterListModel::new(Some(store.clone()), Some(filter.clone()));
6969+4970 // Initially Create SortListModel without a sorter
5050- let sort_model = SortListModel::new(Some(store.clone()), None::<gtk::Sorter>);
7171+ let sort_model = SortListModel::new(Some(filter_model), None::<gtk::Sorter>);
5172 let selection_model = gtk::NoSelection::new(Some(sort_model.clone()));
52735374 let column_view = ColumnView::new(Some(selection_model));
···94115 Some(50),
95116 true,
96117 );
118118+ add_column(
119119+ &column_view,
120120+ "Path",
121121+ |t: &Track| t.file_path.clone(),
122122+ |a, b| a.file_path.cmp(&b.file_path).into(),
123123+ None,
124124+ true,
125125+ );
9712698127 // Set the sorter on the model to use the column view's sorter
99128 if let Some(sorter) = column_view.sorter() {
···113142114143 let header_bar = adw::HeaderBar::new();
115144145145+ let search_button = gtk::ToggleButton::builder()
146146+ .icon_name("system-search-symbolic")
147147+ .build();
148148+ header_bar.pack_start(&search_button);
149149+150150+ let search_entry = SearchEntry::builder()
151151+ .placeholder_text("Search...")
152152+ .margin_top(8)
153153+ .margin_bottom(8)
154154+ .margin_start(8)
155155+ .margin_end(8)
156156+ .build();
157157+158158+ let search_revealer = gtk::Revealer::builder()
159159+ .child(&search_entry)
160160+ .transition_type(gtk::RevealerTransitionType::SlideDown)
161161+ .build();
162162+163163+ search_button
164164+ .bind_property("active", &search_revealer, "reveal-child")
165165+ .sync_create()
166166+ .build();
167167+168168+ let search_query_clone = search_query.clone();
169169+ let filter_clone = filter.clone();
170170+ search_entry.connect_search_changed(move |entry| {
171171+ *search_query_clone.borrow_mut() = entry.text().to_string();
172172+ filter_clone.changed(FilterChange::Different);
173173+ });
174174+116175 // Menu Setup
117176 let menu = gio::Menu::new();
118177 menu.append(Some("Open Library"), Some("app.open"));
···128187129188 header_bar.pack_end(&menu_button);
130189131131- let toolbar_view = adw::ToolbarView::builder()
132132- .content(&scrolled_window)
190190+ let main_box = gtk::Box::builder()
191191+ .orientation(gtk::Orientation::Vertical)
133192 .build();
193193+ main_box.append(&search_revealer);
194194+ main_box.append(&scrolled_window);
195195+196196+ let toolbar_view = adw::ToolbarView::builder().content(&main_box).build();
134197135198 toolbar_view.add_top_bar(&header_bar);
136199