server can now send error messages to clients

This commit is contained in:
Mark 2023-10-24 22:50:21 +02:00
parent 94df757f0c
commit 1eee22bb4b
10 changed files with 370 additions and 93 deletions

View File

@ -6,7 +6,7 @@ use std::{
net::TcpStream, net::TcpStream,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread::JoinHandle, thread::JoinHandle,
time::Instant, time::{Duration, Instant},
usize, usize,
}; };
@ -28,7 +28,14 @@ use speedy2d::{
Graphics2D, 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 { pub enum GuiEvent {
Refresh, Refresh,
@ -192,6 +199,7 @@ impl Gui {
scroll_pages_multiplier: f64, scroll_pages_multiplier: f64,
gui_config: GuiConfig, gui_config: GuiConfig,
) -> Self { ) -> Self {
let (notif_overlay, notif_sender) = NotifOverlay::new();
database.lock().unwrap().update_endpoints.push( database.lock().unwrap().update_endpoints.push(
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd { musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
Command::Resume Command::Resume
@ -225,6 +233,37 @@ impl Gui {
_ = s.send_event(GuiEvent::UpdatedLibrary); _ = 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 { Gui {
@ -236,6 +275,7 @@ impl Gui {
VirtualKeyCode::Escape, VirtualKeyCode::Escape,
GuiScreen::new( GuiScreen::new(
GuiElemCfg::default(), GuiElemCfg::default(),
notif_overlay,
line_height, line_height,
scroll_pixels_multiplier, scroll_pixels_multiplier,
scroll_lines_multiplier, scroll_lines_multiplier,

View File

@ -1,7 +1,7 @@
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::HashSet, collections::HashSet,
rc::Rc, sync::Arc,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize}, atomic::{AtomicBool, AtomicUsize},
mpsc, Mutex, mpsc, Mutex,
@ -57,17 +57,17 @@ pub struct LibraryBrowser {
search_album_regex: Option<Regex>, search_album_regex: Option<Regex>,
search_song: String, search_song: String,
search_song_regex: Option<Regex>, search_song_regex: Option<Regex>,
filter_target_state: Rc<AtomicBool>, filter_target_state: Arc<AtomicBool>,
filter_state: f32, filter_state: f32,
library_updated: bool, library_updated: bool,
search_settings_changed: Rc<AtomicBool>, search_settings_changed: Arc<AtomicBool>,
search_is_case_sensitive: Rc<AtomicBool>, search_is_case_sensitive: Arc<AtomicBool>,
search_was_case_sensitive: bool, search_was_case_sensitive: bool,
search_prefer_start_matches: Rc<AtomicBool>, search_prefer_start_matches: Arc<AtomicBool>,
search_prefers_start_matches: bool, search_prefers_start_matches: bool,
filter_songs: Rc<Mutex<Filter>>, filter_songs: Arc<Mutex<Filter>>,
filter_albums: Rc<Mutex<Filter>>, filter_albums: Arc<Mutex<Filter>>,
filter_artists: Rc<Mutex<Filter>>, filter_artists: Arc<Mutex<Filter>>,
do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>, do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
} }
impl Clone for LibraryBrowser { impl Clone for LibraryBrowser {
@ -76,7 +76,7 @@ impl Clone for LibraryBrowser {
} }
} }
#[derive(Clone)] #[derive(Clone)]
struct Selected(Rc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>); struct Selected(Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>);
impl Selected { impl Selected {
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> { pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
let lock = self.0.lock().unwrap(); let lock = self.0.lock().unwrap();
@ -185,13 +185,13 @@ impl LibraryBrowser {
vec![], vec![],
); );
let (do_something_sender, do_something_receiver) = mpsc::channel(); 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_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_prefers_start_matches = true;
let search_prefer_start_matches = Rc::new(AtomicBool::new(search_prefers_start_matches)); let search_prefer_start_matches = Arc::new(AtomicBool::new(search_prefers_start_matches));
let filter_target_state = Rc::new(AtomicBool::new(false)); let filter_target_state = Arc::new(AtomicBool::new(false));
let fts = Rc::clone(&filter_target_state); let fts = Arc::clone(&filter_target_state);
let filter_button = Button::new( let filter_button = Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))), GuiElemCfg::at(Rectangle::from_tuples((0.46, 0.01), (0.54, 0.05))),
move |_| { move |_| {
@ -209,19 +209,19 @@ impl LibraryBrowser {
Vec2::new(0.5, 0.5), Vec2::new(0.5, 0.5),
))], ))],
); );
let filter_songs = Rc::new(Mutex::new(Filter { let filter_songs = Arc::new(Mutex::new(Filter {
and: true, and: true,
filters: vec![], filters: vec![],
})); }));
let filter_albums = Rc::new(Mutex::new(Filter { let filter_albums = Arc::new(Mutex::new(Filter {
and: true, and: true,
filters: vec![], filters: vec![],
})); }));
let filter_artists = Rc::new(Mutex::new(Filter { let filter_artists = Arc::new(Mutex::new(Filter {
and: true, and: true,
filters: vec![], filters: vec![],
})); }));
let selected = Selected(Rc::new(Mutex::new(( let selected = Selected(Arc::new(Mutex::new((
HashSet::new(), HashSet::new(),
HashSet::new(), HashSet::new(),
HashSet::new(), HashSet::new(),
@ -235,12 +235,12 @@ impl LibraryBrowser {
GuiElem::new(library_scroll_box), GuiElem::new(library_scroll_box),
GuiElem::new(filter_button), GuiElem::new(filter_button),
GuiElem::new(FilterPanel::new( GuiElem::new(FilterPanel::new(
Rc::clone(&search_settings_changed), Arc::clone(&search_settings_changed),
Rc::clone(&search_is_case_sensitive), Arc::clone(&search_is_case_sensitive),
Rc::clone(&search_prefer_start_matches), Arc::clone(&search_prefer_start_matches),
Rc::clone(&filter_songs), Arc::clone(&filter_songs),
Rc::clone(&filter_albums), Arc::clone(&filter_albums),
Rc::clone(&filter_artists), Arc::clone(&filter_artists),
selected.clone(), selected.clone(),
do_something_sender.clone(), do_something_sender.clone(),
)), )),
@ -1137,13 +1137,13 @@ impl GuiElemTrait for ListSong {
struct FilterPanel { struct FilterPanel {
config: GuiElemCfg, config: GuiElemCfg,
children: Vec<GuiElem>, children: Vec<GuiElem>,
search_settings_changed: Rc<AtomicBool>, search_settings_changed: Arc<AtomicBool>,
tab: usize, tab: usize,
new_tab: Rc<AtomicUsize>, new_tab: Arc<AtomicUsize>,
line_height: f32, line_height: f32,
filter_songs: Rc<Mutex<Filter>>, filter_songs: Arc<Mutex<Filter>>,
filter_albums: Rc<Mutex<Filter>>, filter_albums: Arc<Mutex<Filter>>,
filter_artists: Rc<Mutex<Filter>>, filter_artists: Arc<Mutex<Filter>>,
} }
const FP_CASESENS_N: &'static str = "search is case-insensitive"; const FP_CASESENS_N: &'static str = "search is case-insensitive";
const FP_CASESENS_Y: &'static str = "search is case-sensitive!"; 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"; const FP_PREFSTART_Y: &'static str = "will prefer matches at the start of a word";
impl FilterPanel { impl FilterPanel {
pub fn new( pub fn new(
search_settings_changed: Rc<AtomicBool>, search_settings_changed: Arc<AtomicBool>,
search_is_case_sensitive: Rc<AtomicBool>, search_is_case_sensitive: Arc<AtomicBool>,
search_prefer_start_matches: Rc<AtomicBool>, search_prefer_start_matches: Arc<AtomicBool>,
filter_songs: Rc<Mutex<Filter>>, filter_songs: Arc<Mutex<Filter>>,
filter_albums: Rc<Mutex<Filter>>, filter_albums: Arc<Mutex<Filter>>,
filter_artists: Rc<Mutex<Filter>>, filter_artists: Arc<Mutex<Filter>>,
selected: Selected, selected: Selected,
do_something_sender: mpsc::Sender<Box<dyn FnOnce(&mut LibraryBrowser)>>, do_something_sender: mpsc::Sender<Box<dyn FnOnce(&mut LibraryBrowser)>>,
) -> Self { ) -> Self {
let is_case_sensitive = search_is_case_sensitive.load(std::sync::atomic::Ordering::Relaxed); let is_case_sensitive = search_is_case_sensitive.load(std::sync::atomic::Ordering::Relaxed);
let prefer_start_matches = let prefer_start_matches =
search_prefer_start_matches.load(std::sync::atomic::Ordering::Relaxed); search_prefer_start_matches.load(std::sync::atomic::Ordering::Relaxed);
let ssc1 = Rc::clone(&search_settings_changed); let ssc1 = Arc::clone(&search_settings_changed);
let ssc2 = Rc::clone(&search_settings_changed); let ssc2 = Arc::clone(&search_settings_changed);
let ssc3 = Rc::clone(&search_settings_changed); let ssc3 = Arc::clone(&search_settings_changed);
let sel3 = selected.clone(); let sel3 = selected.clone();
const VSPLIT: f32 = 0.4; const VSPLIT: f32 = 0.4;
let tab_main = GuiElem::new(ScrollBox::new( let tab_main = GuiElem::new(ScrollBox::new(
@ -1387,10 +1387,10 @@ impl FilterPanel {
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![], vec![],
)); ));
let new_tab = Rc::new(AtomicUsize::new(0)); let new_tab = Arc::new(AtomicUsize::new(0));
let set_tab_1 = Rc::clone(&new_tab); let set_tab_1 = Arc::clone(&new_tab);
let set_tab_2 = Rc::clone(&new_tab); let set_tab_2 = Arc::clone(&new_tab);
let set_tab_3 = Rc::clone(&new_tab); let set_tab_3 = Arc::clone(&new_tab);
const HEIGHT: f32 = 0.1; const HEIGHT: f32 = 0.1;
Self { Self {
config: GuiElemCfg::default().disabled(), config: GuiElemCfg::default().disabled(),
@ -1458,17 +1458,17 @@ impl FilterPanel {
} }
} }
fn build_filter( fn build_filter(
filter: &Rc<Mutex<Filter>>, filter: &Arc<Mutex<Filter>>,
line_height: f32, line_height: f32,
on_change: &Rc<impl Fn(bool) + 'static>, on_change: &Arc<impl Fn(bool) + 'static>,
path: Vec<usize>, path: Vec<usize>,
) -> Vec<(GuiElem, f32)> { ) -> Vec<(GuiElem, f32)> {
let f0 = Rc::clone(filter); let f0 = Arc::clone(filter);
let oc0 = Rc::clone(on_change); let oc0 = Arc::clone(on_change);
let f1 = Rc::clone(filter); let f1 = Arc::clone(filter);
let f2 = Rc::clone(filter); let f2 = Arc::clone(filter);
let oc1 = Rc::clone(on_change); let oc1 = Arc::clone(on_change);
let oc2 = Rc::clone(on_change); let oc2 = Arc::clone(on_change);
let mut children = vec![ let mut children = vec![
GuiElem::new(Button::new( GuiElem::new(Button::new(
GuiElemCfg::default(), GuiElemCfg::default(),
@ -1536,16 +1536,16 @@ impl FilterPanel {
} }
fn build_filter_editor( fn build_filter_editor(
filter: &Filter, filter: &Filter,
mutex: &Rc<Mutex<Filter>>, mutex: &Arc<Mutex<Filter>>,
children: &mut Vec<GuiElem>, children: &mut Vec<GuiElem>,
mut indent: f32, mut indent: f32,
indent_by: f32, indent_by: f32,
on_change: &Rc<impl Fn(bool) + 'static>, on_change: &Arc<impl Fn(bool) + 'static>,
path: Vec<usize>, path: Vec<usize>,
) { ) {
if filter.filters.len() > 1 { if filter.filters.len() > 1 {
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
let p = path.clone(); let p = path.clone();
children.push(GuiElem::new(Button::new( children.push(GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((indent, 0.0), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((indent, 0.0), (1.0, 1.0))),
@ -1597,8 +1597,8 @@ impl FilterPanel {
Color::GRAY, Color::GRAY,
Color::WHITE, Color::WHITE,
); );
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
tf.on_changed = Some(Box::new(move |text| { tf.on_changed = Some(Box::new(move |text| {
if let Some(Ok(FilterType::TagEq(v))) = mx.lock().unwrap().get_mut(&path) { if let Some(Ok(FilterType::TagEq(v))) = mx.lock().unwrap().get_mut(&path) {
*v = text.to_owned(); *v = text.to_owned();
@ -1627,8 +1627,8 @@ impl FilterPanel {
Color::GRAY, Color::GRAY,
Color::WHITE, Color::WHITE,
); );
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
tf.on_changed = Some(Box::new(move |text| { tf.on_changed = Some(Box::new(move |text| {
if let Some(Ok(FilterType::TagStartsWith(v))) = if let Some(Ok(FilterType::TagStartsWith(v))) =
mx.lock().unwrap().get_mut(&path) mx.lock().unwrap().get_mut(&path)
@ -1659,8 +1659,8 @@ impl FilterPanel {
Color::GRAY, Color::GRAY,
Color::WHITE, Color::WHITE,
); );
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
let p = path.clone(); let p = path.clone();
tf.on_changed = Some(Box::new(move |text| { tf.on_changed = Some(Box::new(move |text| {
if let Some(Ok(FilterType::TagWithValueInt(v, _, _))) = if let Some(Ok(FilterType::TagWithValueInt(v, _, _))) =
@ -1684,8 +1684,8 @@ impl FilterPanel {
Color::GRAY, Color::GRAY,
Color::WHITE, Color::WHITE,
); );
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
let p = path.clone(); let p = path.clone();
tf1.on_changed = Some(Box::new(move |text| { tf1.on_changed = Some(Box::new(move |text| {
if let Ok(n) = text.parse() { if let Ok(n) = text.parse() {
@ -1697,8 +1697,8 @@ impl FilterPanel {
} }
} }
})); }));
let mx = Rc::clone(mutex); let mx = Arc::clone(mutex);
let oc = Rc::clone(on_change); let oc = Arc::clone(on_change);
let p = path.clone(); let p = path.clone();
tf2.on_changed = Some(Box::new(move |text| { tf2.on_changed = Some(Box::new(move |text| {
if let Ok(n) = text.parse() { if let Ok(n) = text.parse() {
@ -1813,9 +1813,9 @@ impl GuiElemTrait for FilterPanel {
.unwrap() .unwrap()
.try_as_mut::<ScrollBox>() .try_as_mut::<ScrollBox>()
.unwrap(); .unwrap();
let ssc = Rc::clone(&self.search_settings_changed); let ssc = Arc::clone(&self.search_settings_changed);
let my_tab = new_tab; let my_tab = new_tab;
let ntab = Rc::clone(&self.new_tab); let ntab = Arc::clone(&self.new_tab);
sb.children = Self::build_filter( sb.children = Self::build_filter(
match new_tab { match new_tab {
0 => &self.filter_songs, 0 => &self.filter_songs,
@ -1824,7 +1824,7 @@ impl GuiElemTrait for FilterPanel {
_ => unreachable!(), _ => unreachable!(),
}, },
info.line_height, info.line_height,
&Rc::new(move |update_ui| { &Arc::new(move |update_ui| {
if update_ui { if update_ui {
ntab.store(my_tab, std::sync::atomic::Ordering::Relaxed); ntab.store(my_tab, std::sync::atomic::Ordering::Relaxed);
} }

View File

@ -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<Box<dyn FnOnce(&Self) -> (GuiElem, NotifInfo) + Send>>,
}
impl NotifOverlay {
pub fn new() -> (
Self,
mpsc::Sender<Box<dyn FnOnce(&Self) -> (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<Color>,
/// 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<dyn Iterator<Item = &mut GuiElem> + '_> {
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<dyn GuiElemTrait> {
Box::new(self.clone())
}
}

View File

@ -7,6 +7,7 @@ use crate::{
gui::{morph_rect, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{morph_rect, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel}, gui_base::{Button, Panel},
gui_library::LibraryBrowser, gui_library::LibraryBrowser,
gui_notif::NotifOverlay,
gui_playback::{CurrentSong, PlayPauseToggle}, gui_playback::{CurrentSong, PlayPauseToggle},
gui_queue::QueueViewer, gui_queue::QueueViewer,
gui_settings::Settings, gui_settings::Settings,
@ -34,15 +35,16 @@ pub fn transition(p: f32) -> f32 {
#[derive(Clone)] #[derive(Clone)]
pub struct GuiScreen { pub struct GuiScreen {
config: GuiElemCfg, config: GuiElemCfg,
/// 0: StatusBar / Idle display /// 0: Notifications
/// 1: Settings /// 1: StatusBar / Idle display
/// 2: Panel for Main view /// 2: Settings
/// 3: Panel for Main view
/// 0: settings button /// 0: settings button
/// 1: exit button /// 1: exit button
/// 2: library browser /// 2: library browser
/// 3: queue /// 3: queue
/// 4: queue clear button /// 4: queue clear button
/// 3: Edit Panel /// 4: Edit Panel
children: Vec<GuiElem>, children: Vec<GuiElem>,
pub idle: (bool, Option<Instant>), pub idle: (bool, Option<Instant>),
pub settings: (bool, Option<Instant>), pub settings: (bool, Option<Instant>),
@ -59,21 +61,22 @@ impl GuiScreen {
} else { } else {
edit.inner.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (0.5, 0.9)); 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; prev.inner.config_mut().enabled = false;
} }
self.children.insert(3, edit); self.children.insert(4, edit);
} }
pub fn close_edit(&mut self) { pub fn close_edit(&mut self) {
if self.children.len() > 4 { if self.children.len() > 5 {
self.children.remove(3); self.children.remove(4);
self.children[3].inner.config_mut().enabled = true; self.children[4].inner.config_mut().enabled = true;
} else if self.edit_panel.0 { } else if self.edit_panel.0 {
self.edit_panel = (false, Some(Instant::now())); self.edit_panel = (false, Some(Instant::now()));
} }
} }
pub fn new( pub fn new(
config: GuiElemCfg, config: GuiElemCfg,
notif_overlay: NotifOverlay,
line_height: f32, line_height: f32,
scroll_sensitivity_pixels: f64, scroll_sensitivity_pixels: f64,
scroll_sensitivity_lines: f64, scroll_sensitivity_lines: f64,
@ -82,6 +85,7 @@ impl GuiScreen {
Self { Self {
config: config.w_keyboard_watch().w_mouse(), config: config.w_keyboard_watch().w_mouse(),
children: vec![ children: vec![
GuiElem::new(notif_overlay),
GuiElem::new(StatusBar::new( GuiElem::new(StatusBar::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
true, true,
@ -261,20 +265,20 @@ impl GuiElemTrait for GuiScreen {
if let Some(h) = &info.helper { if let Some(h) = &info.helper {
h.set_cursor_visible(!self.idle.0); h.set_cursor_visible(!self.idle.0);
if self.settings.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 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; 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); 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)); Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 1.0));
self.children[0] self.children[1]
.inner .inner
.any_mut() .any_mut()
.downcast_mut::<StatusBar>() .downcast_mut::<StatusBar>()
@ -285,7 +289,7 @@ impl GuiElemTrait for GuiScreen {
if self.settings.1.is_some() { if self.settings.1.is_some() {
let p1 = Self::get_prog(&mut self.settings, 0.3); let p1 = Self::get_prog(&mut self.settings, 0.3);
let p = transition(p1); 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.enabled = p > 0.0;
cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9)); 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() { if self.edit_panel.1.is_some() {
let p1 = Self::get_prog(&mut self.edit_panel, 0.3); let p1 = Self::get_prog(&mut self.edit_panel, 0.3);
let p = transition(p1); 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().enabled = p > 0.0;
c.inner.config_mut().pos = c.inner.config_mut().pos =
Rectangle::from_tuples((-0.5 + 0.5 * p, 0.0), (0.5 * p, 0.9)); Rectangle::from_tuples((-0.5 + 0.5 * p, 0.0), (0.5 * p, 0.9));
} }
if !self.edit_panel.0 && p == 0.0 { if !self.edit_panel.0 && p == 0.0 {
while self.children.len() > 3 { while self.children.len() > 4 {
self.children.pop(); 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)); Rectangle::from_tuples((0.5 * p, 0.0), (1.0 + 0.5 * p, 0.9));
} }
// set idle timeout (only when settings are open) // set idle timeout (only when settings are open)
if self.settings.0 || self.settings.1.is_some() { if self.settings.0 || self.settings.1.is_some() {
self.idle_timeout = self.children[1] self.idle_timeout = self.children[2]
.inner .inner
.any() .any()
.downcast_ref::<Settings>() .downcast_ref::<Settings>()

View File

@ -27,6 +27,8 @@ mod gui_edit;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_library; mod gui_library;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_notif;
#[cfg(feature = "speedy2d")]
mod gui_playback; mod gui_playback;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_queue; mod gui_queue;

View File

@ -232,7 +232,14 @@ impl Database {
Ok(()) 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 // since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
self.broadcast_update(&command); self.broadcast_update(&command);
match command { match command {
@ -349,6 +356,7 @@ impl Database {
Command::InitComplete => { Command::InitComplete => {
self.client_is_init = true; self.client_is_init = true;
} }
Command::ErrorInfo(..) => {}
} }
} }
} }

View File

@ -154,7 +154,7 @@ impl Song {
{ {
Ok(data) => Some(data), Ok(data) => Some(data),
Err(e) => { Err(e) => {
eprintln!("[info] error loading song {id}: {e}"); eprintln!("[WARN] error loading song {id}: {e}");
None None
} }
} }

View File

@ -139,6 +139,13 @@ impl Player {
db.apply_command(Command::NextSong); 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 { } else {
self.source = None; self.source = None;

View File

@ -1,7 +1,6 @@
pub mod get; pub mod get;
use std::{ use std::{
eprintln,
io::{BufRead, BufReader, Read, Write}, io::{BufRead, BufReader, Read, Write},
net::{SocketAddr, TcpListener}, net::{SocketAddr, TcpListener},
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
@ -51,6 +50,7 @@ pub enum Command {
RemoveArtist(ArtistId), RemoveArtist(ArtistId),
ModifyArtist(Artist), ModifyArtist(Artist),
InitComplete, InitComplete,
ErrorInfo(String, String),
} }
impl Command { impl Command {
pub fn send_to_server(self, db: &Database) -> Result<(), Self> { 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 command_sender = command_sender.clone();
let db = Arc::clone(&db); let db = Arc::clone(&db);
thread::spawn(move || { 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 // each connection first has to send one line to tell us what it wants
let mut connection = BufReader::new(connection); let mut connection = BufReader::new(connection);
let mut line = String::new(); let mut line = String::new();
@ -279,6 +278,11 @@ impl ToFromBytes for Command {
Self::InitComplete => { Self::InitComplete => {
s.write_all(&[0b00110001])?; s.write_all(&[0b00110001])?;
} }
Self::ErrorInfo(t, d) => {
s.write_all(&[0b11011011])?;
t.to_bytes(s)?;
d.to_bytes(s)?;
}
} }
Ok(()) Ok(())
} }
@ -324,6 +328,7 @@ impl ToFromBytes for Command {
0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?), 0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?),
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
0b00110001 => Self::InitComplete, 0b00110001 => Self::InitComplete,
0b11011011 => Self::ErrorInfo(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?),
_ => { _ => {
eprintln!("unexpected byte when reading command; stopping playback."); eprintln!("unexpected byte when reading command; stopping playback.");
Self::Stop Self::Stop

View File

@ -438,7 +438,9 @@ async fn sse_handler(
.collect::<String>(), .collect::<String>(),
) )
} }
Command::Save | Command::InitComplete => return Poll::Pending, Command::Save | Command::InitComplete | Command::ErrorInfo(..) => {
return Poll::Pending
}
})) }))
} else { } else {
return Poll::Pending; return Poll::Pending;