mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 05:43:53 +01:00
server can now send error messages to clients
This commit is contained in:
parent
94df757f0c
commit
1eee22bb4b
@ -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,
|
||||
|
@ -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<Regex>,
|
||||
search_song: String,
|
||||
search_song_regex: Option<Regex>,
|
||||
filter_target_state: Rc<AtomicBool>,
|
||||
filter_target_state: Arc<AtomicBool>,
|
||||
filter_state: f32,
|
||||
library_updated: bool,
|
||||
search_settings_changed: Rc<AtomicBool>,
|
||||
search_is_case_sensitive: Rc<AtomicBool>,
|
||||
search_settings_changed: Arc<AtomicBool>,
|
||||
search_is_case_sensitive: Arc<AtomicBool>,
|
||||
search_was_case_sensitive: bool,
|
||||
search_prefer_start_matches: Rc<AtomicBool>,
|
||||
search_prefer_start_matches: Arc<AtomicBool>,
|
||||
search_prefers_start_matches: bool,
|
||||
filter_songs: Rc<Mutex<Filter>>,
|
||||
filter_albums: Rc<Mutex<Filter>>,
|
||||
filter_artists: Rc<Mutex<Filter>>,
|
||||
filter_songs: Arc<Mutex<Filter>>,
|
||||
filter_albums: Arc<Mutex<Filter>>,
|
||||
filter_artists: Arc<Mutex<Filter>>,
|
||||
do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
||||
}
|
||||
impl Clone for LibraryBrowser {
|
||||
@ -76,7 +76,7 @@ impl Clone for LibraryBrowser {
|
||||
}
|
||||
}
|
||||
#[derive(Clone)]
|
||||
struct Selected(Rc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>);
|
||||
struct Selected(Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>);
|
||||
impl Selected {
|
||||
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
|
||||
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<GuiElem>,
|
||||
search_settings_changed: Rc<AtomicBool>,
|
||||
search_settings_changed: Arc<AtomicBool>,
|
||||
tab: usize,
|
||||
new_tab: Rc<AtomicUsize>,
|
||||
new_tab: Arc<AtomicUsize>,
|
||||
line_height: f32,
|
||||
filter_songs: Rc<Mutex<Filter>>,
|
||||
filter_albums: Rc<Mutex<Filter>>,
|
||||
filter_artists: Rc<Mutex<Filter>>,
|
||||
filter_songs: Arc<Mutex<Filter>>,
|
||||
filter_albums: Arc<Mutex<Filter>>,
|
||||
filter_artists: Arc<Mutex<Filter>>,
|
||||
}
|
||||
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<AtomicBool>,
|
||||
search_is_case_sensitive: Rc<AtomicBool>,
|
||||
search_prefer_start_matches: Rc<AtomicBool>,
|
||||
filter_songs: Rc<Mutex<Filter>>,
|
||||
filter_albums: Rc<Mutex<Filter>>,
|
||||
filter_artists: Rc<Mutex<Filter>>,
|
||||
search_settings_changed: Arc<AtomicBool>,
|
||||
search_is_case_sensitive: Arc<AtomicBool>,
|
||||
search_prefer_start_matches: Arc<AtomicBool>,
|
||||
filter_songs: Arc<Mutex<Filter>>,
|
||||
filter_albums: Arc<Mutex<Filter>>,
|
||||
filter_artists: Arc<Mutex<Filter>>,
|
||||
selected: Selected,
|
||||
do_something_sender: mpsc::Sender<Box<dyn FnOnce(&mut LibraryBrowser)>>,
|
||||
) -> 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<Mutex<Filter>>,
|
||||
filter: &Arc<Mutex<Filter>>,
|
||||
line_height: f32,
|
||||
on_change: &Rc<impl Fn(bool) + 'static>,
|
||||
on_change: &Arc<impl Fn(bool) + 'static>,
|
||||
path: Vec<usize>,
|
||||
) -> 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<Filter>>,
|
||||
mutex: &Arc<Mutex<Filter>>,
|
||||
children: &mut Vec<GuiElem>,
|
||||
mut indent: f32,
|
||||
indent_by: f32,
|
||||
on_change: &Rc<impl Fn(bool) + 'static>,
|
||||
on_change: &Arc<impl Fn(bool) + 'static>,
|
||||
path: Vec<usize>,
|
||||
) {
|
||||
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::<ScrollBox>()
|
||||
.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);
|
||||
}
|
||||
|
209
musicdb-client/src/gui_notif.rs
Normal file
209
musicdb-client/src/gui_notif.rs
Normal 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())
|
||||
}
|
||||
}
|
@ -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<GuiElem>,
|
||||
pub idle: (bool, Option<Instant>),
|
||||
pub settings: (bool, Option<Instant>),
|
||||
@ -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::<StatusBar>()
|
||||
@ -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::<Settings>()
|
||||
|
@ -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;
|
||||
|
@ -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(..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -438,7 +438,9 @@ async fn sse_handler(
|
||||
.collect::<String>(),
|
||||
)
|
||||
}
|
||||
Command::Save | Command::InitComplete => return Poll::Pending,
|
||||
Command::Save | Command::InitComplete | Command::ErrorInfo(..) => {
|
||||
return Poll::Pending
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
return Poll::Pending;
|
||||
|
Loading…
Reference in New Issue
Block a user