From 1eee22bb4bb8238aef739af539abb7ba9288cfa2 Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 24 Oct 2023 22:50:21 +0200 Subject: [PATCH] server can now send error messages to clients --- musicdb-client/src/gui.rs | 44 ++++++- musicdb-client/src/gui_library.rs | 134 +++++++++---------- musicdb-client/src/gui_notif.rs | 209 ++++++++++++++++++++++++++++++ musicdb-client/src/gui_screen.rs | 42 +++--- musicdb-client/src/main.rs | 2 + musicdb-lib/src/data/database.rs | 10 +- musicdb-lib/src/data/song.rs | 2 +- musicdb-lib/src/player/mod.rs | 7 + musicdb-lib/src/server/mod.rs | 9 +- musicdb-server/src/web.rs | 4 +- 10 files changed, 370 insertions(+), 93 deletions(-) create mode 100644 musicdb-client/src/gui_notif.rs diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index cf6a41e..e8de835 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -6,7 +6,7 @@ use std::{ net::TcpStream, sync::{Arc, Mutex}, thread::JoinHandle, - time::Instant, + time::{Duration, Instant}, usize, }; @@ -28,7 +28,14 @@ use speedy2d::{ Graphics2D, }; -use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey, textcfg}; +use crate::{ + gui_base::Panel, + gui_notif::{NotifInfo, NotifOverlay}, + gui_screen::GuiScreen, + gui_text::Label, + gui_wrappers::WithFocusHotkey, + textcfg, +}; pub enum GuiEvent { Refresh, @@ -192,6 +199,7 @@ impl Gui { scroll_pages_multiplier: f64, gui_config: GuiConfig, ) -> Self { + let (notif_overlay, notif_sender) = NotifOverlay::new(); database.lock().unwrap().update_endpoints.push( musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { Command::Resume @@ -225,6 +233,37 @@ impl Gui { _ = s.send_event(GuiEvent::UpdatedLibrary); } } + Command::ErrorInfo(t, d) => { + eprintln!("{t:?} | {d:?}"); + let (t, d) = (t.clone(), d.clone()); + notif_sender + .send(Box::new(move |_| { + ( + GuiElem::new(Panel::with_background( + GuiElemCfg::default(), + vec![GuiElem::new(Label::new( + GuiElemCfg::default(), + if t.is_empty() { + format!("Server message\n{d}") + } else { + format!("Server error ({t})\n{d}") + }, + Color::WHITE, + None, + Vec2::new(0.5, 0.5), + ))], + Color::from_rgba(0.0, 0.0, 0.0, 0.8), + )), + if t.is_empty() { + NotifInfo::new(Duration::from_secs(2)) + } else { + NotifInfo::new(Duration::from_secs(5)) + .with_highlight(Color::RED) + }, + ) + })) + .unwrap(); + } })), ); Gui { @@ -236,6 +275,7 @@ impl Gui { VirtualKeyCode::Escape, GuiScreen::new( GuiElemCfg::default(), + notif_overlay, line_height, scroll_pixels_multiplier, scroll_lines_multiplier, diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index faaa22e..ca2bf6c 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -1,7 +1,7 @@ use std::{ cmp::Ordering, collections::HashSet, - rc::Rc, + sync::Arc, sync::{ atomic::{AtomicBool, AtomicUsize}, mpsc, Mutex, @@ -57,17 +57,17 @@ pub struct LibraryBrowser { search_album_regex: Option, search_song: String, search_song_regex: Option, - filter_target_state: Rc, + filter_target_state: Arc, filter_state: f32, library_updated: bool, - search_settings_changed: Rc, - search_is_case_sensitive: Rc, + search_settings_changed: Arc, + search_is_case_sensitive: Arc, search_was_case_sensitive: bool, - search_prefer_start_matches: Rc, + search_prefer_start_matches: Arc, search_prefers_start_matches: bool, - filter_songs: Rc>, - filter_albums: Rc>, - filter_artists: Rc>, + filter_songs: Arc>, + filter_albums: Arc>, + filter_artists: Arc>, do_something_receiver: mpsc::Receiver>, } impl Clone for LibraryBrowser { @@ -76,7 +76,7 @@ impl Clone for LibraryBrowser { } } #[derive(Clone)] -struct Selected(Rc, HashSet, HashSet)>>); +struct Selected(Arc, HashSet, HashSet)>>); impl Selected { pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec { let lock = self.0.lock().unwrap(); @@ -185,13 +185,13 @@ impl LibraryBrowser { vec![], ); let (do_something_sender, do_something_receiver) = mpsc::channel(); - let search_settings_changed = Rc::new(AtomicBool::new(false)); + let search_settings_changed = Arc::new(AtomicBool::new(false)); let search_was_case_sensitive = false; - let search_is_case_sensitive = Rc::new(AtomicBool::new(search_was_case_sensitive)); + let search_is_case_sensitive = Arc::new(AtomicBool::new(search_was_case_sensitive)); let search_prefers_start_matches = true; - let search_prefer_start_matches = Rc::new(AtomicBool::new(search_prefers_start_matches)); - let filter_target_state = Rc::new(AtomicBool::new(false)); - let fts = Rc::clone(&filter_target_state); + let search_prefer_start_matches = Arc::new(AtomicBool::new(search_prefers_start_matches)); + let filter_target_state = Arc::new(AtomicBool::new(false)); + let fts = Arc::clone(&filter_target_state); let filter_button = Button::new( GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))), move |_| { @@ -209,19 +209,19 @@ impl LibraryBrowser { Vec2::new(0.5, 0.5), ))], ); - let filter_songs = Rc::new(Mutex::new(Filter { + let filter_songs = Arc::new(Mutex::new(Filter { and: true, filters: vec![], })); - let filter_albums = Rc::new(Mutex::new(Filter { + let filter_albums = Arc::new(Mutex::new(Filter { and: true, filters: vec![], })); - let filter_artists = Rc::new(Mutex::new(Filter { + let filter_artists = Arc::new(Mutex::new(Filter { and: true, filters: vec![], })); - let selected = Selected(Rc::new(Mutex::new(( + let selected = Selected(Arc::new(Mutex::new(( HashSet::new(), HashSet::new(), HashSet::new(), @@ -235,12 +235,12 @@ impl LibraryBrowser { GuiElem::new(library_scroll_box), GuiElem::new(filter_button), GuiElem::new(FilterPanel::new( - Rc::clone(&search_settings_changed), - Rc::clone(&search_is_case_sensitive), - Rc::clone(&search_prefer_start_matches), - Rc::clone(&filter_songs), - Rc::clone(&filter_albums), - Rc::clone(&filter_artists), + Arc::clone(&search_settings_changed), + Arc::clone(&search_is_case_sensitive), + Arc::clone(&search_prefer_start_matches), + Arc::clone(&filter_songs), + Arc::clone(&filter_albums), + Arc::clone(&filter_artists), selected.clone(), do_something_sender.clone(), )), @@ -1137,13 +1137,13 @@ impl GuiElemTrait for ListSong { struct FilterPanel { config: GuiElemCfg, children: Vec, - search_settings_changed: Rc, + search_settings_changed: Arc, tab: usize, - new_tab: Rc, + new_tab: Arc, line_height: f32, - filter_songs: Rc>, - filter_albums: Rc>, - filter_artists: Rc>, + filter_songs: Arc>, + filter_albums: Arc>, + filter_artists: Arc>, } const FP_CASESENS_N: &'static str = "search is case-insensitive"; const FP_CASESENS_Y: &'static str = "search is case-sensitive!"; @@ -1151,21 +1151,21 @@ const FP_PREFSTART_N: &'static str = "simple search"; const FP_PREFSTART_Y: &'static str = "will prefer matches at the start of a word"; impl FilterPanel { pub fn new( - search_settings_changed: Rc, - search_is_case_sensitive: Rc, - search_prefer_start_matches: Rc, - filter_songs: Rc>, - filter_albums: Rc>, - filter_artists: Rc>, + search_settings_changed: Arc, + search_is_case_sensitive: Arc, + search_prefer_start_matches: Arc, + filter_songs: Arc>, + filter_albums: Arc>, + filter_artists: Arc>, selected: Selected, do_something_sender: mpsc::Sender>, ) -> Self { let is_case_sensitive = search_is_case_sensitive.load(std::sync::atomic::Ordering::Relaxed); let prefer_start_matches = search_prefer_start_matches.load(std::sync::atomic::Ordering::Relaxed); - let ssc1 = Rc::clone(&search_settings_changed); - let ssc2 = Rc::clone(&search_settings_changed); - let ssc3 = Rc::clone(&search_settings_changed); + let ssc1 = Arc::clone(&search_settings_changed); + let ssc2 = Arc::clone(&search_settings_changed); + let ssc3 = Arc::clone(&search_settings_changed); let sel3 = selected.clone(); const VSPLIT: f32 = 0.4; let tab_main = GuiElem::new(ScrollBox::new( @@ -1387,10 +1387,10 @@ impl FilterPanel { crate::gui_base::ScrollBoxSizeUnit::Pixels, vec![], )); - let new_tab = Rc::new(AtomicUsize::new(0)); - let set_tab_1 = Rc::clone(&new_tab); - let set_tab_2 = Rc::clone(&new_tab); - let set_tab_3 = Rc::clone(&new_tab); + let new_tab = Arc::new(AtomicUsize::new(0)); + let set_tab_1 = Arc::clone(&new_tab); + let set_tab_2 = Arc::clone(&new_tab); + let set_tab_3 = Arc::clone(&new_tab); const HEIGHT: f32 = 0.1; Self { config: GuiElemCfg::default().disabled(), @@ -1458,17 +1458,17 @@ impl FilterPanel { } } fn build_filter( - filter: &Rc>, + filter: &Arc>, line_height: f32, - on_change: &Rc, + on_change: &Arc, path: Vec, ) -> Vec<(GuiElem, f32)> { - let f0 = Rc::clone(filter); - let oc0 = Rc::clone(on_change); - let f1 = Rc::clone(filter); - let f2 = Rc::clone(filter); - let oc1 = Rc::clone(on_change); - let oc2 = Rc::clone(on_change); + let f0 = Arc::clone(filter); + let oc0 = Arc::clone(on_change); + let f1 = Arc::clone(filter); + let f2 = Arc::clone(filter); + let oc1 = Arc::clone(on_change); + let oc2 = Arc::clone(on_change); let mut children = vec![ GuiElem::new(Button::new( GuiElemCfg::default(), @@ -1536,16 +1536,16 @@ impl FilterPanel { } fn build_filter_editor( filter: &Filter, - mutex: &Rc>, + mutex: &Arc>, children: &mut Vec, mut indent: f32, indent_by: f32, - on_change: &Rc, + on_change: &Arc, path: Vec, ) { if filter.filters.len() > 1 { - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); let p = path.clone(); children.push(GuiElem::new(Button::new( GuiElemCfg::at(Rectangle::from_tuples((indent, 0.0), (1.0, 1.0))), @@ -1597,8 +1597,8 @@ impl FilterPanel { Color::GRAY, Color::WHITE, ); - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); tf.on_changed = Some(Box::new(move |text| { if let Some(Ok(FilterType::TagEq(v))) = mx.lock().unwrap().get_mut(&path) { *v = text.to_owned(); @@ -1627,8 +1627,8 @@ impl FilterPanel { Color::GRAY, Color::WHITE, ); - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); tf.on_changed = Some(Box::new(move |text| { if let Some(Ok(FilterType::TagStartsWith(v))) = mx.lock().unwrap().get_mut(&path) @@ -1659,8 +1659,8 @@ impl FilterPanel { Color::GRAY, Color::WHITE, ); - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); let p = path.clone(); tf.on_changed = Some(Box::new(move |text| { if let Some(Ok(FilterType::TagWithValueInt(v, _, _))) = @@ -1684,8 +1684,8 @@ impl FilterPanel { Color::GRAY, Color::WHITE, ); - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); let p = path.clone(); tf1.on_changed = Some(Box::new(move |text| { if let Ok(n) = text.parse() { @@ -1697,8 +1697,8 @@ impl FilterPanel { } } })); - let mx = Rc::clone(mutex); - let oc = Rc::clone(on_change); + let mx = Arc::clone(mutex); + let oc = Arc::clone(on_change); let p = path.clone(); tf2.on_changed = Some(Box::new(move |text| { if let Ok(n) = text.parse() { @@ -1813,9 +1813,9 @@ impl GuiElemTrait for FilterPanel { .unwrap() .try_as_mut::() .unwrap(); - let ssc = Rc::clone(&self.search_settings_changed); + let ssc = Arc::clone(&self.search_settings_changed); let my_tab = new_tab; - let ntab = Rc::clone(&self.new_tab); + let ntab = Arc::clone(&self.new_tab); sb.children = Self::build_filter( match new_tab { 0 => &self.filter_songs, @@ -1824,7 +1824,7 @@ impl GuiElemTrait for FilterPanel { _ => unreachable!(), }, info.line_height, - &Rc::new(move |update_ui| { + &Arc::new(move |update_ui| { if update_ui { ntab.store(my_tab, std::sync::atomic::Ordering::Relaxed); } diff --git a/musicdb-client/src/gui_notif.rs b/musicdb-client/src/gui_notif.rs new file mode 100644 index 0000000..e6aed3c --- /dev/null +++ b/musicdb-client/src/gui_notif.rs @@ -0,0 +1,209 @@ +use std::{ + sync::mpsc, + time::{Duration, Instant}, +}; + +use speedy2d::{color::Color, dimen::Vector2, shape::Rectangle}; + +use crate::gui::{GuiElem, GuiElemCfg, GuiElemTrait}; + +/// This should be added on top of overything else and set to fullscreen. +/// It will respond to notification events. +pub struct NotifOverlay { + config: GuiElemCfg, + notifs: Vec<(GuiElem, NotifInfo)>, + light: Option<(Instant, Color)>, + receiver: mpsc::Receiver (GuiElem, NotifInfo) + Send>>, +} + +impl NotifOverlay { + pub fn new() -> ( + Self, + mpsc::Sender (GuiElem, NotifInfo) + Send>>, + ) { + let (sender, receiver) = mpsc::channel(); + ( + Self { + config: GuiElemCfg::default(), + notifs: vec![], + light: None, + receiver, + }, + sender, + ) + } + + fn check_notifs(&mut self) { + let mut adjust_heights = false; + let mut remove = Vec::with_capacity(0); + for (i, (gui, info)) in self.notifs.iter_mut().enumerate() { + match info.time { + NotifInfoTime::Pending => { + if self.light.is_none() { + let now = Instant::now(); + info.time = NotifInfoTime::FadingIn(now); + if let Some(color) = info.color { + self.light = Some((now, color)); + } + adjust_heights = true; + gui.inner.config_mut().enabled = true; + } + } + NotifInfoTime::FadingIn(since) => { + adjust_heights = true; + let p = since.elapsed().as_secs_f32() / 0.25; + if p >= 1.0 { + info.time = NotifInfoTime::Displayed(Instant::now()); + info.progress = 0.0; + } else { + info.progress = p; + } + } + NotifInfoTime::Displayed(since) => { + let p = since.elapsed().as_secs_f32() / info.duration.as_secs_f32(); + if p >= 1.0 { + info.time = NotifInfoTime::FadingOut(Instant::now()); + info.progress = 0.0; + } else { + info.progress = p; + } + } + NotifInfoTime::FadingOut(since) => { + adjust_heights = true; + let p = since.elapsed().as_secs_f32() / 0.25; + if p >= 1.0 { + remove.push(i); + } else { + info.progress = p; + } + } + } + } + for index in remove.into_iter().rev() { + self.notifs.remove(index); + } + if adjust_heights { + self.adjust_heights(); + } + } + + fn adjust_heights(&mut self) { + let screen_size = self.config.pixel_pos.size(); + let width = 0.3; + let left = 0.5 - (0.5 * width); + let right = 0.5 + (0.5 * width); + let height = 0.2 * width * screen_size.x / screen_size.y; + let space = 0.05 / 0.2 * height; + let mut y = 0.0; + for (gui, info) in self.notifs.iter_mut() { + y += space; + let pos_y = if matches!(info.time, NotifInfoTime::FadingOut(..)) { + let v = y - (height + y) * info.progress * info.progress; + // for notifs below this one + y -= (height + space) * crate::gui_screen::transition(info.progress); + v + } else if matches!(info.time, NotifInfoTime::FadingIn(..)) { + -height + (height + y) * (1.0 - (1.0 - info.progress) * (1.0 - info.progress)) + } else { + y + }; + y += height; + gui.inner.config_mut().pos = + Rectangle::from_tuples((left, pos_y), (right, pos_y + height)); + } + } +} + +#[derive(Clone)] +pub struct NotifInfo { + time: NotifInfoTime, + duration: Duration, + /// when the notification is first shown on screen, + /// light up the edges of the screen/window + /// in this color (usually red for important things) + color: Option, + /// used for fade-out animation + progress: f32, +} +#[derive(Clone)] +enum NotifInfoTime { + Pending, + FadingIn(Instant), + Displayed(Instant), + FadingOut(Instant), +} +impl NotifInfo { + pub fn new(duration: Duration) -> Self { + Self { + time: NotifInfoTime::Pending, + duration, + color: None, + progress: 0.0, + } + } + pub fn with_highlight(mut self, color: Color) -> Self { + self.color = Some(color); + self + } +} + +impl Clone for NotifOverlay { + fn clone(&self) -> Self { + Self::new().0 + } +} + +impl GuiElemTrait for NotifOverlay { + fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) { + if let Ok(notif) = self.receiver.try_recv() { + let mut n = notif(self); + n.0.inner.config_mut().enabled = false; + self.notifs.push(n); + } + self.check_notifs(); + // light + if let Some((since, color)) = self.light { + let p = since.elapsed().as_secs_f32() / 0.5; + if p >= 1.0 { + self.light = None; + } else { + let f = p * 2.0 - 1.0; + let f = 1.0 - f * f; + let color = Color::from_rgba(color.r(), color.g(), color.b(), color.a() * f); + let Vector2 { x: x1, y: y1 } = *info.pos.top_left(); + let Vector2 { x: x2, y: y2 } = *info.pos.bottom_right(); + let width = info.pos.width() * 0.01; + g.draw_rectangle(Rectangle::from_tuples((x1, y1), (x1 + width, y2)), color); + g.draw_rectangle(Rectangle::from_tuples((x2 - width, y1), (x2, y2)), color); + } + } + // redraw + if !self.notifs.is_empty() { + if let Some(h) = &info.helper { + h.request_redraw(); + } + } + } + fn draw_rev(&self) -> bool { + true + } + + fn config(&self) -> &GuiElemCfg { + &self.config + } + fn config_mut(&mut self) -> &mut GuiElemCfg { + &mut self.config + } + fn children(&mut self) -> Box + '_> { + Box::new(self.notifs.iter_mut().map(|(v, _)| v)) + } + fn any(&self) -> &dyn std::any::Any { + self + } + fn any_mut(&mut self) -> &mut dyn std::any::Any { + self + } + fn clone_gui(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/musicdb-client/src/gui_screen.rs b/musicdb-client/src/gui_screen.rs index fd00091..2c297f2 100755 --- a/musicdb-client/src/gui_screen.rs +++ b/musicdb-client/src/gui_screen.rs @@ -7,6 +7,7 @@ use crate::{ gui::{morph_rect, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui_base::{Button, Panel}, gui_library::LibraryBrowser, + gui_notif::NotifOverlay, gui_playback::{CurrentSong, PlayPauseToggle}, gui_queue::QueueViewer, gui_settings::Settings, @@ -34,15 +35,16 @@ pub fn transition(p: f32) -> f32 { #[derive(Clone)] pub struct GuiScreen { config: GuiElemCfg, - /// 0: StatusBar / Idle display - /// 1: Settings - /// 2: Panel for Main view + /// 0: Notifications + /// 1: StatusBar / Idle display + /// 2: Settings + /// 3: Panel for Main view /// 0: settings button /// 1: exit button /// 2: library browser /// 3: queue /// 4: queue clear button - /// 3: Edit Panel + /// 4: Edit Panel children: Vec, pub idle: (bool, Option), pub settings: (bool, Option), @@ -59,21 +61,22 @@ impl GuiScreen { } else { edit.inner.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (0.5, 0.9)); } - if let Some(prev) = self.children.get_mut(3) { + if let Some(prev) = self.children.get_mut(4) { prev.inner.config_mut().enabled = false; } - self.children.insert(3, edit); + self.children.insert(4, edit); } pub fn close_edit(&mut self) { - if self.children.len() > 4 { - self.children.remove(3); - self.children[3].inner.config_mut().enabled = true; + if self.children.len() > 5 { + self.children.remove(4); + self.children[4].inner.config_mut().enabled = true; } else if self.edit_panel.0 { self.edit_panel = (false, Some(Instant::now())); } } pub fn new( config: GuiElemCfg, + notif_overlay: NotifOverlay, line_height: f32, scroll_sensitivity_pixels: f64, scroll_sensitivity_lines: f64, @@ -82,6 +85,7 @@ impl GuiScreen { Self { config: config.w_keyboard_watch().w_mouse(), children: vec![ + GuiElem::new(notif_overlay), GuiElem::new(StatusBar::new( GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))), true, @@ -261,20 +265,20 @@ impl GuiElemTrait for GuiScreen { if let Some(h) = &info.helper { h.set_cursor_visible(!self.idle.0); if self.settings.0 { - self.children[1].inner.config_mut().enabled = !self.idle.0; + self.children[2].inner.config_mut().enabled = !self.idle.0; } if self.edit_panel.0 { - if let Some(c) = self.children.get_mut(3) { + if let Some(c) = self.children.get_mut(4) { c.inner.config_mut().enabled = !self.idle.0; } } - self.children[2].inner.config_mut().enabled = !self.idle.0; + self.children[3].inner.config_mut().enabled = !self.idle.0; } } let p = transition(p1); - self.children[0].inner.config_mut().pos = + self.children[1].inner.config_mut().pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 1.0)); - self.children[0] + self.children[1] .inner .any_mut() .downcast_mut::() @@ -285,7 +289,7 @@ impl GuiElemTrait for GuiScreen { 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(); + let cfg = self.children[2].inner.config_mut(); cfg.enabled = p > 0.0; cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9)); } @@ -293,22 +297,22 @@ impl GuiElemTrait for GuiScreen { if self.edit_panel.1.is_some() { let p1 = Self::get_prog(&mut self.edit_panel, 0.3); let p = transition(p1); - if let Some(c) = self.children.get_mut(3) { + if let Some(c) = self.children.get_mut(4) { c.inner.config_mut().enabled = p > 0.0; c.inner.config_mut().pos = Rectangle::from_tuples((-0.5 + 0.5 * p, 0.0), (0.5 * p, 0.9)); } if !self.edit_panel.0 && p == 0.0 { - while self.children.len() > 3 { + while self.children.len() > 4 { self.children.pop(); } } - self.children[2].inner.config_mut().pos = + self.children[3].inner.config_mut().pos = Rectangle::from_tuples((0.5 * p, 0.0), (1.0 + 0.5 * p, 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] + self.idle_timeout = self.children[2] .inner .any() .downcast_ref::() diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index dd468af..ec84971 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -27,6 +27,8 @@ mod gui_edit; #[cfg(feature = "speedy2d")] mod gui_library; #[cfg(feature = "speedy2d")] +mod gui_notif; +#[cfg(feature = "speedy2d")] mod gui_playback; #[cfg(feature = "speedy2d")] mod gui_queue; diff --git a/musicdb-lib/src/data/database.rs b/musicdb-lib/src/data/database.rs index 8ddb26b..f2323e0 100755 --- a/musicdb-lib/src/data/database.rs +++ b/musicdb-lib/src/data/database.rs @@ -232,7 +232,14 @@ impl Database { Ok(()) } - pub fn apply_command(&mut self, command: Command) { + pub fn apply_command(&mut self, mut command: Command) { + if !self.is_client() { + if let Command::ErrorInfo(t, _) = &mut command { + // clients can send ErrorInfo to the server and it will show up on other clients, + // BUT only the server can set the Title of the ErrorInfo. + t.clear(); + } + } // since db.update_endpoints is empty for clients, this won't cause unwanted back and forth self.broadcast_update(&command); match command { @@ -349,6 +356,7 @@ impl Database { Command::InitComplete => { self.client_is_init = true; } + Command::ErrorInfo(..) => {} } } } diff --git a/musicdb-lib/src/data/song.rs b/musicdb-lib/src/data/song.rs index 26e8c18..3008a51 100755 --- a/musicdb-lib/src/data/song.rs +++ b/musicdb-lib/src/data/song.rs @@ -154,7 +154,7 @@ impl Song { { Ok(data) => Some(data), Err(e) => { - eprintln!("[info] error loading song {id}: {e}"); + eprintln!("[WARN] error loading song {id}: {e}"); None } } diff --git a/musicdb-lib/src/player/mod.rs b/musicdb-lib/src/player/mod.rs index 08051c8..209859e 100755 --- a/musicdb-lib/src/player/mod.rs +++ b/musicdb-lib/src/player/mod.rs @@ -139,6 +139,13 @@ impl Player { db.apply_command(Command::NextSong); } } + } else { + // couldn't load song bytes + db.broadcast_update(&Command::ErrorInfo( + "NoSongData".to_owned(), + format!("Couldn't load song #{}\n({})", song.id, song.title), + )); + db.apply_command(Command::NextSong); } } else { self.source = None; diff --git a/musicdb-lib/src/server/mod.rs b/musicdb-lib/src/server/mod.rs index e4672e9..a68cc18 100755 --- a/musicdb-lib/src/server/mod.rs +++ b/musicdb-lib/src/server/mod.rs @@ -1,7 +1,6 @@ pub mod get; use std::{ - eprintln, io::{BufRead, BufReader, Read, Write}, net::{SocketAddr, TcpListener}, sync::{mpsc, Arc, Mutex}, @@ -51,6 +50,7 @@ pub enum Command { RemoveArtist(ArtistId), ModifyArtist(Artist), InitComplete, + ErrorInfo(String, String), } impl Command { pub fn send_to_server(self, db: &Database) -> Result<(), Self> { @@ -106,7 +106,6 @@ pub fn run_server( let command_sender = command_sender.clone(); let db = Arc::clone(&db); thread::spawn(move || { - eprintln!("[info] TCP connection accepted from {con_addr}."); // each connection first has to send one line to tell us what it wants let mut connection = BufReader::new(connection); let mut line = String::new(); @@ -279,6 +278,11 @@ impl ToFromBytes for Command { Self::InitComplete => { s.write_all(&[0b00110001])?; } + Self::ErrorInfo(t, d) => { + s.write_all(&[0b11011011])?; + t.to_bytes(s)?; + d.to_bytes(s)?; + } } Ok(()) } @@ -324,6 +328,7 @@ impl ToFromBytes for Command { 0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?), 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), 0b00110001 => Self::InitComplete, + 0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?), _ => { eprintln!("unexpected byte when reading command; stopping playback."); Self::Stop diff --git a/musicdb-server/src/web.rs b/musicdb-server/src/web.rs index 6222003..7f21f70 100755 --- a/musicdb-server/src/web.rs +++ b/musicdb-server/src/web.rs @@ -438,7 +438,9 @@ async fn sse_handler( .collect::(), ) } - Command::Save | Command::InitComplete => return Poll::Pending, + Command::Save | Command::InitComplete | Command::ErrorInfo(..) => { + return Poll::Pending + } })) } else { return Poll::Pending;