From 8c434743f861ad34180e7101c425ccd65e5e2f8b Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 27 Dec 2023 15:39:53 +0100 Subject: [PATCH] s --- musicdb-client/Cargo.toml | 3 + musicdb-client/src/gui.rs | 146 ++++- musicdb-client/src/gui_base.rs | 56 +- musicdb-client/src/gui_edit_song.rs | 371 +++++++++++ musicdb-client/src/gui_idle_display.rs | 58 +- musicdb-client/src/gui_library.rs | 36 +- musicdb-client/src/gui_playback.rs | 9 +- musicdb-client/src/gui_queue.rs | 1 + musicdb-client/src/gui_screen.rs | 144 +++-- musicdb-client/src/gui_settings.rs | 1 + musicdb-client/src/gui_statusbar.rs | 8 +- musicdb-client/src/main.rs | 33 +- musicdb-client/src/merscfg.rs | 849 +++++++++++++++++++++++++ musicdb-lib/Cargo.toml | 7 +- musicdb-lib/src/lib.rs | 1 + musicdb-lib/src/server/mod.rs | 17 +- musicdb-server/Cargo.toml | 2 +- 17 files changed, 1621 insertions(+), 121 deletions(-) create mode 100644 musicdb-client/src/gui_edit_song.rs create mode 100644 musicdb-client/src/merscfg.rs mode change 100755 => 100644 musicdb-lib/Cargo.toml diff --git a/musicdb-client/Cargo.toml b/musicdb-client/Cargo.toml index 022ff0c..f85e43b 100755 --- a/musicdb-client/Cargo.toml +++ b/musicdb-client/Cargo.toml @@ -12,6 +12,9 @@ musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } regex = "1.9.3" speedy2d = { version = "1.12.0", optional = true } toml = "0.7.6" +mers_lib = { path = "../../mers/mers_lib", optional = true } [features] default = ["speedy2d"] +merscfg = ["mers_lib"] +playback = ["musicdb-lib/playback"] diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index 2f59bf6..5753457 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -9,7 +9,7 @@ use std::{ }; use musicdb_lib::{ - data::{database::Database, queue::Queue, AlbumId, ArtistId, CoverId, SongId}, + data::{database::Database, queue::Queue, song::Song, AlbumId, ArtistId, CoverId, SongId}, load::ToFromBytes, server::{get, Command}, }; @@ -26,8 +26,11 @@ use speedy2d::{ Graphics2D, }; +#[cfg(feature = "merscfg")] +use crate::merscfg::MersCfg; use crate::{ - gui_base::Panel, + gui_base::{Panel, ScrollBox}, + gui_edit_song::EditorForSongs, gui_notif::{NotifInfo, NotifOverlay}, gui_screen::GuiScreen, gui_text::Label, @@ -76,8 +79,8 @@ pub fn main( get_con: get::Client, event_sender_arc: Arc>>>, ) { - let mut config_file = super::get_config_file_path(); - config_file.push("config_gui.toml"); + let config_dir = super::get_config_file_path(); + let config_file = config_dir.join("config_gui.toml"); let mut font = None; let mut line_height = 32.0; let mut scroll_pixels_multiplier = 1.0; @@ -214,7 +217,7 @@ pub fn main( connection, Arc::new(Mutex::new(get_con)), event_sender_arc, - sender, + Arc::new(sender), line_height, scroll_pixels_multiplier, scroll_lines_multiplier, @@ -254,6 +257,8 @@ pub fn main( crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), ), ], + #[cfg(feature = "merscfg")] + merscfg: crate::merscfg::MersCfg::new(config_dir.join("dynamic_config.mers")), }, )); } @@ -266,10 +271,12 @@ pub struct GuiConfig { pub filter_presets_song: Vec<(String, crate::gui_library::FilterType)>, pub filter_presets_album: Vec<(String, crate::gui_library::FilterType)>, pub filter_presets_artist: Vec<(String, crate::gui_library::FilterType)>, + #[cfg(feature = "merscfg")] + pub merscfg: crate::merscfg::MersCfg, } pub struct Gui { - pub event_sender: UserEventSender, + pub event_sender: Arc>, pub database: Arc>, pub connection: TcpStream, pub get_con: Arc>>, @@ -304,7 +311,7 @@ impl Gui { connection: TcpStream, get_con: Arc>>, event_sender_arc: Arc>>>, - event_sender: UserEventSender, + event_sender: Arc>, line_height: f32, scroll_pixels_multiplier: f64, scroll_lines_multiplier: f64, @@ -313,6 +320,28 @@ impl Gui { ) -> Self { let (notif_overlay, notif_sender) = NotifOverlay::new(); let notif_sender_two = notif_sender.clone(); + #[cfg(feature = "merscfg")] + match gui_config + .merscfg + .load(Arc::clone(&event_sender), notif_sender.clone()) + { + Err(e) => { + if !matches!(e.kind(), std::io::ErrorKind::NotFound) { + eprintln!("Couldn't load merscfg: {e}") + } + } + Ok(Err(e)) => { + eprintln!("Error loading merscfg:\n{e}"); + } + Ok(Ok(Err((m, e)))) => { + if let Some(e) = e { + eprintln!("Error loading merscfg:\n{m}\n{e}"); + } else { + eprintln!("Error loading merscfg:\n{m}"); + } + } + Ok(Ok(Ok(()))) => eprintln!("Info: using merscfg"), + } database.lock().unwrap().update_endpoints.push( musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { Command::Resume @@ -662,6 +691,9 @@ pub(crate) trait GuiElemInternal: GuiElem { } } fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool { + if self.config().enabled == false { + return false; + } let mut focus_index = if refocus { usize::MAX } else { @@ -986,7 +1018,7 @@ pub enum GuiAction { /// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc) Build(Box Vec>), SendToServer(Command), - ContextMenu(Option>), + ContextMenu(Option<(Vec>)>), /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one. ResetKeyboardFocus, SetDragging( @@ -998,8 +1030,11 @@ pub enum GuiAction { SetLineHeight(f32), LoadCover(CoverId), /// Run a custom closure with mutable access to the Gui struct - Do(Box), + Do(Box), Exit, + EditSongs(Vec), + // EditAlbums(Vec), + // EditArtists(Vec), } pub enum Dragging { Artist(ArtistId), @@ -1032,7 +1067,6 @@ pub struct DrawInfo<'a> { Dragging, Option>, )>, - pub context_menu: Option>, pub gui_config: &'a mut GuiConfig, pub high_performance: bool, } @@ -1068,7 +1102,40 @@ impl Gui { GuiAction::ResetKeyboardFocus => _ = self.gui._keyboard_reset_focus(), GuiAction::SetDragging(d) => self.dragging = d, GuiAction::SetHighPerformance(d) => self.high_performance = d, - GuiAction::ContextMenu(m) => self.gui.c_context_menu = m, + GuiAction::ContextMenu(elems) => { + self.gui.c_context_menu = if let Some(elems) = elems { + let elem_height = 32.0; + let w = elem_height * 6.0; + let h = elem_height * elems.len() as f32; + let mut ax = self.mouse_pos.x / self.size.x.max(1) as f32; + let mut ay = self.mouse_pos.y / self.size.y.max(1) as f32; + let mut bx = (self.mouse_pos.x + w) / self.size.x.max(1) as f32; + let mut by = (self.mouse_pos.y + h) / self.size.y.max(1) as f32; + if bx > 1.0 { + ax -= bx - 1.0; + bx = 1.0; + } + if by > 1.0 { + ay -= by - 1.0; + by = 1.0; + } + if ax < 0.0 { + ax = 0.0; + } + if ay < 0.0 { + ay = 0.0; + } + Some(Box::new(ScrollBox::new( + GuiElemCfg::at(Rectangle::from_tuples((ax, ay), (bx, by))), + crate::gui_base::ScrollBoxSizeUnit::Pixels, + elems, + vec![], + elem_height, + ))) + } else { + None + }; + } GuiAction::SetLineHeight(h) => { self.line_height = h; self.gui @@ -1080,7 +1147,7 @@ impl Gui { .unwrap() .insert(id, GuiServerImage::new_cover(id, Arc::clone(&self.get_con))); } - GuiAction::Do(mut f) => f(self), + GuiAction::Do(f) => f(self), GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit), GuiAction::EndIdle(v) => { if v { @@ -1090,19 +1157,20 @@ impl Gui { } } GuiAction::OpenSettings(v) => { - self.gui.idle.target = 0.0; - self.gui.last_interaction = Instant::now(); + self.gui.unidle(); if self.gui.settings.0 != v { self.gui.settings = (v, Some(Instant::now())); } } GuiAction::OpenMain => { - self.gui.idle.target = 0.0; - self.gui.last_interaction = Instant::now(); + self.gui.unidle(); if self.gui.settings.0 { self.gui.settings = (false, Some(Instant::now())); } } + GuiAction::EditSongs(songs) => { + self.gui.c_editing_songs = Some(EditorForSongs::new(songs)); + } } } } @@ -1113,10 +1181,13 @@ impl WindowHandler for Gui { Rectangle::new(Vec2::ZERO, self.size.into_f32()), Color::BLACK, ); - let mut dblock = self.database.lock().unwrap(); + let dblock = Arc::clone(&self.database); + let mut dblock = dblock.lock().unwrap(); let mut covers = self.covers.take().unwrap(); let mut custom_images = self.custom_images.take().unwrap(); let mut cfg = self.gui_config.take().unwrap(); + #[cfg(feature = "merscfg")] + MersCfg::run(&mut cfg, self, Some(&mut dblock), |m| &m.func_before_draw); let mut info = DrawInfo { time: draw_start_time, actions: Vec::with_capacity(0), @@ -1133,12 +1204,10 @@ impl WindowHandler for Gui { line_height: self.line_height, high_performance: self.high_performance, dragging: self.dragging.take(), - context_menu: self.gui.c_context_menu.take(), gui_config: &mut cfg, }; self.gui._draw(&mut info, graphics); let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0)); - self.gui.c_context_menu = info.context_menu.take(); self.dragging = info.dragging.take(); if let Some((d, f)) = &mut self.dragging { if let Some(f) = f { @@ -1242,6 +1311,9 @@ impl WindowHandler for Gui { self.exec_gui_action(a) } } + if button != MouseButton::Right { + self.gui.c_context_menu = None; + } helper.request_redraw(); } fn on_mouse_wheel_scroll( @@ -1365,10 +1437,34 @@ impl WindowHandler for Gui { match user_event { GuiEvent::Refresh => helper.request_redraw(), GuiEvent::UpdatedLibrary => { + #[cfg(feature = "merscfg")] + if let Some(mut gc) = self.gui_config.take() { + MersCfg::run( + &mut gc, + self, + self.database.clone().lock().ok().as_mut().map(|v| &mut **v), + |m| &m.func_library_updated, + ); + self.gui_config = Some(gc); + } else { + eprintln!("WARN: Skipping call to merscfg's library_updated because gui_config is not available"); + } self.gui._recursive_all(&mut |e| e.updated_library()); helper.request_redraw(); } GuiEvent::UpdatedQueue => { + #[cfg(feature = "merscfg")] + if let Some(mut gc) = self.gui_config.take() { + MersCfg::run( + &mut gc, + self, + self.database.clone().lock().ok().as_mut().map(|v| &mut **v), + |m| &m.func_queue_updated, + ); + self.gui_config = Some(gc); + } else { + eprintln!("WARN: Skipping call to merscfg's queue_updated because gui_config is not available"); + } self.gui._recursive_all(&mut |e| e.updated_queue()); helper.request_redraw(); } @@ -1472,3 +1568,15 @@ pub fn morph_rect(a: &Rectangle, b: &Rectangle, p: f32) -> Rectangle { ), ) } +pub fn rect_from_rel(v: &Rectangle, outer: &Rectangle) -> Rectangle { + Rectangle::from_tuples( + ( + outer.top_left().x + v.top_left().x * outer.width(), + outer.top_left().y + v.top_left().y * outer.height(), + ), + ( + outer.top_left().x + v.bottom_right().x * outer.width(), + outer.top_left().y + v.bottom_right().y * outer.height(), + ), + ) +} diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index 517e5bf..a1bf156 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -122,9 +122,11 @@ pub struct ScrollBox { config: GuiElemCfg, pub children: C, pub children_heights: Vec, + pub default_size: f32, pub size_unit: ScrollBoxSizeUnit, pub scroll_target: f32, pub scroll_display: f32, + /// the y-position of the bottom edge of the last element (i.e. the total height) height_bottom: f32, /// 0.max(height_bottom - 1) max_scroll: f32, @@ -145,15 +147,16 @@ impl ScrollBox { size_unit: ScrollBoxSizeUnit, children: C, children_heights: Vec, + default_size: f32, ) -> Self { Self { config: config.w_scroll().w_mouse(), children, children_heights, + default_size, 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, @@ -217,7 +220,7 @@ impl GuiElem for ScrollBox { if self.children_heights.len() != self.children.len() { let target = self.children.len(); while self.children_heights.len() < target { - self.children_heights.push(0.0); + self.children_heights.push(self.default_size); } while self.children_heights.len() > target { self.children_heights.pop(); @@ -341,7 +344,7 @@ impl Button { children: C, ) -> Self { Self { - config: config.w_mouse(), + config: config.w_mouse().w_keyboard_focus(), children, action: Arc::new(action), } @@ -376,6 +379,27 @@ impl GuiElem for Button { vec![] } } + fn key_focus( + &mut self, + _modifiers: speedy2d::window::ModifiersState, + down: bool, + key: Option, + _scan: speedy2d::window::KeyScancode, + ) -> Vec { + if !down + && matches!( + key, + Some( + speedy2d::window::VirtualKeyCode::Return + | speedy2d::window::VirtualKeyCode::NumpadEnter, + ) + ) + { + (self.action.clone())(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); @@ -389,6 +413,32 @@ impl GuiElem for Button { Color::from_rgb(0.1, 0.1, 0.1) }, ); + if info.has_keyboard_focus { + g.draw_line( + *info.pos.top_left(), + info.pos.top_right(), + 2.0, + Color::WHITE, + ); + g.draw_line( + *info.pos.top_left(), + info.pos.bottom_left(), + 2.0, + Color::WHITE, + ); + g.draw_line( + info.pos.top_right(), + *info.pos.bottom_right(), + 2.0, + Color::WHITE, + ); + g.draw_line( + info.pos.bottom_left(), + *info.pos.bottom_right(), + 2.0, + Color::WHITE, + ); + } } } diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs new file mode 100644 index 0000000..411653b --- /dev/null +++ b/musicdb-client/src/gui_edit_song.rs @@ -0,0 +1,371 @@ +use std::{ + sync::{atomic::AtomicU8, Arc}, + time::Instant, +}; + +use musicdb_lib::data::{song::Song, ArtistId}; +use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; + +use crate::{ + color_scale, + gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, + gui_anim::AnimationController, + gui_base::{Button, Panel, ScrollBox}, + gui_text::{Label, TextField}, +}; + +// TODO: Fix bug where after selecting an artist you can't mouse-click the buttons anymore (to change it) + +const ELEM_HEIGHT: f32 = 32.0; + +pub struct EditorForSongs { + config: GuiElemCfg, + songs: Vec, + c_title: Label, + c_scrollbox: ScrollBox, + c_buttons: Panel<[Button<[Label; 1]>; 2]>, + c_background: Panel<()>, + created: Option, + event_sender: std::sync::mpsc::Sender, + event_recv: std::sync::mpsc::Receiver, +} +pub enum Event { + Close, + Apply, + SetArtist(String, Option), +} +pub struct EditorForSongElems { + c_title: TextField, + c_artist: EditorForSongArtistChooser, + c_album: Label, +} +impl GuiElemChildren for EditorForSongElems { + fn iter(&mut self) -> Box + '_> { + Box::new( + [ + self.c_title.elem_mut(), + self.c_artist.elem_mut(), + self.c_album.elem_mut(), + ] + .into_iter(), + ) + } + fn len(&self) -> usize { + 3 + } +} + +impl EditorForSongs { + pub fn new(songs: Vec) -> Self { + let (sender, recv) = std::sync::mpsc::channel(); + Self { + config: GuiElemCfg::at(Rectangle::from_tuples((0.0, 1.0), (1.0, 2.0))), + c_title: Label::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.05))), + format!("Editing {} songs", songs.len()), + Color::LIGHT_GRAY, + None, + Vec2::new(0.5, 0.5), + ), + c_scrollbox: ScrollBox::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.05), (1.0, 0.95))), + crate::gui_base::ScrollBoxSizeUnit::Pixels, + EditorForSongElems { + c_title: TextField::new( + GuiElemCfg::default(), + format!( + "Title ({})", + songs + .iter() + .enumerate() + .map(|(i, s)| format!( + "{}{}", + if i == 0 { "" } else { ", " }, + s.title + )) + .collect::() + ), + color_scale(Color::MAGENTA, 0.6, 0.6, 0.6, Some(0.75)), + Color::MAGENTA, + ), + c_artist: EditorForSongArtistChooser::new(sender.clone()), + c_album: Label::new( + GuiElemCfg::default(), + format!("(todo...)"), + Color::GRAY, + None, + Vec2::new(0.0, 0.5), + ), + }, + vec![], + ELEM_HEIGHT, + ), + c_buttons: Panel::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (1.0, 1.0))), + [ + { + let sender = sender.clone(); + Button::new( + GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.5, 1.0))), + move |_| { + sender.send(Event::Close).unwrap(); + vec![] + }, + [Label::new( + GuiElemCfg::default(), + "Close".to_owned(), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + ) + }, + { + let sender = sender.clone(); + Button::new( + GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (1.0, 1.0))), + move |_| { + sender.send(Event::Apply).unwrap(); + vec![] + }, + [Label::new( + GuiElemCfg::default(), + "Apply".to_owned(), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + ) + }, + ], + ), + c_background: Panel::with_background(GuiElemCfg::default(), (), Color::BLACK), + created: Some(Instant::now()), + songs, + event_sender: sender, + event_recv: recv, + } + } +} + +impl GuiElem for EditorForSongs { + fn children(&mut self) -> Box + '_> { + Box::new( + [ + self.c_title.elem_mut(), + self.c_scrollbox.elem_mut(), + self.c_buttons.elem_mut(), + self.c_background.elem_mut(), + ] + .into_iter(), + ) + } + fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { + loop { + match self.event_recv.try_recv() { + Ok(e) => match e { + Event::Close => info.actions.push(GuiAction::Do(Box::new(|gui| { + gui.gui.c_editing_songs = None; + gui.gui.set_normal_ui_enabled(true); + }))), + Event::Apply => eprintln!("TODO: Apply"), + Event::SetArtist(name, id) => { + self.c_scrollbox.children.c_artist.chosen_id = id; + self.c_scrollbox.children.c_artist.last_search = name.to_lowercase(); + self.c_scrollbox.children.c_artist.open_prog.target = 1.0; + *self + .c_scrollbox + .children + .c_artist + .c_name + .c_input + .content + .text() = name; + self.c_scrollbox.children.c_artist.config_mut().redraw = true; + } + }, + Err(_) => break, + } + } + // animation + if let Some(created) = &self.created { + if let Some(h) = &info.helper { + h.request_redraw(); + } + let open_prog = created.elapsed().as_secs_f32() / 0.5; + if open_prog >= 1.0 { + self.created = None; + self.config.pos = Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0)); + info.actions.push(GuiAction::Do(Box::new(|gui| { + gui.gui.set_normal_ui_enabled(false); + }))); + } else { + let offset = 1.0 - open_prog; + let offset = offset * offset; + self.config.pos = Rectangle::from_tuples((0.0, offset), (1.0, 1.0 + offset)); + } + } + // artist sel + if self + .c_scrollbox + .children + .c_artist + .open_prog + .update(Instant::now(), false) + { + if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) { + *v = ELEM_HEIGHT * self.c_scrollbox.children.c_artist.open_prog.value; + self.c_scrollbox.config_mut().redraw = true; + } + if let Some(h) = &info.helper { + h.request_redraw(); + } + } + } + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn elem(&self) -> &dyn GuiElem { + self + } + fn elem_mut(&mut self) -> &mut dyn GuiElem { + self + } +} + +pub struct EditorForSongArtistChooser { + config: GuiElemCfg, + event_sender: std::sync::mpsc::Sender, + /// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6) + open_prog: AnimationController, + expand_to: f32, + chosen_id: Option, + c_name: TextField, + c_picker: ScrollBox>>, + last_search: String, +} +impl EditorForSongArtistChooser { + pub fn new(event_sender: std::sync::mpsc::Sender) -> Self { + let expand_to = 7.0; + Self { + config: GuiElemCfg::default(), + event_sender, + open_prog: AnimationController::new(1.0, 1.0, 0.3, 8.0, 0.5, 0.6, Instant::now()), + expand_to, + chosen_id: None, + c_name: TextField::new( + GuiElemCfg::default(), + "artist".to_owned(), + Color::DARK_GRAY, + Color::WHITE, + ), + c_picker: ScrollBox::new( + GuiElemCfg::default().disabled(), + crate::gui_base::ScrollBoxSizeUnit::Pixels, + vec![], + vec![], + ELEM_HEIGHT, + ), + last_search: String::from("\n"), + } + } +} +impl GuiElem for EditorForSongArtistChooser { + fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) { + let picker_enabled = self.open_prog.value > 1.0; + self.c_picker.config_mut().enabled = picker_enabled; + if picker_enabled { + let split = 1.0 / self.open_prog.value; + self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split)); + self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0)); + } else { + self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0)); + } + + let search = self.c_name.c_input.content.get_text().to_lowercase(); + let search_changed = &self.last_search != &search; + if self.config.redraw || search_changed { + *self.c_name.c_input.content.color() = if self.chosen_id.is_some() { + Color::GREEN + } else { + Color::WHITE + }; + if search_changed { + self.chosen_id = None; + self.open_prog.target = self.expand_to; + if search.is_empty() { + self.open_prog.target = 1.0; + } + } + let artists = info + .database + .artists() + .values() + .filter(|artist| artist.name.to_lowercase().contains(&search)) + // .take(self.open_prog.value as _) + .map(|artist| (artist.name.clone(), artist.id)) + .collect::>(); + let chosen_id = self.chosen_id; + self.c_picker.children = artists + .iter() + .map(|a| { + let sender = self.event_sender.clone(); + let name = a.0.clone(); + let id = a.1; + Button::new( + GuiElemCfg::default(), + move |_| { + sender + .send(Event::SetArtist(name.clone(), Some(id))) + .unwrap(); + vec![] + }, + [Label::new( + GuiElemCfg::default(), + a.0.clone(), + if chosen_id.is_some_and(|c| c == a.1) { + Color::WHITE + } else { + Color::LIGHT_GRAY + }, + None, + Vec2::new(0.0, 0.5), + )], + ) + }) + .collect(); + self.c_picker.config_mut().redraw = true; + self.last_search = search; + } + } + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new([self.c_name.elem_mut(), self.c_picker.elem_mut()].into_iter()) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn elem(&self) -> &dyn GuiElem { + self + } + fn elem_mut(&mut self) -> &mut dyn GuiElem { + self + } +} diff --git a/musicdb-client/src/gui_idle_display.rs b/musicdb-client/src/gui_idle_display.rs index 3078171..2947ebc 100644 --- a/musicdb-client/src/gui_idle_display.rs +++ b/musicdb-client/src/gui_idle_display.rs @@ -4,7 +4,7 @@ use musicdb_lib::data::ArtistId; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; use crate::{ - gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage}, + gui::{rect_from_rel, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage}, gui_anim::AnimationController, gui_base::Button, gui_playback::{get_right_x, image_display, CurrentInfo}, @@ -13,24 +13,32 @@ use crate::{ }; pub struct IdleDisplay { - config: GuiElemCfg, + pub config: GuiElemCfg, pub idle_mode: f32, - current_info: CurrentInfo, - current_artist_image: Option<(ArtistId, Option<(String, Option>)>)>, + pub current_info: CurrentInfo, + pub current_artist_image: Option<(ArtistId, Option<(String, Option>)>)>, pub c_idle_exit_hint: Button<[Label; 1]>, - c_top_label: AdvancedLabel, - c_side1_label: AdvancedLabel, - c_side2_label: AdvancedLabel, - c_buttons: PlayPause, - cover_aspect_ratio: AnimationController, - artist_image_aspect_ratio: AnimationController, - cover_left: f32, - cover_top: f32, - cover_bottom: f32, + pub c_top_label: AdvancedLabel, + pub c_side1_label: AdvancedLabel, + pub c_side2_label: AdvancedLabel, + pub c_buttons: PlayPause, + pub c_buttons_custom_pos: bool, + + pub cover_aspect_ratio: AnimationController, + pub artist_image_aspect_ratio: AnimationController, + + pub cover_pos: Option, + pub cover_left: f32, + pub cover_top: f32, + pub cover_bottom: f32, + + pub artist_image_pos: Option, /// 0.0 -> same height as cover, /// 0.5 -> lower half of cover - artist_image_top: f32, - artist_image_to_cover_margin: f32, + pub artist_image_top: f32, + pub artist_image_to_cover_margin: f32, + + pub force_reset_texts: bool, } impl IdleDisplay { @@ -60,6 +68,7 @@ impl IdleDisplay { c_side1_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), c_side2_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), c_buttons: PlayPause::new(GuiElemCfg::default()), + c_buttons_custom_pos: false, cover_aspect_ratio: AnimationController::new( 1.0, 1.0, @@ -78,11 +87,14 @@ impl IdleDisplay { 0.6, Instant::now(), ), + cover_pos: None, cover_left: 0.01, cover_top: 0.21, cover_bottom, + artist_image_pos: None, artist_image_top: 0.5, artist_image_to_cover_margin: 0.01, + force_reset_texts: false, } } } @@ -108,7 +120,7 @@ impl GuiElem for IdleDisplay { ); // update current_info self.current_info.update(info, g); - if self.current_info.new_song { + if self.current_info.new_song || self.force_reset_texts { self.current_info.new_song = false; self.c_top_label.content = if let Some(song) = self.current_info.current_song { info.gui_config @@ -207,6 +219,7 @@ impl GuiElem for IdleDisplay { image_display( g, cover.as_ref(), + self.cover_pos.as_ref().map(|v| rect_from_rel(v, &info.pos)), info.pos.top_left().x + info.pos.height() * self.cover_left, info.pos.top_left().y + info.pos.height() * self.cover_top, info.pos.top_left().y + info.pos.height() * self.cover_bottom, @@ -220,6 +233,9 @@ impl GuiElem for IdleDisplay { image_display( g, img.as_ref(), + self.artist_image_pos + .as_ref() + .map(|v| rect_from_rel(v, &info.pos)), get_right_x( info.pos.top_left().x + info.pos.height() * self.cover_left, top, @@ -265,10 +281,12 @@ impl GuiElem for IdleDisplay { let buttons_right_pos = 0.99; let buttons_width_max = info.pos.height() * 0.08 / 0.3 / info.pos.width(); let buttons_width = buttons_width_max.min(0.2); - self.c_buttons.config_mut().pos = Rectangle::from_tuples( - (buttons_right_pos - buttons_width, 0.86), - (buttons_right_pos, 0.94), - ); + if !self.c_buttons_custom_pos { + self.c_buttons.config_mut().pos = Rectangle::from_tuples( + (buttons_right_pos - buttons_width, 0.86), + (buttons_right_pos, 0.94), + ); + } } } fn config(&self) -> &GuiElemCfg { diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index a35f01f..1110931 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -123,6 +123,7 @@ impl LibraryBrowser { crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], vec![], + 0.0, ); let (do_something_sender, do_something_receiver) = mpsc::channel(); let search_settings_changed = Arc::new(AtomicBool::new(false)); @@ -936,7 +937,7 @@ impl GuiElem for ListArtist { let selected = self.selected.clone(); info.actions.push(GuiAction::Do(Box::new(move |gui| { let q = selected.as_queue( - &gui.gui.c_main_view.children.2, + &gui.gui.c_main_view.children.library_browser, &gui.database.lock().unwrap(), ); gui.exec_gui_action(GuiAction::SetDragging(Some(( @@ -1074,7 +1075,7 @@ impl GuiElem for ListAlbum { let selected = self.selected.clone(); info.actions.push(GuiAction::Do(Box::new(move |gui| { let q = selected.as_queue( - &gui.gui.c_main_view.children.2, + &gui.gui.c_main_view.children.library_browser, &gui.database.lock().unwrap(), ); gui.exec_gui_action(GuiAction::SetDragging(Some(( @@ -1208,7 +1209,7 @@ impl GuiElem for ListSong { let selected = self.selected.clone(); info.actions.push(GuiAction::Do(Box::new(move |gui| { let q = selected.as_queue( - &gui.gui.c_main_view.children.2, + &gui.gui.c_main_view.children.library_browser, &gui.database.lock().unwrap(), ); gui.exec_gui_action(GuiAction::SetDragging(Some(( @@ -1251,6 +1252,31 @@ impl GuiElem for ListSong { } vec![] } + fn mouse_pressed(&mut self, button: MouseButton) -> Vec { + if button == MouseButton::Right { + let id = self.id; + vec![GuiAction::Build(Box::new(move |db| { + if let Some(me) = db.songs().get(&id) { + let me = me.clone(); + vec![GuiAction::ContextMenu(Some(vec![Box::new(Button::new( + GuiElemCfg::default(), + move |_| vec![GuiAction::EditSongs(vec![me.clone()])], + [Label::new( + GuiElemCfg::default(), + format!("Edit"), + Color::WHITE, + None, + Vec2::new_y(0.5), + )], + ))]))] + } else { + vec![] + } + }))] + } else { + vec![] + } + } } struct FilterPanel { @@ -1484,24 +1510,28 @@ impl FilterPanel { ), ), vec![0.0; 10], + 0.0, ); let c_tab_filters_songs = ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))), crate::gui_base::ScrollBoxSizeUnit::Pixels, FilterTab::default(), vec![], + 0.0, ); let c_tab_filters_albums = ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(), crate::gui_base::ScrollBoxSizeUnit::Pixels, FilterTab::default(), vec![], + 0.0, ); let c_tab_filters_artists = ScrollBox::new( GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(), crate::gui_base::ScrollBoxSizeUnit::Pixels, FilterTab::default(), vec![], + 0.0, ); let new_tab = Arc::new(AtomicUsize::new(0)); let set_tab_1 = Arc::clone(&new_tab); diff --git a/musicdb-client/src/gui_playback.rs b/musicdb-client/src/gui_playback.rs index f02c562..3449571 100755 --- a/musicdb-client/src/gui_playback.rs +++ b/musicdb-client/src/gui_playback.rs @@ -147,6 +147,7 @@ impl CurrentInfo { pub fn image_display( g: &mut speedy2d::Graphics2D, img: Option<&ImageHandle>, + pos: Option, left: f32, top: f32, bottom: f32, @@ -155,8 +156,12 @@ pub fn image_display( if let Some(cover) = &img { let cover_size = cover.size(); aspect_ratio.target = if cover_size.x > 0 && cover_size.y > 0 { - let right_x = get_right_x(left, top, bottom, aspect_ratio.value); - let pos = Rectangle::from_tuples((left, top), (right_x, bottom)); + let pos = if let Some(pos) = pos { + pos + } else { + let right_x = get_right_x(left, top, bottom, aspect_ratio.value); + Rectangle::from_tuples((left, top), (right_x, bottom)) + }; let aspect_ratio = cover_size.x as f32 / cover_size.y as f32; g.draw_rectangle_image(pos, cover); aspect_ratio diff --git a/musicdb-client/src/gui_queue.rs b/musicdb-client/src/gui_queue.rs index f20aa4d..5cf4689 100755 --- a/musicdb-client/src/gui_queue.rs +++ b/musicdb-client/src/gui_queue.rs @@ -96,6 +96,7 @@ impl QueueViewer { crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], vec![], + 0.0, ), c_empty_space_drag_handler: QueueEmptySpaceDragHandler::new(GuiElemCfg::at( Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)), diff --git a/musicdb-client/src/gui_screen.rs b/musicdb-client/src/gui_screen.rs index ddf28e9..07dbbe3 100755 --- a/musicdb-client/src/gui_screen.rs +++ b/musicdb-client/src/gui_screen.rs @@ -4,9 +4,10 @@ use musicdb_lib::{data::queue::QueueContent, server::Command}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; use crate::{ - gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg}, + gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_anim::AnimationController, gui_base::{Button, Panel}, + gui_edit_song::EditorForSongs, gui_idle_display::IdleDisplay, gui_library::LibraryBrowser, gui_notif::NotifOverlay, @@ -37,17 +38,12 @@ pub fn transition(p: f32) -> f32 { pub struct GuiScreen { config: GuiElemCfg, - c_notif_overlay: NotifOverlay, - c_idle_display: IdleDisplay, - c_status_bar: StatusBar, + pub c_notif_overlay: NotifOverlay, + pub c_idle_display: IdleDisplay, + pub c_editing_songs: Option, + pub c_status_bar: StatusBar, pub c_settings: Settings, - pub c_main_view: Panel<( - Button<[Label; 1]>, - Button<[Label; 1]>, - LibraryBrowser, - QueueViewer, - Button<[Label; 1]>, - )>, + pub c_main_view: Panel, pub c_context_menu: Option>, pub idle: AnimationController, // pub settings: (bool, Option), @@ -57,6 +53,30 @@ pub struct GuiScreen { pub prev_mouse_pos: Vec2, pub hotkey: Hotkey, } +pub struct MainView { + pub button_clear_queue: Button<[Label; 1]>, + pub button_settings: Button<[Label; 1]>, + pub button_exit: Button<[Label; 1]>, + pub library_browser: LibraryBrowser, + pub queue_viewer: QueueViewer, +} +impl GuiElemChildren for MainView { + fn iter(&mut self) -> Box + '_> { + Box::new( + [ + self.button_clear_queue.elem_mut(), + self.button_settings.elem_mut(), + self.button_exit.elem_mut(), + self.library_browser.elem_mut(), + self.queue_viewer.elem_mut(), + ] + .into_iter(), + ) + } + fn len(&self) -> usize { + 5 + } +} impl GuiScreen { pub fn new( config: GuiElemCfg, @@ -74,6 +94,7 @@ impl GuiScreen { (0.0, 0.9), (1.0, 1.0), ))), + c_editing_songs: None, c_idle_display: IdleDisplay::new(GuiElemCfg::default().disabled()), c_settings: Settings::new( GuiElemCfg::default().disabled(), @@ -85,38 +106,8 @@ impl GuiScreen { ), c_main_view: Panel::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))), - ( - Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (0.875, 0.03))), - |_| vec![GuiAction::OpenSettings(true)], - [Label::new( - GuiElemCfg::default(), - "Settings".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - )], - ), - Button::new( - GuiElemCfg::at(Rectangle::from_tuples((0.875, 0.0), (1.0, 0.03))), - |_| vec![GuiAction::Exit], - [Label::new( - GuiElemCfg::default(), - "Exit".to_string(), - Color::WHITE, - None, - Vec2::new(0.5, 0.5), - )], - ), - LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples( - (0.0, 0.0), - (0.5, 1.0), - ))), - QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples( - (0.5, 0.03), - (1.0, 1.0), - ))), - Button::new( + MainView { + button_clear_queue: Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), |_| { vec![GuiAction::SendToServer( @@ -139,7 +130,37 @@ impl GuiScreen { Vec2::new(0.5, 0.5), )], ), - ), + button_settings: Button::new( + GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (0.875, 0.03))), + |_| vec![GuiAction::OpenSettings(true)], + [Label::new( + GuiElemCfg::default(), + "Settings".to_string(), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + ), + button_exit: Button::new( + GuiElemCfg::at(Rectangle::from_tuples((0.875, 0.0), (1.0, 0.03))), + |_| vec![GuiAction::Exit], + [Label::new( + GuiElemCfg::default(), + "Exit".to_string(), + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + )], + ), + library_browser: LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples( + (0.0, 0.0), + (0.5, 1.0), + ))), + queue_viewer: QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples( + (0.5, 0.03), + (1.0, 1.0), + ))), + }, ), c_context_menu: None, hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape), @@ -173,6 +194,9 @@ impl GuiScreen { 0.0 } } + pub fn force_idle(&mut self) { + self.idle.target = 1.0; + } pub fn not_idle(&mut self) { self.last_interaction = Instant::now(); if self.idle.target > 0.0 { @@ -197,6 +221,12 @@ impl GuiScreen { } } } + + pub fn set_normal_ui_enabled(&mut self, enabled: bool) { + self.c_status_bar.config_mut().enabled = enabled; + // self.c_settings.config_mut().enabled = enabled; + self.c_main_view.config_mut().enabled = enabled; + } } impl GuiElem for GuiScreen { fn config(&self) -> &GuiElemCfg { @@ -207,15 +237,19 @@ impl GuiElem for GuiScreen { } fn children(&mut self) -> Box + '_> { Box::new( - [ - self.c_notif_overlay.elem_mut(), - self.c_idle_display.elem_mut(), - self.c_status_bar.elem_mut(), - self.c_settings.elem_mut(), - self.c_main_view.elem_mut(), - ] - .into_iter() - .chain(self.c_context_menu.as_mut().map(|v| v.elem_mut())), + self.c_context_menu.iter_mut().map(|v| v.elem_mut()).chain( + [ + self.c_notif_overlay.elem_mut(), + self.c_idle_display.elem_mut(), + ] + .into_iter() + .chain(self.c_editing_songs.as_mut().map(|v| v.elem_mut())) + .chain([ + self.c_status_bar.elem_mut(), + self.c_settings.elem_mut(), + self.c_main_view.elem_mut(), + ]), + ), ) } fn any(&self) -> &dyn std::any::Any { @@ -301,9 +335,7 @@ impl GuiElem for GuiScreen { // animations: idle if idle_changed { let enable_normal_ui = self.idle.value < 1.0; - self.c_main_view.config_mut().enabled = enable_normal_ui; - // self.c_settings.config_mut().enabled = enable_normal_ui; - self.c_status_bar.config_mut().enabled = enable_normal_ui; + self.set_normal_ui_enabled(enable_normal_ui); if let Some(h) = &info.helper { h.set_cursor_visible(enable_normal_ui); } diff --git a/musicdb-client/src/gui_settings.rs b/musicdb-client/src/gui_settings.rs index 77d3182..788164a 100755 --- a/musicdb-client/src/gui_settings.rs +++ b/musicdb-client/src/gui_settings.rs @@ -34,6 +34,7 @@ impl Settings { scroll_sensitivity_pages, ), vec![], + 0.0, ), c_background: Panel::with_background(GuiElemCfg::default().w_mouse(), (), Color::BLACK), } diff --git a/musicdb-client/src/gui_statusbar.rs b/musicdb-client/src/gui_statusbar.rs index 29b95d6..b4a5f1a 100644 --- a/musicdb-client/src/gui_statusbar.rs +++ b/musicdb-client/src/gui_statusbar.rs @@ -1,11 +1,10 @@ use std::time::Instant; -use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; +use speedy2d::{dimen::Vec2, shape::Rectangle}; use crate::{ gui::{DrawInfo, GuiElem, GuiElemCfg}, gui_anim::AnimationController, - gui_base::Panel, gui_playback::{image_display, CurrentInfo}, gui_playpause::PlayPause, gui_text::AdvancedLabel, @@ -17,6 +16,7 @@ pub struct StatusBar { current_info: CurrentInfo, cover_aspect_ratio: AnimationController, c_song_label: AdvancedLabel, + pub force_reset_texts: bool, c_buttons: PlayPause, } @@ -36,6 +36,7 @@ impl StatusBar { Instant::now(), ), c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), + force_reset_texts: false, c_buttons: PlayPause::new(GuiElemCfg::default()), } } @@ -47,7 +48,7 @@ impl GuiElem for StatusBar { } fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { self.current_info.update(info, g); - if self.current_info.new_song { + if self.current_info.new_song || self.force_reset_texts { self.current_info.new_song = false; self.c_song_label.content = if let Some(song) = self.current_info.current_song { info.gui_config @@ -101,6 +102,7 @@ impl GuiElem for StatusBar { image_display( g, cover.as_ref(), + None, info.pos.top_left().x + info.pos.height() * 0.05, info.pos.top_left().y + info.pos.height() * 0.05, info.pos.top_left().y + info.pos.height() * 0.95, diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index 1f51787..ab74097 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -10,21 +10,24 @@ use std::{ use clap::{Parser, Subcommand}; #[cfg(feature = "speedy2d")] use gui::GuiEvent; +#[cfg(feature = "playback")] +use musicdb_lib::player::Player; use musicdb_lib::{ data::{ database::{ClientIo, Database}, CoverId, SongId, }, load::ToFromBytes, - player::Player, server::{get, Command}, }; +use speedy2d::color::Color; #[cfg(feature = "speedy2d")] mod gui; #[cfg(feature = "speedy2d")] mod gui_anim; #[cfg(feature = "speedy2d")] mod gui_base; +mod gui_edit_song; #[cfg(feature = "speedy2d")] mod gui_idle_display; #[cfg(feature = "speedy2d")] @@ -47,6 +50,8 @@ mod gui_statusbar; mod gui_text; #[cfg(feature = "speedy2d")] mod gui_wrappers; +#[cfg(feature = "merscfg")] +mod merscfg; #[cfg(feature = "speedy2d")] mod textcfg; @@ -65,8 +70,10 @@ enum Mode { /// graphical user interface Gui, /// play in sync with the server, but load the songs from a local copy of the lib-dir + #[cfg(feature = "playback")] SyncplayerLocal { lib_dir: PathBuf }, /// play in sync with the server, and fetch the songs from it too. slower than the local variant for obvious reasons + #[cfg(feature = "playback")] SyncplayerNetwork, } @@ -97,23 +104,31 @@ fn main() { let mut con = con.try_clone().unwrap(); // this is all you need to keep the db in sync thread::spawn(move || { + #[cfg(feature = "playback")] let mut player = if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) { Some(Player::new().unwrap().without_sending_commands()) } else { None }; - if let Mode::SyncplayerLocal { lib_dir } = mode { - let mut db = database.lock().unwrap(); - db.lib_directory = lib_dir; - } else { + #[allow(unused_labels)] + 'ifstatementworkaround: { + // use if+break instead of if-else because we can't #[cfg(feature)] the if statement, + // since we want the else part to run if the feature is disabled + #[cfg(feature = "playback")] + if let Mode::SyncplayerLocal { lib_dir } = mode { + let mut db = database.lock().unwrap(); + db.lib_directory = lib_dir; + break 'ifstatementworkaround; + } let mut db = database.lock().unwrap(); let client_con: Box = Box::new(TcpStream::connect(addr).unwrap()); db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new( musicdb_lib::server::get::Client::new(BufReader::new(client_con)).unwrap(), ))); - }; + } loop { + #[cfg(feature = "playback")] if let Some(player) = &mut player { let mut db = database.lock().unwrap(); if db.is_client_init() { @@ -121,6 +136,7 @@ fn main() { } } let update = Command::from_bytes(&mut con).unwrap(); + #[cfg(feature = "playback")] if let Some(player) = &mut player { player.handle_command(&update); } @@ -154,6 +170,7 @@ fn main() { ) }; } + #[cfg(feature = "playback")] Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork => { con_thread.join().unwrap(); } @@ -180,3 +197,7 @@ fn get_cover(song: SongId, database: &Database) -> Option { database.albums().get(song.album.as_ref()?)?.cover } } + +pub(crate) fn color_scale(c: Color, r: f32, g: f32, b: f32, new_alpha: Option) -> Color { + Color::from_rgba(c.r() * r, c.g() * g, c.b() * b, new_alpha.unwrap_or(c.a())) +} diff --git a/musicdb-client/src/merscfg.rs b/musicdb-client/src/merscfg.rs new file mode 100644 index 0000000..13d5ad2 --- /dev/null +++ b/musicdb-client/src/merscfg.rs @@ -0,0 +1,849 @@ +use std::{ + path::PathBuf, + sync::{ + atomic::{AtomicBool, AtomicU8}, + mpsc::Sender, + Arc, Mutex, RwLock, + }, + time::Duration, +}; + +use mers_lib::{ + data::{Data, MersType, Type}, + errors::CheckError, + prelude_compile::CompInfo, +}; +use musicdb_lib::{data::database::Database, server::Command}; +use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::UserEventSender}; + +use crate::{ + gui::{Gui, GuiAction, GuiConfig, GuiElem, GuiElemCfg, GuiEvent}, + gui_base::Panel, + gui_notif::{NotifInfo, NotifOverlay}, + gui_text::Label, + textcfg::TextBuilder, +}; + +pub struct OptFunc(Option); +impl OptFunc { + pub fn none() -> Self { + Self(None) + } + pub fn some(func: mers_lib::data::function::Function) -> Self { + Self(Some(func)) + } + fn run(&self) { + if let Some(func) = &self.0 { + func.run(Data::empty_tuple()); + } + } +} + +/// mers code must return an object `{}` with hook functions. +/// All hook functions will be called with `()` as their argument, +/// and their return value will be ignored. +/// +/// Values: +/// - `is_playing` +/// - `is_idle` +/// - `window_size_in_pixels` +/// - `idle_screen_cover_aspect_ratio` +/// +/// Functions: +/// - `idle_start` +/// - `idle_stop` +/// - `idle_prevent` +/// - `send_notification` +/// - `set_idle_screen_cover_pos` +/// - `set_idle_screen_artist_image_pos` +/// - `set_idle_screen_top_text_pos` +/// - `set_idle_screen_side_text_1_pos` +/// - `set_idle_screen_side_text_2_pos` +/// - `set_statusbar_text_format` +/// - `set_idle_screen_top_text_format` +/// - `set_idle_screen_side_text_1_format` +/// - `set_idle_screen_side_text_2_format` +pub struct MersCfg { + pub source_file: PathBuf, + // - - handler functions - - + pub func_before_draw: OptFunc, + pub func_library_updated: OptFunc, + pub func_queue_updated: OptFunc, + // - - globals that aren't functions - - + pub var_is_playing: Arc>, + pub var_is_idle: Arc>, + pub var_window_size_in_pixels: Arc>, + pub var_idle_screen_cover_aspect_ratio: Arc>, + // - - results from running functions - - + pub updated_playing_status: Arc, + pub updated_idle_status: Arc, + pub updated_idle_screen_cover_pos: Arc>>, + pub updated_idle_screen_artist_image_pos: Arc>>, + pub updated_idle_screen_top_text_pos: Arc>, + pub updated_idle_screen_side_text_1_pos: Arc>, + pub updated_idle_screen_side_text_2_pos: Arc>, + pub updated_idle_screen_playback_buttons_pos: Arc>, + pub updated_statusbar_text_format: Arc>, + pub updated_idle_screen_top_text_format: Arc>, + pub updated_idle_screen_side_text_1_format: Arc>, + pub updated_idle_screen_side_text_2_format: Arc>, +} + +impl MersCfg { + pub fn new(path: PathBuf) -> Self { + Self { + source_file: path, + + func_before_draw: OptFunc::none(), + func_library_updated: OptFunc::none(), + func_queue_updated: OptFunc::none(), + + var_is_playing: Arc::new(RwLock::new(Data::new(mers_lib::data::bool::Bool(false)))), + var_is_idle: Arc::new(RwLock::new(Data::new(mers_lib::data::bool::Bool(false)))), + var_window_size_in_pixels: Arc::new(RwLock::new(Data::new( + mers_lib::data::tuple::Tuple(vec![ + Data::new(mers_lib::data::int::Int(0)), + Data::new(mers_lib::data::int::Int(0)), + ]), + ))), + var_idle_screen_cover_aspect_ratio: Arc::new(RwLock::new(Data::new( + mers_lib::data::float::Float(0.0), + ))), + + updated_playing_status: Arc::new(AtomicU8::new(0)), + updated_idle_status: Arc::new(AtomicU8::new(0)), + updated_idle_screen_cover_pos: Arc::new(Updatable::new()), + updated_idle_screen_artist_image_pos: Arc::new(Updatable::new()), + updated_idle_screen_top_text_pos: Arc::new(Updatable::new()), + updated_idle_screen_side_text_1_pos: Arc::new(Updatable::new()), + updated_idle_screen_side_text_2_pos: Arc::new(Updatable::new()), + updated_idle_screen_playback_buttons_pos: Arc::new(Updatable::new()), + updated_statusbar_text_format: Arc::new(Updatable::new()), + updated_idle_screen_top_text_format: Arc::new(Updatable::new()), + updated_idle_screen_side_text_1_format: Arc::new(Updatable::new()), + updated_idle_screen_side_text_2_format: Arc::new(Updatable::new()), + } + } + fn custom_globals( + &self, + cfg: mers_lib::prelude_extend_config::Config, + event_sender: Arc>, + notif_sender: Sender< + Box (Box, NotifInfo) + Send>, + >, + ) -> mers_lib::prelude_extend_config::Config { + cfg.add_var_arc( + "is_playing".to_owned(), + Arc::clone(&self.var_is_playing), + self.var_is_playing.read().unwrap().get().as_type(), + ) + .add_var_arc( + "is_idle".to_owned(), + Arc::clone(&self.var_is_idle), + self.var_is_idle.read().unwrap().get().as_type(), + ) + .add_var_arc( + "window_size_in_pixels".to_owned(), + Arc::clone(&self.var_window_size_in_pixels), + self.var_window_size_in_pixels.read().unwrap().get().as_type(), + ) + .add_var_arc( + "idle_screen_cover_aspect_ratio".to_owned(), + Arc::clone(&self.var_idle_screen_cover_aspect_ratio), + self.var_idle_screen_cover_aspect_ratio.read().unwrap().get().as_type(), + ) + .add_var("playback_resume".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_playing_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `playback_resume` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(1, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("playback_pause".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_playing_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `playback_pause` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(2, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("playback_stop".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_playing_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `playback_stop` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(3, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("idle_start".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_idle_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `idle_start` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(1, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("idle_stop".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_idle_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `idle_stop` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(2, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("idle_prevent".to_owned(),{ + let es = event_sender.clone(); + let v = Arc::clone(&self.updated_idle_status); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_zero_tuple() { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `idle_prevent` with argument of type `{a}` (must be `()`).").into()) + } + }), + run: Arc::new(move |_, _| { + v.store(3, std::sync::atomic::Ordering::Relaxed); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("send_notification".to_owned(),{ + let es = event_sender.clone(); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::tuple::TupleT(vec![ + mers_lib::data::Type::new(mers_lib::data::string::StringT), + mers_lib::data::Type::new(mers_lib::data::string::StringT), + mers_lib::data::Type::newm(vec![ + Arc::new(mers_lib::data::int::IntT), + Arc::new(mers_lib::data::float::FloatT) + ]), + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `send_notification` with argument of type `{a}` (must be `String`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let t = &a.as_any().downcast_ref::().unwrap().0; + let title = t[0].get().as_any().downcast_ref::().unwrap().0.clone(); + let text = t[1].get().as_any().downcast_ref::().unwrap().0.clone(); + let t = t[2].get(); + let duration = t.as_any().downcast_ref::().map(|s| Duration::from_secs(s.0.max(0) as _)).unwrap_or_else(|| Duration::from_secs_f64(t.as_any().downcast_ref::().unwrap().0)); + notif_sender + .send(Box::new(move |_| { + ( + Box::new(Panel::with_background( + GuiElemCfg::default(), + ( + Label::new( + GuiElemCfg::at(Rectangle::from_tuples( + (0.25, 0.0), + (0.75, 0.5), + )), + title, + Color::WHITE, + None, + Vec2::new(0.5, 0.0), + ), + Label::new( + GuiElemCfg::at(Rectangle::from_tuples( + (0.0, 0.5), + (1.0, 1.0), + )), + text, + Color::WHITE, + None, + Vec2::new(0.5, 1.0), + ), + ), + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + )), + NotifInfo::new(duration), + ) + })) + .unwrap(); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("set_idle_screen_cover_pos".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_idle_screen_cover_pos); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::tuple::TupleT(vec![ + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + ])) + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `set_idle_screen_cover_pos` with argument of type `{a}` (must be `()` or `(Float, Float, Float, Float)`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let mut vals = a.as_any().downcast_ref::().unwrap().0.iter().map(|v| v.get().as_any().downcast_ref::().unwrap().0); + update.update( + if vals.len() >= 4 { + Some(Rectangle::from_tuples((vals.next().unwrap() as _, vals.next().unwrap() as _), (vals.next().unwrap() as _, vals.next().unwrap() as _))) + } else { None }); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }).add_var("set_idle_screen_artist_image_pos".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_idle_screen_artist_image_pos); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::tuple::TupleT(vec![ + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + ])) + ])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `set_idle_screen_artist_image_pos` with argument of type `{a}` (must be `()` or `(Float, Float, Float, Float)`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let mut vals = a.as_any().downcast_ref::().unwrap().0.iter().map(|v| v.get().as_any().downcast_ref::().unwrap().0); + update.update( + if vals.len() >= 4 { + Some(Rectangle::from_tuples((vals.next().unwrap() as _, vals.next().unwrap() as _), (vals.next().unwrap() as _, vals.next().unwrap() as _))) + } else { None }); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) + }) + .add_var("set_idle_screen_top_text_pos".to_owned(), gen_set_pos_func("set_idle_screen_top_text_pos", Arc::clone(&event_sender), Arc::clone(&self.updated_idle_screen_top_text_pos))) + .add_var("set_idle_screen_side_text_1_pos".to_owned(), gen_set_pos_func("set_idle_screen_side_text_1_pos", Arc::clone(&event_sender), Arc::clone(&self.updated_idle_screen_side_text_1_pos))) + .add_var("set_idle_screen_side_text_2_pos".to_owned(), gen_set_pos_func("set_idle_screen_side_text_2_pos", Arc::clone(&event_sender), Arc::clone(&self.updated_idle_screen_side_text_2_pos))) + .add_var("set_idle_screen_playback_buttons_pos".to_owned(), gen_set_pos_func("set_idle_screen_playback_buttons_pos", Arc::clone(&event_sender), Arc::clone(&self.updated_idle_screen_playback_buttons_pos))) + + .add_var("set_statusbar_text_format".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_statusbar_text_format); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::string::StringT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::string::StringT), + ])) + } else { + Err(format!("Can't call `set_statusbar_text_format` with argument of type `{a}` (must be `String`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let o = match a.as_any().downcast_ref::().unwrap().0.parse() { + Ok(v) => { + update.update(v); + Data::empty_tuple() + } + Err(e) => mers_lib::data::Data::new(mers_lib::data::string::String(e.to_string())), + }; + es.send_event(GuiEvent::Refresh).unwrap(); + o + }), + inner_statements: None, + }) + }) + .add_var("set_idle_screen_top_text_format".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_idle_screen_top_text_format); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::string::StringT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::string::StringT), + ])) + } else { + Err(format!("Can't call `set_idle_screen_top_text_format` with argument of type `{a}` (must be `String`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let o = match a.as_any().downcast_ref::().unwrap().0.parse() { + Ok(v) => { + update.update(v); + Data::empty_tuple() + } + Err(e) => mers_lib::data::Data::new(mers_lib::data::string::String(e.to_string())), + }; + es.send_event(GuiEvent::Refresh).unwrap(); + o + }), + inner_statements: None, + }) + }).add_var("set_idle_screen_side_text_1_format".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_idle_screen_side_text_1_format); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::string::StringT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::string::StringT), + ])) + } else { + Err(format!("Can't call `set_idle_screen_side_text_1_format` with argument of type `{a}` (must be `String`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let o = match a.as_any().downcast_ref::().unwrap().0.parse() { + Ok(v) => { + update.update(v); + Data::empty_tuple() + } + Err(e) => mers_lib::data::Data::new(mers_lib::data::string::String(e.to_string())), + }; + es.send_event(GuiEvent::Refresh).unwrap(); + o + }), + inner_statements: None, + }) + }).add_var("set_idle_screen_side_text_2_format".to_owned(),{ + let es = event_sender.clone(); + let update = Arc::clone(&self.updated_idle_screen_side_text_2_format); + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(|a, _| { + if a.is_included_in(&mers_lib::data::string::StringT) { + Ok(Type::newm(vec![ + Arc::new(mers_lib::data::tuple::TupleT(vec![])), + Arc::new(mers_lib::data::string::StringT), + ])) + } else { + Err(format!("Can't call `set_idle_screen_side_text_2_format` with argument of type `{a}` (must be `String`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let o = match a.as_any().downcast_ref::().unwrap().0.parse() { + Ok(v) => { + update.update(v); + Data::empty_tuple() + } + Err(e) => mers_lib::data::Data::new(mers_lib::data::string::String(e.to_string())), + }; + es.send_event(GuiEvent::Refresh).unwrap(); + o + }), + inner_statements: None, + }) + }) + // .add_type("Song".to_owned(), Ok(Arc::new(mers_lib::data::object::ObjectT(vec![ + // ("id".to_owned(), Type::new(mers_lib::data::int::IntT)), + // ("title".to_owned(), Type::new(mers_lib::data::string::StringT)), + // ("album".to_owned(), Type::new(mers_lib::data::string::StringT)), + // ("artist".to_owned(), Type::new(mers_lib::data::string::StringT)), + // ])))) + } + + pub fn run( + gui_cfg: &mut GuiConfig, + gui: &mut Gui, + mut db: Option<&mut Database>, + run: impl FnOnce(&Self) -> &OptFunc, + ) { + // prepare vars + if let Some(db) = &mut db { + *gui_cfg.merscfg.var_is_playing.write().unwrap() = + mers_lib::data::Data::new(mers_lib::data::bool::Bool(db.playing)); + } + *gui_cfg.merscfg.var_window_size_in_pixels.write().unwrap() = + mers_lib::data::Data::new(mers_lib::data::tuple::Tuple(vec![ + mers_lib::data::Data::new(mers_lib::data::int::Int(gui.size.x as _)), + mers_lib::data::Data::new(mers_lib::data::int::Int(gui.size.y as _)), + ])); + *gui_cfg + .merscfg + .var_idle_screen_cover_aspect_ratio + .write() + .unwrap() = mers_lib::data::Data::new(mers_lib::data::float::Float( + gui.gui.c_idle_display.cover_aspect_ratio.value as _, + )); + + // run + run(&gui_cfg.merscfg).run(); + + // apply updates + + match gui_cfg + .merscfg + .updated_playing_status + .load(std::sync::atomic::Ordering::Relaxed) + { + 0 => {} + v => { + match v { + 1 => gui.exec_gui_action(GuiAction::SendToServer(Command::Resume)), + 2 => gui.exec_gui_action(GuiAction::SendToServer(Command::Pause)), + 3 => gui.exec_gui_action(GuiAction::SendToServer(Command::Stop)), + _ => {} + } + gui_cfg + .merscfg + .updated_playing_status + .store(0, std::sync::atomic::Ordering::Relaxed); + } + } + + match gui_cfg + .merscfg + .updated_idle_status + .load(std::sync::atomic::Ordering::Relaxed) + { + 0 => {} + v => { + match v { + 1 => gui.gui.force_idle(), + 2 => gui.gui.unidle(), + 3 => gui.gui.not_idle(), + _ => {} + } + gui_cfg + .merscfg + .updated_idle_status + .store(0, std::sync::atomic::Ordering::Relaxed); + } + } + + if let Some(maybe_rect) = gui_cfg.merscfg.updated_idle_screen_cover_pos.take_val() { + gui.gui.c_idle_display.cover_pos = maybe_rect; + } + if let Some(maybe_rect) = gui_cfg + .merscfg + .updated_idle_screen_artist_image_pos + .take_val() + { + gui.gui.c_idle_display.artist_image_pos = maybe_rect; + } + if let Some(maybe_rect) = gui_cfg.merscfg.updated_idle_screen_top_text_pos.take_val() { + gui.gui.c_idle_display.c_top_label.config_mut().pos = maybe_rect; + } + if let Some(maybe_rect) = gui_cfg + .merscfg + .updated_idle_screen_side_text_1_pos + .take_val() + { + gui.gui.c_idle_display.c_side1_label.config_mut().pos = maybe_rect; + } + if let Some(maybe_rect) = gui_cfg + .merscfg + .updated_idle_screen_side_text_2_pos + .take_val() + { + gui.gui.c_idle_display.c_side2_label.config_mut().pos = maybe_rect; + } + if let Some(maybe_rect) = gui_cfg + .merscfg + .updated_idle_screen_playback_buttons_pos + .take_val() + { + gui.gui.c_idle_display.c_buttons.config_mut().pos = maybe_rect; + gui.gui.c_idle_display.c_buttons_custom_pos = true; + } + if let Some(fmt) = gui_cfg.merscfg.updated_statusbar_text_format.take_val() { + gui_cfg.status_bar_text = fmt; + gui.gui.c_status_bar.force_reset_texts = true; + } + if let Some(fmt) = gui_cfg + .merscfg + .updated_idle_screen_top_text_format + .take_val() + { + gui_cfg.idle_top_text = fmt; + gui.gui.c_idle_display.force_reset_texts = true; + } + if let Some(fmt) = gui_cfg + .merscfg + .updated_idle_screen_side_text_1_format + .take_val() + { + gui_cfg.idle_side1_text = fmt; + gui.gui.c_idle_display.force_reset_texts = true; + } + if let Some(fmt) = gui_cfg + .merscfg + .updated_idle_screen_side_text_2_format + .take_val() + { + gui_cfg.idle_side2_text = fmt; + gui.gui.c_idle_display.force_reset_texts = true; + } + } + + pub fn load( + &mut self, + event_sender: Arc>, + notif_sender: Sender< + Box (Box, NotifInfo) + Send>, + >, + ) -> std::io::Result)>, CheckError>> { + let src = mers_lib::prelude_compile::Source::new_from_file(self.source_file.clone())?; + Ok(self.load2(src, event_sender, notif_sender)) + } + fn load2( + &mut self, + mut src: mers_lib::prelude_compile::Source, + event_sender: Arc>, + notif_sender: Sender< + Box (Box, NotifInfo) + Send>, + >, + ) -> Result)>, CheckError> { + let srca = Arc::new(src.clone()); + let (mut i1, mut i2, mut i3) = self + .custom_globals( + mers_lib::prelude_extend_config::Config::new().bundle_std(), + event_sender, + notif_sender, + ) + .infos(); + let compiled = mers_lib::prelude_compile::parse(&mut src, &srca)? + .compile(&mut i1, CompInfo::default())?; + let _ = compiled.check(&mut i3, None)?; + let out = compiled.run(&mut i2); + Ok(self.load3(out)) + } + fn load3(&mut self, out: mers_lib::data::Data) -> Result<(), (String, Option)> { + if let Some(obj) = out + .get() + .as_any() + .downcast_ref::() + { + for (name, val) in obj.0.iter() { + let name = name.as_str(); + match name { + "before_draw" => { + self.func_before_draw = OptFunc::some(check_handler(name, val)?); + } + "library_updated" => { + self.func_library_updated = OptFunc::some(check_handler(name, val)?); + } + "queue_updated" => { + self.func_queue_updated = OptFunc::some(check_handler(name, val)?); + } + name => { + eprintln!("merscfg: ignoring unexpected field named '{name}'.") + } + } + } + } else { + return Err((format!("mers config file must return an object!"), None)); + } + Ok(()) + } +} + +fn check_handler( + name: &str, + val: &mers_lib::data::Data, +) -> Result)> { + if let Some(func) = val + .get() + .as_any() + .downcast_ref::() + { + match func.check(&Type::empty_tuple()) { + Ok(_) => Ok(func.clone()), + Err(e) => Err((format!("Function '{name}' causes an error:"), Some(e))), + } + } else { + Err((format!("Expected a function for field '{name}'!"), None)) + } +} + +fn gen_set_pos_func( + name: &'static str, + es: Arc>, + update: Arc>, +) -> Data { + Data::new(mers_lib::data::function::Function { + info: Arc::new(mers_lib::info::Info::neverused()), + info_check: Arc::new(Mutex::new(mers_lib::info::Info::neverused())), + out: Arc::new(move |a, _| { + if a.is_included_in(&mers_lib::data::Type::newm(vec![Arc::new( + mers_lib::data::tuple::TupleT(vec![ + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + mers_lib::data::Type::new(mers_lib::data::float::FloatT), + ]), + )])) { + Ok(Type::empty_tuple()) + } else { + Err(format!("Can't call `{name}` with argument of type `{a}` (must be `(Float, Float, Float, Float)`).").into()) + } + }), + run: Arc::new(move |a, _| { + let a = a.get(); + let mut vals = a + .as_any() + .downcast_ref::() + .unwrap() + .0 + .iter() + .map(|v| { + v.get() + .as_any() + .downcast_ref::() + .unwrap() + .0 + }); + update.update(Rectangle::from_tuples( + (vals.next().unwrap() as _, vals.next().unwrap() as _), + (vals.next().unwrap() as _, vals.next().unwrap() as _), + )); + es.send_event(GuiEvent::Refresh).unwrap(); + Data::empty_tuple() + }), + inner_statements: None, + }) +} + +pub struct Updatable { + updated: AtomicBool, + value: Mutex>, +} +impl Updatable { + pub fn new() -> Self { + Self { + updated: AtomicBool::new(false), + value: Mutex::new(None), + } + } + pub fn update(&self, val: T) { + self.updated + .store(true, std::sync::atomic::Ordering::Relaxed); + *self.value.lock().unwrap() = Some(val); + } + pub fn take_val(&self) -> Option { + if self.updated.load(std::sync::atomic::Ordering::Relaxed) { + self.updated + .store(false, std::sync::atomic::Ordering::Relaxed); + self.value.lock().unwrap().take() + } else { + None + } + } +} +impl Updatable +where + T: Default, +{ + pub fn modify(&self, func: impl FnOnce(&mut T) -> R) -> R { + self.updated + .store(true, std::sync::atomic::Ordering::Relaxed); + let mut val = self.value.lock().unwrap(); + if val.is_none() { + *val = Some(Default::default()); + } + func(val.as_mut().unwrap()) + } +} diff --git a/musicdb-lib/Cargo.toml b/musicdb-lib/Cargo.toml old mode 100755 new mode 100644 index d5f4361..d044e26 --- a/musicdb-lib/Cargo.toml +++ b/musicdb-lib/Cargo.toml @@ -6,8 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -awedio = "0.2.0" +awedio = { version = "0.2.0", optional = true } base64 = "0.21.2" rand = "0.8.5" rc-u8-reader = "2.0.16" -tokio = "1.29.1" +tokio = { version = "1.29.1", features = ["sync"] } + +[features] +playback = ["awedio"] diff --git a/musicdb-lib/src/lib.rs b/musicdb-lib/src/lib.rs index 7662770..f6fec59 100755 --- a/musicdb-lib/src/lib.rs +++ b/musicdb-lib/src/lib.rs @@ -1,4 +1,5 @@ pub mod data; pub mod load; +#[cfg(feature = "playback")] pub mod player; pub mod server; diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index 799723e..d30c0a0 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -1,11 +1,8 @@ pub mod get; use std::{ - io::{BufRead, BufReader, Read, Write}, - net::{SocketAddr, TcpListener}, + io::{Read, Write}, sync::{mpsc, Arc, Mutex}, - thread, - time::Duration, }; use crate::{ @@ -18,8 +15,15 @@ use crate::{ AlbumId, ArtistId, SongId, }, load::ToFromBytes, - player::Player, - server::get::handle_one_connection_as_get, +}; +#[cfg(feature = "playback")] +use crate::{player::Player, server::get::handle_one_connection_as_get}; +#[cfg(feature = "playback")] +use std::{ + io::{BufRead, BufReader}, + net::{SocketAddr, TcpListener}, + thread, + time::Duration, }; #[derive(Clone, Debug)] @@ -84,6 +88,7 @@ impl Command { /// 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. +#[cfg(feature = "playback")] pub fn run_server( database: Arc>, addr_tcp: Option, diff --git a/musicdb-server/Cargo.toml b/musicdb-server/Cargo.toml index 221704f..0ad9807 100755 --- a/musicdb-server/Cargo.toml +++ b/musicdb-server/Cargo.toml @@ -10,7 +10,7 @@ axum = { version = "0.6.19", features = ["headers"] } clap = { version = "4.4.6", features = ["derive"] } futures = "0.3.28" headers = "0.3.8" -musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" } +musicdb-lib = { version = "0.1.0", path = "../musicdb-lib", features = ["playback"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.0", features = ["full"] }