mirror of
				https://github.com/Dummi26/musicdb.git
				synced 2025-10-30 03:25:26 +01:00 
			
		
		
		
	init
This commit is contained in:
		
						commit
						922e4fcc00
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | */Cargo.lock | ||||||
|  | */target | ||||||
							
								
								
									
										31
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | |||||||
|  | # musicdb | ||||||
|  | 
 | ||||||
|  | custom music player running on my personal SBC which can be controlled from other WiFi devices (phone/pc) | ||||||
|  | 
 | ||||||
|  | should perform pretty well (it runs well on my Pine A64 with 10k+ songs) | ||||||
|  | 
 | ||||||
|  | ## why??? | ||||||
|  | 
 | ||||||
|  | #### server/client | ||||||
|  | 
 | ||||||
|  | allows you to play music on any device you want while controlling playback from anywhere. | ||||||
|  | you can either run the client and server on the same machine or connect via tcp. | ||||||
|  | 
 | ||||||
|  | if one client makes a change, all other clients will be notified of it and update almost instantly. | ||||||
|  | 
 | ||||||
|  | it is also possible for a fake "client" to mirror the main server's playback, so you could sync up your entire house if you wanted to. | ||||||
|  | 
 | ||||||
|  | #### complicated queue | ||||||
|  | 
 | ||||||
|  | - allows more customization of playback (loops, custom shuffles, etc.) | ||||||
|  | - is more organized (adding an album doesn't add 10-20 songs, it creates a folder so you can (re)move the entire album in/from the queue) | ||||||
|  | 
 | ||||||
|  | #### caching of songs | ||||||
|  | 
 | ||||||
|  | for (almost) gapless playback, even when the data is stored on a NAS or cloud | ||||||
|  | 
 | ||||||
|  | #### central database | ||||||
|  | 
 | ||||||
|  | when storing data on a cloud, it would take forever to load all songs and scan them for metadata. | ||||||
|  | you would also run into issues with different file formats and where to store the cover images. | ||||||
|  | a custom database speeds up server startup and allows for more features. | ||||||
							
								
								
									
										1
									
								
								musicdb-client/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								musicdb-client/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | /target | ||||||
							
								
								
									
										14
									
								
								musicdb-client/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								musicdb-client/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | [package] | ||||||
|  | name = "musicdb-client" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | 
 | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } | ||||||
|  | regex = "1.9.3" | ||||||
|  | speedy2d = { version = "1.12.0", optional = true } | ||||||
|  | 
 | ||||||
|  | [features] | ||||||
|  | default = ["speedy2d"] | ||||||
							
								
								
									
										880
									
								
								musicdb-client/src/gui.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										880
									
								
								musicdb-client/src/gui.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,880 @@ | |||||||
|  | use std::{ | ||||||
|  |     any::Any, | ||||||
|  |     eprintln, | ||||||
|  |     net::TcpStream, | ||||||
|  |     sync::{Arc, Mutex}, | ||||||
|  |     time::Instant, | ||||||
|  |     usize, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use musicdb_lib::{ | ||||||
|  |     data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId}, | ||||||
|  |     load::ToFromBytes, | ||||||
|  |     server::Command, | ||||||
|  | }; | ||||||
|  | use speedy2d::{ | ||||||
|  |     color::Color, | ||||||
|  |     dimen::{UVec2, Vec2}, | ||||||
|  |     font::Font, | ||||||
|  |     shape::Rectangle, | ||||||
|  |     window::{ | ||||||
|  |         KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender, | ||||||
|  |         VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper, | ||||||
|  |     }, | ||||||
|  |     Graphics2D, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey}; | ||||||
|  | 
 | ||||||
