use std::{ cmp::Ordering, collections::HashSet, rc::Rc, sync::{ atomic::{AtomicBool, AtomicUsize}, mpsc, Mutex, }, }; use musicdb_lib::data::{ album::Album, artist::Artist, database::Database, queue::{Queue, QueueContent}, song::Song, AlbumId, ArtistId, GeneralData, SongId, }; use regex::{Regex, RegexBuilder}; use speedy2d::{ color::Color, dimen::Vec2, shape::Rectangle, window::{MouseButton, VirtualKeyCode}, }; use crate::{ gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui_base::{Button, Panel, ScrollBox}, gui_text::{Label, TextField}, gui_wrappers::WithFocusHotkey, }; /* This is responsible for showing the library, with Regex search and drag-n-drop. */ pub struct LibraryBrowser { config: GuiElemCfg, pub children: Vec, // - - - library_sorted: Vec<(ArtistId, Vec, Vec<(AlbumId, Vec)>)>, library_filtered: Vec<( ArtistId, Vec<(SongId, f32)>, Vec<(AlbumId, Vec<(SongId, f32)>, f32)>, f32, )>, selected: Selected, // - - - search_artist: String, search_artist_regex: Option, search_album: String, search_album_regex: Option, search_song: String, search_song_regex: Option, filter_target_state: Rc, filter_state: f32, library_updated: bool, search_settings_changed: Rc, search_is_case_sensitive: Rc, search_was_case_sensitive: bool, search_prefer_start_matches: Rc, search_prefers_start_matches: bool, filter_songs: Rc>, filter_albums: Rc>, filter_artists: Rc>, do_something_receiver: mpsc::Receiver>, } impl Clone for LibraryBrowser { fn clone(&self) -> Self { Self::new(self.config.clone()) } } #[derive(Clone)] struct Selected(Rc, HashSet, HashSet)>>); impl Selected { pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec { let lock = self.0.lock().unwrap(); let (sel_artists, sel_albums, sel_songs) = &*lock; let mut out = vec![]; for (artist, singles, albums, _) in &lb.library_filtered { let artist_selected = sel_artists.contains(artist); let mut local_artist_owned = vec![]; let mut local_artist = if artist_selected { &mut local_artist_owned } else { &mut out }; for (song, _) in singles { let song_selected = sel_songs.contains(song); if song_selected { local_artist.push(QueueContent::Song(*song).into()); } } for (album, songs, _) in albums { let album_selected = sel_albums.contains(album); let mut local_album_owned = vec![]; let local_album = if album_selected { &mut local_album_owned } else { &mut local_artist }; for (song, _) in songs { let song_selected = sel_songs.contains(song); if song_selected { local_album.push(QueueContent::Song(*song).into()); } } if album_selected { local_artist.push( QueueContent::Folder( 0, local_album_owned, match db.albums().get(album) { Some(v) => v.name.clone(), None => "< unknown album >".to_owned(), }, ) .into(), ); } } if artist_selected { out.push( QueueContent::Folder( 0, local_artist_owned, match db.artists().get(artist) { Some(v) => v.name.to_owned(), None => "< unknown artist >".to_owned(), }, ) .into(), ); } } out } } fn search_regex_new(pat: &str, case_insensitive: bool) -> Result, regex::Error> { if pat.is_empty() { Ok(None) } else { Ok(Some( RegexBuilder::new(pat) .unicode(true) .case_insensitive(case_insensitive) .build()?, )) } } const LP_LIB1: f32 = 0.1; const LP_LIB2: f32 = 1.0; const LP_LIB1S: f32 = 0.4; impl LibraryBrowser { pub fn new(config: GuiElemCfg) -> Self { let search_artist = TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.01), (0.45, 0.05))), "artist".to_string(), Color::GRAY, Color::WHITE, ); let search_album = TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.55, 0.01), (0.99, 0.05))), "album".to_string(), Color::GRAY, Color::WHITE, ); let search_song = WithFocusHotkey::new_ctrl( VirtualKeyCode::F, TextField::new( GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.06), (0.99, 0.1))), "song".to_string(), Color::GRAY, Color::WHITE, ), ); let library_scroll_box = ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, LP_LIB1), (1.0, LP_LIB2))), crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], ); let (do_something_sender, do_something_receiver) = mpsc::channel(); let search_settings_changed = Rc::new(AtomicBool::new(false)); let search_was_case_sensitive = false; let search_is_case_sensitive = Rc::new(AtomicBool::new(search_was_case_sensitive)); let search_prefers_start_matches = true; let search_prefer_start_matches = Rc::new(AtomicBool::new(search_prefers_start_matches)); let filter_target_state = Rc::new(AtomicBool::new(false)); let fts = Rc::clone(&filter_target_state); let filter_button = Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))), move |_| { fts.store( !fts.load(std::sync::atomic::Ordering::Relaxed), std::sync::atomic::Ordering::Relaxed, ); vec![] }, vec![GuiElem::new(Label::new( GuiElemCfg::default(), "more".to_owned(), Color::GRAY, None, Vec2::new(0.5, 0.5), ))], ); let filter_songs = Rc::new(Mutex::new(Filter { and: true, filters: vec![], })); let filter_albums = Rc::new(Mutex::new(Filter { and: true, filters: vec![], })); let filter_artists = Rc::new(Mutex::new(Filter { and: true, filters: vec![], })); let selected = Selected(Rc::new(Mutex::new(( HashSet::new(), HashSet::new(), HashSet::new(), )))); Self { config, children: vec![ GuiElem::new(search_artist), GuiElem::new(search_album), GuiElem::new(search_song), GuiElem::new(library_scroll_box), GuiElem::new(filter_button), GuiElem::new(FilterPanel::new( Rc::clone(&search_settings_changed), Rc::clone(&search_is_case_sensitive), Rc::clone(&search_prefer_start_matches), Rc::clone(&filter_songs), Rc::clone(&filter_albums), Rc::clone(&filter_artists), selected.clone(), do_something_sender.clone(), )), ], // - - - library_sorted: vec![], library_filtered: vec![], selected, // - - - search_artist: String::new(), search_artist_regex: None, search_album: String::new(), search_album_regex: None, search_song: String::new(), search_song_regex: None, filter_target_state, filter_state: 0.0, library_updated: true, search_settings_changed, search_is_case_sensitive, search_was_case_sensitive, search_prefer_start_matches, search_prefers_start_matches, filter_songs, filter_albums, filter_artists, do_something_receiver, } } } impl GuiElemTrait for LibraryBrowser { fn config(&self) -> &GuiElemCfg { &self.config } fn config_mut(&mut self) -> &mut GuiElemCfg { &mut self.config } fn children(&mut self) -> Box + '_> { Box::new(self.children.iter_mut()) } fn any(&self) -> &dyn std::any::Any { self } fn any_mut(&mut self) -> &mut dyn std::any::Any { self } fn clone_gui(&self) -> Box { Box::new(self.clone()) } fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { loop { if let Ok(action) = self.do_something_receiver.try_recv() { action(self); } else { break; } } // search let mut search_changed = false; let mut rebuild_regex = false; if self .search_settings_changed .load(std::sync::atomic::Ordering::Relaxed) { search_changed = true; self.search_settings_changed .store(false, std::sync::atomic::Ordering::Relaxed); let case_sensitive = self .search_is_case_sensitive .load(std::sync::atomic::Ordering::Relaxed); if self.search_was_case_sensitive != case_sensitive { self.search_was_case_sensitive = case_sensitive; rebuild_regex = true; } let pref_start = self .search_prefer_start_matches .load(std::sync::atomic::Ordering::Relaxed); if self.search_prefers_start_matches != pref_start { self.search_prefers_start_matches = pref_start; } } { let v = &mut self.children[0].try_as_mut::().unwrap().children[0] .try_as_mut::