|  | pub enum GuiEvent { | ||||||
|  |     Refresh, | ||||||
|  |     UpdatedQueue, | ||||||
|  |     UpdatedLibrary, | ||||||
|  |     Exit, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn main( | ||||||
|  |     database: Arc<Mutex<Database>>, | ||||||
|  |     connection: TcpStream, | ||||||
|  |     event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>, | ||||||
|  | ) { | ||||||
|  |     let window = speedy2d::Window::<GuiEvent>::new_with_user_events( | ||||||
|  |         "MusicDB Client", | ||||||
|  |         WindowCreationOptions::new_fullscreen_borderless(), | ||||||
|  |     ) | ||||||
|  |     .expect("couldn't open window"); | ||||||
|  |     *event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender()); | ||||||
|  |     let sender = window.create_user_event_sender(); | ||||||
|  |     window.run_loop(Gui::new(database, connection, event_sender_arc, sender)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct Gui { | ||||||
|  |     pub event_sender: UserEventSender<GuiEvent>, | ||||||
|  |     pub database: Arc<Mutex<Database>>, | ||||||
|  |     pub connection: TcpStream, | ||||||
|  |     pub gui: GuiElem, | ||||||
|  |     pub size: UVec2, | ||||||
|  |     pub mouse_pos: Vec2, | ||||||
|  |     pub font: Font, | ||||||
|  |     pub last_draw: Instant, | ||||||
|  |     pub modifiers: ModifiersState, | ||||||
|  |     pub dragging: Option<( | ||||||
|  |         Dragging, | ||||||
|  |         Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>, | ||||||
|  |     )>, | ||||||
|  |     pub line_height: f32, | ||||||
|  |     pub last_height: f32, | ||||||
|  |     pub scroll_pixels_multiplier: f64, | ||||||
|  |     pub scroll_lines_multiplier: f64, | ||||||
|  |     pub scroll_pages_multiplier: f64, | ||||||
|  | } | ||||||
|  | impl Gui { | ||||||
|  |     fn new( | ||||||
|  |         database: Arc<Mutex<Database>>, | ||||||
|  |         connection: TcpStream, | ||||||
|  |         event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>, | ||||||
|  |         event_sender: UserEventSender<GuiEvent>, | ||||||
|  |     ) -> Self { | ||||||
|  |         database.lock().unwrap().update_endpoints.push( | ||||||
|  |             musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { | ||||||
|  |                 Command::Resume | ||||||
|  |                 | Command::Pause | ||||||
|  |                 | Command::Stop | ||||||
|  |                 | Command::Save | ||||||
|  |                 | Command::SetLibraryDirectory(..) => {} | ||||||
|  |                 Command::NextSong | ||||||
|  |                 | Command::QueueUpdate(..) | ||||||
|  |                 | Command::QueueAdd(..) | ||||||
|  |                 | Command::QueueInsert(..) | ||||||
|  |                 | Command::QueueRemove(..) | ||||||
|  |                 | Command::QueueGoto(..) => { | ||||||
|  |                     if let Some(s) = &*event_sender_arc.lock().unwrap() { | ||||||
|  |                         _ = s.send_event(GuiEvent::UpdatedQueue); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 Command::SyncDatabase(..) | ||||||
|  |                 | Command::AddSong(_) | ||||||
|  |                 | Command::AddAlbum(_) | ||||||
|  |                 | Command::AddArtist(_) | ||||||
|  |                 | Command::ModifySong(_) | ||||||
|  |                 | Command::ModifyAlbum(_) | ||||||
|  |                 | Command::ModifyArtist(_) => { | ||||||
|  |                     if let Some(s) = &*event_sender_arc.lock().unwrap() { | ||||||
|  |                         _ = s.send_event(GuiEvent::UpdatedLibrary); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             })), | ||||||
|  |         ); | ||||||
|  |         let line_height = 32.0; | ||||||
|  |         let scroll_pixels_multiplier = 1.0; | ||||||
|  |         let scroll_lines_multiplier = 3.0; | ||||||
|  |         let scroll_pages_multiplier = 0.75; | ||||||
|  |         Gui { | ||||||
|  |             event_sender, | ||||||
|  |             database, | ||||||
|  |             connection, | ||||||
|  |             gui: GuiElem::new(WithFocusHotkey::new_noshift( | ||||||
|  |                 VirtualKeyCode::Escape, | ||||||
|  |                 GuiScreen::new( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     line_height, | ||||||
|  |                     scroll_pixels_multiplier, | ||||||
|  |                     scroll_lines_multiplier, | ||||||
|  |                     scroll_pages_multiplier, | ||||||
|  |                 ), | ||||||
|  |             )), | ||||||
|  |             size: UVec2::ZERO, | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |             font: Font::new(include_bytes!( | ||||||
|  |                 "/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf" | ||||||
|  |             )) | ||||||
|  |             .unwrap(), | ||||||
|  |             // font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
 | ||||||
|  |             last_draw: Instant::now(), | ||||||
|  |             modifiers: ModifiersState::default(), | ||||||
|  |             dragging: None, | ||||||
|  |             line_height, | ||||||
|  |             last_height: 720.0, | ||||||
|  |             scroll_pixels_multiplier, | ||||||
|  |             scroll_lines_multiplier, | ||||||
|  |             scroll_pages_multiplier, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// the trait implemented by all Gui elements.
 | ||||||
|  | /// feel free to override the methods you wish to use.
 | ||||||
|  | #[allow(unused)] | ||||||
|  | pub trait GuiElemTrait { | ||||||
|  |     fn config(&self) -> &GuiElemCfg; | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg; | ||||||
|  |     /// note: drawing happens from the last to the first element, while priority is from first to last.
 | ||||||
|  |     /// if you wish to add a "high priority" child to a Vec<GuiElem> using push, .rev() the iterator in this method.
 | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_>; | ||||||
|  |     fn any(&self) -> &dyn Any; | ||||||
|  |     fn any_mut(&mut self) -> &mut dyn Any; | ||||||
|  |     fn clone_gui(&self) -> Box<dyn GuiElemTrait>; | ||||||
|  |     /// handles drawing.
 | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {} | ||||||
|  |     /// an event that is invoked whenever a mouse button is pressed on the element.
 | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     /// an event that is invoked whenever a mouse button that was pressed on the element is released anywhere.
 | ||||||
|  |     fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     /// an event that is invoked after a mouse button was pressed and released on the same GUI element.
 | ||||||
|  |     fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn char_watch(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn key_watch( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: ModifiersState, | ||||||
|  |         down: bool, | ||||||
|  |         key: Option<VirtualKeyCode>, | ||||||
|  |         scan: KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn key_focus( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: ModifiersState, | ||||||
|  |         down: bool, | ||||||
|  |         key: Option<VirtualKeyCode>, | ||||||
|  |         scan: KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     /// When something is dragged and released over this element
 | ||||||
|  |     fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> { | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  |     fn updated_library(&mut self) {} | ||||||
|  |     fn updated_queue(&mut self) {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Clone)] | ||||||
|  | pub struct GuiElemCfg { | ||||||
|  |     pub enabled: bool, | ||||||
|  |     /// if true, indicates that something (text size, screen size, ...) has changed
 | ||||||
|  |     /// and you should probably relayout and redraw from scratch.
 | ||||||
|  |     pub redraw: bool, | ||||||
|  |     pub pos: Rectangle, | ||||||
|  |     /// the pixel position after the last call to draw().
 | ||||||
|  |     /// in draw, use info.pos instead, as pixel_pos is only updated *after* draw().
 | ||||||
|  |     /// this can act like a "previous pos" field within draw.
 | ||||||
|  |     pub pixel_pos: Rectangle, | ||||||
|  |     pub mouse_down: (bool, bool, bool), | ||||||
|  |     pub mouse_events: bool, | ||||||
|  |     pub scroll_events: bool, | ||||||
|  |     /// allows elements to watch all keyboard events, regardless of keyboard focus.
 | ||||||
|  |     pub keyboard_events_watch: bool, | ||||||
|  |     /// indicates that this element can have the keyboard focus
 | ||||||
|  |     pub keyboard_events_focus: bool, | ||||||
|  |     /// index of the child that has keyboard focus. if usize::MAX, `self` has focus.
 | ||||||
|  |     /// will automatically be changed when Tab is pressed. [TODO]
 | ||||||
|  |     pub keyboard_focus_index: usize, | ||||||
|  |     pub request_keyboard_focus: bool, | ||||||
|  |     pub drag_target: bool, | ||||||
|  | } | ||||||
|  | impl GuiElemCfg { | ||||||
|  |     pub fn at(pos: Rectangle) -> Self { | ||||||
|  |         Self { | ||||||
|  |             pos, | ||||||
|  |             ..Default::default() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn w_mouse(mut self) -> Self { | ||||||
|  |         self.mouse_events = true; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     pub fn w_scroll(mut self) -> Self { | ||||||
|  |         self.scroll_events = true; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     pub fn w_keyboard_watch(mut self) -> Self { | ||||||
|  |         self.keyboard_events_watch = true; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     pub fn w_keyboard_focus(mut self) -> Self { | ||||||
|  |         self.keyboard_events_focus = true; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     pub fn w_drag_target(mut self) -> Self { | ||||||
|  |         self.drag_target = true; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     pub fn disabled(mut self) -> Self { | ||||||
|  |         self.enabled = false; | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl Default for GuiElemCfg { | ||||||
|  |     fn default() -> Self { | ||||||
|  |         Self { | ||||||
|  |             enabled: true, | ||||||
|  |             redraw: false, | ||||||
|  |             pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), | ||||||
|  |             pixel_pos: Rectangle::ZERO, | ||||||
|  |             mouse_down: (false, false, false), | ||||||
|  |             mouse_events: false, | ||||||
|  |             scroll_events: false, | ||||||
|  |             keyboard_events_watch: false, | ||||||
|  |             keyboard_events_focus: false, | ||||||
|  |             keyboard_focus_index: usize::MAX, | ||||||
|  |             request_keyboard_focus: false, | ||||||
|  |             drag_target: false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | pub enum GuiAction { | ||||||
|  |     OpenMain, | ||||||
|  |     SetIdle(bool), | ||||||
|  |     OpenSettings(bool), | ||||||
|  |     Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>), | ||||||
|  |     SendToServer(Command), | ||||||
|  |     /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true.
 | ||||||
|  |     ResetKeyboardFocus, | ||||||
|  |     SetDragging( | ||||||
|  |         Option<( | ||||||
|  |             Dragging, | ||||||
|  |             Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>, | ||||||
|  |         )>, | ||||||
|  |     ), | ||||||
|  |     SetLineHeight(f32), | ||||||
|  |     Do(Box<dyn FnMut(&mut Gui)>), | ||||||
|  |     Exit, | ||||||
|  | } | ||||||
|  | pub enum Dragging { | ||||||
|  |     Artist(ArtistId), | ||||||
|  |     Album(AlbumId), | ||||||
|  |     Song(SongId), | ||||||
|  |     Queue(Queue), | ||||||
|  | } | ||||||
|  | pub struct DrawInfo<'a> { | ||||||
|  |     pub actions: Vec<GuiAction>, | ||||||
|  |     pub pos: Rectangle, | ||||||
|  |     pub database: &'a mut Database, | ||||||
|  |     pub font: &'a Font, | ||||||
|  |     /// absolute position of the mouse on the screen.
 | ||||||
|  |     /// compare this to `pos` to find the mouse's relative position.
 | ||||||
|  |     pub mouse_pos: Vec2, | ||||||
|  |     pub helper: Option<&'a mut WindowHelper<GuiEvent>>, | ||||||
|  |     pub has_keyboard_focus: bool, | ||||||
|  |     pub child_has_keyboard_focus: bool, | ||||||
|  |     /// the height of one line of text (in pixels)
 | ||||||
|  |     pub line_height: f32, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct GuiElem { | ||||||
|  |     pub inner: Box<dyn GuiElemTrait>, | ||||||
|  | } | ||||||
|  | impl Clone for GuiElem { | ||||||
|  |     fn clone(&self) -> Self { | ||||||
|  |         Self { | ||||||
|  |             inner: self.inner.clone_gui(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElem { | ||||||
|  |     pub fn new<T: GuiElemTrait + 'static>(inner: T) -> Self { | ||||||
|  |         Self { | ||||||
|  |             inner: Box::new(inner), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn try_as<T: Any>(&self) -> Option<&T> { | ||||||
|  |         self.inner.any().downcast_ref() | ||||||
|  |     } | ||||||
|  |     pub fn try_as_mut<T: Any>(&mut self) -> Option<&mut T> { | ||||||
|  |         self.inner.any_mut().downcast_mut() | ||||||
|  |     } | ||||||
|  |     pub fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { | ||||||
|  |         if !self.inner.config_mut().enabled { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         // adjust info
 | ||||||
|  |         let npos = adjust_area(&info.pos, &self.inner.config_mut().pos); | ||||||
|  |         let ppos = std::mem::replace(&mut info.pos, npos); | ||||||
|  |         if info.child_has_keyboard_focus { | ||||||
|  |             if self.inner.config().keyboard_focus_index == usize::MAX { | ||||||
|  |                 info.has_keyboard_focus = true; | ||||||
|  |                 info.child_has_keyboard_focus = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // call trait's draw function
 | ||||||
|  |         self.inner.draw(info, g); | ||||||
|  |         // reset info
 | ||||||
|  |         info.has_keyboard_focus = false; | ||||||
|  |         let focus_path = info.child_has_keyboard_focus; | ||||||
|  |         // children (in reverse order - first element has the highest priority)
 | ||||||
|  |         let kbd_focus_index = self.inner.config().keyboard_focus_index; | ||||||
|  |         for (i, c) in self | ||||||
|  |             .inner | ||||||
|  |             .children() | ||||||
|  |             .collect::<Vec<_>>() | ||||||
|  |             .into_iter() | ||||||
|  |             .enumerate() | ||||||
|  |             .rev() | ||||||
|  |         { | ||||||
|  |             info.child_has_keyboard_focus = focus_path && i == kbd_focus_index; | ||||||
|  |             c.draw(info, g); | ||||||
|  |         } | ||||||
|  |         // reset pt. 2
 | ||||||
|  |         info.child_has_keyboard_focus = focus_path; | ||||||
|  |         self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos); | ||||||
|  |     } | ||||||
|  |     /// recursively applies the function to all gui elements below and including this one
 | ||||||
|  |     pub fn recursive_all<F: FnMut(&mut GuiElem)>(&mut self, f: &mut F) { | ||||||
|  |         f(self); | ||||||
|  |         for c in self.inner.children() { | ||||||
|  |             c.recursive_all(f); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn mouse_event<F: FnMut(&mut GuiElem) -> Option<Vec<GuiAction>>>( | ||||||
|  |         &mut self, | ||||||
|  |         condition: &mut F, | ||||||
|  |         pos: Vec2, | ||||||
|  |     ) -> Option<Vec<GuiAction>> { | ||||||
|  |         for c in &mut self.inner.children() { | ||||||
|  |             if c.inner.config().enabled { | ||||||
|  |                 if c.inner.config().pixel_pos.contains(pos) { | ||||||
|  |                     if let Some(v) = c.mouse_event(condition, pos) { | ||||||
|  |                         return Some(v); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         condition(self) | ||||||
|  |     } | ||||||
|  |     fn release_drag( | ||||||
|  |         &mut self, | ||||||
|  |         dragged: &mut Option<Dragging>, | ||||||
|  |         pos: Vec2, | ||||||
|  |     ) -> Option<Vec<GuiAction>> { | ||||||
|  |         self.mouse_event( | ||||||
|  |             &mut |v| { | ||||||
|  |                 if v.inner.config().drag_target { | ||||||
|  |                     if let Some(d) = dragged.take() { | ||||||
|  |                         return Some(v.inner.dragged(d)); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 None | ||||||
|  |             }, | ||||||
|  |             pos, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     fn mouse_button( | ||||||
|  |         &mut self, | ||||||
|  |         button: MouseButton, | ||||||
|  |         down: bool, | ||||||
|  |         pos: Vec2, | ||||||
|  |     ) -> Option<Vec<GuiAction>> { | ||||||
|  |         if down { | ||||||
|  |             self.mouse_event( | ||||||
|  |                 &mut |v: &mut GuiElem| { | ||||||
|  |                     if v.inner.config().mouse_events { | ||||||
|  |                         match button { | ||||||
|  |                             MouseButton::Left => v.inner.config_mut().mouse_down.0 = true, | ||||||
|  |                             MouseButton::Middle => v.inner.config_mut().mouse_down.1 = true, | ||||||
|  |                             MouseButton::Right => v.inner.config_mut().mouse_down.2 = true, | ||||||
|  |                             MouseButton::Other(_) => {} | ||||||
|  |                         } | ||||||
|  |                         Some(v.inner.mouse_down(button)) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 pos, | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             let mut vec = vec![]; | ||||||
|  |             if let Some(a) = self.mouse_event( | ||||||
|  |                 &mut |v: &mut GuiElem| { | ||||||
|  |                     let down = v.inner.config().mouse_down; | ||||||
|  |                     if v.inner.config().mouse_events | ||||||
|  |                         && ((button == MouseButton::Left && down.0) | ||||||
|  |                             || (button == MouseButton::Middle && down.1) | ||||||
|  |                             || (button == MouseButton::Right && down.2)) | ||||||
|  |                     { | ||||||
|  |                         Some(v.inner.mouse_pressed(button)) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 pos, | ||||||
|  |             ) { | ||||||
|  |                 vec.extend(a); | ||||||
|  |             }; | ||||||
|  |             self.recursive_all(&mut |v| { | ||||||
|  |                 if v.inner.config().mouse_events { | ||||||
|  |                     match button { | ||||||
|  |                         MouseButton::Left => v.inner.config_mut().mouse_down.0 = false, | ||||||
|  |                         MouseButton::Middle => v.inner.config_mut().mouse_down.1 = false, | ||||||
|  |                         MouseButton::Right => v.inner.config_mut().mouse_down.2 = false, | ||||||
|  |                         MouseButton::Other(_) => {} | ||||||
|  |                     } | ||||||
|  |                     vec.extend(v.inner.mouse_up(button)); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             Some(vec) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn mouse_wheel(&mut self, diff: f32, pos: Vec2) -> Option<Vec<GuiAction>> { | ||||||
|  |         self.mouse_event( | ||||||
|  |             &mut |v| { | ||||||
|  |                 if v.inner.config().scroll_events { | ||||||
|  |                     Some(v.inner.mouse_wheel(diff)) | ||||||
|  |                 } else { | ||||||
|  |                     None | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             pos, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     fn keyboard_event< | ||||||
|  |         F: FnOnce(&mut Self, &mut Vec<GuiAction>), | ||||||
|  |         G: FnMut(&mut Self, &mut Vec<GuiAction>), | ||||||
|  |     >( | ||||||
|  |         &mut self, | ||||||
|  |         f_focus: F, | ||||||
|  |         mut f_watch: G, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         let mut o = vec![]; | ||||||
|  |         self.keyboard_event_inner(&mut Some(f_focus), &mut f_watch, &mut o, true); | ||||||
|  |         o | ||||||
|  |     } | ||||||
|  |     fn keyboard_event_inner< | ||||||
|  |         F: FnOnce(&mut Self, &mut Vec<GuiAction>), | ||||||
|  |         G: FnMut(&mut Self, &mut Vec<GuiAction>), | ||||||
|  |     >( | ||||||
|  |         &mut self, | ||||||
|  |         f_focus: &mut Option<F>, | ||||||
|  |         f_watch: &mut G, | ||||||
|  |         events: &mut Vec<GuiAction>, | ||||||
|  |         focus: bool, | ||||||
|  |     ) { | ||||||
|  |         f_watch(self, events); | ||||||
|  |         let focus_index = self.inner.config().keyboard_focus_index; | ||||||
|  |         for (i, child) in self.inner.children().enumerate() { | ||||||
|  |             child.keyboard_event_inner(f_focus, f_watch, events, focus && i == focus_index); | ||||||
|  |         } | ||||||
|  |         if focus { | ||||||
|  |             // we have focus and no child has consumed f_focus
 | ||||||
|  |             if let Some(f) = f_focus.take() { | ||||||
|  |                 f(self, events) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool { | ||||||
|  |         let mut focus_index = if refocus { | ||||||
|  |             usize::MAX | ||||||
|  |         } else { | ||||||
|  |             self.inner.config().keyboard_focus_index | ||||||
|  |         }; | ||||||
|  |         let allow_focus = self.inner.config().keyboard_events_focus; | ||||||
|  |         let mut children = self.inner.children().collect::<Vec<_>>(); | ||||||
|  |         if focus_index == usize::MAX { | ||||||
|  |             if decrement { | ||||||
|  |                 focus_index = children.len().saturating_sub(1); | ||||||
|  |             } else { | ||||||
|  |                 focus_index = 0; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let mut changed = refocus; | ||||||
|  |         let ok = loop { | ||||||
|  |             if let Some(child) = children.get_mut(focus_index) { | ||||||
|  |                 if child.keyboard_move_focus(decrement, changed) { | ||||||
|  |                     break true; | ||||||
|  |                 } else { | ||||||
|  |                     changed = true; | ||||||
|  |                     if !decrement { | ||||||
|  |                         focus_index += 1; | ||||||
|  |                     } else { | ||||||
|  |                         focus_index = focus_index.wrapping_sub(1); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 focus_index = usize::MAX; | ||||||
|  |                 break allow_focus && refocus; | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         self.inner.config_mut().keyboard_focus_index = focus_index; | ||||||
|  |         ok | ||||||
|  |     } | ||||||
|  |     fn keyboard_reset_focus(&mut self) -> bool { | ||||||
|  |         let mut index = usize::MAX; | ||||||
|  |         for (i, c) in self.inner.children().enumerate() { | ||||||
|  |             if c.keyboard_reset_focus() { | ||||||
|  |                 index = i; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let wants = std::mem::replace(&mut self.inner.config_mut().request_keyboard_focus, false); | ||||||
|  |         self.inner.config_mut().keyboard_focus_index = index; | ||||||
|  |         index != usize::MAX || wants | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn adjust_area(outer: &Rectangle, rel_area: &Rectangle) -> Rectangle { | ||||||
|  |     Rectangle::new( | ||||||
|  |         adjust_pos(outer, rel_area.top_left()), | ||||||
|  |         adjust_pos(outer, rel_area.bottom_right()), | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | pub fn adjust_pos(outer: &Rectangle, rel_pos: &Vec2) -> Vec2 { | ||||||
|  |     Vec2::new( | ||||||
|  |         outer.top_left().x + outer.width() * rel_pos.x, | ||||||
|  |         outer.top_left().y + outer.height() * rel_pos.y, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Gui { | ||||||
|  |     fn exec_gui_action(&mut self, action: GuiAction) { | ||||||
|  |         match action { | ||||||
|  |             GuiAction::Build(f) => { | ||||||
|  |                 let actions = f(&mut *self.database.lock().unwrap()); | ||||||
|  |                 for action in actions { | ||||||
|  |                     self.exec_gui_action(action); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             GuiAction::SendToServer(cmd) => { | ||||||
|  |                 if let Err(e) = cmd.to_bytes(&mut self.connection) { | ||||||
|  |                     eprintln!("Error sending command to server: {e}"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             GuiAction::ResetKeyboardFocus => _ = self.gui.keyboard_reset_focus(), | ||||||
|  |             GuiAction::SetDragging(d) => self.dragging = d, | ||||||
|  |             GuiAction::SetLineHeight(h) => { | ||||||
|  |                 self.line_height = h; | ||||||
|  |                 self.gui | ||||||
|  |                     .recursive_all(&mut |e| e.inner.config_mut().redraw = true); | ||||||
|  |             } | ||||||
|  |             GuiAction::Do(mut f) => f(self), | ||||||
|  |             GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit), | ||||||
|  |             GuiAction::SetIdle(v) => { | ||||||
|  |                 if let Some(gui) = self | ||||||
|  |                     .gui | ||||||
|  |                     .inner | ||||||
|  |                     .any_mut() | ||||||
|  |                     .downcast_mut::<WithFocusHotkey<GuiScreen>>() | ||||||
|  |                 { | ||||||
|  |                     if gui.inner.idle.0 != v { | ||||||
|  |                         gui.inner.idle = (v, Some(Instant::now())); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             GuiAction::OpenSettings(v) => { | ||||||
|  |                 if let Some(gui) = self | ||||||
|  |                     .gui | ||||||
|  |                     .inner | ||||||
|  |                     .any_mut() | ||||||
|  |                     .downcast_mut::<WithFocusHotkey<GuiScreen>>() | ||||||
|  |                 { | ||||||
|  |                     if gui.inner.idle.0 { | ||||||
|  |                         gui.inner.idle = (false, Some(Instant::now())); | ||||||
|  |                     } | ||||||
|  |                     if gui.inner.settings.0 != v { | ||||||
|  |                         gui.inner.settings = (v, Some(Instant::now())); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             GuiAction::OpenMain => { | ||||||
|  |                 if let Some(gui) = self | ||||||
|  |                     .gui | ||||||
|  |                     .inner | ||||||
|  |                     .any_mut() | ||||||
|  |                     .downcast_mut::<WithFocusHotkey<GuiScreen>>() | ||||||
|  |                 { | ||||||
|  |                     if gui.inner.idle.0 { | ||||||
|  |                         gui.inner.idle = (false, Some(Instant::now())); | ||||||
|  |                     } | ||||||
|  |                     if gui.inner.settings.0 { | ||||||
|  |                         gui.inner.settings = (false, Some(Instant::now())); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl WindowHandler<GuiEvent> for Gui { | ||||||
|  |     fn on_draw(&mut self, helper: &mut WindowHelper<GuiEvent>, graphics: &mut Graphics2D) { | ||||||
|  |         let start = Instant::now(); | ||||||
|  |         graphics.draw_rectangle( | ||||||
|  |             Rectangle::new(Vec2::ZERO, self.size.into_f32()), | ||||||
|  |             Color::BLACK, | ||||||
|  |         ); | ||||||
|  |         let mut dblock = self.database.lock().unwrap(); | ||||||
|  |         let mut info = DrawInfo { | ||||||
|  |             actions: Vec::with_capacity(0), | ||||||
|  |             pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()), | ||||||
|  |             database: &mut *dblock, | ||||||
|  |             font: &self.font, | ||||||
|  |             mouse_pos: self.mouse_pos, | ||||||
|  |             helper: Some(helper), | ||||||
|  |             has_keyboard_focus: false, | ||||||
|  |             child_has_keyboard_focus: true, | ||||||
|  |             line_height: self.line_height, | ||||||
|  |         }; | ||||||
|  |         self.gui.draw(&mut info, graphics); | ||||||
|  |         let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0)); | ||||||
|  |         if let Some((d, f)) = &mut self.dragging { | ||||||
|  |             if let Some(f) = f { | ||||||
|  |                 f(&mut info, graphics); | ||||||
|  |             } else { | ||||||
|  |                 match d { | ||||||
|  |                     Dragging::Artist(_) => graphics.draw_circle( | ||||||
|  |                         self.mouse_pos, | ||||||
|  |                         25.0, | ||||||
|  |                         Color::from_int_rgba(0, 100, 255, 100), | ||||||
|  |                     ), | ||||||
|  |                     Dragging::Album(_) => graphics.draw_circle( | ||||||
|  |                         self.mouse_pos, | ||||||
|  |                         25.0, | ||||||
|  |                         Color::from_int_rgba(0, 100, 255, 100), | ||||||
|  |                     ), | ||||||
|  |                     Dragging::Song(_) => graphics.draw_circle( | ||||||
|  |                         self.mouse_pos, | ||||||
|  |                         25.0, | ||||||
|  |                         Color::from_int_rgba(0, 100, 255, 100), | ||||||
|  |                     ), | ||||||
|  |                     Dragging::Queue(_) => graphics.draw_circle( | ||||||
|  |                         self.mouse_pos, | ||||||
|  |                         25.0, | ||||||
|  |                         Color::from_int_rgba(100, 0, 255, 100), | ||||||
|  |                     ), | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         drop(info); | ||||||
|  |         drop(dblock); | ||||||
|  |         for a in actions { | ||||||
|  |             self.exec_gui_action(a); | ||||||
|  |         } | ||||||
|  |         // eprintln!(
 | ||||||
|  |         //     "fps <= {}",
 | ||||||
|  |         //     1000 / self.last_draw.elapsed().as_millis().max(1)
 | ||||||
|  |         // );
 | ||||||
|  |         self.last_draw = start; | ||||||
|  |     } | ||||||
|  |     fn on_mouse_button_down(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) { | ||||||
|  |         if let Some(a) = self.gui.mouse_button(button, true, self.mouse_pos.clone()) { | ||||||
|  |             for a in a { | ||||||
|  |                 self.exec_gui_action(a) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         helper.request_redraw(); | ||||||
|  |     } | ||||||
|  |     fn on_mouse_button_up(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) { | ||||||
|  |         if self.dragging.is_some() { | ||||||
|  |             if let Some(a) = self.gui.release_drag( | ||||||
|  |                 &mut self.dragging.take().map(|v| v.0), | ||||||
|  |                 self.mouse_pos.clone(), | ||||||
|  |             ) { | ||||||
|  |                 for a in a { | ||||||
|  |                     self.exec_gui_action(a) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if let Some(a) = self.gui.mouse_button(button, false, self.mouse_pos.clone()) { | ||||||
|  |             for a in a { | ||||||
|  |                 self.exec_gui_action(a) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         helper.request_redraw(); | ||||||
|  |     } | ||||||
|  |     fn on_mouse_wheel_scroll( | ||||||
|  |         &mut self, | ||||||
|  |         helper: &mut WindowHelper<GuiEvent>, | ||||||
|  |         distance: speedy2d::window::MouseScrollDistance, | ||||||
|  |     ) { | ||||||
|  |         let dist = match distance { | ||||||
|  |             MouseScrollDistance::Pixels { y, .. } => (self.scroll_pixels_multiplier * y) as f32, | ||||||
|  |             MouseScrollDistance::Lines { y, .. } => { | ||||||
|  |                 (self.scroll_lines_multiplier * y) as f32 * self.line_height | ||||||
|  |             } | ||||||
|  |             MouseScrollDistance::Pages { y, .. } => { | ||||||
|  |                 (self.scroll_pages_multiplier * y) as f32 * self.last_height | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         if let Some(a) = self.gui.mouse_wheel(dist, self.mouse_pos.clone()) { | ||||||
|  |             for a in a { | ||||||
|  |                 self.exec_gui_action(a) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         helper.request_redraw(); | ||||||
|  |     } | ||||||
|  |     fn on_keyboard_char(&mut self, helper: &mut WindowHelper<GuiEvent>, unicode_codepoint: char) { | ||||||
|  |         helper.request_redraw(); | ||||||
|  |         for a in self.gui.keyboard_event( | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_focus { | ||||||
|  |                     a.append( | ||||||
|  |                         &mut e | ||||||
|  |                             .inner | ||||||
|  |                             .char_focus(self.modifiers.clone(), unicode_codepoint), | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_watch { | ||||||
|  |                     a.append( | ||||||
|  |                         &mut e | ||||||
|  |                             .inner | ||||||
|  |                             .char_watch(self.modifiers.clone(), unicode_codepoint), | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         ) { | ||||||
|  |             self.exec_gui_action(a); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn on_key_down( | ||||||
|  |         &mut self, | ||||||
|  |         helper: &mut WindowHelper<GuiEvent>, | ||||||
|  |         virtual_key_code: Option<VirtualKeyCode>, | ||||||
|  |         scancode: KeyScancode, | ||||||
|  |     ) { | ||||||
|  |         helper.request_redraw(); | ||||||
|  |         if let Some(VirtualKeyCode::Tab) = virtual_key_code { | ||||||
|  |             if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) { | ||||||
|  |                 self.gui.keyboard_move_focus(self.modifiers.shift(), false); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         for a in self.gui.keyboard_event( | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_focus { | ||||||
|  |                     a.append(&mut e.inner.key_focus( | ||||||
|  |                         self.modifiers.clone(), | ||||||
|  |                         true, | ||||||
|  |                         virtual_key_code, | ||||||
|  |                         scancode, | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_watch { | ||||||
|  |                     a.append(&mut e.inner.key_watch( | ||||||
|  |                         self.modifiers.clone(), | ||||||
|  |                         true, | ||||||
|  |                         virtual_key_code, | ||||||
|  |                         scancode, | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         ) { | ||||||
|  |             self.exec_gui_action(a); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn on_key_up( | ||||||
|  |         &mut self, | ||||||
|  |         helper: &mut WindowHelper<GuiEvent>, | ||||||
|  |         virtual_key_code: Option<VirtualKeyCode>, | ||||||
|  |         scancode: KeyScancode, | ||||||
|  |     ) { | ||||||
|  |         helper.request_redraw(); | ||||||
|  |         for a in self.gui.keyboard_event( | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_focus { | ||||||
|  |                     a.append(&mut e.inner.key_focus( | ||||||
|  |                         self.modifiers.clone(), | ||||||
|  |                         false, | ||||||
|  |                         virtual_key_code, | ||||||
|  |                         scancode, | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             |e, a| { | ||||||
|  |                 if e.inner.config().keyboard_events_watch { | ||||||
|  |                     a.append(&mut e.inner.key_watch( | ||||||
|  |                         self.modifiers.clone(), | ||||||
|  |                         false, | ||||||
|  |                         virtual_key_code, | ||||||
|  |                         scancode, | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         ) { | ||||||
|  |             self.exec_gui_action(a); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn on_keyboard_modifiers_changed( | ||||||
|  |         &mut self, | ||||||
|  |         _helper: &mut WindowHelper<GuiEvent>, | ||||||
|  |         state: ModifiersState, | ||||||
|  |     ) { | ||||||
|  |         self.modifiers = state; | ||||||
|  |     } | ||||||
|  |     fn on_user_event(&mut self, helper: &mut WindowHelper<GuiEvent>, user_event: GuiEvent) { | ||||||
|  |         match user_event { | ||||||
|  |             GuiEvent::Refresh => helper.request_redraw(), | ||||||
|  |             GuiEvent::UpdatedLibrary => { | ||||||
|  |                 self.gui.recursive_all(&mut |e| e.inner.updated_library()); | ||||||
|  |                 helper.request_redraw(); | ||||||
|  |             } | ||||||
|  |             GuiEvent::UpdatedQueue => { | ||||||
|  |                 self.gui.recursive_all(&mut |e| e.inner.updated_queue()); | ||||||
|  |                 helper.request_redraw(); | ||||||
|  |             } | ||||||
|  |             GuiEvent::Exit => helper.terminate_loop(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn on_mouse_move(&mut self, helper: &mut WindowHelper<GuiEvent>, position: Vec2) { | ||||||
|  |         self.mouse_pos = position; | ||||||
|  |         helper.request_redraw(); | ||||||
|  |     } | ||||||
|  |     fn on_resize(&mut self, _helper: &mut WindowHelper<GuiEvent>, size_pixels: UVec2) { | ||||||
|  |         self.size = size_pixels; | ||||||
|  |         self.gui | ||||||
|  |             .recursive_all(&mut |e| e.inner.config_mut().redraw = true); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										459
									
								
								musicdb-client/src/gui_base.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										459
									
								
								musicdb-client/src/gui_base.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,459 @@ | |||||||
|  | use std::{sync::Arc, time::Instant}; | ||||||
|  | 
 | ||||||
|  | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, | ||||||
|  |     gui_text::Label, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color.
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Panel { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  |     pub background: Option<Color>, | ||||||
|  | } | ||||||
|  | impl Panel { | ||||||
|  |     pub fn new(config: GuiElemCfg, children: Vec<GuiElem>) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children, | ||||||
|  |             background: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn with_background(config: GuiElemCfg, children: Vec<GuiElem>, background: Color) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children, | ||||||
|  |             background: Some(background), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for Panel { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if let Some(c) = self.background { | ||||||
|  |             g.draw_rectangle(info.pos.clone(), c); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct ScrollBox { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     pub children: Vec<(GuiElem, f32)>, | ||||||
|  |     pub size_unit: ScrollBoxSizeUnit, | ||||||
|  |     pub scroll_target: f32, | ||||||
|  |     pub scroll_display: f32, | ||||||
|  |     height_bottom: f32, | ||||||
|  |     /// 0.max(height_bottom - 1)
 | ||||||
|  |     max_scroll: f32, | ||||||
|  |     last_height_px: f32, | ||||||
|  | } | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub enum ScrollBoxSizeUnit { | ||||||
|  |     Relative, | ||||||
|  |     Pixels, | ||||||
|  | } | ||||||
|  | impl ScrollBox { | ||||||
|  |     pub fn new( | ||||||
|  |         mut config: GuiElemCfg, | ||||||
|  |         size_unit: ScrollBoxSizeUnit, | ||||||
|  |         children: Vec<(GuiElem, f32)>, | ||||||
|  |     ) -> Self { | ||||||
|  |         // config.redraw = true;
 | ||||||
|  |         Self { | ||||||
|  |             config: config.w_scroll(), | ||||||
|  |             children, | ||||||
|  |             size_unit, | ||||||
|  |             scroll_target: 0.0, | ||||||
|  |             scroll_display: 0.0, | ||||||
|  |             /// the y-position of the bottom edge of the last element (i.e. the total height)
 | ||||||
|  |             height_bottom: 0.0, | ||||||
|  |             max_scroll: 0.0, | ||||||
|  |             last_height_px: 0.0, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for ScrollBox { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         Box::new( | ||||||
|  |             self.children | ||||||
|  |                 .iter_mut() | ||||||
|  |                 .rev() | ||||||
|  |                 .map(|(v, _)| v) | ||||||
|  |                 .skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0) | ||||||
|  |                 .take_while(|v| v.inner.config().pos.top_left().y < 1.0), | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     fn any(&self) -> &dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn any_mut(&mut self) -> &mut dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn clone_gui(&self) -> Box<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if self.config.pixel_pos.size() != info.pos.size() { | ||||||
|  |             self.config.redraw = true; | ||||||
|  |         } | ||||||
|  |         // smooth scrolling animation
 | ||||||
|  |         if self.scroll_target > self.max_scroll { | ||||||
|  |             self.scroll_target = self.max_scroll; | ||||||
|  |         } else if self.scroll_target < 0.0 { | ||||||
|  |             self.scroll_target = 0.0; | ||||||
|  |         } | ||||||
|  |         self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display; | ||||||
|  |         if self.scroll_display != self.scroll_target { | ||||||
|  |             self.config.redraw = true; | ||||||
|  |             if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() { | ||||||
|  |                 self.scroll_display = self.scroll_target; | ||||||
|  |             } else if let Some(h) = &info.helper { | ||||||
|  |                 h.request_redraw(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // recalculate positions
 | ||||||
|  |         if self.config.redraw { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             let mut y_pos = -self.scroll_display; | ||||||
|  |             for (e, h) in self.children.iter_mut() { | ||||||
|  |                 let h_rel = self.size_unit.to_rel(*h, info.pos.height()); | ||||||
|  |                 let y_rel = self.size_unit.to_rel(y_pos, info.pos.height()); | ||||||
|  |                 if y_rel + h_rel >= 0.0 && y_rel <= 1.0 { | ||||||
|  |                     let cfg = e.inner.config_mut(); | ||||||
|  |                     cfg.enabled = true; | ||||||
|  |                     cfg.pos = Rectangle::new( | ||||||
|  |                         Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)), | ||||||
|  |                         Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)), | ||||||
|  |                     ); | ||||||
|  |                 } else { | ||||||
|  |                     e.inner.config_mut().enabled = false; | ||||||
|  |                 } | ||||||
|  |                 y_pos += *h; | ||||||
|  |             } | ||||||
|  |             self.height_bottom = y_pos + self.scroll_display; | ||||||
|  |             self.max_scroll = | ||||||
|  |                 0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height())); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn mouse_wheel(&mut self, diff: f32) -> Vec<crate::gui::GuiAction> { | ||||||
|  |         self.scroll_target = (self.scroll_target | ||||||
|  |             - self.size_unit.from_abs(diff as f32, self.last_height_px)) | ||||||
|  |         .max(0.0); | ||||||
|  |         Vec::with_capacity(0) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ScrollBoxSizeUnit { | ||||||
|  |     fn to_rel(&self, val: f32, draw_height: f32) -> f32 { | ||||||
|  |         match self { | ||||||
|  |             Self::Relative => val, | ||||||
|  |             Self::Pixels => val / draw_height, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn from_rel(&self, val: f32, draw_height: f32) -> f32 { | ||||||
|  |         match self { | ||||||
|  |             Self::Relative => val, | ||||||
|  |             Self::Pixels => val * draw_height, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn from_abs(&self, val: f32, draw_height: f32) -> f32 { | ||||||
|  |         match self { | ||||||
|  |             Self::Relative => val / draw_height, | ||||||
|  |             Self::Pixels => val, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Button { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  |     action: Arc<dyn Fn(&Self) -> Vec<GuiAction> + 'static>, | ||||||
|  | } | ||||||
|  | impl Button { | ||||||
|  |     /// automatically adds w_mouse to config
 | ||||||
|  |     pub fn new<F: Fn(&Self) -> Vec<GuiAction> + 'static>( | ||||||
|  |         config: GuiElemCfg, | ||||||
|  |         action: F, | ||||||
|  |         children: Vec<GuiElem>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse(), | ||||||
|  |             children, | ||||||
|  |             action: Arc::new(action), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for Button { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             (self.action)(self) | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         let mouse_down = self.config.mouse_down.0; | ||||||
|  |         let contains = info.pos.contains(info.mouse_pos); | ||||||
|  |         g.draw_rectangle( | ||||||
|  |             info.pos.clone(), | ||||||
|  |             if mouse_down && contains { | ||||||
|  |                 Color::from_rgb(0.25, 0.25, 0.25) | ||||||
|  |             } else if contains || mouse_down { | ||||||
|  |                 Color::from_rgb(0.15, 0.15, 0.15) | ||||||
|  |             } else { | ||||||
|  |                 Color::from_rgb(0.1, 0.1, 0.1) | ||||||
|  |             }, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Slider { | ||||||
|  |     pub config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  |     pub slider_pos: Rectangle, | ||||||
|  |     pub min: f64, | ||||||
|  |     pub max: f64, | ||||||
|  |     pub val: f64, | ||||||
|  |     val_changed: bool, | ||||||
|  |     pub val_changed_subs: Vec<bool>, | ||||||
|  |     /// if true, the display should be visible.
 | ||||||
|  |     pub display: bool, | ||||||
|  |     /// if Some, the display is in a transition period.
 | ||||||
|  |     /// you can set this to None to indicate that the transition has finished, but this is not required.
 | ||||||
|  |     pub display_since: Option<Instant>, | ||||||
|  |     pub on_update: Arc<dyn Fn(&mut Self, &mut DrawInfo)>, | ||||||
|  | } | ||||||
|  | impl Slider { | ||||||
|  |     /// returns true if the value of the slider has changed since the last time this function was called.
 | ||||||
|  |     /// this is usually used by the closure responsible for directly handling updates. if you wish to check for changes
 | ||||||
|  |     /// from outside, push a `false` to `val_changed_subs` and remember your index.
 | ||||||
|  |     /// when the value changes, this will be set to `true`. don't forget to reset it to `false` again if you find it set to `true`,
 | ||||||
|  |     /// or your code will run every time.
 | ||||||
|  |     pub fn val_changed(&mut self) -> bool { | ||||||
|  |         if self.val_changed { | ||||||
|  |             self.val_changed = false; | ||||||
|  |             true | ||||||
|  |         } else { | ||||||
|  |             false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn val_changed_peek(&self) -> bool { | ||||||
|  |         self.val_changed | ||||||
|  |     } | ||||||
|  |     pub fn new<F: Fn(&mut Self, &mut DrawInfo) + 'static>( | ||||||
|  |         config: GuiElemCfg, | ||||||
|  |         slider_pos: Rectangle, | ||||||
|  |         min: f64, | ||||||
|  |         max: f64, | ||||||
|  |         val: f64, | ||||||
|  |         children: Vec<GuiElem>, | ||||||
|  |         on_update: F, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse().w_scroll(), | ||||||
|  |             children, | ||||||
|  |             slider_pos, | ||||||
|  |             min, | ||||||
|  |             max, | ||||||
|  |             val, | ||||||
|  |             val_changed: true, | ||||||
|  |             val_changed_subs: vec![], | ||||||
|  |             display: false, | ||||||
|  |             display_since: None, | ||||||
|  |             on_update: Arc::new(on_update), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn new_labeled<F: Fn(&mut Self, &mut Label, &mut DrawInfo) + 'static>( | ||||||
|  |         config: GuiElemCfg, | ||||||
|  |         min: f64, | ||||||
|  |         max: f64, | ||||||
|  |         val: f64, | ||||||
|  |         mktext: F, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self::new( | ||||||
|  |             config, | ||||||
|  |             Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)), | ||||||
|  |             min, | ||||||
|  |             max, | ||||||
|  |             val, | ||||||
|  |             vec![GuiElem::new(Label::new( | ||||||
|  |                 GuiElemCfg::default(), | ||||||
|  |                 String::new(), | ||||||
|  |                 Color::WHITE, | ||||||
|  |                 // Some(Color::from_int_rgba(0, 0, 0, 150)),
 | ||||||
|  |                 None, | ||||||
|  |                 Vec2::new(0.5, 1.0), | ||||||
|  |             ))], | ||||||
|  |             move |s, i| { | ||||||
|  |                 if s.display || s.display_since.is_some() { | ||||||
|  |                     let mut label = s.children.pop().unwrap(); | ||||||
|  |                     if let Some(l) = label.inner.any_mut().downcast_mut::<Label>() { | ||||||
|  |                         let display_state = if let Some(since) = | ||||||
|  |                             s.display_since.map(|v| v.elapsed().as_secs_f64() / 0.2) | ||||||
|  |                         { | ||||||
|  |                             if since >= 1.0 { | ||||||
|  |                                 s.display_since = None; | ||||||
|  |                                 if s.display { | ||||||
|  |                                     1.0 | ||||||
|  |                                 } else { | ||||||
|  |                                     0.0 | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 if let Some(h) = &i.helper { | ||||||
|  |                                     h.request_redraw(); | ||||||
|  |                                 } | ||||||
|  |                                 s.config.redraw = true; | ||||||
|  |                                 if s.display { | ||||||
|  |                                     since | ||||||
|  |                                 } else { | ||||||
|  |                                     1.0 - since | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             1.0 | ||||||
|  |                         }; | ||||||
|  |                         let display_state = | ||||||
|  |                             (1.0 - (1.0 - display_state) * (1.0 - display_state)) as _; | ||||||
|  |                         if display_state == 0.0 { | ||||||
|  |                             l.config_mut().enabled = false; | ||||||
|  |                         } else { | ||||||
|  |                             l.pos.x = ((s.val - s.min) / (s.max - s.min)) as _; | ||||||
|  |                             *l.content.color() = Color::from_rgba(0.8, 0.8, 0.8, display_state); | ||||||
|  |                             let cfg = l.config_mut(); | ||||||
|  |                             cfg.enabled = true; | ||||||
|  |                             let label_height = i.line_height / i.pos.height(); | ||||||
|  |                             cfg.pos = Rectangle::from_tuples( | ||||||
|  |                                 (0.05, 1.0 - label_height - display_state), | ||||||
|  |                                 (0.95, 1.0 - display_state), | ||||||
|  |                             ); | ||||||
|  |                             mktext(s, l, i); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     s.children.push(label); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for Slider { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) { | ||||||
|  |             self.display = !self.display; | ||||||
|  |             self.display_since = Some(Instant::now()); | ||||||
|  |             self.config.redraw = true; | ||||||
|  |         } | ||||||
|  |         let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25); | ||||||
|  |         let y_mid_line = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y); | ||||||
|  |         let line_radius = dot_size * 0.25; | ||||||
|  |         let line_pos = Rectangle::from_tuples( | ||||||
|  |             (info.pos.top_left().x + dot_size, y_mid_line - line_radius), | ||||||
|  |             ( | ||||||
|  |                 info.pos.bottom_right().x - dot_size, | ||||||
|  |                 y_mid_line + line_radius, | ||||||
|  |             ), | ||||||
|  |         ); | ||||||
|  |         let line_left = line_pos.top_left().x; | ||||||
|  |         let line_width = line_pos.width(); | ||||||
|  |         if self.config.mouse_down.0 { | ||||||
|  |             self.val = self.min | ||||||
|  |                 + (self.max - self.min) | ||||||
|  |                     * 1.0f64.min(0.0f64.max( | ||||||
|  |                         (info.mouse_pos.x - line_pos.top_left().x) as f64 / line_pos.width() as f64, | ||||||
|  |                     )); | ||||||
|  |             self.val_changed = true; | ||||||
|  |             for v in &mut self.val_changed_subs { | ||||||
|  |                 *v = true; | ||||||
|  |             } | ||||||
|  |             self.config.redraw = true; | ||||||
|  |         } | ||||||
|  |         let line_color = Color::from_int_rgb(50, 50, 100); | ||||||
|  |         g.draw_circle( | ||||||
|  |             Vec2::new(line_pos.top_left().x, y_mid_line), | ||||||
|  |             line_radius, | ||||||
|  |             line_color, | ||||||
|  |         ); | ||||||
|  |         g.draw_circle( | ||||||
|  |             Vec2::new(line_pos.bottom_right().x, y_mid_line), | ||||||
|  |             line_radius, | ||||||
|  |             line_color, | ||||||
|  |         ); | ||||||
|  |         g.draw_rectangle(line_pos, line_color); | ||||||
|  |         g.draw_circle( | ||||||
|  |             Vec2::new( | ||||||
|  |                 line_left | ||||||
|  |                     + (line_width as f64 * (self.val - self.min) / (self.max - self.min)) as f32, | ||||||
|  |                 y_mid_line, | ||||||
|  |             ), | ||||||
|  |             0.5 * dot_size, | ||||||
|  |             Color::CYAN, | ||||||
|  |         ); | ||||||
|  |         if self.config.redraw { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             (Arc::clone(&self.on_update))(self, info); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										454
									
								
								musicdb-client/src/gui_library.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										454
									
								
								musicdb-client/src/gui_library.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,454 @@ | |||||||
|  | use musicdb_lib::data::{database::Database, AlbumId, ArtistId, 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::ScrollBox, | ||||||
|  |     gui_text::{Label, TextField}, | ||||||
|  |     gui_wrappers::WithFocusHotkey, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct LibraryBrowser { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  |     search_artist: String, | ||||||
|  |     search_artist_regex: Option<Regex>, | ||||||
|  |     search_album: String, | ||||||
|  |     search_album_regex: Option<Regex>, | ||||||
|  |     search_song: String, | ||||||
|  |     search_song_regex: Option<Regex>, | ||||||
|  | } | ||||||
|  | fn search_regex_new(pat: &str) -> Result<Regex, regex::Error> { | ||||||
|  |     RegexBuilder::new(pat) | ||||||
|  |         .unicode(true) | ||||||
|  |         .case_insensitive(true) | ||||||
|  |         .build() | ||||||
|  | } | ||||||
|  | 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, 0.1), (1.0, 1.0))), | ||||||
|  |             crate::gui_base::ScrollBoxSizeUnit::Pixels, | ||||||
|  |             vec![], | ||||||
|  |         ); | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(search_artist), | ||||||
|  |                 GuiElem::new(search_album), | ||||||
|  |                 GuiElem::new(search_song), | ||||||
|  |                 GuiElem::new(library_scroll_box), | ||||||
|  |             ], | ||||||
|  |             search_artist: String::new(), | ||||||
|  |             search_artist_regex: None, | ||||||
|  |             search_album: String::new(), | ||||||
|  |             search_album_regex: None, | ||||||
|  |             search_song: String::new(), | ||||||
|  |             search_song_regex: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 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<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         let mut search_changed = false; | ||||||
|  |         { | ||||||
|  |             let v = &mut self.children[0].try_as_mut::<TextField>().unwrap().children[0] | ||||||
|  |                 .try_as_mut::<Label>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .content; | ||||||
|  |             if self.search_artist != *v.get_text() { | ||||||
|  |                 search_changed = true; | ||||||
|  |                 self.search_artist = v.get_text().clone(); | ||||||
|  |                 self.search_artist_regex = search_regex_new(&self.search_artist).ok(); | ||||||
|  |                 *v.color() = if self.search_artist_regex.is_some() { | ||||||
|  |                     Color::WHITE | ||||||
|  |                 } else { | ||||||
|  |                     Color::RED | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         { | ||||||
|  |             let v = &mut self.children[1].try_as_mut::<TextField>().unwrap().children[0] | ||||||
|  |                 .try_as_mut::<Label>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .content; | ||||||
|  |             if self.search_album != *v.get_text() { | ||||||
|  |                 search_changed = true; | ||||||
|  |                 self.search_album = v.get_text().clone(); | ||||||
|  |                 self.search_album_regex = search_regex_new(&self.search_album).ok(); | ||||||
|  |                 *v.color() = if self.search_album_regex.is_some() { | ||||||
|  |                     Color::WHITE | ||||||
|  |                 } else { | ||||||
|  |                     Color::RED | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         { | ||||||
|  |             let v = &mut self.children[2] | ||||||
|  |                 .try_as_mut::<WithFocusHotkey<TextField>>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .inner | ||||||
|  |                 .children[0] | ||||||
|  |                 .try_as_mut::<Label>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .content; | ||||||
|  |             if self.search_song != *v.get_text() { | ||||||
|  |                 search_changed = true; | ||||||
|  |                 self.search_song = v.get_text().clone(); | ||||||
|  |                 self.search_song_regex = search_regex_new(&self.search_song).ok(); | ||||||
|  |                 *v.color() = if self.search_song_regex.is_some() { | ||||||
|  |                     Color::WHITE | ||||||
|  |                 } else { | ||||||
|  |                     Color::RED | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if self.config.redraw || search_changed || info.pos.size() != self.config.pixel_pos.size() { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             self.update_list(&info.database, info.line_height); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn updated_library(&mut self) { | ||||||
|  |         self.config.redraw = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl LibraryBrowser { | ||||||
|  |     fn update_list(&mut self, db: &Database, line_height: f32) { | ||||||
|  |         let song_height = line_height; | ||||||
|  |         let artist_height = song_height * 3.0; | ||||||
|  |         let album_height = song_height * 2.0; | ||||||
|  |         // sort artists by name
 | ||||||
|  |         let mut artists = db.artists().iter().collect::<Vec<_>>(); | ||||||
|  |         artists.sort_by_key(|v| &v.1.name); | ||||||
|  |         let mut gui_elements = vec![]; | ||||||
|  |         for (artist_id, artist) in artists { | ||||||
|  |             if self.search_artist.is_empty() | ||||||
|  |                 || self | ||||||
|  |                     .search_artist_regex | ||||||
|  |                     .as_ref() | ||||||
|  |                     .is_some_and(|regex| regex.is_match(&artist.name)) | ||||||
|  |             { | ||||||
|  |                 let mut artist_gui = Some(( | ||||||
|  |                     GuiElem::new(ListArtist::new( | ||||||
|  |                         GuiElemCfg::default(), | ||||||
|  |                         *artist_id, | ||||||
|  |                         artist.name.clone(), | ||||||
|  |                     )), | ||||||
|  |                     artist_height, | ||||||
|  |                 )); | ||||||
|  |                 for album_id in &artist.albums { | ||||||
|  |                     if let Some(album) = db.albums().get(album_id) { | ||||||
|  |                         if self.search_album.is_empty() | ||||||
|  |                             || self | ||||||
|  |                                 .search_album_regex | ||||||
|  |                                 .as_ref() | ||||||
|  |                                 .is_some_and(|regex| regex.is_match(&album.name)) | ||||||
|  |                         { | ||||||
|  |                             let mut album_gui = Some(( | ||||||
|  |                                 GuiElem::new(ListAlbum::new( | ||||||
|  |                                     GuiElemCfg::default(), | ||||||
|  |                                     *album_id, | ||||||
|  |                                     album.name.clone(), | ||||||
|  |                                 )), | ||||||
|  |                                 album_height, | ||||||
|  |                             )); | ||||||
|  |                             for song_id in &album.songs { | ||||||
|  |                                 if let Some(song) = db.songs().get(song_id) { | ||||||
|  |                                     if self.search_song.is_empty() | ||||||
|  |                                         || self | ||||||
|  |                                             .search_song_regex | ||||||
|  |                                             .as_ref() | ||||||
|  |                                             .is_some_and(|regex| regex.is_match(&song.title)) | ||||||
|  |                                     { | ||||||
|  |                                         if let Some(g) = artist_gui.take() { | ||||||
|  |                                             gui_elements.push(g); | ||||||
|  |                                         } | ||||||
|  |                                         if let Some(g) = album_gui.take() { | ||||||
|  |                                             gui_elements.push(g); | ||||||
|  |                                         } | ||||||
|  |                                         gui_elements.push(( | ||||||
|  |                                             GuiElem::new(ListSong::new( | ||||||
|  |                                                 GuiElemCfg::default(), | ||||||
|  |                                                 *song_id, | ||||||
|  |                                                 song.title.clone(), | ||||||
|  |                                             )), | ||||||
|  |                                             song_height, | ||||||
|  |                                         )); | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let scroll_box = self.children[3].try_as_mut::<ScrollBox>().unwrap(); | ||||||
|  |         scroll_box.children = gui_elements; | ||||||
|  |         scroll_box.config_mut().redraw = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct ListArtist { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     id: ArtistId, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     mouse_pos: Vec2, | ||||||
|  | } | ||||||
|  | impl ListArtist { | ||||||
|  |     pub fn new(config: GuiElemCfg, id: ArtistId, name: String) -> Self { | ||||||
|  |         let label = Label::new( | ||||||
|  |             GuiElemCfg::default(), | ||||||
|  |             name, | ||||||
|  |             Color::from_int_rgb(81, 24, 125), | ||||||
|  |             None, | ||||||
|  |             Vec2::new(0.0, 0.5), | ||||||
|  |         ); | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse(), | ||||||
|  |             id, | ||||||
|  |             children: vec![GuiElem::new(label)], | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for ListArtist { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         self.mouse_pos = Vec2::new( | ||||||
|  |             info.mouse_pos.x - self.config.pixel_pos.top_left().x, | ||||||
|  |             info.mouse_pos.y - self.config.pixel_pos.top_left().y, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             let mouse_pos = self.mouse_pos; | ||||||
|  |             let w = self.config.pixel_pos.width(); | ||||||
|  |             let h = self.config.pixel_pos.height(); | ||||||
|  |             let mut el = GuiElem::new(self.clone()); | ||||||
|  |             vec![GuiAction::SetDragging(Some(( | ||||||
|  |                 Dragging::Artist(self.id), | ||||||
|  |                 Some(Box::new(move |i, g| { | ||||||
|  |                     let sw = i.pos.width(); | ||||||
|  |                     let sh = i.pos.height(); | ||||||
|  |                     let x = (i.mouse_pos.x - mouse_pos.x) / sw; | ||||||
|  |                     let y = (i.mouse_pos.y - mouse_pos.y) / sh; | ||||||
|  |                     el.inner.config_mut().pos = | ||||||
|  |                         Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); | ||||||
|  |                     el.draw(i, g) | ||||||
|  |                 })), | ||||||
|  |             )))] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct ListAlbum { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     id: AlbumId, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     mouse_pos: Vec2, | ||||||
|  | } | ||||||
|  | impl ListAlbum { | ||||||
|  |     pub fn new(config: GuiElemCfg, id: AlbumId, name: String) -> Self { | ||||||
|  |         let label = Label::new( | ||||||
|  |             GuiElemCfg::default(), | ||||||
|  |             name, | ||||||
|  |             Color::from_int_rgb(8, 61, 47), | ||||||
|  |             None, | ||||||
|  |             Vec2::new(0.0, 0.5), | ||||||
|  |         ); | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse(), | ||||||
|  |             id, | ||||||
|  |             children: vec![GuiElem::new(label)], | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for ListAlbum { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         self.mouse_pos = Vec2::new( | ||||||
|  |             info.mouse_pos.x - self.config.pixel_pos.top_left().x, | ||||||
|  |             info.mouse_pos.y - self.config.pixel_pos.top_left().y, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             let mouse_pos = self.mouse_pos; | ||||||
|  |             let w = self.config.pixel_pos.width(); | ||||||
|  |             let h = self.config.pixel_pos.height(); | ||||||
|  |             let mut el = GuiElem::new(self.clone()); | ||||||
|  |             vec![GuiAction::SetDragging(Some(( | ||||||
|  |                 Dragging::Album(self.id), | ||||||
|  |                 Some(Box::new(move |i, g| { | ||||||
|  |                     let sw = i.pos.width(); | ||||||
|  |                     let sh = i.pos.height(); | ||||||
|  |                     let x = (i.mouse_pos.x - mouse_pos.x) / sw; | ||||||
|  |                     let y = (i.mouse_pos.y - mouse_pos.y) / sh; | ||||||
|  |                     el.inner.config_mut().pos = | ||||||
|  |                         Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); | ||||||
|  |                     el.draw(i, g) | ||||||
|  |                 })), | ||||||
|  |             )))] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct ListSong { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     id: SongId, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     mouse_pos: Vec2, | ||||||
|  | } | ||||||
|  | impl ListSong { | ||||||
|  |     pub fn new(config: GuiElemCfg, id: SongId, name: String) -> Self { | ||||||
|  |         let label = Label::new( | ||||||
|  |             GuiElemCfg::default(), | ||||||
|  |             name, | ||||||
|  |             Color::from_int_rgb(175, 175, 175), | ||||||
|  |             None, | ||||||
|  |             Vec2::new(0.0, 0.5), | ||||||
|  |         ); | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse(), | ||||||
|  |             id, | ||||||
|  |             children: vec![GuiElem::new(label)], | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for ListSong { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         self.mouse_pos = Vec2::new( | ||||||
|  |             info.mouse_pos.x - self.config.pixel_pos.top_left().x, | ||||||
|  |             info.mouse_pos.y - self.config.pixel_pos.top_left().y, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             let mouse_pos = self.mouse_pos; | ||||||
|  |             let w = self.config.pixel_pos.width(); | ||||||
|  |             let h = self.config.pixel_pos.height(); | ||||||
|  |             let mut el = GuiElem::new(self.clone()); | ||||||
|  |             vec![GuiAction::SetDragging(Some(( | ||||||
|  |                 Dragging::Song(self.id), | ||||||
|  |                 Some(Box::new(move |i, g| { | ||||||
|  |                     let sw = i.pos.width(); | ||||||
|  |                     let sh = i.pos.height(); | ||||||
|  |                     let x = (i.mouse_pos.x - mouse_pos.x) / sw; | ||||||
|  |                     let y = (i.mouse_pos.y - mouse_pos.y) / sh; | ||||||
|  |                     el.inner.config_mut().pos = | ||||||
|  |                         Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); | ||||||
|  |                     el.draw(i, g) | ||||||
|  |                 })), | ||||||
|  |             )))] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										238
									
								
								musicdb-client/src/gui_playback.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										238
									
								
								musicdb-client/src/gui_playback.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,238 @@ | |||||||
|  | use musicdb_lib::{ | ||||||
|  |     data::{queue::QueueContent, SongId}, | ||||||
|  |     server::Command, | ||||||
|  | }; | ||||||
|  | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, | ||||||
|  |     gui_text::Label, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct CurrentSong { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     prev_song: Option<SongId>, | ||||||
|  | } | ||||||
|  | impl CurrentSong { | ||||||
|  |     pub fn new(config: GuiElemCfg) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(Label::new( | ||||||
|  |                     GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.5))), | ||||||
|  |                     "".to_owned(), | ||||||
|  |                     Color::from_int_rgb(180, 180, 210), | ||||||
|  |                     None, | ||||||
|  |                     Vec2::new(0.1, 1.0), | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Label::new( | ||||||
|  |                     GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.5), (0.5, 1.0))), | ||||||
|  |                     "".to_owned(), | ||||||
|  |                     Color::from_int_rgb(120, 120, 120), | ||||||
|  |                     None, | ||||||
|  |                     Vec2::new(0.3, 0.0), | ||||||
|  |                 )), | ||||||
|  |             ], | ||||||
|  | 
 | ||||||
|  |             prev_song: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for CurrentSong { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         let song = if let Some(v) = info.database.queue.get_current() { | ||||||
|  |             if let QueueContent::Song(song) = v.content() { | ||||||
|  |                 if Some(*song) == self.prev_song { | ||||||
|  |                     // same song as before
 | ||||||
|  |                     return; | ||||||
|  |                 } else { | ||||||
|  |                     Some(*song) | ||||||
|  |                 } | ||||||
|  |             } else if self.prev_song.is_none() { | ||||||
|  |                 // no song, nothing in queue
 | ||||||
|  |                 return; | ||||||
|  |             } else { | ||||||
|  |                 None | ||||||
|  |             } | ||||||
|  |         } else if self.prev_song.is_none() { | ||||||
|  |             // no song, nothing in queue
 | ||||||
|  |             return; | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         }; | ||||||
|  |         if self.prev_song != song { | ||||||
|  |             self.config.redraw = true; | ||||||
|  |             self.prev_song = song; | ||||||
|  |         } | ||||||
|  |         if self.config.redraw { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             let (name, subtext) = if let Some(song) = song { | ||||||
|  |                 if let Some(song) = info.database.get_song(&song) { | ||||||
|  |                     let sub = match ( | ||||||
|  |                         song.artist | ||||||
|  |                             .as_ref() | ||||||
|  |                             .and_then(|id| info.database.artists().get(id)), | ||||||
|  |                         song.album | ||||||
|  |                             .as_ref() | ||||||
|  |                             .and_then(|id| info.database.albums().get(id)), | ||||||
|  |                     ) { | ||||||
|  |                         (None, None) => String::new(), | ||||||
|  |                         (Some(artist), None) => format!("by {}", artist.name), | ||||||
|  |                         (None, Some(album)) => { | ||||||
|  |                             if let Some(artist) = album | ||||||
|  |                                 .artist | ||||||
|  |                                 .as_ref() | ||||||
|  |                                 .and_then(|id| info.database.artists().get(id)) | ||||||
|  |                             { | ||||||
|  |                                 format!("on {} by {}", album.name, artist.name) | ||||||
|  |                             } else { | ||||||
|  |                                 format!("on {}", album.name) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         (Some(artist), Some(album)) => { | ||||||
|  |                             format!("by {} on {}", artist.name, album.name) | ||||||
|  |                         } | ||||||
|  |                     }; | ||||||
|  |                     (song.title.clone(), sub) | ||||||
|  |                 } else { | ||||||
|  |                     ( | ||||||
|  |                         "< song not in db >".to_owned(), | ||||||
|  |                         "maybe restart the client to resync the database?".to_owned(), | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 (String::new(), String::new()) | ||||||
|  |             }; | ||||||
|  |             *self.children[0] | ||||||
|  |                 .try_as_mut::<Label>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .content | ||||||
|  |                 .text() = name; | ||||||
|  |             *self.children[1] | ||||||
|  |                 .try_as_mut::<Label>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .content | ||||||
|  |                 .text() = subtext; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct PlayPauseToggle { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     playing_target: bool, | ||||||
|  |     playing_waiting_for_change: bool, | ||||||
|  | } | ||||||
|  | impl PlayPauseToggle { | ||||||
|  |     /// automatically adds w_mouse to config
 | ||||||
|  |     pub fn new(config: GuiElemCfg, playing: bool) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse(), | ||||||
|  |             children: vec![], | ||||||
|  |             playing_target: playing, | ||||||
|  |             playing_waiting_for_change: false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for PlayPauseToggle { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if self.playing_waiting_for_change { | ||||||
|  |             if info.database.playing == self.playing_target { | ||||||
|  |                 self.playing_waiting_for_change = false; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // not waiting for change, update if the value changes
 | ||||||
|  |             self.playing_target = info.database.playing; | ||||||
|  |         } | ||||||
|  |         let pos = if info.pos.width() > info.pos.height() { | ||||||
|  |             let a = 0.5 * info.pos.height(); | ||||||
|  |             let m = 0.5 * (info.pos.top_left().x + info.pos.bottom_right().x); | ||||||
|  |             Rectangle::new( | ||||||
|  |                 Vec2::new(m - a, info.pos.top_left().y), | ||||||
|  |                 Vec2::new(m + a, info.pos.bottom_right().y), | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             let a = 0.5 * info.pos.width(); | ||||||
|  |             let m = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y); | ||||||
|  |             Rectangle::new( | ||||||
|  |                 Vec2::new(info.pos.top_left().x, m - a), | ||||||
|  |                 Vec2::new(info.pos.bottom_right().x, m + a), | ||||||
|  |             ) | ||||||
|  |         }; | ||||||
|  |         if self.playing_target { | ||||||
|  |             g.draw_triangle( | ||||||
|  |                 [ | ||||||
|  |                     adjust_pos(&pos, &Vec2::new(0.25, 0.25)), | ||||||
|  |                     adjust_pos(&pos, &Vec2::new(0.75, 0.5)), | ||||||
|  |                     adjust_pos(&pos, &Vec2::new(0.25, 0.75)), | ||||||
|  |                 ], | ||||||
|  |                 if self.playing_waiting_for_change { | ||||||
|  |                     Color::GRAY | ||||||
|  |                 } else { | ||||||
|  |                     Color::GREEN | ||||||
|  |                 }, | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             g.draw_rectangle( | ||||||
|  |                 adjust_area(&pos, &Rectangle::from_tuples((0.25, 0.25), (0.75, 0.75))), | ||||||
|  |                 if self.playing_waiting_for_change { | ||||||
|  |                     Color::RED | ||||||
|  |                 } else { | ||||||
|  |                     Color::GRAY | ||||||
|  |                 }, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if !self.playing_waiting_for_change { | ||||||
|  |             self.playing_target = !self.playing_target; | ||||||
|  |             self.playing_waiting_for_change = true; | ||||||
|  |             vec![GuiAction::SendToServer(if self.playing_target { | ||||||
|  |                 Command::Resume | ||||||
|  |             } else { | ||||||
|  |                 Command::Pause | ||||||
|  |             })] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										689
									
								
								musicdb-client/src/gui_queue.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										689
									
								
								musicdb-client/src/gui_queue.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,689 @@ | |||||||
|  | use musicdb_lib::{ | ||||||
|  |     data::{ | ||||||
|  |         database::Database, | ||||||
|  |         queue::{Queue, QueueContent}, | ||||||
|  |         song::Song, | ||||||
|  |         AlbumId, ArtistId, | ||||||
|  |     }, | ||||||
|  |     server::Command, | ||||||
|  | }; | ||||||
|  | use speedy2d::{ | ||||||
|  |     color::Color, | ||||||
|  |     dimen::Vec2, | ||||||
|  |     shape::Rectangle, | ||||||
|  |     window::{ModifiersState, MouseButton, VirtualKeyCode}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, | ||||||
|  |     gui_base::ScrollBox, | ||||||
|  |     gui_text::Label, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct QueueViewer { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  | } | ||||||
|  | impl QueueViewer { | ||||||
|  |     pub fn new(config: GuiElemCfg) -> Self { | ||||||
|  |         let queue_scroll_box = ScrollBox::new( | ||||||
|  |             GuiElemCfg::default(), | ||||||
|  |             crate::gui_base::ScrollBoxSizeUnit::Pixels, | ||||||
|  |             vec![( | ||||||
|  |                 GuiElem::new(Label::new( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     "loading...".to_string(), | ||||||
|  |                     Color::DARK_GRAY, | ||||||
|  |                     None, | ||||||
|  |                     Vec2::new(0.5, 0.5), | ||||||
|  |                 )), | ||||||
|  |                 1.0, | ||||||
|  |             )], | ||||||
|  |         ); | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(queue_scroll_box), | ||||||
|  |                 GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::default())), | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for QueueViewer { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             let mut c = vec![]; | ||||||
|  |             queue_gui( | ||||||
|  |                 &info.database.queue, | ||||||
|  |                 &info.database, | ||||||
|  |                 0.0, | ||||||
|  |                 0.02, | ||||||
|  |                 info.line_height, | ||||||
|  |                 &mut c, | ||||||
|  |                 vec![], | ||||||
|  |                 true, | ||||||
|  |             ); | ||||||
|  |             let mut scroll_box = self.children[0].try_as_mut::<ScrollBox>().unwrap(); | ||||||
|  |             scroll_box.children = c; | ||||||
|  |             scroll_box.config_mut().redraw = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn updated_queue(&mut self) { | ||||||
|  |         self.config.redraw = true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn queue_gui( | ||||||
|  |     queue: &Queue, | ||||||
|  |     db: &Database, | ||||||
|  |     depth: f32, | ||||||
|  |     depth_inc_by: f32, | ||||||
|  |     line_height: f32, | ||||||
|  |     target: &mut Vec<(GuiElem, f32)>, | ||||||
|  |     path: Vec<usize>, | ||||||
|  |     current: bool, | ||||||
|  | ) { | ||||||
|  |     let cfg = GuiElemCfg::at(Rectangle::from_tuples((depth, 0.0), (1.0, 1.0))); | ||||||
|  |     match queue.content() { | ||||||
|  |         QueueContent::Song(id) => { | ||||||
|  |             if let Some(s) = db.songs().get(id) { | ||||||
|  |                 target.push(( | ||||||
|  |                     GuiElem::new(QueueSong::new(cfg, path, s.clone(), current)), | ||||||
|  |                     line_height, | ||||||
|  |                 )); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         QueueContent::Folder(ia, q, _) => { | ||||||
|  |             target.push(( | ||||||
|  |                 GuiElem::new(QueueFolder::new(cfg, path.clone(), queue.clone(), current)), | ||||||
|  |                 line_height * 0.67, | ||||||
|  |             )); | ||||||
|  |             for (i, q) in q.iter().enumerate() { | ||||||
|  |                 let mut p = path.clone(); | ||||||
|  |                 p.push(i); | ||||||
|  |                 queue_gui( | ||||||
|  |                     q, | ||||||
|  |                     db, | ||||||
|  |                     depth + depth_inc_by, | ||||||
|  |                     depth_inc_by, | ||||||
|  |                     line_height, | ||||||
|  |                     target, | ||||||
|  |                     p, | ||||||
|  |                     current && *ia == i, | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct QueueEmptySpaceDragHandler { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  | } | ||||||
|  | impl QueueEmptySpaceDragHandler { | ||||||
|  |     pub fn new(config: GuiElemCfg) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_drag_target(), | ||||||
|  |             children: vec![], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for QueueEmptySpaceDragHandler { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> { | ||||||
|  |         dragged_add_to_queue(dragged, |q| Command::QueueAdd(vec![], q)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn generic_queue_draw( | ||||||
|  |     info: &mut DrawInfo, | ||||||
|  |     path: &Vec<usize>, | ||||||
|  |     mouse: &mut bool, | ||||||
|  |     copy_on_mouse_down: bool, | ||||||
|  | ) -> bool { | ||||||
|  |     if *mouse && !info.pos.contains(info.mouse_pos) { | ||||||
|  |         *mouse = false; | ||||||
|  |         if !copy_on_mouse_down { | ||||||
|  |             info.actions | ||||||
|  |                 .push(GuiAction::SendToServer(Command::QueueRemove(path.clone()))); | ||||||
|  |         } | ||||||
|  |         true | ||||||
|  |     } else { | ||||||
|  |         false | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct QueueSong { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     path: Vec<usize>, | ||||||
|  |     song: Song, | ||||||
|  |     current: bool, | ||||||
|  |     mouse: bool, | ||||||
|  |     mouse_pos: Vec2, | ||||||
|  |     copy: bool, | ||||||
|  |     copy_on_mouse_down: bool, | ||||||
|  | } | ||||||
|  | impl QueueSong { | ||||||
|  |     pub fn new(config: GuiElemCfg, path: Vec<usize>, song: Song, current: bool) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse().w_keyboard_watch().w_drag_target(), | ||||||
|  |             children: vec![GuiElem::new(Label::new( | ||||||
|  |                 GuiElemCfg::default(), | ||||||
|  |                 song.title.clone(), | ||||||
|  |                 if current { | ||||||
|  |                     Color::from_int_rgb(194, 76, 178) | ||||||
|  |                 } else { | ||||||
|  |                     Color::from_int_rgb(120, 76, 194) | ||||||
|  |                 }, | ||||||
|  |                 None, | ||||||
|  |                 Vec2::new(0.0, 0.5), | ||||||
|  |             ))], | ||||||
|  |             path, | ||||||
|  |             song, | ||||||
|  |             current, | ||||||
|  |             mouse: false, | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |             copy: false, | ||||||
|  |             copy_on_mouse_down: false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl GuiElemTrait for QueueSong { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             self.mouse = true; | ||||||
|  |             self.copy_on_mouse_down = self.copy; | ||||||
|  |         } | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if self.mouse && button == MouseButton::Left { | ||||||
|  |             self.mouse = false; | ||||||
|  |             vec![GuiAction::SendToServer(Command::QueueGoto( | ||||||
|  |                 self.path.clone(), | ||||||
|  |             ))] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if !self.mouse { | ||||||
|  |             self.mouse_pos = Vec2::new( | ||||||
|  |                 info.mouse_pos.x - self.config.pixel_pos.top_left().x, | ||||||
|  |                 info.mouse_pos.y - self.config.pixel_pos.top_left().y, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) { | ||||||
|  |             let mouse_pos = self.mouse_pos; | ||||||
|  |             let w = self.config.pixel_pos.width(); | ||||||
|  |             let h = self.config.pixel_pos.height(); | ||||||
|  |             let mut el = GuiElem::new(self.clone()); | ||||||
|  |             info.actions.push(GuiAction::SetDragging(Some(( | ||||||
|  |                 Dragging::Queue(QueueContent::Song(self.song.id).into()), | ||||||
|  |                 Some(Box::new(move |i, g| { | ||||||
|  |                     let sw = i.pos.width(); | ||||||
|  |                     let sh = i.pos.height(); | ||||||
|  |                     let x = (i.mouse_pos.x - mouse_pos.x) / sw; | ||||||
|  |                     let y = (i.mouse_pos.y - mouse_pos.y) / sh; | ||||||
|  |                     el.inner.config_mut().pos = | ||||||
|  |                         Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); | ||||||
|  |                     el.draw(i, g) | ||||||
|  |                 })), | ||||||
|  |             )))); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn key_watch( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: ModifiersState, | ||||||
|  |         _down: bool, | ||||||
|  |         _key: Option<VirtualKeyCode>, | ||||||
|  |         _scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.copy = modifiers.ctrl(); | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> { | ||||||
|  |         let mut p = self.path.clone(); | ||||||
|  |         dragged_add_to_queue(dragged, move |q| { | ||||||
|  |             if let Some(i) = p.pop() { | ||||||
|  |                 Command::QueueInsert(p, i, q) | ||||||
|  |             } else { | ||||||
|  |                 Command::QueueAdd(p, q) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | struct QueueFolder { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     path: Vec<usize>, | ||||||
|  |     queue: Queue, | ||||||
|  |     current: bool, | ||||||
|  |     mouse: bool, | ||||||
|  |     mouse_pos: Vec2, | ||||||
|  |     copy: bool, | ||||||
|  |     copy_on_mouse_down: bool, | ||||||
|  | } | ||||||
|  | impl QueueFolder { | ||||||
|  |     pub fn new(config: GuiElemCfg, path: Vec<usize>, queue: Queue, current: bool) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: if path.is_empty() { | ||||||
|  |                 config | ||||||
|  |             } else { | ||||||
|  |                 config.w_mouse().w_keyboard_watch() | ||||||
|  |             } | ||||||
|  |             .w_drag_target(), | ||||||
|  |             children: vec![GuiElem::new(Label::new( | ||||||
|  |                 GuiElemCfg::default(), | ||||||
|  |                 match queue.content() { | ||||||
|  |                     QueueContent::Folder(_, q, n) => format!( | ||||||
|  |                         "{}  ({})", | ||||||
|  |                         if path.is_empty() && n.is_empty() { | ||||||
|  |                             "Queue" | ||||||
|  |                         } else { | ||||||
|  |                             n | ||||||
|  |                         }, | ||||||
|  |                         q.len() | ||||||
|  |                     ), | ||||||
|  |                     _ => "[???]".to_string(), | ||||||
|  |                 }, | ||||||
|  |                 Color::from_int_rgb(52, 132, 50), | ||||||
|  |                 None, | ||||||
|  |                 Vec2::new(0.0, 0.5), | ||||||
|  |             ))], | ||||||
|  |             path, | ||||||
|  |             queue, | ||||||
|  |             current, | ||||||
|  |             mouse: false, | ||||||
|  |             mouse_pos: Vec2::ZERO, | ||||||
|  |             copy: false, | ||||||
|  |             copy_on_mouse_down: false, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for QueueFolder { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if !self.mouse { | ||||||
|  |             self.mouse_pos = Vec2::new( | ||||||
|  |                 info.mouse_pos.x - self.config.pixel_pos.top_left().x, | ||||||
|  |                 info.mouse_pos.y - self.config.pixel_pos.top_left().y, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) { | ||||||
|  |             let mouse_pos = self.mouse_pos; | ||||||
|  |             let w = self.config.pixel_pos.width(); | ||||||
|  |             let h = self.config.pixel_pos.height(); | ||||||
|  |             let mut el = GuiElem::new(self.clone()); | ||||||
|  |             info.actions.push(GuiAction::SetDragging(Some(( | ||||||
|  |                 Dragging::Queue(self.queue.clone()), | ||||||
|  |                 Some(Box::new(move |i, g| { | ||||||
|  |                     let sw = i.pos.width(); | ||||||
|  |                     let sh = i.pos.height(); | ||||||
|  |                     let x = (i.mouse_pos.x - mouse_pos.x) / sw; | ||||||
|  |                     let y = (i.mouse_pos.y - mouse_pos.y) / sh; | ||||||
|  |                     el.inner.config_mut().pos = | ||||||
|  |                         Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh)); | ||||||
|  |                     el.draw(i, g) | ||||||
|  |                 })), | ||||||
|  |             )))); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if button == MouseButton::Left { | ||||||
|  |             self.mouse = true; | ||||||
|  |             self.copy_on_mouse_down = self.copy; | ||||||
|  |         } | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         if self.mouse && button == MouseButton::Left { | ||||||
|  |             self.mouse = false; | ||||||
|  |             vec![GuiAction::SendToServer(Command::QueueGoto( | ||||||
|  |                 self.path.clone(), | ||||||
|  |             ))] | ||||||
|  |         } else { | ||||||
|  |             vec![] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn key_watch( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: ModifiersState, | ||||||
|  |         _down: bool, | ||||||
|  |         _key: Option<VirtualKeyCode>, | ||||||
|  |         _scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.copy = modifiers.ctrl(); | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> { | ||||||
|  |         let p = self.path.clone(); | ||||||
|  |         dragged_add_to_queue(dragged, move |q| Command::QueueAdd(p, q)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn dragged_add_to_queue<F: FnOnce(Queue) -> Command + 'static>( | ||||||
|  |     dragged: Dragging, | ||||||
|  |     f: F, | ||||||
|  | ) -> Vec<GuiAction> { | ||||||
|  |     match dragged { | ||||||
|  |         Dragging::Artist(id) => { | ||||||
|  |             vec![GuiAction::Build(Box::new(move |db| { | ||||||
|  |                 if let Some(q) = add_to_queue_artist_by_id(id, db) { | ||||||
|  |                     vec![GuiAction::SendToServer(f(q))] | ||||||
|  |                 } else { | ||||||
|  |                     vec![] | ||||||
|  |                 } | ||||||
|  |             }))] | ||||||
|  |         } | ||||||
|  |         Dragging::Album(id) => { | ||||||
|  |             vec![GuiAction::Build(Box::new(move |db| { | ||||||
|  |                 if let Some(q) = add_to_queue_album_by_id(id, db) { | ||||||
|  |                     vec![GuiAction::SendToServer(f(q))] | ||||||
|  |                 } else { | ||||||
|  |                     vec![] | ||||||
|  |                 } | ||||||
|  |             }))] | ||||||
|  |         } | ||||||
|  |         Dragging::Song(id) => { | ||||||
|  |             let q = QueueContent::Song(id).into(); | ||||||
|  |             vec![GuiAction::SendToServer(f(q))] | ||||||
|  |         } | ||||||
|  |         Dragging::Queue(q) => { | ||||||
|  |             vec![GuiAction::SendToServer(f(q))] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> { | ||||||
|  |     if let Some(album) = db.albums().get(&id) { | ||||||
|  |         Some( | ||||||
|  |             QueueContent::Folder( | ||||||
|  |                 0, | ||||||
|  |                 album | ||||||
|  |                     .songs | ||||||
|  |                     .iter() | ||||||
|  |                     .map(|id| QueueContent::Song(*id).into()) | ||||||
|  |                     .collect(), | ||||||
|  |                 album.name.clone(), | ||||||
|  |             ) | ||||||
|  |             .into(), | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> { | ||||||
|  |     if let Some(artist) = db.artists().get(&id) { | ||||||
|  |         Some( | ||||||
|  |             QueueContent::Folder( | ||||||
|  |                 0, | ||||||
|  |                 artist | ||||||
|  |                     .albums | ||||||
|  |                     .iter() | ||||||
|  |                     .filter_map(|id| add_to_queue_album_by_id(*id, db)) | ||||||
|  |                     .collect(), | ||||||
|  |                 artist.name.clone(), | ||||||
|  |             ) | ||||||
|  |             .into(), | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         None | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // use musicdb_lib::{
 | ||||||
|  | //     data::{
 | ||||||
|  | //         database::Database,
 | ||||||
|  | //         queue::{Queue, QueueContent},
 | ||||||
|  | //         AlbumId, ArtistId,
 | ||||||
|  | //     },
 | ||||||
|  | //     server::Command,
 | ||||||
|  | // };
 | ||||||
|  | // use speedy2d::{
 | ||||||
|  | //     color::Color,
 | ||||||
|  | //     dimen::Vec2,
 | ||||||
|  | //     font::{TextLayout, TextOptions},
 | ||||||
|  | // };
 | ||||||
|  | 
 | ||||||
|  | // use crate::gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
 | ||||||
|  | 
 | ||||||
|  | // pub struct QueueViewer {
 | ||||||
|  | //     config: GuiElemCfg,
 | ||||||
|  | //     children: Vec<GuiElem>,
 | ||||||
|  | //     /// 0.0 = bottom
 | ||||||
|  | //     scroll: f32,
 | ||||||
|  | // }
 | ||||||
|  | // impl QueueViewer {
 | ||||||
|  | //     pub fn new(config: GuiElemCfg) -> Self {
 | ||||||
|  | //         Self {
 | ||||||
|  | //             config: config.w_drag_target(),
 | ||||||
|  | //             children: vec![],
 | ||||||
|  | //             scroll: 0.0,
 | ||||||
|  | //         }
 | ||||||
|  | //     }
 | ||||||
|  | // }
 | ||||||
|  | // impl GuiElemTrait for QueueViewer {
 | ||||||
|  | //     fn config(&self) -> &GuiElemCfg {
 | ||||||
|  | //         &self.config
 | ||||||
|  | //     }
 | ||||||
|  | //     fn config_mut(&mut self) -> &mut GuiElemCfg {
 | ||||||
|  | //         &mut self.config
 | ||||||
|  | //     }
 | ||||||
|  | //     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
 | ||||||
|  | //         Box::new(self.children.iter_mut())
 | ||||||
|  | //     }
 | ||||||
|  | //     fn draw(&mut self, info: &DrawInfo, g: &mut speedy2d::Graphics2D) {
 | ||||||
|  | //         g.draw_rectangle(info.pos.clone(), Color::from_rgb(0.0, 0.1, 0.0));
 | ||||||
|  | //         let queue_height = info.database.queue.len();
 | ||||||
|  | //         let start_y_pos = info.pos.bottom_right().y
 | ||||||
|  | //             + (self.scroll - queue_height as f32) * info.queue_song_height;
 | ||||||
|  | //         let mut skip = 0;
 | ||||||
|  | //         let limit = queue_height.saturating_sub(self.scroll.floor() as usize + skip);
 | ||||||
|  | //         self.draw_queue(
 | ||||||
|  | //             &info.database.queue,
 | ||||||
|  | //             &mut skip,
 | ||||||
|  | //             &mut 0,
 | ||||||
|  | //             limit,
 | ||||||
|  | //             &mut Vec2::new(info.pos.top_left().x, start_y_pos),
 | ||||||
|  | //             info.pos.width(),
 | ||||||
|  | //             info,
 | ||||||
|  | //             g,
 | ||||||
|  | //         );
 | ||||||
|  | //     }
 | ||||||
|  | //     fn dragged(&mut self, dragged: Dragging) -> Vec<crate::gui::GuiAction> {
 | ||||||
|  | //         match dragged {
 | ||||||
|  | //             Dragging::Song(id) => vec![GuiAction::SendToServer(Command::QueueAdd(
 | ||||||
|  | //                 vec![],
 | ||||||
|  | //                 QueueContent::Song(id).into(),
 | ||||||
|  | //             ))],
 | ||||||
|  | //             Dragging::Album(id) => vec![GuiAction::Build(Box::new(move |db| {
 | ||||||
|  | //                 if let Some(q) = Self::add_to_queue_album_by_id(id, db) {
 | ||||||
|  | //                     vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
 | ||||||
|  | //                 } else {
 | ||||||
|  | //                     vec![]
 | ||||||
|  | //                 }
 | ||||||
|  | //             }))],
 | ||||||
|  | //             Dragging::Artist(id) => vec![GuiAction::Build(Box::new(move |db| {
 | ||||||
|  | //                 if let Some(q) = Self::add_to_queue_artist_by_id(id, db) {
 | ||||||
|  | //                     vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
 | ||||||
|  | //                 } else {
 | ||||||
|  | //                     vec![]
 | ||||||
|  | //                 }
 | ||||||
|  | //             }))],
 | ||||||
|  | //             _ => vec![],
 | ||||||
|  | //         }
 | ||||||
|  | //     }
 | ||||||
|  | // }
 | ||||||
|  | // impl QueueViewer {
 | ||||||
|  | //     fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
 | ||||||
|  | //         if let Some(album) = db.albums().get(&id) {
 | ||||||
|  | //             Some(
 | ||||||
|  | //                 QueueContent::Folder(
 | ||||||
|  | //                     0,
 | ||||||
|  | //                     album
 | ||||||
|  | //                         .songs
 | ||||||
|  | //                         .iter()
 | ||||||
|  | //                         .map(|id| QueueContent::Song(*id).into())
 | ||||||
|  | //                         .collect(),
 | ||||||
|  | //                     album.name.clone(),
 | ||||||
|  | //                 )
 | ||||||
|  | //                 .into(),
 | ||||||
|  | //             )
 | ||||||
|  | //         } else {
 | ||||||
|  | //             None
 | ||||||
|  | //         }
 | ||||||
|  | //     }
 | ||||||
|  | //     fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
 | ||||||
|  | //         if let Some(artist) = db.artists().get(&id) {
 | ||||||
|  | //             Some(
 | ||||||
|  | //                 QueueContent::Folder(
 | ||||||
|  | //                     0,
 | ||||||
|  | //                     artist
 | ||||||
|  | //                         .albums
 | ||||||
|  | //                         .iter()
 | ||||||
|  | //                         .filter_map(|id| Self::add_to_queue_album_by_id(*id, db))
 | ||||||
|  | //                         .collect(),
 | ||||||
|  | //                     artist.name.clone(),
 | ||||||
|  | //                 )
 | ||||||
|  | //                 .into(),
 | ||||||
|  | //             )
 | ||||||
|  | //         } else {
 | ||||||
|  | //             None
 | ||||||
|  | //         }
 | ||||||
|  | //     }
 | ||||||
|  | // }
 | ||||||
|  | 
 | ||||||
|  | // const INDENT_PX: f32 = 8.0;
 | ||||||
|  | 
 | ||||||
|  | // impl QueueViewer {
 | ||||||
|  | //     fn draw_queue(
 | ||||||
|  | //         &mut self,
 | ||||||
|  | //         queue: &Queue,
 | ||||||
|  | //         skip: &mut usize,
 | ||||||
|  | //         drawn: &mut usize,
 | ||||||
|  | //         limit: usize,
 | ||||||
|  | //         top_left: &mut Vec2,
 | ||||||
|  | //         width: f32,
 | ||||||
|  | //         info: &DrawInfo,
 | ||||||
|  | //         g: &mut speedy2d::Graphics2D,
 | ||||||
|  | //     ) {
 | ||||||
|  | //         // eprintln!("[queue: {} : {}/{}]", *skip, *drawn, limit);
 | ||||||
|  | //         match queue.content() {
 | ||||||
|  | //             QueueContent::Song(id) => {
 | ||||||
|  | //                 if *skip == 0 {
 | ||||||
|  | //                     if *drawn < limit {
 | ||||||
|  | //                         *drawn += 1;
 | ||||||
|  | //                         let text = if let Some(song) = info.database.get_song(id) {
 | ||||||
|  | //                             song.title.clone()
 | ||||||
|  | //                         } else {
 | ||||||
|  | //                             format!("< {id} >")
 | ||||||
|  | //                         };
 | ||||||
|  | //                         let height = info
 | ||||||
|  | //                             .font
 | ||||||
|  | //                             .layout_text(&text, 1.0, TextOptions::new())
 | ||||||
|  | //                             .height();
 | ||||||
|  | //                         g.draw_text_cropped(
 | ||||||
|  | //                             top_left.clone(),
 | ||||||
|  | //                             info.pos.clone(),
 | ||||||
|  | //                             Color::from_int_rgb(112, 41, 99),
 | ||||||
|  | //                             &info.font.layout_text(
 | ||||||
|  | //                                 &text,
 | ||||||
|  | //                                 0.75 * info.queue_song_height / height,
 | ||||||
|  | //                                 TextOptions::new(),
 | ||||||
|  | //                             ),
 | ||||||
|  | //                         );
 | ||||||
|  | //                         top_left.y += info.queue_song_height;
 | ||||||
|  | //                     }
 | ||||||
|  | //                 } else {
 | ||||||
|  | //                     *skip -= 1;
 | ||||||
|  | //                 }
 | ||||||
|  | //             }
 | ||||||
|  | //             QueueContent::Folder(index, vec, _name) => {
 | ||||||
|  | //                 top_left.x += INDENT_PX;
 | ||||||
|  | //                 for v in vec {
 | ||||||
|  | //                     self.draw_queue(v, skip, drawn, limit, top_left, width - INDENT_PX, info, g);
 | ||||||
|  | //                 }
 | ||||||
|  | //                 top_left.x -= INDENT_PX;
 | ||||||
|  | //             }
 | ||||||
|  | //         }
 | ||||||
|  | //     }
 | ||||||
|  | // }
 | ||||||
							
								
								
									
										290
									
								
								musicdb-client/src/gui_screen.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										290
									
								
								musicdb-client/src/gui_screen.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,290 @@ | |||||||
|  | use std::time::{Duration, Instant}; | ||||||
|  | 
 | ||||||
|  | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, | ||||||
|  |     gui_base::{Button, Panel}, | ||||||
|  |     gui_library::LibraryBrowser, | ||||||
|  |     gui_playback::{CurrentSong, PlayPauseToggle}, | ||||||
|  |     gui_queue::QueueViewer, | ||||||
|  |     gui_settings::Settings, | ||||||
|  |     gui_text::Label, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /// calculates f(p) (f(x) = 3x^2 - 2x^3)):
 | ||||||
|  | /// f(0) = 0
 | ||||||
|  | /// f(0.5) = 0.5
 | ||||||
|  | /// f(1) = 1
 | ||||||
|  | /// f'(0) = f'(1) = 0
 | ||||||
|  | /// -> smooth animation, fast to calculate
 | ||||||
|  | pub fn transition(p: f32) -> f32 { | ||||||
|  |     3.0 * p * p - 2.0 * p * p * p | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct GuiScreen { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     /// 0: StatusBar / Idle display
 | ||||||
|  |     /// 1: Settings
 | ||||||
|  |     /// 2: Panel for Main view
 | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     pub idle: (bool, Option<Instant>), | ||||||
|  |     pub settings: (bool, Option<Instant>), | ||||||
|  |     pub last_interaction: Instant, | ||||||
|  |     idle_timeout: Option<f64>, | ||||||
|  |     pub prev_mouse_pos: Vec2, | ||||||
|  | } | ||||||
|  | impl GuiScreen { | ||||||
|  |     fn i_statusbar() -> usize { | ||||||
|  |         0 | ||||||
|  |     } | ||||||
|  |     pub fn new( | ||||||
|  |         config: GuiElemCfg, | ||||||
|  |         line_height: f32, | ||||||
|  |         scroll_sensitivity_pixels: f64, | ||||||
|  |         scroll_sensitivity_lines: f64, | ||||||
|  |         scroll_sensitivity_pages: f64, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_keyboard_watch(), | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(StatusBar::new( | ||||||
|  |                     GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))), | ||||||
|  |                     true, | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Settings::new( | ||||||
|  |                     GuiElemCfg::default().disabled(), | ||||||
|  |                     line_height, | ||||||
|  |                     scroll_sensitivity_pixels, | ||||||
|  |                     scroll_sensitivity_lines, | ||||||
|  |                     scroll_sensitivity_pages, | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Panel::new( | ||||||
|  |                     GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))), | ||||||
|  |                     vec![ | ||||||
|  |                         GuiElem::new(Button::new( | ||||||
|  |                             GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.1))), | ||||||
|  |                             |_| vec![GuiAction::OpenSettings(true)], | ||||||
|  |                             vec![GuiElem::new(Label::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 "Settings".to_string(), | ||||||
|  |                                 Color::WHITE, | ||||||
|  |                                 None, | ||||||
|  |                                 Vec2::new(0.5, 0.5), | ||||||
|  |                             ))], | ||||||
|  |                         )), | ||||||
|  |                         GuiElem::new(Button::new( | ||||||
|  |                             GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.1))), | ||||||
|  |                             |_| vec![GuiAction::Exit], | ||||||
|  |                             vec![GuiElem::new(Label::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 "Exit".to_string(), | ||||||
|  |                                 Color::WHITE, | ||||||
|  |                                 None, | ||||||
|  |                                 Vec2::new(0.5, 0.5), | ||||||
|  |                             ))], | ||||||
|  |                         )), | ||||||
|  |                         GuiElem::new(LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                             (0.0, 0.0), | ||||||
|  |                             (0.5, 1.0), | ||||||
|  |                         )))), | ||||||
|  |                         GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                             (0.5, 0.1), | ||||||
|  |                             (1.0, 1.0), | ||||||
|  |                         )))), | ||||||
|  |                     ], | ||||||
|  |                 )), | ||||||
|  |             ], | ||||||
|  |             idle: (false, None), | ||||||
|  |             settings: (false, None), | ||||||
|  |             last_interaction: Instant::now(), | ||||||
|  |             idle_timeout: Some(60.0), | ||||||
|  |             prev_mouse_pos: Vec2::ZERO, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn get_prog(v: &mut (bool, Option<Instant>), seconds: f32) -> f32 { | ||||||
|  |         if let Some(since) = &mut v.1 { | ||||||
|  |             let prog = since.elapsed().as_secs_f32() / seconds; | ||||||
|  |             if prog >= 1.0 { | ||||||
|  |                 v.1 = None; | ||||||
|  |                 if v.0 { | ||||||
|  |                     1.0 | ||||||
|  |                 } else { | ||||||
|  |                     0.0 | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if v.0 { | ||||||
|  |                     prog | ||||||
|  |                 } else { | ||||||
|  |                     1.0 - prog | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else if v.0 { | ||||||
|  |             1.0 | ||||||
|  |         } else { | ||||||
|  |             0.0 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn not_idle(&mut self) { | ||||||
|  |         self.last_interaction = Instant::now(); | ||||||
|  |         if self.idle.0 { | ||||||
|  |             self.idle = (false, Some(Instant::now())); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn idle_check(&mut self) { | ||||||
|  |         if !self.idle.0 { | ||||||
|  |             if let Some(dur) = &self.idle_timeout { | ||||||
|  |                 if self.last_interaction.elapsed().as_secs_f64() > *dur { | ||||||
|  |                     self.idle = (true, Some(Instant::now())); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for GuiScreen { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn key_watch( | ||||||
|  |         &mut self, | ||||||
|  |         _modifiers: speedy2d::window::ModifiersState, | ||||||
|  |         _down: bool, | ||||||
|  |         _key: Option<speedy2d::window::VirtualKeyCode>, | ||||||
|  |         _scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.not_idle(); | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { | ||||||
|  |         // idle stuff
 | ||||||
|  |         if self.prev_mouse_pos != info.mouse_pos { | ||||||
|  |             self.prev_mouse_pos = info.mouse_pos; | ||||||
|  |             self.not_idle(); | ||||||
|  |         } else if !self.idle.0 && self.config.pixel_pos.size() != info.pos.size() { | ||||||
|  |             // resizing prevents idle, but doesn't un-idle
 | ||||||
|  |             self.not_idle(); | ||||||
|  |         } | ||||||
|  |         self.idle_check(); | ||||||
|  |         // request_redraw for animations
 | ||||||
|  |         if self.idle.1.is_some() | self.settings.1.is_some() { | ||||||
|  |             if let Some(h) = &info.helper { | ||||||
|  |                 h.request_redraw() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // animations: idle
 | ||||||
|  |         if self.idle.1.is_some() { | ||||||
|  |             let seconds = if self.idle.0 { 2.0 } else { 0.5 }; | ||||||
|  |             let p1 = Self::get_prog(&mut self.idle, seconds); | ||||||
|  |             if !self.idle.0 || self.idle.1.is_none() { | ||||||
|  |                 if let Some(h) = &info.helper { | ||||||
|  |                     h.set_cursor_visible(!self.idle.0); | ||||||
|  |                     for el in self.children.iter_mut().skip(1) { | ||||||
|  |                         el.inner.config_mut().enabled = !self.idle.0; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             let p = transition(p1); | ||||||
|  |             self.children[0].inner.config_mut().pos = | ||||||
|  |                 Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 1.0)); | ||||||
|  |             self.children[0] | ||||||
|  |                 .inner | ||||||
|  |                 .any_mut() | ||||||
|  |                 .downcast_mut::<StatusBar>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .idle_mode = p1; | ||||||
|  |         } | ||||||
|  |         // animations: settings
 | ||||||
|  |         if self.settings.1.is_some() { | ||||||
|  |             let p1 = Self::get_prog(&mut self.settings, 0.3); | ||||||
|  |             let p = transition(p1); | ||||||
|  |             let cfg = self.children[1].inner.config_mut(); | ||||||
|  |             cfg.enabled = p > 0.0; | ||||||
|  |             cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9)); | ||||||
|  |         } | ||||||
|  |         // set idle timeout (only when settings are open)
 | ||||||
|  |         if self.settings.0 || self.settings.1.is_some() { | ||||||
|  |             self.idle_timeout = self.children[1] | ||||||
|  |                 .inner | ||||||
|  |                 .any() | ||||||
|  |                 .downcast_ref::<Settings>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .get_timeout_val(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct StatusBar { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     idle_mode: f32, | ||||||
|  | } | ||||||
|  | impl StatusBar { | ||||||
|  |     pub fn new(config: GuiElemCfg, playing: bool) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new( | ||||||
|  |                     Vec2::ZERO, | ||||||
|  |                     Vec2::new(0.8, 1.0), | ||||||
|  |                 )))), | ||||||
|  |                 GuiElem::new(PlayPauseToggle::new( | ||||||
|  |                     GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))), | ||||||
|  |                     false, | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Panel::with_background( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     vec![], | ||||||
|  |                     Color::BLACK, | ||||||
|  |                 )), | ||||||
|  |             ], | ||||||
|  |             idle_mode: 0.0, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for StatusBar { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { | ||||||
|  |         if self.idle_mode < 1.0 { | ||||||
|  |             g.draw_line( | ||||||
|  |                 info.pos.top_left(), | ||||||
|  |                 info.pos.top_right(), | ||||||
|  |                 2.0, | ||||||
|  |                 Color::from_rgba(1.0, 1.0, 1.0, 1.0 - self.idle_mode), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										316
									
								
								musicdb-client/src/gui_settings.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										316
									
								
								musicdb-client/src/gui_settings.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,316 @@ | |||||||
|  | use std::{ops::DerefMut, time::Duration}; | ||||||
|  | 
 | ||||||
|  | use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D}; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, | ||||||
|  |     gui_base::{Button, Panel, ScrollBox, Slider}, | ||||||
|  |     gui_text::Label, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Settings { | ||||||
|  |     pub config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  | } | ||||||
|  | impl Settings { | ||||||
|  |     pub fn new( | ||||||
|  |         mut config: GuiElemCfg, | ||||||
|  |         line_height: f32, | ||||||
|  |         scroll_sensitivity_pixels: f64, | ||||||
|  |         scroll_sensitivity_lines: f64, | ||||||
|  |         scroll_sensitivity_pages: f64, | ||||||
|  |     ) -> Self { | ||||||
|  |         config.redraw = true; | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(ScrollBox::new( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     crate::gui_base::ScrollBoxSizeUnit::Pixels, | ||||||
|  |                     vec![ | ||||||
|  |                         ( | ||||||
|  |                             GuiElem::new(Button::new( | ||||||
|  |                                 GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 1.0))), | ||||||
|  |                                 |btn| vec![GuiAction::OpenSettings(false)], | ||||||
|  |                                 vec![GuiElem::new(Label::new( | ||||||
|  |                                     GuiElemCfg::default(), | ||||||
|  |                                     "Back".to_string(), | ||||||
|  |                                     Color::WHITE, | ||||||
|  |                                     None, | ||||||
|  |                                     Vec2::new(0.5, 0.5), | ||||||
|  |                                 ))], | ||||||
|  |                             )), | ||||||
|  |                             0.0, | ||||||
|  |                         ), | ||||||
|  |                         ( | ||||||
|  |                             GuiElem::new(Panel::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 vec![ | ||||||
|  |                                     GuiElem::new(Label::new( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.0, 0.0), | ||||||
|  |                                             (0.33, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         "Settings panel opacity".to_string(), | ||||||
|  |                                         Color::WHITE, | ||||||
|  |                                         None, | ||||||
|  |                                         Vec2::new(0.9, 0.5), | ||||||
|  |                                     )), | ||||||
|  |                                     GuiElem::new({ | ||||||
|  |                                         let mut s = Slider::new_labeled( | ||||||
|  |                                             GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                                 (0.33, 0.0), | ||||||
|  |                                                 (1.0, 1.0), | ||||||
|  |                                             )), | ||||||
|  |                                             0.0, | ||||||
|  |                                             1.0, | ||||||
|  |                                             1.0, | ||||||
|  |                                             |slider, label, _info| { | ||||||
|  |                                                 if slider.val_changed() { | ||||||
|  |                                                     *label.content.text() = | ||||||
|  |                                                         format!("{:.0}%", slider.val * 100.0); | ||||||
|  |                                                 } | ||||||
|  |                                             }, | ||||||
|  |                                         ); | ||||||
|  |                                         s.val_changed_subs.push(false); | ||||||
|  |                                         s | ||||||
|  |                                     }), | ||||||
|  |                                 ], | ||||||
|  |                             )), | ||||||
|  |                             0.0, | ||||||
|  |                         ), | ||||||
|  |                         ( | ||||||
|  |                             GuiElem::new(Panel::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 vec![ | ||||||
|  |                                     GuiElem::new(Label::new( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.0, 0.0), | ||||||
|  |                                             (0.33, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         "Line Height / Text Size".to_string(), | ||||||
|  |                                         Color::WHITE, | ||||||
|  |                                         None, | ||||||
|  |                                         Vec2::new(0.9, 0.5), | ||||||
|  |                                     )), | ||||||
|  |                                     GuiElem::new(Slider::new_labeled( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.33, 0.0), | ||||||
|  |                                             (1.0, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         16.0, | ||||||
|  |                                         80.0, | ||||||
|  |                                         line_height as _, | ||||||
|  |                                         |slider, label, info| { | ||||||
|  |                                             if slider.val_changed() { | ||||||
|  |                                                 *label.content.text() = | ||||||
|  |                                                     format!("line height: {:.0}", slider.val); | ||||||
|  |                                                 let h = slider.val as _; | ||||||
|  |                                                 info.actions.push(GuiAction::SetLineHeight(h)); | ||||||
|  |                                             } | ||||||
|  |                                         }, | ||||||
|  |                                     )), | ||||||
|  |                                 ], | ||||||
|  |                             )), | ||||||
|  |                             0.0, | ||||||
|  |                         ), | ||||||
|  |                         ( | ||||||
|  |                             GuiElem::new(Panel::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 vec![ | ||||||
|  |                                     GuiElem::new(Label::new( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.0, 0.0), | ||||||
|  |                                             (0.33, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         "Scroll Sensitivity (lines)".to_string(), | ||||||
|  |                                         Color::WHITE, | ||||||
|  |                                         None, | ||||||
|  |                                         Vec2::new(0.9, 0.5), | ||||||
|  |                                     )), | ||||||
|  |                                     GuiElem::new(Slider::new_labeled( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.33, 0.0), | ||||||
|  |                                             (1.0, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         0.0, | ||||||
|  |                                         12.0, | ||||||
|  |                                         scroll_sensitivity_lines, | ||||||
|  |                                         |slider, label, info| { | ||||||
|  |                                             if slider.val_changed() { | ||||||
|  |                                                 *label.content.text() = | ||||||
|  |                                                     format!("{:.1}", slider.val); | ||||||
|  |                                                 let h = slider.val as _; | ||||||
|  |                                                 info.actions.push(GuiAction::Do(Box::new( | ||||||
|  |                                                     move |gui| gui.scroll_lines_multiplier = h, | ||||||
|  |                                                 ))); | ||||||
|  |                                             } | ||||||
|  |                                         }, | ||||||
|  |                                     )), | ||||||
|  |                                 ], | ||||||
|  |                             )), | ||||||
|  |                             0.0, | ||||||
|  |                         ), | ||||||
|  |                         ( | ||||||
|  |                             GuiElem::new(Panel::new( | ||||||
|  |                                 GuiElemCfg::default(), | ||||||
|  |                                 vec![ | ||||||
|  |                                     GuiElem::new(Label::new( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.0, 0.0), | ||||||
|  |                                             (0.33, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         "Idle time".to_string(), | ||||||
|  |                                         Color::WHITE, | ||||||
|  |                                         None, | ||||||
|  |                                         Vec2::new(0.9, 0.5), | ||||||
|  |                                     )), | ||||||
|  |                                     GuiElem::new(Slider::new_labeled( | ||||||
|  |                                         GuiElemCfg::at(Rectangle::from_tuples( | ||||||
|  |                                             (0.33, 0.0), | ||||||
|  |                                             (1.0, 1.0), | ||||||
|  |                                         )), | ||||||
|  |                                         0.0, | ||||||
|  |                                         (60.0f64 * 60.0 * 6.0).sqrt(), | ||||||
|  |                                         60.0f64.sqrt(), | ||||||
|  |                                         |slider, label, info| { | ||||||
|  |                                             if slider.val_changed() { | ||||||
|  |                                                 *label.content.text() = if slider.val > 0.0 { | ||||||
|  |                                                     let mut s = String::new(); | ||||||
|  |                                                     let seconds = (slider.val * slider.val) as u64; | ||||||
|  |                                                     let hours = seconds / 3600; | ||||||
|  |                                                     let seconds = seconds % 3600; | ||||||
|  |                                                     let minutes = seconds / 60; | ||||||
|  |                                                     let seconds = seconds % 60; | ||||||
|  |                                                     if hours > 0 { | ||||||
|  |                                                         s = hours.to_string(); | ||||||
|  |                                                         s.push_str("h "); | ||||||
|  |                                                     } | ||||||
|  |                                                     if minutes > 0 || hours > 0 && seconds > 0 { | ||||||
|  |                                                         s.push_str(&minutes.to_string()); | ||||||
|  |                                                         s.push_str("m "); | ||||||
|  |                                                     } | ||||||
|  |                                                     if hours == 0 | ||||||
|  |                                                         && minutes < 10 | ||||||
|  |                                                         && (seconds > 0 || minutes == 0) | ||||||
|  |                                                     { | ||||||
|  |                                                         s.push_str(&seconds.to_string()); | ||||||
|  |                                                         s.push_str("s"); | ||||||
|  |                                                     } else if s.ends_with(" ") { | ||||||
|  |                                                         s.pop(); | ||||||
|  |                                                     } | ||||||
|  |                                                     s | ||||||
|  |                                                 } else { | ||||||
|  |                                                     "no timeout".to_string() | ||||||
|  |                                                 } | ||||||
|  |                                             }; | ||||||
|  |                                             let h = slider.val as _; | ||||||
|  |                                             if slider.val_changed() { | ||||||
|  |                                                 info.actions.push(GuiAction::Do(Box::new( | ||||||
|  |                                                     move |gui| gui.scroll_lines_multiplier = h, | ||||||
|  |                                                 ))); | ||||||
|  |                                             } | ||||||
|  |                                         }, | ||||||
|  |                                     )), | ||||||
|  |                                 ], | ||||||
|  |                             )), | ||||||
|  |                             0.0, | ||||||
|  |                         ), | ||||||
|  |                     ], | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Panel::with_background( | ||||||
|  |                     GuiElemCfg::default().w_mouse(), | ||||||
|  |                     vec![], | ||||||
|  |                     Color::BLACK, | ||||||
|  |                 )), | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn get_timeout_val(&self) -> Option<f64> { | ||||||
|  |         let v = self.children[0] | ||||||
|  |             .inner | ||||||
|  |             .any() | ||||||
|  |             .downcast_ref::<ScrollBox>() | ||||||
|  |             .unwrap() | ||||||
|  |             .children[4] | ||||||
|  |             .0 | ||||||
|  |             .inner | ||||||
|  |             .any() | ||||||
|  |             .downcast_ref::<Panel>() | ||||||
|  |             .unwrap() | ||||||
|  |             .children[1] | ||||||
|  |             .inner | ||||||
|  |             .any() | ||||||
|  |             .downcast_ref::<Slider>() | ||||||
|  |             .unwrap() | ||||||
|  |             .val; | ||||||
|  |         if v > 0.0 { | ||||||
|  |             Some(v * v) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for Settings { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         Box::new(self.children.iter_mut()) | ||||||
|  |     } | ||||||
|  |     fn clone_gui(&self) -> Box<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn any(&self) -> &dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn any_mut(&mut self) -> &mut dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) { | ||||||
|  |         let (rest, background) = self.children.split_at_mut(1); | ||||||
|  |         let scrollbox = rest[0].inner.any_mut().downcast_mut::<ScrollBox>().unwrap(); | ||||||
|  |         let settings_opacity_slider = scrollbox.children[1] | ||||||
|  |             .0 | ||||||
|  |             .inner | ||||||
|  |             .any_mut() | ||||||
|  |             .downcast_mut::<Panel>() | ||||||
|  |             .unwrap() | ||||||
|  |             .children[1] | ||||||
|  |             .inner | ||||||
|  |             .any_mut() | ||||||
|  |             .downcast_mut::<Slider>() | ||||||
|  |             .unwrap(); | ||||||
|  |         if settings_opacity_slider.val_changed_subs[0] { | ||||||
|  |             settings_opacity_slider.val_changed_subs[0] = false; | ||||||
|  |             let color = background[0] | ||||||
|  |                 .inner | ||||||
|  |                 .any_mut() | ||||||
|  |                 .downcast_mut::<Panel>() | ||||||
|  |                 .unwrap() | ||||||
|  |                 .background | ||||||
|  |                 .as_mut() | ||||||
|  |                 .unwrap(); | ||||||
|  |             *color = Color::from_rgba( | ||||||
|  |                 color.r(), | ||||||
|  |                 color.g(), | ||||||
|  |                 color.b(), | ||||||
|  |                 settings_opacity_slider.val as _, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         if self.config.redraw { | ||||||
|  |             self.config.redraw = false; | ||||||
|  |             for (i, (_, h)) in scrollbox.children.iter_mut().enumerate() { | ||||||
|  |                 *h = if i == 0 { | ||||||
|  |                     info.line_height * 2.0 | ||||||
|  |                 } else { | ||||||
|  |                     info.line_height | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										226
									
								
								musicdb-client/src/gui_text.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										226
									
								
								musicdb-client/src/gui_text.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,226 @@ | |||||||
|  | use std::rc::Rc; | ||||||
|  | 
 | ||||||
|  | use speedy2d::{ | ||||||
|  |     color::Color, | ||||||
|  |     dimen::Vec2, | ||||||
|  |     font::{FormattedTextBlock, TextLayout, TextOptions}, | ||||||
|  |     shape::Rectangle, | ||||||
|  |     window::{ModifiersState, MouseButton}, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Label { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     children: Vec<GuiElem>, | ||||||
|  |     pub content: Content, | ||||||
|  |     pub pos: Vec2, | ||||||
|  | } | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct Content { | ||||||
|  |     text: String, | ||||||
|  |     color: Color, | ||||||
|  |     background: Option<Color>, | ||||||
|  |     formatted: Option<Rc<FormattedTextBlock>>, | ||||||
|  | } | ||||||
|  | impl Content { | ||||||
|  |     pub fn get_text(&self) -> &String { | ||||||
|  |         &self.text | ||||||
|  |     } | ||||||
|  |     pub fn get_color(&self) -> &Color { | ||||||
|  |         &self.color | ||||||
|  |     } | ||||||
|  |     /// causes text layout reset
 | ||||||
|  |     pub fn text(&mut self) -> &mut String { | ||||||
|  |         self.formatted = None; | ||||||
|  |         &mut self.text | ||||||
|  |     } | ||||||
|  |     pub fn color(&mut self) -> &mut Color { | ||||||
|  |         &mut self.color | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl Label { | ||||||
|  |     pub fn new( | ||||||
|  |         config: GuiElemCfg, | ||||||
|  |         text: String, | ||||||
|  |         color: Color, | ||||||
|  |         background: Option<Color>, | ||||||
|  |         pos: Vec2, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config, | ||||||
|  |             children: vec![], | ||||||
|  |             content: Content { | ||||||
|  |                 text, | ||||||
|  |                 color, | ||||||
|  |                 background, | ||||||
|  |                 formatted: None, | ||||||
|  |             }, | ||||||
|  |             pos, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for Label { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         if self.config.pixel_pos.size() != info.pos.size() { | ||||||
|  |             // resize
 | ||||||
|  |             self.content.formatted = None; | ||||||
|  |         } | ||||||
|  |         let text = if let Some(text) = &self.content.formatted { | ||||||
|  |             text | ||||||
|  |         } else { | ||||||
|  |             let l = info | ||||||
|  |                 .font | ||||||
|  |                 .layout_text(&self.content.text, 1.0, TextOptions::new()); | ||||||
|  |             let l = info.font.layout_text( | ||||||
|  |                 &self.content.text, | ||||||
|  |                 (info.pos.width() / l.width()).min(info.pos.height() / l.height()), | ||||||
|  |                 TextOptions::new(), | ||||||
|  |             ); | ||||||
|  |             self.content.formatted = Some(l); | ||||||
|  |             self.content.formatted.as_ref().unwrap() | ||||||
|  |         }; | ||||||
|  |         let top_left = Vec2::new( | ||||||
|  |             info.pos.top_left().x + self.pos.x * (info.pos.width() - text.width()), | ||||||
|  |             info.pos.top_left().y + self.pos.y * (info.pos.height() - text.height()), | ||||||
|  |         ); | ||||||
|  |         if let Some(bg) = self.content.background { | ||||||
|  |             g.draw_rectangle( | ||||||
|  |                 Rectangle::new( | ||||||
|  |                     top_left, | ||||||
|  |                     Vec2::new(top_left.x + text.width(), top_left.y + text.height()), | ||||||
|  |                 ), | ||||||
|  |                 bg, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         g.draw_text(top_left, self.content.color, text); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO! this, but requires keyboard events first
 | ||||||
|  | 
 | ||||||
|  | /// a single-line text fields for users to type text into.
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct TextField { | ||||||
|  |     config: GuiElemCfg, | ||||||
|  |     pub children: Vec<GuiElem>, | ||||||
|  | } | ||||||
|  | impl TextField { | ||||||
|  |     pub fn new(config: GuiElemCfg, hint: String, color_hint: Color, color_input: Color) -> Self { | ||||||
|  |         Self { | ||||||
|  |             config: config.w_mouse().w_keyboard_focus(), | ||||||
|  |             children: vec![ | ||||||
|  |                 GuiElem::new(Label::new( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     String::new(), | ||||||
|  |                     color_input, | ||||||
|  |                     None, | ||||||
|  |                     Vec2::new(0.0, 0.5), | ||||||
|  |                 )), | ||||||
|  |                 GuiElem::new(Label::new( | ||||||
|  |                     GuiElemCfg::default(), | ||||||
|  |                     hint, | ||||||
|  |                     color_hint, | ||||||
|  |                     None, | ||||||
|  |                     Vec2::new(0.0, 0.5), | ||||||
|  |                 )), | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl GuiElemTrait for TextField { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         &self.config | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         &mut self.config | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         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<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { | ||||||
|  |         let (t, c) = if info.has_keyboard_focus { | ||||||
|  |             (3.0, Color::WHITE) | ||||||
|  |         } else { | ||||||
|  |             (1.0, Color::GRAY) | ||||||
|  |         }; | ||||||
|  |         g.draw_line(info.pos.top_left(), info.pos.top_right(), t, c); | ||||||
|  |         g.draw_line(info.pos.bottom_left(), info.pos.bottom_right(), t, c); | ||||||
|  |         g.draw_line(info.pos.top_left(), info.pos.bottom_left(), t, c); | ||||||
|  |         g.draw_line(info.pos.top_right(), info.pos.bottom_right(), t, c); | ||||||
|  |     } | ||||||
|  |     fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         self.config.request_keyboard_focus = true; | ||||||
|  |         vec![GuiAction::ResetKeyboardFocus] | ||||||
|  |     } | ||||||
|  |     fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> { | ||||||
|  |         if !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && !key.is_control() { | ||||||
|  |             let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content; | ||||||
|  |             let was_empty = content.get_text().is_empty(); | ||||||
|  |             content.text().push(key); | ||||||
|  |             if was_empty { | ||||||
|  |                 self.children[1].inner.config_mut().enabled = false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  |     fn key_focus( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: ModifiersState, | ||||||
|  |         down: bool, | ||||||
|  |         key: Option<speedy2d::window::VirtualKeyCode>, | ||||||
|  |         _scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         if down | ||||||
|  |             && !(modifiers.alt() || modifiers.logo()) | ||||||
|  |             && key == Some(speedy2d::window::VirtualKeyCode::Backspace) | ||||||
|  |         { | ||||||
|  |             let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content; | ||||||
|  |             if !content.get_text().is_empty() { | ||||||
|  |                 if modifiers.ctrl() { | ||||||
|  |                     for s in [true, false, true] { | ||||||
|  |                         while !content.get_text().is_empty() | ||||||
|  |                             && content.get_text().ends_with(' ') == s | ||||||
|  |                         { | ||||||
|  |                             content.text().pop(); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     content.text().pop(); | ||||||
|  |                 } | ||||||
|  |                 if content.get_text().is_empty() { | ||||||
|  |                     self.children[1].inner.config_mut().enabled = true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         vec![] | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										147
									
								
								musicdb-client/src/gui_wrappers.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										147
									
								
								musicdb-client/src/gui_wrappers.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,147 @@ | |||||||
|  | use speedy2d::{ | ||||||
|  |     window::{MouseButton, VirtualKeyCode}, | ||||||
|  |     Graphics2D, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct WithFocusHotkey<T: GuiElemTrait + Clone> { | ||||||
|  |     pub inner: T, | ||||||
|  |     /// 4 * (ignore, pressed): 10 or 11 -> doesn't matter, 01 -> must be pressed, 00 -> must not be pressed
 | ||||||
|  |     /// logo alt shift ctrl
 | ||||||
|  |     pub modifiers: u8, | ||||||
|  |     pub key: VirtualKeyCode, | ||||||
|  | } | ||||||
|  | impl<T: GuiElemTrait + Clone> WithFocusHotkey<T> { | ||||||
|  |     /// unlike noshift, this ignores the shift modifier
 | ||||||
|  |     pub fn new_key(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b1000, key, inner) | ||||||
|  |     } | ||||||
|  |     /// requires the key to be pressed without any modifiers
 | ||||||
|  |     pub fn new_noshift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b0100, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_ctrl(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b01, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_ctrl_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b0101, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b010000, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b010100, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_ctrl_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b010001, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new_ctrl_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         Self::new(0b010101, key, inner) | ||||||
|  |     } | ||||||
|  |     pub fn new(modifiers: u8, key: VirtualKeyCode, mut inner: T) -> WithFocusHotkey<T> { | ||||||
|  |         inner.config_mut().keyboard_events_watch = true; | ||||||
|  |         WithFocusHotkey { | ||||||
|  |             inner, | ||||||
|  |             modifiers, | ||||||
|  |             key, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl<T: Clone + 'static> GuiElemTrait for WithFocusHotkey<T> | ||||||
|  | where | ||||||
|  |     T: GuiElemTrait, | ||||||
|  | { | ||||||
|  |     fn config(&self) -> &GuiElemCfg { | ||||||
|  |         self.inner.config() | ||||||
|  |     } | ||||||
|  |     fn config_mut(&mut self) -> &mut GuiElemCfg { | ||||||
|  |         self.inner.config_mut() | ||||||
|  |     } | ||||||
|  |     fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> { | ||||||
|  |         self.inner.children() | ||||||
|  |     } | ||||||
|  |     fn any(&self) -> &dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn any_mut(&mut self) -> &mut dyn std::any::Any { | ||||||
|  |         self | ||||||
|  |     } | ||||||
|  |     fn clone_gui(&self) -> Box<dyn GuiElemTrait> { | ||||||
|  |         Box::new(self.clone()) | ||||||
|  |     } | ||||||
|  |     fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) { | ||||||
|  |         self.inner.draw(info, g) | ||||||
|  |     } | ||||||
|  |     fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         self.inner.mouse_down(button) | ||||||
|  |     } | ||||||
|  |     fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         self.inner.mouse_up(button) | ||||||
|  |     } | ||||||
|  |     fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> { | ||||||
|  |         self.inner.mouse_pressed(button) | ||||||
|  |     } | ||||||
|  |     fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> { | ||||||
|  |         self.inner.mouse_wheel(diff) | ||||||
|  |     } | ||||||
|  |     fn char_watch( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: speedy2d::window::ModifiersState, | ||||||
|  |         key: char, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.inner.char_watch(modifiers, key) | ||||||
|  |     } | ||||||
|  |     fn char_focus( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: speedy2d::window::ModifiersState, | ||||||
|  |         key: char, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.inner.char_focus(modifiers, key) | ||||||
|  |     } | ||||||
|  |     fn key_watch( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: speedy2d::window::ModifiersState, | ||||||
|  |         down: bool, | ||||||
|  |         key: Option<speedy2d::window::VirtualKeyCode>, | ||||||
|  |         scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         let hotkey = down == false | ||||||
|  |             && key.is_some_and(|v| v == self.key) | ||||||
|  |             && (self.modifiers & 0b10 == 1 || (self.modifiers & 0b01 == 1) == modifiers.ctrl()) | ||||||
|  |             && (self.modifiers & 0b1000 == 1 | ||||||
|  |                 || (self.modifiers & 0b0100 == 1) == modifiers.shift()) | ||||||
|  |             && (self.modifiers & 0b100000 == 1 | ||||||
|  |                 || (self.modifiers & 0b010000 == 1) == modifiers.alt()) | ||||||
|  |             && (self.modifiers & 0b10000000 == 1 | ||||||
|  |                 || (self.modifiers & 0b01000000 == 1) == modifiers.logo()); | ||||||
|  |         let mut o = self.inner.key_watch(modifiers, down, key, scan); | ||||||
|  |         if hotkey { | ||||||
|  |             self.config_mut().request_keyboard_focus = true; | ||||||
|  |             o.push(GuiAction::ResetKeyboardFocus); | ||||||
|  |         } | ||||||
|  |         o | ||||||
|  |     } | ||||||
|  |     fn key_focus( | ||||||
|  |         &mut self, | ||||||
|  |         modifiers: speedy2d::window::ModifiersState, | ||||||
|  |         down: bool, | ||||||
|  |         key: Option<speedy2d::window::VirtualKeyCode>, | ||||||
|  |         scan: speedy2d::window::KeyScancode, | ||||||
|  |     ) -> Vec<GuiAction> { | ||||||
|  |         self.inner.key_focus(modifiers, down, key, scan) | ||||||
|  |     } | ||||||
|  |     fn dragged(&mut self, dragged: crate::gui::Dragging) -> Vec<GuiAction> { | ||||||
|  |         self.inner.dragged(dragged) | ||||||
|  |     } | ||||||
|  |     fn updated_library(&mut self) { | ||||||
|  |         self.inner.updated_library() | ||||||
|  |     } | ||||||
|  |     fn updated_queue(&mut self) { | ||||||
|  |         self.inner.updated_queue() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										436
									
								
								musicdb-client/src/main.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										436
									
								
								musicdb-client/src/main.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,436 @@ | |||||||
|  | use std::{ | ||||||
|  |     eprintln, fs, | ||||||
|  |     net::{SocketAddr, TcpStream}, | ||||||
|  |     path::PathBuf, | ||||||
|  |     sync::{Arc, Mutex}, | ||||||
|  |     thread, | ||||||
|  |     time::Duration, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use gui::GuiEvent; | ||||||
|  | use musicdb_lib::{ | ||||||
|  |     data::{ | ||||||
|  |         album::Album, artist::Artist, database::Database, queue::QueueContent, song::Song, | ||||||
|  |         DatabaseLocation, GeneralData, | ||||||
|  |     }, | ||||||
|  |     load::ToFromBytes, | ||||||
|  |     player::Player, | ||||||
|  |     server::Command, | ||||||
|  | }; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_base; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_library; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_playback; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_queue; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_screen; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_settings; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_text; | ||||||
|  | #[cfg(feature = "speedy2d")] | ||||||
|  | mod gui_wrappers; | ||||||
|  | 
 | ||||||
|  | enum Mode { | ||||||
|  |     Cli, | ||||||
|  |     Gui, | ||||||
|  |     SyncPlayer, | ||||||
|  |     FillDb, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn main() { | ||||||
|  |     let mut args = std::env::args().skip(1); | ||||||
|  |     let mode = match args.next().as_ref().map(|v| v.trim()) { | ||||||
|  |         Some("cli") => Mode::Cli, | ||||||
|  |         Some("gui") => Mode::Gui, | ||||||
|  |         Some("syncplayer") => Mode::SyncPlayer, | ||||||
|  |         Some("filldb") => Mode::FillDb, | ||||||
|  |         _ => { | ||||||
|  |             println!("Run with argument <cli/gui/syncplayer/filldb>!"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     let addr = args.next().unwrap_or("127.0.0.1:26314".to_string()); | ||||||
|  |     let mut con = TcpStream::connect(addr.parse::<SocketAddr>().unwrap()).unwrap(); | ||||||
|  |     let database = Arc::new(Mutex::new(Database::new_clientside())); | ||||||
|  |     #[cfg(feature = "speedy2d")] | ||||||
|  |     let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> = | ||||||
|  |         Arc::new(Mutex::new(None)); | ||||||
|  |     #[cfg(feature = "speedy2d")] | ||||||
|  |     let sender = Arc::clone(&update_gui_sender); | ||||||
|  |     let wants_player = matches!(mode, Mode::SyncPlayer); | ||||||
|  |     let con_thread = { | ||||||
|  |         let database = Arc::clone(&database); | ||||||
|  |         let mut con = con.try_clone().unwrap(); | ||||||
|  |         // this is all you need to keep the db in sync
 | ||||||
|  |         thread::spawn(move || { | ||||||
|  |             let mut player = if wants_player { | ||||||
|  |                 Some(Player::new().unwrap()) | ||||||
|  |             } else { | ||||||
|  |                 None | ||||||
|  |             }; | ||||||
|  |             loop { | ||||||
|  |                 if let Some(player) = &mut player { | ||||||
|  |                     let mut db = database.lock().unwrap(); | ||||||
|  |                     if !db.lib_directory.as_os_str().is_empty() { | ||||||
|  |                         player.update(&mut db); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 let update = Command::from_bytes(&mut con).unwrap(); | ||||||
|  |                 if let Some(player) = &mut player { | ||||||
|  |                     player.handle_command(&update); | ||||||
|  |                 } | ||||||
|  |                 database.lock().unwrap().apply_command(update); | ||||||
|  |                 #[cfg(feature = "speedy2d")] | ||||||
|  |                 if let Some(v) = &*update_gui_sender.lock().unwrap() { | ||||||
|  |                     v.send_event(GuiEvent::Refresh).unwrap(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
|  |     match mode { | ||||||
|  |         Mode::Cli => { | ||||||
|  |             Looper { | ||||||
|  |                 con: &mut con, | ||||||
|  |                 database: &database, | ||||||
|  |             } | ||||||
|  |             .cmd_loop(); | ||||||
|  |         } | ||||||
|  |         Mode::Gui => { | ||||||
|  |             #[cfg(feature = "speedy2d")] | ||||||
|  |             { | ||||||
|  |                 let occasional_refresh_sender = Arc::clone(&sender); | ||||||
|  |                 thread::spawn(move || loop { | ||||||
|  |                     std::thread::sleep(Duration::from_secs(1)); | ||||||
|  |                     if let Some(v) = &*occasional_refresh_sender.lock().unwrap() { | ||||||
|  |                         v.send_event(GuiEvent::Refresh).unwrap(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |                 gui::main(database, con, sender) | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         Mode::SyncPlayer => { | ||||||
|  |             con_thread.join().unwrap(); | ||||||
|  |         } | ||||||
|  |         Mode::FillDb => { | ||||||
|  |             // wait for init
 | ||||||
|  |             let dir = loop { | ||||||
|  |                 let db = database.lock().unwrap(); | ||||||
|  |                 if !db.lib_directory.as_os_str().is_empty() { | ||||||
|  |                     break db.lib_directory.clone(); | ||||||
|  |                 } | ||||||
|  |                 drop(db); | ||||||
|  |                 std::thread::sleep(Duration::from_millis(300)); | ||||||
|  |             }; | ||||||
|  |             eprintln!(" | ||||||
|  |   WARN: This will add all audio files in the lib-dir to the library, even if they were already added! | ||||||
|  |         lib-dir: {:?} | ||||||
|  |         If you really want to continue, type Yes.", dir);
 | ||||||
|  |             let mut line = String::new(); | ||||||
|  |             std::io::stdin().read_line(&mut line).unwrap(); | ||||||
|  |             if line.trim().to_lowercase() == "yes" { | ||||||
|  |                 for artist in fs::read_dir(&dir) | ||||||
|  |                     .expect("reading lib-dir") | ||||||
|  |                     .filter_map(|v| v.ok()) | ||||||
|  |                 { | ||||||
|  |                     if let Ok(albums) = fs::read_dir(artist.path()) { | ||||||
|  |                         let artist_name = artist.file_name().to_string_lossy().into_owned(); | ||||||
|  |                         let mut artist_id = None; | ||||||
|  |                         for album in albums.filter_map(|v| v.ok()) { | ||||||
|  |                             if let Ok(songs) = fs::read_dir(album.path()) { | ||||||
|  |                                 let album_name = album.file_name().to_string_lossy().into_owned(); | ||||||
|  |                                 let mut album_id = None; | ||||||
|  |                                 let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect(); | ||||||
|  |                                 songs.sort_unstable_by_key(|v| v.file_name()); | ||||||
|  |                                 for song in songs { | ||||||
|  |                                     match song.path().extension().map(|v| v.to_str()) { | ||||||
|  |                                         Some(Some( | ||||||
|  |                                             "mp3" | "wav" | "wma" | "aac" | "flac" | "m4a" | "m4p" | ||||||
|  |                                             | "ogg" | "oga" | "mogg" | "opus" | "tta", | ||||||
|  |                                         )) => { | ||||||
|  |                                             println!("> {:?}", song.path()); | ||||||
|  |                                             let song_name = | ||||||
|  |                                                 song.file_name().to_string_lossy().into_owned(); | ||||||
|  |                                             println!( | ||||||
|  |                                                 "  {}  -  {}  -  {}", | ||||||
|  |                                                 song_name, artist_name, album_name | ||||||
|  |                                             ); | ||||||
|  |                                             // get artist id
 | ||||||
|  |                                             let artist_id = if let Some(v) = artist_id { | ||||||
|  |                                                 v | ||||||
|  |                                             } else { | ||||||
|  |                                                 let mut adding_artist = false; | ||||||
|  |                                                 loop { | ||||||
|  |                                                     let db = database.lock().unwrap(); | ||||||
|  |                                                     let artists = db | ||||||
|  |                                                         .artists() | ||||||
|  |                                                         .iter() | ||||||
|  |                                                         .filter(|(_, v)| v.name == artist_name) | ||||||
|  |                                                         .collect::<Vec<_>>(); | ||||||
|  |                                                     if artists.len() > 1 { | ||||||
|  |                                                         eprintln!("Choosing the first of {} artists named {}.", artists.len(), artist_name); | ||||||
|  |                                                     } | ||||||
|  |                                                     if let Some((id, _)) = artists.first() { | ||||||
|  |                                                         artist_id = Some(**id); | ||||||
|  |                                                         break **id; | ||||||
|  |                                                     } else { | ||||||
|  |                                                         drop(db); | ||||||
|  |                                                         if !adding_artist { | ||||||
|  |                                                             adding_artist = true; | ||||||
|  |                                                             Command::AddArtist(Artist { | ||||||
|  |                                                                 id: 0, | ||||||
|  |                                                                 name: artist_name.clone(), | ||||||
|  |                                                                 cover: None, | ||||||
|  |                                                                 albums: vec![], | ||||||
|  |                                                                 singles: vec![], | ||||||
|  |                                                                 general: GeneralData::default(), | ||||||
|  |                                                             }) | ||||||
|  |                                                             .to_bytes(&mut con) | ||||||
|  |                                                             .expect( | ||||||
|  |                                                                 "sending AddArtist to db failed", | ||||||
|  |                                                             ); | ||||||
|  |                                                         } | ||||||
|  |                                                         std::thread::sleep(Duration::from_millis( | ||||||
|  |                                                             300, | ||||||
|  |                                                         )); | ||||||
|  |                                                     }; | ||||||
|  |                                                 } | ||||||
|  |                                             }; | ||||||
|  |                                             // get album id
 | ||||||
|  |                                             let album_id = if let Some(v) = album_id { | ||||||
|  |                                                 v | ||||||
|  |                                             } else { | ||||||
|  |                                                 let mut adding_album = false; | ||||||
|  |                                                 loop { | ||||||
|  |                                                     let db = database.lock().unwrap(); | ||||||
|  |                                                     let albums = db | ||||||
|  |                                                         .artists() | ||||||
|  |                                                         .get(&artist_id) | ||||||
|  |                                                         .expect("artist_id not valid (bug)") | ||||||
|  |                                                         .albums | ||||||
|  |                                                         .iter() | ||||||
|  |                                                         .filter_map(|v| { | ||||||
|  |                                                             Some((v, db.albums().get(&v)?)) | ||||||
|  |                                                         }) | ||||||
|  |                                                         .filter(|(_, v)| v.name == album_name) | ||||||
|  |                                                         .collect::<Vec<_>>(); | ||||||
|  |                                                     if albums.len() > 1 { | ||||||
|  |                                                         eprintln!("Choosing the first of {} albums named {} by the artist {}.", albums.len(), album_name, artist_name); | ||||||
|  |                                                     } | ||||||
|  |                                                     if let Some((id, _)) = albums.first() { | ||||||
|  |                                                         album_id = Some(**id); | ||||||
|  |                                                         break **id; | ||||||
|  |                                                     } else { | ||||||
|  |                                                         drop(db); | ||||||
|  |                                                         if !adding_album { | ||||||
|  |                                                             adding_album = true; | ||||||
|  |                                                             Command::AddAlbum(Album { | ||||||
|  |                                                                 id: 0, | ||||||
|  |                                                                 name: album_name.clone(), | ||||||
|  |                                                                 artist: Some(artist_id), | ||||||
|  |                                                                 cover: None, | ||||||
|  |                                                                 songs: vec![], | ||||||
|  |                                                                 general: GeneralData::default(), | ||||||
|  |                                                             }) | ||||||
|  |                                                             .to_bytes(&mut con) | ||||||
|  |                                                             .expect("sending AddAlbum to db failed"); | ||||||
|  |                                                         } | ||||||
|  |                                                         std::thread::sleep(Duration::from_millis( | ||||||
|  |                                                             300, | ||||||
|  |                                                         )); | ||||||
|  |                                                     }; | ||||||
|  |                                                 } | ||||||
|  |                                             }; | ||||||
|  |                                             Command::AddSong(Song::new( | ||||||
|  |                                                 DatabaseLocation { | ||||||
|  |                                                     rel_path: PathBuf::from(artist.file_name()) | ||||||
|  |                                                         .join(album.file_name()) | ||||||
|  |                                                         .join(song.file_name()), | ||||||
|  |                                                 }, | ||||||
|  |                                                 song_name, | ||||||
|  |                                                 Some(album_id), | ||||||
|  |                                                 Some(artist_id), | ||||||
|  |                                                 vec![], | ||||||
|  |                                                 None, | ||||||
|  |                                             )) | ||||||
|  |                                             .to_bytes(&mut con) | ||||||
|  |                                             .expect("sending AddSong to db failed"); | ||||||
|  |                                         } | ||||||
|  |                                         _ => {} | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | struct Looper<'a> { | ||||||
|  |     pub con: &'a mut TcpStream, | ||||||
|  |     pub database: &'a Arc<Mutex<Database>>, | ||||||
|  | } | ||||||
|  | impl<'a> Looper<'a> { | ||||||
|  |     pub fn cmd_loop(&mut self) { | ||||||
|  |         loop { | ||||||
|  |             println!(); | ||||||
|  |             let line = self.read_line(" > enter a command (help for help)"); | ||||||
|  |             let line = line.trim(); | ||||||
|  |             match line { | ||||||
|  |                 "resume" => Command::Resume, | ||||||
|  |                 "pause" => Command::Pause, | ||||||
|  |                 "stop" => Command::Stop, | ||||||
|  |                 "next" => Command::NextSong, | ||||||
|  |                 "set-lib-dir" => { | ||||||
|  |                     let line = self.read_line("Enter the new (absolute) library directory, or leave empty to abort"); | ||||||
|  |                     if !line.is_empty() { | ||||||
|  |                         Command::SetLibraryDirectory(line.into()) | ||||||
|  |                     } else { | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 "add-song" => { | ||||||
|  |                     let song = Song { | ||||||
|  |                         id: 0, | ||||||
|  |                         location: self.read_line("The songs file is located, relative to the library root, at...").into(), | ||||||
|  |                         title: self.read_line("The songs title is..."), | ||||||
|  |                         album: self.read_line_ido("The song is part of the album with the id... (empty for None)"), | ||||||
|  |                         artist: self.read_line_ido("The song is made by the artist with the id... (empty for None)"), | ||||||
|  |                         more_artists: accumulate(|| self.read_line_ido("The song is made with support by other artist, one of which has the id... (will ask repeatedly; leave empty once done)")), | ||||||
|  |                         cover: self.read_line_ido("The song should use the cover with the id... (empty for None - will default to album or artist cover, if available)"), | ||||||
|  |                         general: GeneralData::default(), | ||||||
|  |                         cached_data: Arc::new(Mutex::new(None)), | ||||||
|  |                     }; | ||||||
|  |                     println!("You are about to add the following song to the database:"); | ||||||
|  |                     println!("  + {song}"); | ||||||
|  |                     if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" { | ||||||
|  |                             Command::AddSong(song) | ||||||
|  |                     } else { | ||||||
|  |                         println!("[-] Aborted - no event will be sent to the database."); | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 "update-song" => { | ||||||
|  |                     let song_id = self.read_line_id("The ID of the song is..."); | ||||||
|  |                     if let Some(mut song) = self.database.lock().unwrap().get_song(&song_id).cloned() { | ||||||
|  |                         println!("You are now editing the song {song}."); | ||||||
|  |                         loop { | ||||||
|  |                             match self.read_line("What do you want to edit? (title/album/artist/location or done)").to_lowercase().trim() { | ||||||
|  |                                 "done" => break, | ||||||
|  |                                 "title" => { | ||||||
|  |                                     println!("prev: '{}'", song.title); | ||||||
|  |                                     song.title = self.read_line(""); | ||||||
|  |                                 } | ||||||
|  |                                 "album" => { | ||||||
|  |                                     println!("prev: '{}'", song.album.map_or(String::new(), |v| v.to_string())); | ||||||
|  |                                     song.album = self.read_line_ido(""); | ||||||
|  |                                 } | ||||||
|  |                                 "artist" => { | ||||||
|  |                                     println!("prev: '{}'", song.artist.map_or(String::new(), |v| v.to_string())); | ||||||
|  |                                     song.artist = self.read_line_ido(""); | ||||||
|  |                                 } | ||||||
|  |                                 "location" => { | ||||||
|  |                                     println!("prev: '{:?}'", song.location); | ||||||
|  |                                     song.location = self.read_line("").into(); | ||||||
|  |                                 } | ||||||
|  |                                 _ => println!("[-] must be title/album/artist/location or done"), | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         println!("You are about to update the song:"); | ||||||
|  |                         println!("  + {song}"); | ||||||
|  |                         if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" { | ||||||
|  |                             Command::ModifySong(song) | ||||||
|  |                         } else { | ||||||
|  |                             println!("[-] Aborted - no event will be sent to the database."); | ||||||
|  |                             continue; | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         println!("[-] No song with that ID found, aborting."); | ||||||
|  |                         continue; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 "queue-clear" => Command::QueueUpdate(vec![], QueueContent::Folder(0, vec![], String::new()).into()), | ||||||
|  |                 "queue-add-to-end" => Command::QueueAdd(vec![], QueueContent::Song(self.read_line_id("The ID of the song that should be added to the end of the queue is...")).into()), | ||||||
|  |                 "save" => Command::Save, | ||||||
|  |                 "status" => { | ||||||
|  |                     let db = self.database.lock().unwrap(); | ||||||
|  |                     println!("DB contains {} songs:", db.songs().len()); | ||||||
|  |                     for song in db.songs().values() { | ||||||
|  |                         println!("> [{}]: {}", song.id, song); | ||||||
|  |                     } | ||||||
|  |                     println!("Queue: {:?}, then {:?}", db.queue.get_current(), db.queue.get_next()); | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |                 "exit" => { | ||||||
|  |                     println!("<< goodbye"); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 _ => { | ||||||
|  |                     println!("Type 'exit' to exit, 'status' to see the db, 'resume', 'pause', 'stop', 'next', 'queue-clear', 'queue-add-to-end', 'add-song', 'add-album', 'add-artist', 'update-song', 'update-album', 'update-artist', 'set-lib-dir', or 'save' to control playback or update the db."); | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .to_bytes(self.con) | ||||||
|  |             .unwrap(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn read_line(&mut self, q: &str) -> String { | ||||||
|  |         loop { | ||||||
|  |             if !q.is_empty() { | ||||||
|  |                 println!("{q}"); | ||||||
|  |             } | ||||||
|  |             let mut line = String::new(); | ||||||
|  |             std::io::stdin().read_line(&mut line).unwrap(); | ||||||
|  |             while line.ends_with('\n') || line.ends_with('\r') { | ||||||
|  |                 line.pop(); | ||||||
|  |             } | ||||||
|  |             if line.trim() == "#" { | ||||||
|  |                 self.cmd_loop(); | ||||||
|  |             } else { | ||||||
|  |                 return line; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn read_line_id(&mut self, q: &str) -> u64 { | ||||||
|  |         loop { | ||||||
|  |             if let Ok(v) = self.read_line(q).trim().parse() { | ||||||
|  |                 return v; | ||||||
|  |             } else { | ||||||
|  |                 println!("[-] Must be a positive integer."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn read_line_ido(&mut self, q: &str) -> Option<u64> { | ||||||
|  |         loop { | ||||||
|  |             let line = self.read_line(q); | ||||||
|  |             let line = line.trim(); | ||||||
|  |             if line.is_empty() { | ||||||
|  |                 return None; | ||||||
|  |             } | ||||||
|  |             if let Ok(v) = line.parse() { | ||||||
|  |                 return Some(v); | ||||||
|  |             } else { | ||||||
|  |                 println!("[-] Must be a positive integer or nothing for None."); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> { | ||||||
|  |     let mut o = vec![]; | ||||||
|  |     loop { | ||||||
|  |         if let Some(v) = f() { | ||||||
|  |             o.push(v); | ||||||
|  |         } else { | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     o | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								musicdb-lib/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								musicdb-lib/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | /target | ||||||
|  | /Cargo.lock | ||||||
							
								
								
									
										12
									
								
								musicdb-lib/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										12
									
								
								musicdb-lib/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | [package] | ||||||
|  | name = "musicdb-lib" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | 
 | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | awedio = "0.2.0" | ||||||
|  | base64 = "0.21.2" | ||||||
|  | rc-u8-reader = "2.0.16" | ||||||
|  | tokio = "1.29.1" | ||||||
							
								
								
									
										43
									
								
								musicdb-lib/src/data/album.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										43
									
								
								musicdb-lib/src/data/album.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | use std::io::{Read, Write}; | ||||||
|  | 
 | ||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId}; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct Album { | ||||||
|  |     pub id: AlbumId, | ||||||
|  |     pub name: String, | ||||||
|  |     pub artist: Option<ArtistId>, | ||||||
|  |     pub cover: Option<CoverId>, | ||||||
|  |     pub songs: Vec<SongId>, | ||||||
|  |     pub general: GeneralData, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for Album { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.id.to_bytes(s)?; | ||||||
|  |         self.name.to_bytes(s)?; | ||||||
|  |         self.artist.to_bytes(s)?; | ||||||
|  |         self.songs.to_bytes(s)?; | ||||||
|  |         self.cover.to_bytes(s)?; | ||||||
|  |         self.general.to_bytes(s)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(Self { | ||||||
|  |             id: ToFromBytes::from_bytes(s)?, | ||||||
|  |             name: ToFromBytes::from_bytes(s)?, | ||||||
|  |             artist: ToFromBytes::from_bytes(s)?, | ||||||
|  |             songs: ToFromBytes::from_bytes(s)?, | ||||||
|  |             cover: ToFromBytes::from_bytes(s)?, | ||||||
|  |             general: ToFromBytes::from_bytes(s)?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										43
									
								
								musicdb-lib/src/data/artist.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										43
									
								
								musicdb-lib/src/data/artist.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | use std::io::{Read, Write}; | ||||||
|  | 
 | ||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId}; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct Artist { | ||||||
|  |     pub id: ArtistId, | ||||||
|  |     pub name: String, | ||||||
|  |     pub cover: Option<CoverId>, | ||||||
|  |     pub albums: Vec<AlbumId>, | ||||||
|  |     pub singles: Vec<SongId>, | ||||||
|  |     pub general: GeneralData, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for Artist { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.id.to_bytes(s)?; | ||||||
|  |         self.name.to_bytes(s)?; | ||||||
|  |         self.albums.to_bytes(s)?; | ||||||
|  |         self.singles.to_bytes(s)?; | ||||||
|  |         self.cover.to_bytes(s)?; | ||||||
|  |         self.general.to_bytes(s)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(Self { | ||||||
|  |             id: ToFromBytes::from_bytes(s)?, | ||||||
|  |             name: ToFromBytes::from_bytes(s)?, | ||||||
|  |             albums: ToFromBytes::from_bytes(s)?, | ||||||
|  |             singles: ToFromBytes::from_bytes(s)?, | ||||||
|  |             cover: ToFromBytes::from_bytes(s)?, | ||||||
|  |             general: ToFromBytes::from_bytes(s)?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										377
									
								
								musicdb-lib/src/data/database.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										377
									
								
								musicdb-lib/src/data/database.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,377 @@ | |||||||
|  | use std::{ | ||||||
|  |     collections::HashMap, | ||||||
|  |     fs::{self, File}, | ||||||
|  |     io::{BufReader, Write}, | ||||||
|  |     path::PathBuf, | ||||||
|  |     sync::{mpsc, Arc}, | ||||||
|  |     time::Instant, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::{load::ToFromBytes, server::Command}; | ||||||
|  | 
 | ||||||
|  | use super::{ | ||||||
|  |     album::Album, | ||||||
|  |     artist::Artist, | ||||||
|  |     queue::{Queue, QueueContent}, | ||||||
|  |     song::Song, | ||||||
|  |     AlbumId, ArtistId, CoverId, DatabaseLocation, SongId, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | pub struct Database { | ||||||
|  |     db_file: PathBuf, | ||||||
|  |     pub lib_directory: PathBuf, | ||||||
|  |     artists: HashMap<ArtistId, Artist>, | ||||||
|  |     albums: HashMap<AlbumId, Album>, | ||||||
|  |     songs: HashMap<SongId, Song>, | ||||||
|  |     covers: HashMap<CoverId, DatabaseLocation>, | ||||||
|  |     // TODO! make sure this works out for the server AND clients
 | ||||||
|  |     // cover_cache: HashMap<CoverId, Vec<u8>>,
 | ||||||
|  |     db_data_file_change_first: Option<Instant>, | ||||||
|  |     db_data_file_change_last: Option<Instant>, | ||||||
|  |     pub queue: Queue, | ||||||
|  |     pub update_endpoints: Vec<UpdateEndpoint>, | ||||||
|  |     pub playing: bool, | ||||||
|  |     pub command_sender: Option<mpsc::Sender<Command>>, | ||||||
|  | } | ||||||
|  | pub enum UpdateEndpoint { | ||||||
|  |     Bytes(Box<dyn Write + Sync + Send>), | ||||||
|  |     CmdChannel(mpsc::Sender<Arc<Command>>), | ||||||
|  |     CmdChannelTokio(tokio::sync::mpsc::UnboundedSender<Arc<Command>>), | ||||||
|  |     Custom(Box<dyn FnMut(&Command) + Send>), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Database { | ||||||
|  |     fn panic(&self, msg: &str) -> ! { | ||||||
|  |         // custom panic handler
 | ||||||
|  |         // make a backup
 | ||||||
|  |         // exit
 | ||||||
|  |         panic!("DatabasePanic: {msg}"); | ||||||
|  |     } | ||||||
|  |     pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf { | ||||||
|  |         self.lib_directory.join(&location.rel_path) | ||||||
|  |     } | ||||||
|  |     pub fn get_song(&self, song: &SongId) -> Option<&Song> { | ||||||
|  |         self.songs.get(song) | ||||||
|  |     } | ||||||
|  |     pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> { | ||||||
|  |         self.songs.get_mut(song) | ||||||
|  |     } | ||||||
|  |     /// adds a song to the database.
 | ||||||
|  |     /// ignores song.id and just assigns a new id, which it then returns.
 | ||||||
|  |     /// this function also adds a reference to the new song to the album (or artist.singles, if no album)
 | ||||||
|  |     pub fn add_song_new(&mut self, song: Song) -> SongId { | ||||||
|  |         let album = song.album.clone(); | ||||||
|  |         let artist = song.artist.clone(); | ||||||
|  |         let id = self.add_song_new_nomagic(song); | ||||||
|  |         if let Some(Some(album)) = album.map(|v| self.albums.get_mut(&v)) { | ||||||
|  |             album.songs.push(id); | ||||||
|  |         } else { | ||||||
|  |             if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) { | ||||||
|  |                 artist.singles.push(id); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         id | ||||||
|  |     } | ||||||
|  |     pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId { | ||||||
|  |         for key in 0.. { | ||||||
|  |             if !self.songs.contains_key(&key) { | ||||||
|  |                 song.id = key; | ||||||
|  |                 self.songs.insert(key, song); | ||||||
|  |                 return key; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         self.panic("database.songs all keys used - no more capacity for new songs!"); | ||||||
|  |     } | ||||||
|  |     /// adds an artist to the database.
 | ||||||
|  |     /// ignores artist.id and just assigns a new id, which it then returns.
 | ||||||
|  |     /// this function does nothing special.
 | ||||||
|  |     pub fn add_artist_new(&mut self, artist: Artist) -> ArtistId { | ||||||
|  |         let id = self.add_artist_new_nomagic(artist); | ||||||
|  |         id | ||||||
|  |     } | ||||||
|  |     fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId { | ||||||
|  |         for key in 0.. { | ||||||
|  |             if !self.artists.contains_key(&key) { | ||||||
|  |                 artist.id = key; | ||||||
|  |                 self.artists.insert(key, artist); | ||||||
|  |                 return key; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         self.panic("database.artists all keys used - no more capacity for new artists!"); | ||||||
|  |     } | ||||||
|  |     /// adds an album to the database.
 | ||||||
|  |     /// ignores album.id and just assigns a new id, which it then returns.
 | ||||||
|  |     /// this function also adds a reference to the new album to the artist
 | ||||||
|  |     pub fn add_album_new(&mut self, album: Album) -> AlbumId { | ||||||
|  |         let artist = album.artist.clone(); | ||||||
|  |         let id = self.add_album_new_nomagic(album); | ||||||
|  |         if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) { | ||||||
|  |             artist.albums.push(id); | ||||||
|  |         } | ||||||
|  |         id | ||||||
|  |     } | ||||||
|  |     fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId { | ||||||
|  |         for key in 0.. { | ||||||
|  |             if !self.albums.contains_key(&key) { | ||||||
|  |                 album.id = key; | ||||||
|  |                 self.albums.insert(key, album); | ||||||
|  |                 return key; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         self.panic("database.artists all keys used - no more capacity for new artists!"); | ||||||
|  |     } | ||||||
|  |     /// updates an existing song in the database with the new value.
 | ||||||
|  |     /// uses song.id to find the correct song.
 | ||||||
|  |     /// if the id doesn't exist in the db, Err(()) is returned.
 | ||||||
|  |     /// Otherwise Some(old_data) is returned.
 | ||||||
|  |     pub fn update_song(&mut self, song: Song) -> Result<Song, ()> { | ||||||
|  |         if let Some(prev_song) = self.songs.get_mut(&song.id) { | ||||||
|  |             Ok(std::mem::replace(prev_song, song)) | ||||||
|  |         } else { | ||||||
|  |             Err(()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn update_album(&mut self, album: Album) -> Result<Album, ()> { | ||||||
|  |         if let Some(prev_album) = self.albums.get_mut(&album.id) { | ||||||
|  |             Ok(std::mem::replace(prev_album, album)) | ||||||
|  |         } else { | ||||||
|  |             Err(()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> { | ||||||
|  |         if let Some(prev_artist) = self.artists.get_mut(&artist.id) { | ||||||
|  |             Ok(std::mem::replace(prev_artist, artist)) | ||||||
|  |         } else { | ||||||
|  |             Err(()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     /// [NOT RECOMMENDED - use add_song_new or update_song instead!] inserts the song into the database.
 | ||||||
|  |     /// uses song.id. If another song with that ID exists, it is replaced and Some(other_song) is returned.
 | ||||||
|  |     /// If no other song exists, the song will be added to the database with the given ID and None is returned.
 | ||||||
|  |     pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> { | ||||||
|  |         self.songs.insert(song.id, song) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> { | ||||||
|  |         // TODO! this is slow because it clones everything - there has to be a better way...
 | ||||||
|  |         Command::SyncDatabase( | ||||||
|  |             self.artists().iter().map(|v| v.1.clone()).collect(), | ||||||
|  |             self.albums().iter().map(|v| v.1.clone()).collect(), | ||||||
|  |             self.songs().iter().map(|v| v.1.clone()).collect(), | ||||||
|  |         ) | ||||||
|  |         .to_bytes(con)?; | ||||||
|  |         Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?; | ||||||
|  |         if self.playing { | ||||||
|  |             Command::Resume.to_bytes(con)?; | ||||||
|  |         } | ||||||
|  |         // since this is so easy to check for, it comes last.
 | ||||||
|  |         // this allows clients to find out when init_connection is done.
 | ||||||
|  |         Command::SetLibraryDirectory(self.lib_directory.clone()).to_bytes(con)?; | ||||||
|  |         // is initialized now - client can receive updates after this point.
 | ||||||
|  |         // NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
 | ||||||
|  |         // we just need to handle commands (receive from the connection).
 | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn apply_command(&mut self, command: Command) { | ||||||
|  |         // since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
 | ||||||
|  |         self.broadcast_update(&command); | ||||||
|  |         match command { | ||||||
|  |             Command::Resume => self.playing = true, | ||||||
|  |             Command::Pause => self.playing = false, | ||||||
|  |             Command::Stop => self.playing = false, | ||||||
|  |             Command::NextSong => { | ||||||
|  |                 self.queue.advance_index(); | ||||||
|  |             } | ||||||
|  |             Command::Save => { | ||||||
|  |                 if let Err(e) = self.save_database(None) { | ||||||
|  |                     eprintln!("Couldn't save: {e}"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Command::SyncDatabase(a, b, c) => self.sync(a, b, c), | ||||||
|  |             Command::QueueUpdate(index, new_data) => { | ||||||
|  |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|  |                     *v = new_data; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Command::QueueAdd(mut index, new_data) => { | ||||||
|  |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|  |                     v.add_to_end(new_data); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Command::QueueInsert(mut index, pos, new_data) => { | ||||||
|  |                 if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) { | ||||||
|  |                     v.insert(new_data, pos); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Command::QueueRemove(index) => { | ||||||
|  |                 self.queue.remove_by_index(&index, 0); | ||||||
|  |             } | ||||||
|  |             Command::QueueGoto(index) => self.queue.set_index(&index, 0), | ||||||
|  |             Command::AddSong(song) => { | ||||||
|  |                 self.add_song_new(song); | ||||||
|  |             } | ||||||
|  |             Command::AddAlbum(album) => { | ||||||
|  |                 self.add_album_new(album); | ||||||
|  |             } | ||||||
|  |             Command::AddArtist(artist) => { | ||||||
|  |                 self.add_artist_new(artist); | ||||||
|  |             } | ||||||
|  |             Command::ModifySong(song) => { | ||||||
|  |                 _ = self.update_song(song); | ||||||
|  |             } | ||||||
|  |             Command::ModifyAlbum(album) => { | ||||||
|  |                 _ = self.update_album(album); | ||||||
|  |             } | ||||||
|  |             Command::ModifyArtist(artist) => { | ||||||
|  |                 _ = self.update_artist(artist); | ||||||
|  |             } | ||||||
|  |             Command::SetLibraryDirectory(new_dir) => { | ||||||
|  |                 self.lib_directory = new_dir; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // file saving/loading
 | ||||||
|  | 
 | ||||||
|  | impl Database { | ||||||
|  |     /// Database is also used for clients, to keep things consistent.
 | ||||||
|  |     /// A client database doesn't need any storage paths and won't perform autosaves.
 | ||||||
|  |     pub fn new_clientside() -> Self { | ||||||
|  |         Self { | ||||||
|  |             db_file: PathBuf::new(), | ||||||
|  |             lib_directory: PathBuf::new(), | ||||||
|  |             artists: HashMap::new(), | ||||||
|  |             albums: HashMap::new(), | ||||||
|  |             songs: HashMap::new(), | ||||||
|  |             covers: HashMap::new(), | ||||||
|  |             db_data_file_change_first: None, | ||||||
|  |             db_data_file_change_last: None, | ||||||
|  |             queue: QueueContent::Folder(0, vec![], String::new()).into(), | ||||||
|  |             update_endpoints: vec![], | ||||||
|  |             playing: false, | ||||||
|  |             command_sender: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self { | ||||||
|  |         Self { | ||||||
|  |             db_file: path, | ||||||
|  |             lib_directory: lib_dir, | ||||||
|  |             artists: HashMap::new(), | ||||||
|  |             albums: HashMap::new(), | ||||||
|  |             songs: HashMap::new(), | ||||||
|  |             covers: HashMap::new(), | ||||||
|  |             db_data_file_change_first: None, | ||||||
|  |             db_data_file_change_last: None, | ||||||
|  |             queue: QueueContent::Folder(0, vec![], String::new()).into(), | ||||||
|  |             update_endpoints: vec![], | ||||||
|  |             playing: false, | ||||||
|  |             command_sender: None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> { | ||||||
|  |         let mut file = BufReader::new(File::open(&path)?); | ||||||
|  |         eprintln!("[info] loading library from {file:?}"); | ||||||
|  |         let lib_directory = ToFromBytes::from_bytes(&mut file)?; | ||||||
|  |         eprintln!("[info] library directory is {lib_directory:?}"); | ||||||
|  |         Ok(Self { | ||||||
|  |             db_file: path, | ||||||
|  |             lib_directory, | ||||||
|  |             artists: ToFromBytes::from_bytes(&mut file)?, | ||||||
|  |             albums: ToFromBytes::from_bytes(&mut file)?, | ||||||
|  |             songs: ToFromBytes::from_bytes(&mut file)?, | ||||||
|  |             covers: ToFromBytes::from_bytes(&mut file)?, | ||||||
|  |             db_data_file_change_first: None, | ||||||
|  |             db_data_file_change_last: None, | ||||||
|  |             queue: QueueContent::Folder(0, vec![], String::new()).into(), | ||||||
|  |             update_endpoints: vec![], | ||||||
|  |             playing: false, | ||||||
|  |             command_sender: None, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |     pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> { | ||||||
|  |         let path = if let Some(p) = path { | ||||||
|  |             p | ||||||
|  |         } else { | ||||||
|  |             self.db_file.clone() | ||||||
|  |         }; | ||||||
|  |         // if no path is set (client mode), do nothing
 | ||||||
|  |         if path.as_os_str().is_empty() { | ||||||
|  |             return Ok(path); | ||||||
|  |         } | ||||||
|  |         eprintln!("[info] saving db to {path:?}."); | ||||||
|  |         let mut file = fs::OpenOptions::new() | ||||||
|  |             .write(true) | ||||||
|  |             .truncate(true) | ||||||
|  |             .create(true) | ||||||
|  |             .open(&path)?; | ||||||
|  |         self.lib_directory.to_bytes(&mut file)?; | ||||||
|  |         self.artists.to_bytes(&mut file)?; | ||||||
|  |         self.albums.to_bytes(&mut file)?; | ||||||
|  |         self.songs.to_bytes(&mut file)?; | ||||||
|  |         self.covers.to_bytes(&mut file)?; | ||||||
|  |         Ok(path) | ||||||
|  |     } | ||||||
|  |     pub fn broadcast_update(&mut self, update: &Command) { | ||||||
|  |         let mut remove = vec![]; | ||||||
|  |         let mut bytes = None; | ||||||
|  |         let mut arc = None; | ||||||
|  |         for (i, udep) in self.update_endpoints.iter_mut().enumerate() { | ||||||
|  |             match udep { | ||||||
|  |                 UpdateEndpoint::Bytes(writer) => { | ||||||
|  |                     if bytes.is_none() { | ||||||
|  |                         bytes = Some(update.to_bytes_vec()); | ||||||
|  |                     } | ||||||
|  |                     if writer.write_all(bytes.as_ref().unwrap()).is_err() { | ||||||
|  |                         remove.push(i); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 UpdateEndpoint::CmdChannel(sender) => { | ||||||
|  |                     if arc.is_none() { | ||||||
|  |                         arc = Some(Arc::new(update.clone())); | ||||||
|  |                     } | ||||||
|  |                     if sender.send(arc.clone().unwrap()).is_err() { | ||||||
|  |                         remove.push(i); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 UpdateEndpoint::CmdChannelTokio(sender) => { | ||||||
|  |                     if arc.is_none() { | ||||||
|  |                         arc = Some(Arc::new(update.clone())); | ||||||
|  |                     } | ||||||
|  |                     if sender.send(arc.clone().unwrap()).is_err() { | ||||||
|  |                         remove.push(i); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 UpdateEndpoint::Custom(func) => func(update), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if !remove.is_empty() { | ||||||
|  |             eprintln!( | ||||||
|  |                 "[info] closing {} connections, {} are still active", | ||||||
|  |                 remove.len(), | ||||||
|  |                 self.update_endpoints.len() - remove.len() | ||||||
|  |             ); | ||||||
|  |             for i in remove.into_iter().rev() { | ||||||
|  |                 self.update_endpoints.remove(i); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) { | ||||||
|  |         self.artists = artists.iter().map(|v| (v.id, v.clone())).collect(); | ||||||
|  |         self.albums = albums.iter().map(|v| (v.id, v.clone())).collect(); | ||||||
|  |         self.songs = songs.iter().map(|v| (v.id, v.clone())).collect(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Database { | ||||||
|  |     pub fn songs(&self) -> &HashMap<SongId, Song> { | ||||||
|  |         &self.songs | ||||||
|  |     } | ||||||
|  |     pub fn albums(&self) -> &HashMap<AlbumId, Album> { | ||||||
|  |         &self.albums | ||||||
|  |     } | ||||||
|  |     pub fn artists(&self) -> &HashMap<ArtistId, Artist> { | ||||||
|  |         &self.artists | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								musicdb-lib/src/data/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										73
									
								
								musicdb-lib/src/data/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | use std::{ | ||||||
|  |     io::{Read, Write}, | ||||||
|  |     path::PathBuf, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | pub mod album; | ||||||
|  | pub mod artist; | ||||||
|  | pub mod database; | ||||||
|  | pub mod queue; | ||||||
|  | pub mod song; | ||||||
|  | 
 | ||||||
|  | pub type SongId = u64; | ||||||
|  | pub type AlbumId = u64; | ||||||
|  | pub type ArtistId = u64; | ||||||
|  | pub type CoverId = u64; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Default, Debug)] | ||||||
|  | pub struct GeneralData { | ||||||
|  |     pub tags: Vec<String>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct DatabaseLocation { | ||||||
|  |     pub rel_path: PathBuf, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for DatabaseLocation { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.rel_path.to_bytes(s) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(Self { | ||||||
|  |             rel_path: ToFromBytes::from_bytes(s)?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<P> From<P> for DatabaseLocation | ||||||
|  | where | ||||||
|  |     P: Into<PathBuf>, | ||||||
|  | { | ||||||
|  |     fn from(value: P) -> Self { | ||||||
|  |         Self { | ||||||
|  |             rel_path: value.into(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for GeneralData { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.tags.to_bytes(s)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(Self { | ||||||
|  |             tags: ToFromBytes::from_bytes(s)?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										286
									
								
								musicdb-lib/src/data/queue.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										286
									
								
								musicdb-lib/src/data/queue.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,286 @@ | |||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | use super::SongId; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct Queue { | ||||||
|  |     enabled: bool, | ||||||
|  |     content: QueueContent, | ||||||
|  | } | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub enum QueueContent { | ||||||
|  |     Song(SongId), | ||||||
|  |     Folder(usize, Vec<Queue>, String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Queue { | ||||||
|  |     pub fn enabled(&self) -> bool { | ||||||
|  |         self.enabled | ||||||
|  |     } | ||||||
|  |     pub fn content(&self) -> &QueueContent { | ||||||
|  |         &self.content | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn add_to_end(&mut self, v: Self) -> bool { | ||||||
|  |         match &mut self.content { | ||||||
|  |             QueueContent::Song(_) => false, | ||||||
|  |             QueueContent::Folder(_, vec, _) => { | ||||||
|  |                 vec.push(v); | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn insert(&mut self, v: Self, index: usize) -> bool { | ||||||
|  |         match &mut self.content { | ||||||
|  |             QueueContent::Song(_) => false, | ||||||
|  |             QueueContent::Folder(_, vec, _) => { | ||||||
|  |                 if index <= vec.len() { | ||||||
|  |                     vec.insert(index, v); | ||||||
|  |                     true | ||||||
|  |                 } else { | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn len(&self) -> usize { | ||||||
|  |         if !self.enabled { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |         match &self.content { | ||||||
|  |             QueueContent::Song(_) => 1, | ||||||
|  |             QueueContent::Folder(_, v, _) => v.iter().map(|v| v.len()).sum(), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// recursively descends the queue until the current active element is found, then returns it.
 | ||||||
|  |     pub fn get_current(&self) -> Option<&Self> { | ||||||
|  |         match &self.content { | ||||||
|  |             QueueContent::Folder(i, v, _) => { | ||||||
|  |                 let i = *i; | ||||||
|  |                 if let Some(v) = v.get(i) { | ||||||
|  |                     v.get_current() | ||||||
|  |                 } else { | ||||||
|  |                     None | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             QueueContent::Song(_) => Some(self), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn get_current_song(&self) -> Option<&SongId> { | ||||||
|  |         if let QueueContent::Song(id) = self.get_current()?.content() { | ||||||
|  |             Some(id) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn get_next_song(&self) -> Option<&SongId> { | ||||||
|  |         if let QueueContent::Song(id) = self.get_next()?.content() { | ||||||
|  |             Some(id) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn get_next(&self) -> Option<&Self> { | ||||||
|  |         match &self.content { | ||||||
|  |             QueueContent::Folder(i, vec, _) => { | ||||||
|  |                 let i = *i; | ||||||
|  |                 if let Some(v) = vec.get(i) { | ||||||
|  |                     if let Some(v) = v.get_next() { | ||||||
|  |                         Some(v) | ||||||
|  |                     } else { | ||||||
|  |                         if let Some(v) = vec.get(i + 1) { | ||||||
|  |                             v.get_current() | ||||||
|  |                         } else { | ||||||
|  |                             None | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     None | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             QueueContent::Song(_) => None, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn advance_index(&mut self) -> bool { | ||||||
|  |         match &mut self.content { | ||||||
|  |             QueueContent::Song(_) => false, | ||||||
|  |             QueueContent::Folder(index, contents, _) => { | ||||||
|  |                 if let Some(c) = contents.get_mut(*index) { | ||||||
|  |                     // inner value could advance index, do nothing.
 | ||||||
|  |                     if c.advance_index() { | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         loop { | ||||||
|  |                             if *index + 1 < contents.len() { | ||||||
|  |                                 // can advance
 | ||||||
|  |                                 *index += 1; | ||||||
|  |                                 if contents[*index].enabled { | ||||||
|  |                                     break true; | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 // can't advance: index would be out of bounds
 | ||||||
|  |                                 *index = 0; | ||||||
|  |                                 break false; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     *index = 0; | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn set_index(&mut self, index: &Vec<usize>, depth: usize) { | ||||||
|  |         let i = index.get(depth).map(|v| *v).unwrap_or(0); | ||||||
|  |         match &mut self.content { | ||||||
|  |             QueueContent::Song(_) => {} | ||||||
|  |             QueueContent::Folder(idx, contents, _) => { | ||||||
|  |                 *idx = i; | ||||||
|  |                 for (i2, c) in contents.iter_mut().enumerate() { | ||||||
|  |                     if i2 != i { | ||||||
|  |                         c.set_index(&vec![], 0) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 if let Some(c) = contents.get_mut(i) { | ||||||
|  |                     c.set_index(index, depth + 1); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_item_at_index(&self, index: &Vec<usize>, depth: usize) -> Option<&Self> { | ||||||
|  |         if let Some(i) = index.get(depth) { | ||||||
|  |             match &self.content { | ||||||
|  |                 QueueContent::Song(_) => None, | ||||||
|  |                 QueueContent::Folder(_, v, _) => { | ||||||
|  |                     if let Some(v) = v.get(*i) { | ||||||
|  |                         v.get_item_at_index(index, depth + 1) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Some(self) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn get_item_at_index_mut(&mut self, index: &Vec<usize>, depth: usize) -> Option<&mut Self> { | ||||||
|  |         if let Some(i) = index.get(depth) { | ||||||
|  |             match &mut self.content { | ||||||
|  |                 QueueContent::Song(_) => None, | ||||||
|  |                 QueueContent::Folder(_, v, _) => { | ||||||
|  |                     if let Some(v) = v.get_mut(*i) { | ||||||
|  |                         v.get_item_at_index_mut(index, depth + 1) | ||||||
|  |                     } else { | ||||||
|  |                         None | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             Some(self) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn remove_by_index(&mut self, index: &Vec<usize>, depth: usize) -> Option<Self> { | ||||||
|  |         if let Some(i) = index.get(depth) { | ||||||
|  |             match &mut self.content { | ||||||
|  |                 QueueContent::Song(_) => None, | ||||||
|  |                 QueueContent::Folder(ci, v, _) => { | ||||||
|  |                     if depth + 1 < index.len() { | ||||||
|  |                         if let Some(v) = v.get_mut(*i) { | ||||||
|  |                             v.remove_by_index(index, depth + 1) | ||||||
|  |                         } else { | ||||||
|  |                             None | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         if *i < v.len() { | ||||||
|  |                             // if current playback is past this point,
 | ||||||
|  |                             // reduce the index by 1 so that it still points to the same element
 | ||||||
|  |                             if *ci > *i { | ||||||
|  |                                 *ci -= 1; | ||||||
|  |                             } | ||||||
|  |                             Some(v.remove(*i)) | ||||||
|  |                         } else { | ||||||
|  |                             None | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl From<QueueContent> for Queue { | ||||||
|  |     fn from(value: QueueContent) -> Self { | ||||||
|  |         Self { | ||||||
|  |             enabled: true, | ||||||
|  |             content: value, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for Queue { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: std::io::Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&[if self.enabled { 0b11111111 } else { 0b00000000 }])?; | ||||||
|  |         self.content.to_bytes(s)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: std::io::Read, | ||||||
|  |     { | ||||||
|  |         let mut enabled = [0]; | ||||||
|  |         s.read_exact(&mut enabled)?; | ||||||
|  |         Ok(Self { | ||||||
|  |             enabled: enabled[0].count_ones() >= 4, | ||||||
|  |             content: ToFromBytes::from_bytes(s)?, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for QueueContent { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: std::io::Write, | ||||||
|  |     { | ||||||
|  |         match self { | ||||||
|  |             Self::Song(id) => { | ||||||
|  |                 s.write_all(&[0b11111111])?; | ||||||
|  |                 id.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::Folder(index, contents, name) => { | ||||||
|  |                 s.write_all(&[0b00000000])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |                 contents.to_bytes(s)?; | ||||||
|  |                 name.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: std::io::Read, | ||||||
|  |     { | ||||||
|  |         let mut switch_on = [0]; | ||||||
|  |         s.read_exact(&mut switch_on)?; | ||||||
|  |         Ok(if switch_on[0].count_ones() > 4 { | ||||||
|  |             Self::Song(ToFromBytes::from_bytes(s)?) | ||||||
|  |         } else { | ||||||
|  |             Self::Folder( | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |             ) | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										166
									
								
								musicdb-lib/src/data/song.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										166
									
								
								musicdb-lib/src/data/song.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,166 @@ | |||||||
|  | use std::{ | ||||||
|  |     fmt::Display, | ||||||
|  |     io::{Read, Write}, | ||||||
|  |     path::Path, | ||||||
|  |     sync::{Arc, Mutex}, | ||||||
|  |     thread::JoinHandle, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | use super::{ | ||||||
|  |     database::Database, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub struct Song { | ||||||
|  |     pub id: SongId, | ||||||
|  |     pub location: DatabaseLocation, | ||||||
|  |     pub title: String, | ||||||
|  |     pub album: Option<AlbumId>, | ||||||
|  |     pub artist: Option<ArtistId>, | ||||||
|  |     pub more_artists: Vec<ArtistId>, | ||||||
|  |     pub cover: Option<CoverId>, | ||||||
|  |     pub general: GeneralData, | ||||||
|  |     /// None => No cached data
 | ||||||
|  |     /// Some(Err) => No cached data yet, but a thread is working on loading it.
 | ||||||
|  |     /// Some(Ok(data)) => Cached data is available.
 | ||||||
|  |     pub cached_data: Arc<Mutex<Option<Result<Arc<Vec<u8>>, JoinHandle<Option<Arc<Vec<u8>>>>>>>>, | ||||||
|  | } | ||||||
|  | impl Song { | ||||||
|  |     pub fn new( | ||||||
|  |         location: DatabaseLocation, | ||||||
|  |         title: String, | ||||||
|  |         album: Option<AlbumId>, | ||||||
|  |         artist: Option<ArtistId>, | ||||||
|  |         more_artists: Vec<ArtistId>, | ||||||
|  |         cover: Option<CoverId>, | ||||||
|  |     ) -> Self { | ||||||
|  |         Self { | ||||||
|  |             id: 0, | ||||||
|  |             location, | ||||||
|  |             title, | ||||||
|  |             album, | ||||||
|  |             artist, | ||||||
|  |             more_artists, | ||||||
|  |             cover, | ||||||
|  |             general: GeneralData::default(), | ||||||
|  |             cached_data: Arc::new(Mutex::new(None)), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn uncache_data(&self) { | ||||||
|  |         *self.cached_data.lock().unwrap() = None; | ||||||
|  |     } | ||||||
|  |     /// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
 | ||||||
|  |     pub fn cache_data_start_thread(&self, db: &Database) -> bool { | ||||||
|  |         let mut cd = self.cached_data.lock().unwrap(); | ||||||
|  |         let start_thread = match cd.as_ref() { | ||||||
|  |             None => true, | ||||||
|  |             Some(Err(_)) | Some(Ok(_)) => false, | ||||||
|  |         }; | ||||||
|  |         if start_thread { | ||||||
|  |             let path = db.get_path(&self.location); | ||||||
|  |             *cd = Some(Err(std::thread::spawn(move || { | ||||||
|  |                 eprintln!("[info] thread started"); | ||||||
|  |                 let data = Self::load_data(&path)?; | ||||||
|  |                 eprintln!("[info] thread stopping after loading {path:?}"); | ||||||
|  |                 Some(Arc::new(data)) | ||||||
|  |             }))); | ||||||
|  |             true | ||||||
|  |         } else { | ||||||
|  |             false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     /// Gets the cached data, if available.
 | ||||||
|  |     /// If a thread is running to load the data, it is not awaited.
 | ||||||
|  |     /// This function doesn't block.
 | ||||||
|  |     pub fn cached_data(&self) -> Option<Arc<Vec<u8>>> { | ||||||
|  |         if let Some(Ok(v)) = self.cached_data.lock().unwrap().as_ref() { | ||||||
|  |             Some(Arc::clone(v)) | ||||||
|  |         } else { | ||||||
|  |             None | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     /// Gets the cached data, if available.
 | ||||||
|  |     /// If a thread is running to load the data, it *is* awaited.
 | ||||||
|  |     /// This function will block until the data is loaded.
 | ||||||
|  |     /// If it still returns none, some error must have occured.
 | ||||||
|  |     pub fn cached_data_now(&self, db: &Database) -> Option<Arc<Vec<u8>>> { | ||||||
|  |         let mut cd = self.cached_data.lock().unwrap(); | ||||||
|  |         *cd = match cd.take() { | ||||||
|  |             None => { | ||||||
|  |                 if let Some(v) = Self::load_data(db.get_path(&self.location)) { | ||||||
|  |                     Some(Ok(Arc::new(v))) | ||||||
|  |                 } else { | ||||||
|  |                     None | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Some(Err(t)) => match t.join() { | ||||||
|  |                 Err(_e) => None, | ||||||
|  |                 Ok(Some(v)) => Some(Ok(v)), | ||||||
|  |                 Ok(None) => None, | ||||||
|  |             }, | ||||||
|  |             Some(Ok(v)) => Some(Ok(v)), | ||||||
|  |         }; | ||||||
|  |         drop(cd); | ||||||
|  |         self.cached_data() | ||||||
|  |     } | ||||||
|  |     fn load_data<P: AsRef<Path>>(path: P) -> Option<Vec<u8>> { | ||||||
|  |         eprintln!("[info] loading song from {:?}", path.as_ref()); | ||||||
|  |         match std::fs::read(&path) { | ||||||
|  |             Ok(v) => { | ||||||
|  |                 eprintln!("[info] loaded song from {:?}", path.as_ref()); | ||||||
|  |                 Some(v) | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 eprintln!("[info] error loading {:?}: {e:?}", path.as_ref()); | ||||||
|  |                 None | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl Display for Song { | ||||||
|  |     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||||
|  |         write!(f, "{}", self.title)?; | ||||||
|  |         match (self.artist, self.album) { | ||||||
|  |             (Some(artist), Some(album)) => write!(f, " (by {artist} on {album})")?, | ||||||
|  |             (None, Some(album)) => write!(f, " (on {album})")?, | ||||||
|  |             (Some(artist), None) => write!(f, " (by {artist})")?, | ||||||
|  |             (None, None) => {} | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for Song { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.id.to_bytes(s)?; | ||||||
|  |         self.location.to_bytes(s)?; | ||||||
|  |         self.title.to_bytes(s)?; | ||||||
|  |         self.album.to_bytes(s)?; | ||||||
|  |         self.artist.to_bytes(s)?; | ||||||
|  |         self.more_artists.to_bytes(s)?; | ||||||
|  |         self.cover.to_bytes(s)?; | ||||||
|  |         self.general.to_bytes(s)?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(Self { | ||||||
|  |             id: ToFromBytes::from_bytes(s)?, | ||||||
|  |             location: ToFromBytes::from_bytes(s)?, | ||||||
|  |             title: ToFromBytes::from_bytes(s)?, | ||||||
|  |             album: ToFromBytes::from_bytes(s)?, | ||||||
|  |             artist: ToFromBytes::from_bytes(s)?, | ||||||
|  |             more_artists: ToFromBytes::from_bytes(s)?, | ||||||
|  |             cover: ToFromBytes::from_bytes(s)?, | ||||||
|  |             general: ToFromBytes::from_bytes(s)?, | ||||||
|  |             cached_data: Arc::new(Mutex::new(None)), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								musicdb-lib/src/lib.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								musicdb-lib/src/lib.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | pub mod data; | ||||||
|  | pub mod load; | ||||||
|  | pub mod player; | ||||||
|  | pub mod server; | ||||||
							
								
								
									
										330
									
								
								musicdb-lib/src/load/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										330
									
								
								musicdb-lib/src/load/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,330 @@ | |||||||
|  | use std::{ | ||||||
|  |     collections::HashMap, | ||||||
|  |     io::{Read, Write}, | ||||||
|  |     path::PathBuf, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | pub trait ToFromBytes: Sized { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write; | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read; | ||||||
|  |     fn to_bytes_vec(&self) -> Vec<u8> { | ||||||
|  |         let mut b = Vec::new(); | ||||||
|  |         _ = self.to_bytes(&mut b); | ||||||
|  |         b | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // impl ToFromBytes
 | ||||||
|  | 
 | ||||||
|  | // common types (String, Vec, ...)
 | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for String { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.len().to_bytes(s)?; | ||||||
|  |         s.write_all(self.as_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let len = ToFromBytes::from_bytes(s)?; | ||||||
|  |         let mut buf = vec![0; len]; | ||||||
|  |         s.read_exact(&mut buf)?; | ||||||
|  |         Ok(String::from_utf8_lossy(&buf).into_owned()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for PathBuf { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.to_string_lossy().into_owned().to_bytes(s) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(String::from_bytes(s)?.into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<C> ToFromBytes for Vec<C> | ||||||
|  | where | ||||||
|  |     C: ToFromBytes, | ||||||
|  | { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.len().to_bytes(s)?; | ||||||
|  |         for elem in self { | ||||||
|  |             elem.to_bytes(s)?; | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let len = ToFromBytes::from_bytes(s)?; | ||||||
|  |         let mut buf = Vec::with_capacity(len); | ||||||
|  |         for _ in 0..len { | ||||||
|  |             buf.push(ToFromBytes::from_bytes(s)?); | ||||||
|  |         } | ||||||
|  |         Ok(buf) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl<A> ToFromBytes for Option<A> | ||||||
|  | where | ||||||
|  |     A: ToFromBytes, | ||||||
|  | { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         match self { | ||||||
|  |             None => s.write_all(&[0b11001100]), | ||||||
|  |             Some(v) => { | ||||||
|  |                 s.write_all(&[0b00111010])?; | ||||||
|  |                 v.to_bytes(s) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0u8]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         match b[0] { | ||||||
|  |             0b00111010 => Ok(Some(ToFromBytes::from_bytes(s)?)), | ||||||
|  |             _ => Ok(None), | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl<K, V> ToFromBytes for HashMap<K, V> | ||||||
|  | where | ||||||
|  |     K: ToFromBytes + std::cmp::Eq + std::hash::Hash, | ||||||
|  |     V: ToFromBytes, | ||||||
|  | { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         self.len().to_bytes(s)?; | ||||||
|  |         for (key, val) in self.iter() { | ||||||
|  |             key.to_bytes(s)?; | ||||||
|  |             val.to_bytes(s)?; | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let len = ToFromBytes::from_bytes(s)?; | ||||||
|  |         let mut o = Self::with_capacity(len); | ||||||
|  |         for _ in 0..len { | ||||||
|  |             o.insert(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?); | ||||||
|  |         } | ||||||
|  |         Ok(o) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // - for (i/u)(size/8/16/32/64/128)
 | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for usize { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         (*self as u64).to_bytes(s) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(u64::from_bytes(s)? as _) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for isize { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         (*self as i64).to_bytes(s) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         Ok(i64::from_bytes(s)? as _) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for u8 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&[*self]) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 1]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(b[0]) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for i8 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 1]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for u16 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 2]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for i16 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 2]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for u32 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 4]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for i32 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 4]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for u64 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 8]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for i64 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 8]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for u128 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 16]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | impl ToFromBytes for i128 { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         s.write_all(&self.to_be_bytes()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Read, | ||||||
|  |     { | ||||||
|  |         let mut b = [0; 16]; | ||||||
|  |         s.read_exact(&mut b)?; | ||||||
|  |         Ok(Self::from_be_bytes(b)) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										160
									
								
								musicdb-lib/src/player/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										160
									
								
								musicdb-lib/src/player/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,160 @@ | |||||||
|  | use std::sync::Arc; | ||||||
|  | 
 | ||||||
|  | use awedio::{ | ||||||
|  |     backends::CpalBackend, | ||||||
|  |     manager::Manager, | ||||||
|  |     sounds::wrappers::{AsyncCompletionNotifier, Controller, Pausable}, | ||||||
|  |     Sound, | ||||||
|  | }; | ||||||
|  | use rc_u8_reader::ArcU8Reader; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     data::{database::Database, SongId}, | ||||||
|  |     server::Command, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | pub struct Player { | ||||||
|  |     /// can be unused, but must be present otherwise audio playback breaks
 | ||||||
|  |     #[allow(unused)] | ||||||
|  |     backend: CpalBackend, | ||||||
|  |     source: Option<( | ||||||
|  |         Controller<AsyncCompletionNotifier<Pausable<Box<dyn Sound>>>>, | ||||||
|  |         tokio::sync::oneshot::Receiver<()>, | ||||||
|  |     )>, | ||||||
|  |     manager: Manager, | ||||||
|  |     current_song_id: SongOpt, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub enum SongOpt { | ||||||
|  |     None, | ||||||
|  |     Some(SongId), | ||||||
|  |     /// Will be set to Some or None once handeled
 | ||||||
|  |     New(Option<SongId>), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Player { | ||||||
|  |     pub fn new() -> Result<Self, Box<dyn std::error::Error>> { | ||||||
|  |         let (manager, backend) = awedio::start()?; | ||||||
|  |         Ok(Self { | ||||||
|  |             manager, | ||||||
|  |             backend, | ||||||
|  |             source: None, | ||||||
|  |             current_song_id: SongOpt::None, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |     pub fn handle_command(&mut self, command: &Command) { | ||||||
|  |         match command { | ||||||
|  |             Command::Resume => self.resume(), | ||||||
|  |             Command::Pause => self.pause(), | ||||||
|  |             Command::Stop => self.stop(), | ||||||
|  |             _ => {} | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn pause(&mut self) { | ||||||
|  |         if let Some((source, _notif)) = &mut self.source { | ||||||
|  |             source.set_paused(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn resume(&mut self) { | ||||||
|  |         if let Some((source, _notif)) = &mut self.source { | ||||||
|  |             source.set_paused(false); | ||||||
|  |         } else if let SongOpt::Some(id) = &self.current_song_id { | ||||||
|  |             // there is no source to resume playback on, but there is a current song
 | ||||||
|  |             self.current_song_id = SongOpt::New(Some(*id)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn stop(&mut self) { | ||||||
|  |         if let Some((source, _notif)) = &mut self.source { | ||||||
|  |             source.set_paused(true); | ||||||
|  |         } | ||||||
|  |         self.current_song_id = SongOpt::New(None); | ||||||
|  |     } | ||||||
|  |     pub fn update(&mut self, db: &mut Database) { | ||||||
|  |         if db.playing && self.source.is_none() { | ||||||
|  |             if let Some(song) = db.queue.get_current_song() { | ||||||
|  |                 // db playing, but no source - initialize a source (via SongOpt::New)
 | ||||||
|  |                 self.current_song_id = SongOpt::New(Some(*song)); | ||||||
|  |             } else { | ||||||
|  |                 // db.playing, but no song in queue...
 | ||||||
|  |             } | ||||||
|  |         } else if let Some((_source, notif)) = &mut self.source { | ||||||
|  |             if let Ok(()) = notif.try_recv() { | ||||||
|  |                 // song has finished playing
 | ||||||
|  |                 db.apply_command(Command::NextSong); | ||||||
|  |                 self.current_song_id = SongOpt::New(db.queue.get_current_song().cloned()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // check the queue's current index
 | ||||||
|  |         if let SongOpt::None = self.current_song_id { | ||||||
|  |             if let Some(id) = db.queue.get_current_song() { | ||||||
|  |                 self.current_song_id = SongOpt::New(Some(*id)); | ||||||
|  |             } | ||||||
|  |         } else if let SongOpt::Some(l_id) = &self.current_song_id { | ||||||
|  |             if let Some(id) = db.queue.get_current_song() { | ||||||
|  |                 if *id != *l_id { | ||||||
|  |                     self.current_song_id = SongOpt::New(Some(*id)); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 self.current_song_id = SongOpt::New(None); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // new current song
 | ||||||
|  |         if let SongOpt::New(song_opt) = &self.current_song_id { | ||||||
|  |             // stop playback
 | ||||||
|  |             eprintln!("[play] stopping playback"); | ||||||
|  |             self.manager.clear(); | ||||||
|  |             if let Some(song_id) = song_opt { | ||||||
|  |                 if db.playing { | ||||||
|  |                     // start playback again
 | ||||||
|  |                     if let Some(song) = db.get_song(song_id) { | ||||||
|  |                         eprintln!("[play] starting playback..."); | ||||||
|  |                         // add our song
 | ||||||
|  |                         let ext = match &song.location.rel_path.extension() { | ||||||
|  |                             Some(s) => s.to_str().unwrap_or(""), | ||||||
|  |                             None => "", | ||||||
|  |                         }; | ||||||
|  |                         let (sound, notif) = Self::sound_from_bytes( | ||||||
|  |                             ext, | ||||||
|  |                             song.cached_data_now(db).expect("no cached data"), | ||||||
|  |                         ) | ||||||
|  |                         .unwrap() | ||||||
|  |                         .pausable() | ||||||
|  |                         .with_async_completion_notifier(); | ||||||
|  |                         // add it
 | ||||||
|  |                         let (sound, controller) = sound.controllable(); | ||||||
|  |                         self.source = Some((controller, notif)); | ||||||
|  |                         // and play it
 | ||||||
|  |                         self.manager.play(Box::new(sound)); | ||||||
|  |                         eprintln!("[play] started playback"); | ||||||
|  |                     } else { | ||||||
|  |                         panic!("invalid song ID: current_song_id not found in DB!"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 self.current_song_id = SongOpt::Some(*song_id); | ||||||
|  |             } else { | ||||||
|  |                 self.current_song_id = SongOpt::None; | ||||||
|  |             } | ||||||
|  |             if let Some(Some(song)) = db.queue.get_next_song().map(|v| db.get_song(v)) { | ||||||
|  |                 song.cache_data_start_thread(&db); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// partly identical to awedio/src/sounds/open_file.rs open_file_with_reader(), which is a private function I can't access
 | ||||||
|  |     fn sound_from_bytes( | ||||||
|  |         extension: &str, | ||||||
|  |         bytes: Arc<Vec<u8>>, | ||||||
|  |     ) -> Result<Box<dyn Sound>, std::io::Error> { | ||||||
|  |         let reader = ArcU8Reader::new(bytes); | ||||||
|  |         Ok(match extension { | ||||||
|  |             "wav" => Box::new( | ||||||
|  |                 awedio::sounds::decoders::WavDecoder::new(reader) | ||||||
|  |                     .map_err(|_e| std::io::Error::from(std::io::ErrorKind::InvalidData))?, | ||||||
|  |             ), | ||||||
|  |             "mp3" => Box::new(awedio::sounds::decoders::Mp3Decoder::new(reader)), | ||||||
|  |             _ => return Err(std::io::Error::from(std::io::ErrorKind::Unsupported)), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										265
									
								
								musicdb-lib/src/server/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										265
									
								
								musicdb-lib/src/server/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,265 @@ | |||||||
|  | use std::{ | ||||||
|  |     eprintln, | ||||||
|  |     io::Write, | ||||||
|  |     net::{SocketAddr, TcpListener}, | ||||||
|  |     path::PathBuf, | ||||||
|  |     sync::{mpsc, Arc, Mutex}, | ||||||
|  |     thread::{self, JoinHandle}, | ||||||
|  |     time::Duration, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::{ | ||||||
|  |     data::{ | ||||||
|  |         album::Album, | ||||||
|  |         artist::Artist, | ||||||
|  |         database::{Database, UpdateEndpoint}, | ||||||
|  |         queue::Queue, | ||||||
|  |         song::Song, | ||||||
|  |     }, | ||||||
|  |     load::ToFromBytes, | ||||||
|  |     player::Player, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #[derive(Clone, Debug)] | ||||||
|  | pub enum Command { | ||||||
|  |     Resume, | ||||||
|  |     Pause, | ||||||
|  |     Stop, | ||||||
|  |     Save, | ||||||
|  |     NextSong, | ||||||
|  |     SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>), | ||||||
|  |     QueueUpdate(Vec<usize>, Queue), | ||||||
|  |     QueueAdd(Vec<usize>, Queue), | ||||||
|  |     QueueInsert(Vec<usize>, usize, Queue), | ||||||
|  |     QueueRemove(Vec<usize>), | ||||||
|  |     QueueGoto(Vec<usize>), | ||||||
|  |     /// .id field is ignored!
 | ||||||
|  |     AddSong(Song), | ||||||
|  |     /// .id field is ignored!
 | ||||||
|  |     AddAlbum(Album), | ||||||
|  |     /// .id field is ignored!
 | ||||||
|  |     AddArtist(Artist), | ||||||
|  |     ModifySong(Song), | ||||||
|  |     ModifyAlbum(Album), | ||||||
|  |     ModifyArtist(Artist), | ||||||
|  |     SetLibraryDirectory(PathBuf), | ||||||
|  | } | ||||||
|  | impl Command { | ||||||
|  |     pub fn send_to_server(self, db: &Database) -> Result<(), Self> { | ||||||
|  |         if let Some(sender) = &db.command_sender { | ||||||
|  |             sender.send(self).unwrap(); | ||||||
|  |             Ok(()) | ||||||
|  |         } else { | ||||||
|  |             Err(self) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     pub fn send_to_server_or_apply(self, db: &mut Database) { | ||||||
|  |         if let Some(sender) = &db.command_sender { | ||||||
|  |             sender.send(self).unwrap(); | ||||||
|  |         } else { | ||||||
|  |             db.apply_command(self); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// starts handling database.command_sender events and optionally spawns a tcp server.
 | ||||||
|  | /// this function creates a new command_sender.
 | ||||||
|  | /// if you wish to implement your own server, set db.command_sender to None,
 | ||||||
|  | /// start a new thread running this function,
 | ||||||
|  | /// wait for db.command_sender to be Some,
 | ||||||
|  | /// then start your server.
 | ||||||
|  | /// for tcp-like protocols, you only need to
 | ||||||
|  | /// a) sync and register new connections using db.init_connection and db.update_endpoints.push
 | ||||||
|  | /// b) handle the decoding of messages using Command::from_bytes(), then send them to the db using db.command_sender.
 | ||||||
|  | /// for other protocols (like http + sse)
 | ||||||
|  | /// a) initialize new connections using db.init_connection() to synchronize the new client
 | ||||||
|  | /// b) handle the decoding of messages using Command::from_bytes()
 | ||||||
|  | /// c) re-encode all received messages using Command::to_bytes_vec(), send them to the db, and send them to all your clients.
 | ||||||
|  | pub fn run_server( | ||||||
|  |     database: Arc<Mutex<Database>>, | ||||||
|  |     addr_tcp: Option<SocketAddr>, | ||||||
|  |     sender_sender: Option<tokio::sync::mpsc::Sender<mpsc::Sender<Command>>>, | ||||||
|  | ) { | ||||||
|  |     let mut player = Player::new().unwrap(); | ||||||
|  |     let (command_sender, command_receiver) = mpsc::channel(); | ||||||
|  |     if let Some(s) = sender_sender { | ||||||
|  |         s.blocking_send(command_sender.clone()).unwrap(); | ||||||
|  |     } | ||||||
|  |     database.lock().unwrap().command_sender = Some(command_sender.clone()); | ||||||
|  |     if let Some(addr) = addr_tcp { | ||||||
|  |         match TcpListener::bind(addr) { | ||||||
|  |             Ok(v) => { | ||||||
|  |                 let command_sender = command_sender.clone(); | ||||||
|  |                 let db = Arc::clone(&database); | ||||||
|  |                 thread::spawn(move || loop { | ||||||
|  |                     if let Ok((mut connection, con_addr)) = v.accept() { | ||||||
|  |                         eprintln!("[info] TCP connection accepted from {con_addr}."); | ||||||
|  |                         let command_sender = command_sender.clone(); | ||||||
|  |                         let db = Arc::clone(&db); | ||||||
|  |                         thread::spawn(move || { | ||||||
|  |                             // sync database
 | ||||||
|  |                             let mut db = db.lock().unwrap(); | ||||||
|  |                             db.init_connection(&mut connection)?; | ||||||
|  |                             db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new( | ||||||
|  |                                 connection.try_clone().unwrap(), | ||||||
|  |                             ))); | ||||||
|  |                             drop(db); | ||||||
|  |                             loop { | ||||||
|  |                                 if let Ok(command) = Command::from_bytes(&mut connection) { | ||||||
|  |                                     command_sender.send(command).unwrap(); | ||||||
|  |                                 } else { | ||||||
|  |                                     break; | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             Ok::<(), std::io::Error>(()) | ||||||
|  |                         }); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 eprintln!("[WARN] Couldn't start TCP listener: {e}"); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     let dur = Duration::from_secs_f32(0.1); | ||||||
|  |     loop { | ||||||
|  |         player.update(&mut database.lock().unwrap()); | ||||||
|  |         if let Ok(command) = command_receiver.recv_timeout(dur) { | ||||||
|  |             player.handle_command(&command); | ||||||
|  |             database.lock().unwrap().apply_command(command); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub trait Connection: Sized + Send + 'static { | ||||||
|  |     type SendError: Send; | ||||||
|  |     fn send_command(&mut self, command: Command) -> Result<(), Self::SendError>; | ||||||
|  |     fn receive_updates(&mut self) -> Result<Vec<Command>, Self::SendError>; | ||||||
|  |     fn receive_update_blocking(&mut self) -> Result<Command, Self::SendError>; | ||||||
|  |     fn move_to_thread<F: FnMut(&mut Self, Command) -> bool + Send + 'static>( | ||||||
|  |         mut self, | ||||||
|  |         mut handler: F, | ||||||
|  |     ) -> JoinHandle<Result<Self, Self::SendError>> { | ||||||
|  |         std::thread::spawn(move || loop { | ||||||
|  |             let update = self.receive_update_blocking()?; | ||||||
|  |             if handler(&mut self, update) { | ||||||
|  |                 return Ok(self); | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl ToFromBytes for Command { | ||||||
|  |     fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: Write, | ||||||
|  |     { | ||||||
|  |         match self { | ||||||
|  |             Self::Resume => s.write_all(&[0b11000000])?, | ||||||
|  |             Self::Pause => s.write_all(&[0b00110000])?, | ||||||
|  |             Self::Stop => s.write_all(&[0b11110000])?, | ||||||
|  |             Self::Save => s.write_all(&[0b11110011])?, | ||||||
|  |             Self::NextSong => s.write_all(&[0b11110010])?, | ||||||
|  |             Self::SyncDatabase(a, b, c) => { | ||||||
|  |                 s.write_all(&[0b01011000])?; | ||||||
|  |                 a.to_bytes(s)?; | ||||||
|  |                 b.to_bytes(s)?; | ||||||
|  |                 c.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::QueueUpdate(index, new_data) => { | ||||||
|  |                 s.write_all(&[0b00011100])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |                 new_data.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::QueueAdd(index, new_data) => { | ||||||
|  |                 s.write_all(&[0b00011010])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |                 new_data.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::QueueInsert(index, pos, new_data) => { | ||||||
|  |                 s.write_all(&[0b00011110])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |                 pos.to_bytes(s)?; | ||||||
|  |                 new_data.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::QueueRemove(index) => { | ||||||
|  |                 s.write_all(&[0b00011001])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::QueueGoto(index) => { | ||||||
|  |                 s.write_all(&[0b00011011])?; | ||||||
|  |                 index.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::AddSong(song) => { | ||||||
|  |                 s.write_all(&[0b01010000])?; | ||||||
|  |                 song.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::AddAlbum(album) => { | ||||||
|  |                 s.write_all(&[0b01010011])?; | ||||||
|  |                 album.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::AddArtist(artist) => { | ||||||
|  |                 s.write_all(&[0b01011100])?; | ||||||
|  |                 artist.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::ModifySong(song) => { | ||||||
|  |                 s.write_all(&[0b10010000])?; | ||||||
|  |                 song.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::ModifyAlbum(album) => { | ||||||
|  |                 s.write_all(&[0b10010011])?; | ||||||
|  |                 album.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::ModifyArtist(artist) => { | ||||||
|  |                 s.write_all(&[0b10011100])?; | ||||||
|  |                 artist.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |             Self::SetLibraryDirectory(path) => { | ||||||
|  |                 s.write_all(&[0b00110001])?; | ||||||
|  |                 path.to_bytes(s)?; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |     fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error> | ||||||
|  |     where | ||||||
|  |         T: std::io::Read, | ||||||
|  |     { | ||||||
|  |         let mut kind = [0]; | ||||||
|  |         s.read_exact(&mut kind)?; | ||||||
|  |         Ok(match kind[0] { | ||||||
|  |             0b11000000 => Self::Resume, | ||||||
|  |             0b00110000 => Self::Pause, | ||||||
|  |             0b11110000 => Self::Stop, | ||||||
|  |             0b11110011 => Self::Save, | ||||||
|  |             0b11110010 => Self::NextSong, | ||||||
|  |             0b01011000 => Self::SyncDatabase( | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |             ), | ||||||
|  |             0b00011100 => { | ||||||
|  |                 Self::QueueUpdate(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?) | ||||||
|  |             } | ||||||
|  |             0b00011010 => Self::QueueAdd(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b00011110 => Self::QueueInsert( | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |                 ToFromBytes::from_bytes(s)?, | ||||||
|  |             ), | ||||||
|  |             0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b00011011 => Self::QueueGoto(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b10011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?), | ||||||
|  |             0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?), | ||||||
|  |             _ => { | ||||||
|  |                 eprintln!("unexpected byte when reading command; stopping playback."); | ||||||
|  |                 Self::Stop | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										34
									
								
								musicdb-lib/src/test.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										34
									
								
								musicdb-lib/src/test.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | #![cfg(test)] | ||||||
|  | use std::{assert_eq, path::PathBuf}; | ||||||
|  | 
 | ||||||
|  | use crate::load::ToFromBytes; | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn string() { | ||||||
|  |     for v in ["dskjh2d89dnas2d90", "aosu 89d 89a 89", "a/b/c/12"] { | ||||||
|  |         let v = v.to_owned(); | ||||||
|  |         assert_eq!(v, String::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()); | ||||||
|  |         let v = PathBuf::from(v); | ||||||
|  |         assert_eq!(v, PathBuf::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn vec() { | ||||||
|  |     for v in [vec!["asdad".to_owned(), "dsnakf".to_owned()], vec![]] { | ||||||
|  |         assert_eq!( | ||||||
|  |             v, | ||||||
|  |             Vec::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[test] | ||||||
|  | fn option() { | ||||||
|  |     for v in [None, Some("value".to_owned())] { | ||||||
|  |         assert_eq!( | ||||||
|  |             v, | ||||||
|  |             Option::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								musicdb-server/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							
							
						
						
									
										1
									
								
								musicdb-server/.gitignore
									
									
									
									
										vendored
									
									
										Executable file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | /target | ||||||
							
								
								
									
										19
									
								
								musicdb-server/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										19
									
								
								musicdb-server/Cargo.toml
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | [package] | ||||||
|  | name = "musicdb-server" | ||||||
|  | version = "0.1.0" | ||||||
|  | edition = "2021" | ||||||
|  | 
 | ||||||
|  | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | axum = { version = "0.6.19", features = ["headers"] } | ||||||
|  | futures = "0.3.28" | ||||||
|  | headers = "0.3.8" | ||||||
|  | musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } | ||||||
|  | serde = { version = "1.0", features = ["derive"] } | ||||||
|  | serde_json = "1.0" | ||||||
|  | tokio = { version = "1.0", features = ["full"] } | ||||||
|  | tokio-stream = "0.1.14" | ||||||
|  | tower = { version = "0.4", features = ["util"] } | ||||||
|  | tower-http = { version = "0.4.0", features = ["fs", "trace"] } | ||||||
|  | trace = "0.1.7" | ||||||
							
								
								
									
										3
									
								
								musicdb-server/assets/album-view.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								musicdb-server/assets/album-view.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <h3>\:name</h3> | ||||||
|  | <button hx-post="/queue/add-album/\:id" hx-swap="none">Queue</button> | ||||||
|  | \:songs | ||||||
							
								
								
									
										1
									
								
								musicdb-server/assets/albums_one.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								musicdb-server/assets/albums_one.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <button hx-get="/album-view/\:id" hx-target="#album-view">\:name</button> | ||||||
							
								
								
									
										2
									
								
								musicdb-server/assets/artist-view.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								musicdb-server/assets/artist-view.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | <h3>\:name</h3> | ||||||
|  | \:albums | ||||||
							
								
								
									
										2
									
								
								musicdb-server/assets/artists.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								musicdb-server/assets/artists.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | <h3>Artists</h3> | ||||||
|  | \:artists | ||||||
							
								
								
									
										1
									
								
								musicdb-server/assets/artists_one.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								musicdb-server/assets/artists_one.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <button hx-get="/artist-view/\:id" hx-target="#artist-view">\:name</button> | ||||||
							
								
								
									
										3
									
								
								musicdb-server/assets/queue.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								musicdb-server/assets/queue.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <h2>Queue</h2> | ||||||
|  | <div>Now Playing: <b>\:currentTitle</b></div> | ||||||
|  | \:content | ||||||
							
								
								
									
										10
									
								
								musicdb-server/assets/queue_folder.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								musicdb-server/assets/queue_folder.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <div> | ||||||
|  |   <small>>></small> | ||||||
|  |   <button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button> | ||||||
|  |   <small>\:name</small> | ||||||
|  | </div> | ||||||
|  | \:content | ||||||
|  | <div> | ||||||
|  |   <small><<</small> | ||||||
|  |   <button hx-post="/queue/remove/\:path" hx-swap="none">x</button> | ||||||
|  | </div> | ||||||
							
								
								
									
										10
									
								
								musicdb-server/assets/queue_folder_current.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								musicdb-server/assets/queue_folder_current.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <div> | ||||||
|  |   <small>>></small> | ||||||
|  |   <button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button> | ||||||
|  |   <small><b>\:name</b></small> | ||||||
|  | </div> | ||||||
|  | \:content | ||||||
|  | <div> | ||||||
|  |   <small><<</small> | ||||||
|  |   <button hx-post="/queue/remove/\:path" hx-swap="none">x</button> | ||||||
|  | </div> | ||||||
							
								
								
									
										5
									
								
								musicdb-server/assets/queue_song.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								musicdb-server/assets/queue_song.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | <div> | ||||||
|  |   <button hx-post="/queue/remove/\:path" hx-swap="none">x</button> | ||||||
|  |   <button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button> | ||||||
|  |   \:title | ||||||
|  | </div> | ||||||
							
								
								
									
										5
									
								
								musicdb-server/assets/queue_song_current.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								musicdb-server/assets/queue_song_current.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | <div> | ||||||
|  |   <button hx-post="/queue/remove/\:path" hx-swap="none">x</button> | ||||||
|  |   <button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button> | ||||||
|  |   <b>\:title</b> | ||||||
|  | </div> | ||||||
							
								
								
									
										24
									
								
								musicdb-server/assets/root.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								musicdb-server/assets/root.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | <!DOCTYPE HTML> | ||||||
|  | <html> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  |     <meta name="color-scheme" content="light dark"> | ||||||
|  |     <script src="https://unpkg.com/htmx.org@1.9.3"></script> | ||||||
|  |     <title>MusicDb</title> | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     <div hx-sse="connect:/sse"> | ||||||
|  |       <div hx-sse="swap:playing">(loading)</div> | ||||||
|  |       <button hx-post="/resume" hx-swap="none">⏵</button> | ||||||
|  |       <button hx-post="/pause" hx-swap="none">⏸</button> | ||||||
|  |       <button hx-post="/stop" hx-swap="none">⏹</button> | ||||||
|  |       <button hx-post="/next" hx-swap="none">⏭</button> | ||||||
|  |       <button hx-post="/queue/clear" hx-swap="none">-</button> | ||||||
|  |       <div hx-sse="swap:queue">(loading)</div> | ||||||
|  |       <div hx-sse="swap:artists">(loading)</div> | ||||||
|  |       <div id="artist-view"></div> | ||||||
|  |       <div id="album-view"></div> | ||||||
|  |     </div> | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										1
									
								
								musicdb-server/assets/songs_one.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								musicdb-server/assets/songs_one.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <button hx-post="/queue/add-song/\:id" hx-swap="none">\:title</button> | ||||||
							
								
								
									
										184
									
								
								musicdb-server/src/main.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										184
									
								
								musicdb-server/src/main.rs
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,184 @@ | |||||||
|  | mod web; | ||||||
|  | 
 | ||||||
|  | use std::{ | ||||||
|  |     path::PathBuf, | ||||||
|  |     process::exit, | ||||||
|  |     sync::{Arc, Mutex}, | ||||||
|  |     thread, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use musicdb_lib::server::{run_server, Command}; | ||||||
|  | 
 | ||||||
|  | use musicdb_lib::data::database::Database; | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 
 | ||||||
|  | # Exit codes | ||||||
|  | 
 | ||||||
|  | 0 => exited as requested by the user | ||||||
|  | 1 => exit after printing help message | ||||||
|  | 3 => error parsing cli arguments | ||||||
|  | 10 => tried to start with a path that caused some io::Error | ||||||
|  | 11 => tried to start with a path that does not exist (--init prevents this) | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | #[tokio::main] | ||||||
|  | async fn main() { | ||||||
|  |     let mut args = std::env::args().skip(1); | ||||||
|  |     let mut tcp_addr = None; | ||||||
|  |     let mut web_addr = None; | ||||||
|  |     let mut lib_dir_for_init = None; | ||||||
|  |     let database = if let Some(path_s) = args.next() { | ||||||
|  |         loop { | ||||||
|  |             if let Some(arg) = args.next() { | ||||||
|  |                 if arg.starts_with("--") { | ||||||
|  |                     match &arg[2..] { | ||||||
|  |                         "init" => { | ||||||
|  |                             if let Some(lib_dir) = args.next() { | ||||||
|  |                                 lib_dir_for_init = Some(lib_dir); | ||||||
|  |                             } else { | ||||||
|  |                                 eprintln!( | ||||||
|  |                                     "[EXIT]
 | ||||||
|  | missing argument: --init <lib path>" | ||||||
|  |                                 ); | ||||||
|  |                                 exit(3); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         "tcp" => { | ||||||
|  |                             if let Some(addr) = args.next() { | ||||||
|  |                                 if let Ok(addr) = addr.parse() { | ||||||
|  |                                     tcp_addr = Some(addr) | ||||||
|  |                                 } else { | ||||||
|  |                                     eprintln!( | ||||||
|  |                                         "[EXIT]
 | ||||||
|  | bad argument: --tcp <addr:port>: couldn't parse <addr:port>" | ||||||
|  |                                     ); | ||||||
|  |                                     exit(3); | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 eprintln!( | ||||||
|  |                                     "[EXIT]
 | ||||||
|  | missing argument: --tcp <addr:port>" | ||||||
|  |                                 ); | ||||||
|  |                                 exit(3); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         "web" => { | ||||||
|  |                             if let Some(addr) = args.next() { | ||||||
|  |                                 if let Ok(addr) = addr.parse() { | ||||||
|  |                                     web_addr = Some(addr) | ||||||
|  |                                 } else { | ||||||
|  |                                     eprintln!( | ||||||
|  |                                         "[EXIT]
 | ||||||
|  | bad argument: --web <addr:port>: couldn't parse <addr:port>" | ||||||
|  |                                     ); | ||||||
|  |                                     exit(3); | ||||||
|  |                                 } | ||||||
|  |                             } else { | ||||||
|  |                                 eprintln!( | ||||||
|  |                                     "[EXIT]
 | ||||||
|  | missing argument: --web <addr:port>" | ||||||
|  |                                 ); | ||||||
|  |                                 exit(3); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         o => { | ||||||
|  |                             eprintln!( | ||||||
|  |                                 "[EXIT]
 | ||||||
|  | Unknown long argument --{o}" | ||||||
|  |                             ); | ||||||
|  |                             exit(3); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else if arg.starts_with("-") { | ||||||
|  |                     match &arg[1..] { | ||||||
|  |                         o => { | ||||||
|  |                             eprintln!( | ||||||
|  |                                 "[EXIT]
 | ||||||
|  | Unknown short argument -{o}" | ||||||
|  |                             ); | ||||||
|  |                             exit(3); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     eprintln!( | ||||||
|  |                         "[EXIT]
 | ||||||
|  | Argument didn't start with - or -- ({arg})." | ||||||
|  |                     ); | ||||||
|  |                     exit(3); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let path = PathBuf::from(&path_s); | ||||||
|  |         match path.try_exists() { | ||||||
|  |             Ok(exists) => { | ||||||
|  |                 if let Some(lib_directory) = lib_dir_for_init { | ||||||
|  |                     Database::new_empty(path, lib_directory.into()) | ||||||
|  |                 } else if exists { | ||||||
|  |                     Database::load_database(path).unwrap() | ||||||
|  |                 } else { | ||||||
|  |                     eprintln!( | ||||||
|  |                         "[EXIT]
 | ||||||
|  | The provided path does not exist." | ||||||
|  |                     ); | ||||||
|  |                     exit(11); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 eprintln!( | ||||||
|  |                     "[EXIT]
 | ||||||
|  | Error getting information about the provided path '{path_s}': {e}" | ||||||
|  |                 ); | ||||||
|  |                 exit(10); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         eprintln!( | ||||||
|  |             "[EXIT]
 | ||||||
|  | musicdb - help | ||||||
|  | musicdb <path to database file> <options> <options> <...> | ||||||
|  | options: | ||||||
|  |   --init <lib directory> | ||||||
|  |   --tcp <addr:port> | ||||||
|  |   --web <addr:port> | ||||||
|  | this help was shown because no arguments were provided." | ||||||
|  |         ); | ||||||
|  |         exit(1); | ||||||
|  |     }; | ||||||
|  |     // database.add_song_new(Song::new(
 | ||||||
|  |     //     "Amaranthe/Manifest/02 Make It Better.mp3".into(),
 | ||||||
|  |     //     "Make It Better".to_owned(),
 | ||||||
|  |     //     None,
 | ||||||
|  |     //     None,
 | ||||||
|  |     //     vec![],
 | ||||||
|  |     //     None,
 | ||||||
|  |     // ));
 | ||||||
|  |     // let mut player = Player::new();
 | ||||||
|  |     // eprintln!("[info] database.songs: {:?}", database.songs());
 | ||||||
|  |     // database.save_database(Some("/tmp/dbfile".into())).unwrap();
 | ||||||
|  |     // eprintln!("{}", database.get_song(&0).unwrap());
 | ||||||
|  |     // database.queue.add_to_end(QueueContent::Song(1).into());
 | ||||||
|  |     // player.update_and_restart_playing_song(&database);
 | ||||||
|  |     let database = Arc::new(Mutex::new(database)); | ||||||
|  |     if tcp_addr.is_some() || web_addr.is_some() { | ||||||
|  |         if let Some(addr) = web_addr { | ||||||
|  |             let (s, mut r) = tokio::sync::mpsc::channel(2); | ||||||
|  |             let db = Arc::clone(&database); | ||||||
|  |             thread::spawn(move || run_server(database, tcp_addr, Some(s))); | ||||||
|  |             if let Some(sender) = r.recv().await { | ||||||
|  |                 web::main(db, sender, addr).await; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             run_server(database, tcp_addr, None); | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         eprintln!("nothing to do, not starting the server."); | ||||||
|  |     } | ||||||
|  |     // std::io::stdin().read_line(&mut String::new()).unwrap();
 | ||||||
|  |     // dbg!(Update::from_bytes(&mut BufReader::new(
 | ||||||
|  |     //     TcpStream::connect("127.0.0.1:26314".parse::<SocketAddr>().unwrap()).unwrap()
 | ||||||
|  |     // )));
 | ||||||
|  | } | ||||||
							
								
								
									
										573
									
								
								musicdb-server/src/web.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										573
									
								
								musicdb-server/src/web.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,573 @@ | |||||||
|  | use std::convert::Infallible; | ||||||
|  | use std::mem; | ||||||
|  | use std::net::SocketAddr; | ||||||
|  | use std::sync::{mpsc, Arc, Mutex}; | ||||||
|  | use std::task::Poll; | ||||||
|  | use std::time::Duration; | ||||||
|  | 
 | ||||||
|  | use axum::extract::{Path, State}; | ||||||
|  | use axum::response::sse::Event; | ||||||
|  | use axum::response::{Html, Sse}; | ||||||
|  | use axum::routing::{get, post}; | ||||||
|  | use axum::{Router, TypedHeader}; | ||||||
|  | use futures::{stream, Stream}; | ||||||
|  | use musicdb_lib::data::database::{Database, UpdateEndpoint}; | ||||||
|  | use musicdb_lib::data::queue::{Queue, QueueContent}; | ||||||
|  | use musicdb_lib::server::Command; | ||||||
|  | use tokio_stream::StreamExt as _; | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  | 
 | ||||||
|  | 23E9 ⏩︎ fast forward | ||||||
|  | 23EA ⏪︎ rewind, fast backwards | ||||||
|  | 23EB ⏫︎ fast increase | ||||||
|  | 23EC ⏬︎ fast decrease | ||||||
|  | 23ED ⏭︎ skip to end, next | ||||||
|  | 23EE ⏮︎ skip to start, previous | ||||||
|  | 23EF ⏯︎ play/pause toggle | ||||||
|  | 23F1 ⏱︎ stopwatch | ||||||
|  | 23F2 ⏲︎ timer clock | ||||||
|  | 23F3 ⏳︎ hourglass | ||||||
|  | 23F4 ⏴︎ reverse, back | ||||||
|  | 23F5 ⏵︎ forward, next, play | ||||||
|  | 23F6 ⏶︎ increase | ||||||
|  | 23F7 ⏷︎ decrease | ||||||
|  | 23F8 ⏸︎ pause | ||||||
|  | 23F9 ⏹︎ stop | ||||||
|  | 23FA ⏺︎ record | ||||||
|  | 
 | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | #[derive(Clone)] | ||||||
|  | pub struct AppState { | ||||||
|  |     db: Arc<Mutex<Database>>, | ||||||
|  |     html: Arc<AppHtml>, | ||||||
|  | } | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct AppHtml { | ||||||
|  |     /// /
 | ||||||
|  |     /// can use:
 | ||||||
|  |     root: Vec<HtmlPart>, | ||||||
|  | 
 | ||||||
|  |     /// sse:artists
 | ||||||
|  |     /// can use: artists (0+ repeats of artists_one)
 | ||||||
|  |     artists: Vec<HtmlPart>, | ||||||
|  |     /// can use: id, name
 | ||||||
|  |     artists_one: Vec<HtmlPart>, | ||||||
|  | 
 | ||||||
|  |     /// /artist-view/:artist-id
 | ||||||
|  |     /// can use: albums (0+ repeats of albums_one)
 | ||||||
|  |     artist_view: Vec<HtmlPart>, | ||||||
|  |     /// can use: name
 | ||||||
|  |     albums_one: Vec<HtmlPart>, | ||||||
|  | 
 | ||||||
|  |     /// /album-view/:album-id
 | ||||||
|  |     /// can use: id, name, songs (0+ repeats of songs_one)
 | ||||||
|  |     album_view: Vec<HtmlPart>, | ||||||
|  |     /// can use: title
 | ||||||
|  |     songs_one: Vec<HtmlPart>, | ||||||
|  | 
 | ||||||
|  |     /// /queue
 | ||||||
|  |     /// can use: currentTitle, nextTitle, content
 | ||||||
|  |     queue: Vec<HtmlPart>, | ||||||
|  |     /// can use: path, title
 | ||||||
|  |     queue_song: Vec<HtmlPart>, | ||||||
|  |     /// can use: path, title
 | ||||||
|  |     queue_song_current: Vec<HtmlPart>, | ||||||
|  |     /// can use: path, content, name
 | ||||||
|  |     queue_folder: Vec<HtmlPart>, | ||||||
|  |     /// can use: path, content, name
 | ||||||
|  |     queue_folder_current: Vec<HtmlPart>, | ||||||
|  | } | ||||||
|  | impl AppHtml { | ||||||
|  |     pub fn from_dir<P: AsRef<std::path::Path>>(dir: P) -> std::io::Result<Self> { | ||||||
|  |         let dir = dir.as_ref(); | ||||||
|  |         Ok(Self { | ||||||
|  |             root: Self::parse(&std::fs::read_to_string(dir.join("root.html"))?), | ||||||
|  |             artists: Self::parse(&std::fs::read_to_string(dir.join("artists.html"))?), | ||||||
|  |             artists_one: Self::parse(&std::fs::read_to_string(dir.join("artists_one.html"))?), | ||||||
|  |             artist_view: Self::parse(&std::fs::read_to_string(dir.join("artist-view.html"))?), | ||||||
|  |             albums_one: Self::parse(&std::fs::read_to_string(dir.join("albums_one.html"))?), | ||||||
|  |             album_view: Self::parse(&std::fs::read_to_string(dir.join("album-view.html"))?), | ||||||
|  |             songs_one: Self::parse(&std::fs::read_to_string(dir.join("songs_one.html"))?), | ||||||
|  |             queue: Self::parse(&std::fs::read_to_string(dir.join("queue.html"))?), | ||||||
|  |             queue_song: Self::parse(&std::fs::read_to_string(dir.join("queue_song.html"))?), | ||||||
|  |             queue_song_current: Self::parse(&std::fs::read_to_string( | ||||||
|  |                 dir.join("queue_song_current.html"), | ||||||
|  |             )?), | ||||||
|  |             queue_folder: Self::parse(&std::fs::read_to_string(dir.join("queue_folder.html"))?), | ||||||
|  |             queue_folder_current: Self::parse(&std::fs::read_to_string( | ||||||
|  |                 dir.join("queue_folder_current.html"), | ||||||
|  |             )?), | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |     pub fn parse(s: &str) -> Vec<HtmlPart> { | ||||||
|  |         let mut o = Vec::new(); | ||||||
|  |         let mut c = String::new(); | ||||||
|  |         let mut chars = s.chars().peekable(); | ||||||
|  |         loop { | ||||||
|  |             if let Some(ch) = chars.next() { | ||||||
|  |                 if ch == '\\' && chars.peek().is_some_and(|ch| *ch == ':') { | ||||||
|  |                     chars.next(); | ||||||
|  |                     o.push(HtmlPart::Plain(mem::replace(&mut c, String::new()))); | ||||||
|  |                     loop { | ||||||
|  |                         if let Some(ch) = chars.peek() { | ||||||
|  |                             if !ch.is_ascii_alphabetic() { | ||||||
|  |                                 o.push(HtmlPart::Insert(mem::replace(&mut c, String::new()))); | ||||||
|  |                                 break; | ||||||
|  |                             } else { | ||||||
|  |                                 c.push(*ch); | ||||||
|  |                                 chars.next(); | ||||||
|  |                             } | ||||||
|  |                         } else { | ||||||
|  |                             if c.len() > 0 { | ||||||
|  |                                 o.push(HtmlPart::Insert(c)); | ||||||
|  |                             } | ||||||
|  |                             return o; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     c.push(ch); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 if c.len() > 0 { | ||||||
|  |                     o.push(HtmlPart::Plain(c)); | ||||||
|  |                 } | ||||||
|  |                 return o; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum HtmlPart { | ||||||
|  |     /// text as plain html
 | ||||||
|  |     Plain(String), | ||||||
|  |     /// insert some value depending on context and key
 | ||||||
|  |     Insert(String), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub async fn main(db: Arc<Mutex<Database>>, sender: mpsc::Sender<Command>, addr: SocketAddr) { | ||||||
|  |     let db1 = Arc::clone(&db); | ||||||
|  |     let state = AppState { | ||||||
|  |         db, | ||||||
|  |         html: Arc::new(AppHtml::from_dir("assets").unwrap()), | ||||||
|  |     }; | ||||||
|  |     let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = ( | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender.clone(), | ||||||
|  |         sender, | ||||||
|  |     ); | ||||||
|  |     let state1 = state.clone(); | ||||||
|  | 
 | ||||||
|  |     let app = Router::new() | ||||||
|  |         // root
 | ||||||
|  |         .nest_service( | ||||||
|  |             "/", | ||||||
|  |             get(move || async move { | ||||||
|  |                 Html( | ||||||
|  |                     state1 | ||||||
|  |                         .html | ||||||
|  |                         .root | ||||||
|  |                         .iter() | ||||||
|  |                         .map(|v| match v { | ||||||
|  |                             HtmlPart::Plain(v) => v, | ||||||
|  |                             HtmlPart::Insert(_) => "", | ||||||
|  |                         }) | ||||||
|  |                         .collect::<String>(), | ||||||
|  |                 ) | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         // server-sent events
 | ||||||
|  |         .route("/sse", get(sse_handler)) | ||||||
|  |         // inner views (embedded in root)
 | ||||||
|  |         .route("/artist-view/:artist-id", get(artist_view_handler)) | ||||||
|  |         .route("/album-view/:album-id", get(album_view_handler)) | ||||||
|  |         // handle POST requests via the mpsc::Sender instead of locking the db.
 | ||||||
|  |         .route( | ||||||
|  |             "/pause", | ||||||
|  |             post(move || async move { | ||||||
|  |                 _ = s1.send(Command::Pause); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/resume", | ||||||
|  |             post(move || async move { | ||||||
|  |                 _ = s2.send(Command::Resume); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/stop", | ||||||
|  |             post(move || async move { | ||||||
|  |                 _ = s3.send(Command::Stop); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/next", | ||||||
|  |             post(move || async move { | ||||||
|  |                 _ = s4.send(Command::NextSong); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/queue/clear", | ||||||
|  |             post(move || async move { | ||||||
|  |                 _ = s5.send(Command::QueueUpdate( | ||||||
|  |                     vec![], | ||||||
|  |                     QueueContent::Folder(0, vec![], String::new()).into(), | ||||||
|  |                 )); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/queue/remove/:i", | ||||||
|  |             post(move |Path(i): Path<String>| async move { | ||||||
|  |                 let mut ids = vec![]; | ||||||
|  |                 for id in i.split('-') { | ||||||
|  |                     if let Ok(n) = id.parse() { | ||||||
|  |                         ids.push(n); | ||||||
|  |                     } else { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 _ = s8.send(Command::QueueRemove(ids)); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/queue/goto/:i", | ||||||
|  |             post(move |Path(i): Path<String>| async move { | ||||||
|  |                 let mut ids = vec![]; | ||||||
|  |                 for id in i.split('-') { | ||||||
|  |                     if let Ok(n) = id.parse() { | ||||||
|  |                         ids.push(n); | ||||||
|  |                     } else { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 _ = s9.send(Command::QueueGoto(ids)); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/queue/add-song/:song-id", | ||||||
|  |             post(move |Path(song_id)| async move { | ||||||
|  |                 _ = s6.send(Command::QueueAdd( | ||||||
|  |                     vec![], | ||||||
|  |                     QueueContent::Song(song_id).into(), | ||||||
|  |                 )); | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .route( | ||||||
|  |             "/queue/add-album/:album-id", | ||||||
|  |             post(move |Path(album_id)| async move { | ||||||
|  |                 if let Some(album) = db1.lock().unwrap().albums().get(&album_id) { | ||||||
|  |                     _ = s7.send(Command::QueueAdd( | ||||||
|  |                         vec![], | ||||||
|  |                         QueueContent::Folder( | ||||||
|  |                             0, | ||||||
|  |                             album | ||||||
|  |                                 .songs | ||||||
|  |                                 .iter() | ||||||
|  |                                 .map(|id| QueueContent::Song(*id).into()) | ||||||
|  |                                 .collect(), | ||||||
|  |                             album.name.clone(), | ||||||
|  |                         ) | ||||||
|  |                         .into(), | ||||||
|  |                     )); | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |         ) | ||||||
|  |         .with_state(state); | ||||||
|  |     axum::Server::bind(&addr) | ||||||
|  |         .serve(app.into_make_service()) | ||||||
|  |         .await | ||||||
|  |         .unwrap(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn sse_handler( | ||||||
|  |     TypedHeader(user_agent): TypedHeader<headers::UserAgent>, | ||||||
|  |     State(state): State<AppState>, | ||||||
|  | ) -> Sse<impl Stream<Item = Result<Event, Infallible>>> { | ||||||
|  |     println!("`{}` connected", user_agent.as_str()); | ||||||
|  | 
 | ||||||
|  |     let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); | ||||||
|  |     let mut db = state.db.lock().unwrap(); | ||||||
|  |     _ = sender.send(Arc::new(Command::SyncDatabase(vec![], vec![], vec![]))); | ||||||
|  |     _ = sender.send(Arc::new(Command::NextSong)); | ||||||
|  |     _ = sender.send(Arc::new(if db.playing { | ||||||
|  |         Command::Resume | ||||||
|  |     } else { | ||||||
|  |         Command::Pause | ||||||
|  |     })); | ||||||
|  |     db.update_endpoints | ||||||
|  |         .push(UpdateEndpoint::CmdChannelTokio(sender)); | ||||||
|  |     drop(db); | ||||||
|  | 
 | ||||||
|  |     let stream = stream::poll_fn(move |_ctx| { | ||||||
|  |         if let Ok(cmd) = receiver.try_recv() { | ||||||
|  |             Poll::Ready(Some(match cmd.as_ref() { | ||||||
|  |                 Command::Resume => Event::default().event("playing").data("playing"), | ||||||
|  |                 Command::Pause => Event::default().event("playing").data("paused"), | ||||||
|  |                 Command::Stop => Event::default().event("playing").data("stopped"), | ||||||
|  |                 Command::SyncDatabase(..) | ||||||
|  |                 | Command::ModifySong(..) | ||||||
|  |                 | Command::ModifyAlbum(..) | ||||||
|  |                 | Command::ModifyArtist(..) | ||||||
|  |                 | Command::AddSong(..) | ||||||
|  |                 | Command::AddAlbum(..) | ||||||
|  |                 | Command::AddArtist(..) => Event::default().event("artists").data({ | ||||||
|  |                     let db = state.db.lock().unwrap(); | ||||||
|  |                     let mut a = db.artists().iter().collect::<Vec<_>>(); | ||||||
|  |                     a.sort_unstable_by_key(|(_id, artist)| &artist.name); | ||||||
|  |                     let mut artists = String::new(); | ||||||
|  |                     for (id, artist) in a { | ||||||
|  |                         for v in &state.html.artists_one { | ||||||
|  |                             match v { | ||||||
|  |                                 HtmlPart::Plain(v) => artists.push_str(v), | ||||||
|  |                                 HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                                     "id" => artists.push_str(&id.to_string()), | ||||||
|  |                                     "name" => artists.push_str(&artist.name), | ||||||
|  |                                     _ => {} | ||||||
|  |                                 }, | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     state | ||||||
|  |                         .html | ||||||
|  |                         .artists | ||||||
|  |                         .iter() | ||||||
|  |                         .map(|v| match v { | ||||||
|  |                             HtmlPart::Plain(v) => v, | ||||||
|  |                             HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                                 "artists" => &artists, | ||||||
|  |                                 _ => "", | ||||||
|  |                             }, | ||||||
|  |                         }) | ||||||
|  |                         .collect::<String>() | ||||||
|  |                 }), | ||||||
|  |                 Command::NextSong | ||||||
|  |                 | Command::QueueUpdate(..) | ||||||
|  |                 | Command::QueueAdd(..) | ||||||
|  |                 | Command::QueueInsert(..) | ||||||
|  |                 | Command::QueueRemove(..) | ||||||
|  |                 | Command::QueueGoto(..) => { | ||||||
|  |                     let db = state.db.lock().unwrap(); | ||||||
|  |                     let current = db | ||||||
|  |                         .queue | ||||||
|  |                         .get_current_song() | ||||||
|  |                         .map_or(None, |id| db.songs().get(id)); | ||||||
|  |                     let next = db | ||||||
|  |                         .queue | ||||||
|  |                         .get_next_song() | ||||||
|  |                         .map_or(None, |id| db.songs().get(id)); | ||||||
|  |                     let mut content = String::new(); | ||||||
|  |                     build_queue_content_build( | ||||||
|  |                         &db, | ||||||
|  |                         &state, | ||||||
|  |                         &mut content, | ||||||
|  |                         &db.queue, | ||||||
|  |                         String::new(), | ||||||
|  |                         true, | ||||||
|  |                     ); | ||||||
|  |                     Event::default().event("queue").data( | ||||||
|  |                         state | ||||||
|  |                             .html | ||||||
|  |                             .queue | ||||||
|  |                             .iter() | ||||||
|  |                             .map(|v| match v { | ||||||
|  |                                 HtmlPart::Plain(v) => v, | ||||||
|  |                                 HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                                     "currentTitle" => { | ||||||
|  |                                         if let Some(s) = current { | ||||||
|  |                                             &s.title | ||||||
|  |                                         } else { | ||||||
|  |                                             "" | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                     "nextTitle" => { | ||||||
|  |                                         if let Some(s) = next { | ||||||
|  |                                             &s.title | ||||||
|  |                                         } else { | ||||||
|  |                                             "" | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                     "content" => &content, | ||||||
|  |                                     _ => "", | ||||||
|  |                                 }, | ||||||
|  |                             }) | ||||||
|  |                             .collect::<String>(), | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 Command::Save | Command::SetLibraryDirectory(_) => return Poll::Pending, | ||||||
|  |             })) | ||||||
|  |         } else { | ||||||
|  |             return Poll::Pending; | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  |     .map(Ok); | ||||||
|  |     // .throttle(Duration::from_millis(100));
 | ||||||
|  | 
 | ||||||
|  |     Sse::new(stream) | ||||||
|  |         .keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_millis(250))) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn artist_view_handler( | ||||||
|  |     State(state): State<AppState>, | ||||||
|  |     Path(artist_id): Path<u64>, | ||||||
|  | ) -> Html<String> { | ||||||
|  |     let db = state.db.lock().unwrap(); | ||||||
|  |     if let Some(artist) = db.artists().get(&artist_id) { | ||||||
|  |         let mut albums = String::new(); | ||||||
|  |         for id in artist.albums.iter() { | ||||||
|  |             if let Some(album) = db.albums().get(id) { | ||||||
|  |                 for v in &state.html.albums_one { | ||||||
|  |                     match v { | ||||||
|  |                         HtmlPart::Plain(v) => albums.push_str(v), | ||||||
|  |                         HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                             "id" => albums.push_str(&id.to_string()), | ||||||
|  |                             "name" => albums.push_str(&album.name), | ||||||
|  |                             _ => {} | ||||||
|  |                         }, | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let id = artist_id.to_string(); | ||||||
|  |         Html( | ||||||
|  |             state | ||||||
|  |                 .html | ||||||
|  |                 .artist_view | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|v| match v { | ||||||
|  |                     HtmlPart::Plain(v) => v, | ||||||
|  |                     HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                         "id" => &id, | ||||||
|  |                         "name" => &artist.name, | ||||||
|  |                         "albums" => &albums, | ||||||
|  |                         _ => "", | ||||||
|  |                     }, | ||||||
|  |                 }) | ||||||
|  |                 .collect(), | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         Html(format!( | ||||||
|  |             "<h1>Bad ID</h1><p>There is no artist with the id {artist_id} in the database</p>" | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async fn album_view_handler( | ||||||
|  |     State(state): State<AppState>, | ||||||
|  |     Path(album_id): Path<u64>, | ||||||
|  | ) -> Html<String> { | ||||||
|  |     let db = state.db.lock().unwrap(); | ||||||
|  |     if let Some(album) = db.albums().get(&album_id) { | ||||||
|  |         let mut songs = String::new(); | ||||||
|  |         for id in album.songs.iter() { | ||||||
|  |             if let Some(song) = db.songs().get(id) { | ||||||
|  |                 for v in &state.html.songs_one { | ||||||
|  |                     match v { | ||||||
|  |                         HtmlPart::Plain(v) => songs.push_str(v), | ||||||
|  |                         HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                             "id" => songs.push_str(&id.to_string()), | ||||||
|  |                             "title" => songs.push_str(&song.title), | ||||||
|  |                             _ => {} | ||||||
|  |                         }, | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         let id = album_id.to_string(); | ||||||
|  |         Html( | ||||||
|  |             state | ||||||
|  |                 .html | ||||||
|  |                 .album_view | ||||||
|  |                 .iter() | ||||||
|  |                 .map(|v| match v { | ||||||
|  |                     HtmlPart::Plain(v) => v, | ||||||
|  |                     HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                         "id" => &id, | ||||||
|  |                         "name" => &album.name, | ||||||
|  |                         "songs" => &songs, | ||||||
|  |                         _ => "", | ||||||
|  |                     }, | ||||||
|  |                 }) | ||||||
|  |                 .collect(), | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         Html(format!( | ||||||
|  |             "<h1>Bad ID</h1><p>There is no album with the id {album_id} in the database</p>" | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn build_queue_content_build( | ||||||
|  |     db: &Database, | ||||||
|  |     state: &AppState, | ||||||
|  |     html: &mut String, | ||||||
|  |     queue: &Queue, | ||||||
|  |     path: String, | ||||||
|  |     current: bool, | ||||||
|  | ) { | ||||||
|  |     // TODO: Do something for disabled ones too (they shouldn't just be hidden)
 | ||||||
|  |     if queue.enabled() { | ||||||
|  |         match queue.content() { | ||||||
|  |             QueueContent::Song(id) => { | ||||||
|  |                 if let Some(song) = db.songs().get(id) { | ||||||
|  |                     for v in if current { | ||||||
|  |                         &state.html.queue_song_current | ||||||
|  |                     } else { | ||||||
|  |                         &state.html.queue_song | ||||||
|  |                     } { | ||||||
|  |                         match v { | ||||||
|  |                             HtmlPart::Plain(v) => html.push_str(v), | ||||||
|  |                             HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                                 "path" => html.push_str(&path), | ||||||
|  |                                 "title" => html.push_str(&song.title), | ||||||
|  |                                 _ => {} | ||||||
|  |                             }, | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             QueueContent::Folder(ci, c, name) => { | ||||||
|  |                 if path.is_empty() { | ||||||
|  |                     for (i, c) in c.iter().enumerate() { | ||||||
|  |                         let current = current && *ci == i; | ||||||
|  |                         build_queue_content_build(db, state, html, c, i.to_string(), current) | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     for v in if current { | ||||||
|  |                         &state.html.queue_folder_current | ||||||
|  |                     } else { | ||||||
|  |                         &state.html.queue_folder | ||||||
|  |                     } { | ||||||
|  |                         match v { | ||||||
|  |                             HtmlPart::Plain(v) => html.push_str(v), | ||||||
|  |                             HtmlPart::Insert(key) => match key.as_str() { | ||||||
|  |                                 "path" => html.push_str(&path), | ||||||
|  |                                 "name" => html.push_str(name), | ||||||
|  |                                 "content" => { | ||||||
|  |                                     for (i, c) in c.iter().enumerate() { | ||||||
|  |                                         let current = current && *ci == i; | ||||||
|  |                                         build_queue_content_build( | ||||||
|  |                                             db, | ||||||
|  |                                             state, | ||||||
|  |                                             html, | ||||||
|  |                                             c, | ||||||
|  |                                             format!("{path}-{i}"), | ||||||
|  |                                             current, | ||||||
|  |                                         ) | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 _ => {} | ||||||
|  |                             }, | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Mark
						Mark