This commit is contained in:
Mark 2023-12-27 15:39:53 +01:00
parent 168f51a5fc
commit 8c434743f8
17 changed files with 1621 additions and 121 deletions

View File

@ -12,6 +12,9 @@ musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
regex = "1.9.3" regex = "1.9.3"
speedy2d = { version = "1.12.0", optional = true } speedy2d = { version = "1.12.0", optional = true }
toml = "0.7.6" toml = "0.7.6"
mers_lib = { path = "../../mers/mers_lib", optional = true }
[features] [features]
default = ["speedy2d"] default = ["speedy2d"]
merscfg = ["mers_lib"]
playback = ["musicdb-lib/playback"]

View File

@ -9,7 +9,7 @@ use std::{
}; };
use musicdb_lib::{ 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, load::ToFromBytes,
server::{get, Command}, server::{get, Command},
}; };
@ -26,8 +26,11 @@ use speedy2d::{
Graphics2D, Graphics2D,
}; };
#[cfg(feature = "merscfg")]
use crate::merscfg::MersCfg;
use crate::{ use crate::{
gui_base::Panel, gui_base::{Panel, ScrollBox},
gui_edit_song::EditorForSongs,
gui_notif::{NotifInfo, NotifOverlay}, gui_notif::{NotifInfo, NotifOverlay},
gui_screen::GuiScreen, gui_screen::GuiScreen,
gui_text::Label, gui_text::Label,
@ -76,8 +79,8 @@ pub fn main(
get_con: get::Client<TcpStream>, get_con: get::Client<TcpStream>,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>, event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
) { ) {
let mut config_file = super::get_config_file_path(); let config_dir = super::get_config_file_path();
config_file.push("config_gui.toml"); let config_file = config_dir.join("config_gui.toml");
let mut font = None; let mut font = None;
let mut line_height = 32.0; let mut line_height = 32.0;
let mut scroll_pixels_multiplier = 1.0; let mut scroll_pixels_multiplier = 1.0;
@ -214,7 +217,7 @@ pub fn main(
connection, connection,
Arc::new(Mutex::new(get_con)), Arc::new(Mutex::new(get_con)),
event_sender_arc, event_sender_arc,
sender, Arc::new(sender),
line_height, line_height,
scroll_pixels_multiplier, scroll_pixels_multiplier,
scroll_lines_multiplier, scroll_lines_multiplier,
@ -254,6 +257,8 @@ pub fn main(
crate::gui_library::FilterType::TagWithValueInt("Year".to_owned(), 1990, 2000), 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_song: Vec<(String, crate::gui_library::FilterType)>,
pub filter_presets_album: 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)>, pub filter_presets_artist: Vec<(String, crate::gui_library::FilterType)>,
#[cfg(feature = "merscfg")]
pub merscfg: crate::merscfg::MersCfg,
} }
pub struct Gui { pub struct Gui {
pub event_sender: UserEventSender<GuiEvent>, pub event_sender: Arc<UserEventSender<GuiEvent>>,
pub database: Arc<Mutex<Database>>, pub database: Arc<Mutex<Database>>,
pub connection: TcpStream, pub connection: TcpStream,
pub get_con: Arc<Mutex<get::Client<TcpStream>>>, pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
@ -304,7 +311,7 @@ impl Gui {
connection: TcpStream, connection: TcpStream,
get_con: Arc<Mutex<get::Client<TcpStream>>>, get_con: Arc<Mutex<get::Client<TcpStream>>>,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>, event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
event_sender: UserEventSender<GuiEvent>, event_sender: Arc<UserEventSender<GuiEvent>>,
line_height: f32, line_height: f32,
scroll_pixels_multiplier: f64, scroll_pixels_multiplier: f64,
scroll_lines_multiplier: f64, scroll_lines_multiplier: f64,
@ -313,6 +320,28 @@ impl Gui {
) -> Self { ) -> Self {
let (notif_overlay, notif_sender) = NotifOverlay::new(); let (notif_overlay, notif_sender) = NotifOverlay::new();
let notif_sender_two = notif_sender.clone(); 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( 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
@ -662,6 +691,9 @@ pub(crate) trait GuiElemInternal: GuiElem {
} }
} }
fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool { fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool {
if self.config().enabled == false {
return false;
}
let mut focus_index = if refocus { let mut focus_index = if refocus {
usize::MAX usize::MAX
} else { } 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 the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc)
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>), Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
SendToServer(Command), SendToServer(Command),
ContextMenu(Option<Box<dyn GuiElem>>), ContextMenu(Option<(Vec<Box<dyn GuiElem>>)>),
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one. /// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true if there is one.
ResetKeyboardFocus, ResetKeyboardFocus,
SetDragging( SetDragging(
@ -998,8 +1030,11 @@ pub enum GuiAction {
SetLineHeight(f32), SetLineHeight(f32),
LoadCover(CoverId), LoadCover(CoverId),
/// Run a custom closure with mutable access to the Gui struct /// Run a custom closure with mutable access to the Gui struct
Do(Box<dyn FnMut(&mut Gui)>), Do(Box<dyn FnOnce(&mut Gui)>),
Exit, Exit,
EditSongs(Vec<Song>),
// EditAlbums(Vec<Album>),
// EditArtists(Vec<Artist>),
} }
pub enum Dragging { pub enum Dragging {
Artist(ArtistId), Artist(ArtistId),
@ -1032,7 +1067,6 @@ pub struct DrawInfo<'a> {
Dragging, Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>, Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>, )>,
pub context_menu: Option<Box<dyn GuiElem>>,
pub gui_config: &'a mut GuiConfig, pub gui_config: &'a mut GuiConfig,
pub high_performance: bool, pub high_performance: bool,
} }
@ -1068,7 +1102,40 @@ impl Gui {
GuiAction::ResetKeyboardFocus => _ = self.gui._keyboard_reset_focus(), GuiAction::ResetKeyboardFocus => _ = self.gui._keyboard_reset_focus(),
GuiAction::SetDragging(d) => self.dragging = d, GuiAction::SetDragging(d) => self.dragging = d,
GuiAction::SetHighPerformance(d) => self.high_performance = 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) => { GuiAction::SetLineHeight(h) => {
self.line_height = h; self.line_height = h;
self.gui self.gui
@ -1080,7 +1147,7 @@ impl Gui {
.unwrap() .unwrap()
.insert(id, GuiServerImage::new_cover(id, Arc::clone(&self.get_con))); .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::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
GuiAction::EndIdle(v) => { GuiAction::EndIdle(v) => {
if v { if v {
@ -1090,19 +1157,20 @@ impl Gui {
} }
} }
GuiAction::OpenSettings(v) => { GuiAction::OpenSettings(v) => {
self.gui.idle.target = 0.0; self.gui.unidle();
self.gui.last_interaction = Instant::now();
if self.gui.settings.0 != v { if self.gui.settings.0 != v {
self.gui.settings = (v, Some(Instant::now())); self.gui.settings = (v, Some(Instant::now()));
} }
} }
GuiAction::OpenMain => { GuiAction::OpenMain => {
self.gui.idle.target = 0.0; self.gui.unidle();
self.gui.last_interaction = Instant::now();
if self.gui.settings.0 { if self.gui.settings.0 {
self.gui.settings = (false, Some(Instant::now())); 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<GuiEvent> for Gui {
Rectangle::new(Vec2::ZERO, self.size.into_f32()), Rectangle::new(Vec2::ZERO, self.size.into_f32()),
Color::BLACK, 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 covers = self.covers.take().unwrap();
let mut custom_images = self.custom_images.take().unwrap(); let mut custom_images = self.custom_images.take().unwrap();
let mut cfg = self.gui_config.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 { let mut info = DrawInfo {
time: draw_start_time, time: draw_start_time,
actions: Vec::with_capacity(0), actions: Vec::with_capacity(0),
@ -1133,12 +1204,10 @@ impl WindowHandler<GuiEvent> for Gui {
line_height: self.line_height, line_height: self.line_height,
high_performance: self.high_performance, high_performance: self.high_performance,
dragging: self.dragging.take(), dragging: self.dragging.take(),
context_menu: self.gui.c_context_menu.take(),
gui_config: &mut cfg, gui_config: &mut cfg,
}; };
self.gui._draw(&mut info, graphics); self.gui._draw(&mut info, graphics);
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0)); 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(); self.dragging = info.dragging.take();
if let Some((d, f)) = &mut self.dragging { if let Some((d, f)) = &mut self.dragging {
if let Some(f) = f { if let Some(f) = f {
@ -1242,6 +1311,9 @@ impl WindowHandler<GuiEvent> for Gui {
self.exec_gui_action(a) self.exec_gui_action(a)
} }
} }
if button != MouseButton::Right {
self.gui.c_context_menu = None;
}
helper.request_redraw(); helper.request_redraw();
} }
fn on_mouse_wheel_scroll( fn on_mouse_wheel_scroll(
@ -1365,10 +1437,34 @@ impl WindowHandler<GuiEvent> for Gui {
match user_event { match user_event {
GuiEvent::Refresh => helper.request_redraw(), GuiEvent::Refresh => helper.request_redraw(),
GuiEvent::UpdatedLibrary => { 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()); self.gui._recursive_all(&mut |e| e.updated_library());
helper.request_redraw(); helper.request_redraw();
} }
GuiEvent::UpdatedQueue => { 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()); self.gui._recursive_all(&mut |e| e.updated_queue());
helper.request_redraw(); 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(),
),
)
}

View File

@ -122,9 +122,11 @@ pub struct ScrollBox<C: GuiElemChildren> {
config: GuiElemCfg, config: GuiElemCfg,
pub children: C, pub children: C,
pub children_heights: Vec<f32>, pub children_heights: Vec<f32>,
pub default_size: f32,
pub size_unit: ScrollBoxSizeUnit, pub size_unit: ScrollBoxSizeUnit,
pub scroll_target: f32, pub scroll_target: f32,
pub scroll_display: f32, pub scroll_display: f32,
/// the y-position of the bottom edge of the last element (i.e. the total height)
height_bottom: f32, height_bottom: f32,
/// 0.max(height_bottom - 1) /// 0.max(height_bottom - 1)
max_scroll: f32, max_scroll: f32,
@ -145,15 +147,16 @@ impl<C: GuiElemChildren> ScrollBox<C> {
size_unit: ScrollBoxSizeUnit, size_unit: ScrollBoxSizeUnit,
children: C, children: C,
children_heights: Vec<f32>, children_heights: Vec<f32>,
default_size: f32,
) -> Self { ) -> Self {
Self { Self {
config: config.w_scroll().w_mouse(), config: config.w_scroll().w_mouse(),
children, children,
children_heights, children_heights,
default_size,
size_unit, size_unit,
scroll_target: 0.0, scroll_target: 0.0,
scroll_display: 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, height_bottom: 0.0,
max_scroll: 0.0, max_scroll: 0.0,
last_height_px: 0.0, last_height_px: 0.0,
@ -217,7 +220,7 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
if self.children_heights.len() != self.children.len() { if self.children_heights.len() != self.children.len() {
let target = self.children.len(); let target = self.children.len();
while self.children_heights.len() < target { 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 { while self.children_heights.len() > target {
self.children_heights.pop(); self.children_heights.pop();
@ -341,7 +344,7 @@ impl<C: GuiElemChildren> Button<C> {
children: C, children: C,
) -> Self { ) -> Self {
Self { Self {
config: config.w_mouse(), config: config.w_mouse().w_keyboard_focus(),
children, children,
action: Arc::new(action), action: Arc::new(action),
} }
@ -376,6 +379,27 @@ impl<C: GuiElemChildren + 'static> GuiElem for Button<C> {
vec![] vec![]
} }
} }
fn key_focus(
&mut self,
_modifiers: speedy2d::window::ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
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) { fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
let mouse_down = self.config.mouse_down.0; let mouse_down = self.config.mouse_down.0;
let contains = info.pos.contains(info.mouse_pos); let contains = info.pos.contains(info.mouse_pos);
@ -389,6 +413,32 @@ impl<C: GuiElemChildren + 'static> GuiElem for Button<C> {
Color::from_rgb(0.1, 0.1, 0.1) 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,
);
}
} }
} }

View File

@ -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<Song>,
c_title: Label,
c_scrollbox: ScrollBox<EditorForSongElems>,
c_buttons: Panel<[Button<[Label; 1]>; 2]>,
c_background: Panel<()>,
created: Option<Instant>,
event_sender: std::sync::mpsc::Sender<Event>,
event_recv: std::sync::mpsc::Receiver<Event>,
}
pub enum Event {
Close,
Apply,
SetArtist(String, Option<ArtistId>),
}
pub struct EditorForSongElems {
c_title: TextField,
c_artist: EditorForSongArtistChooser,
c_album: Label,
}
impl GuiElemChildren for EditorForSongElems {
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn crate::gui::GuiElem> + '_> {
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<Song>) -> 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::<String>()
),
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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
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<Event>,
/// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6)
open_prog: AnimationController<f32>,
expand_to: f32,
chosen_id: Option<ArtistId>,
c_name: TextField,
c_picker: ScrollBox<Vec<Button<[Label; 1]>>>,
last_search: String,
}
impl EditorForSongArtistChooser {
pub fn new(event_sender: std::sync::mpsc::Sender<Event>) -> 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::<Vec<_>>();
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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
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
}
}

View File

@ -4,7 +4,7 @@ use musicdb_lib::data::ArtistId;
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle}; use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
use crate::{ use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage}, gui::{rect_from_rel, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiServerImage},
gui_anim::AnimationController, gui_anim::AnimationController,
gui_base::Button, gui_base::Button,
gui_playback::{get_right_x, image_display, CurrentInfo}, gui_playback::{get_right_x, image_display, CurrentInfo},
@ -13,24 +13,32 @@ use crate::{
}; };
pub struct IdleDisplay { pub struct IdleDisplay {
config: GuiElemCfg, pub config: GuiElemCfg,
pub idle_mode: f32, pub idle_mode: f32,
current_info: CurrentInfo, pub current_info: CurrentInfo,
current_artist_image: Option<(ArtistId, Option<(String, Option<Option<ImageHandle>>)>)>, pub current_artist_image: Option<(ArtistId, Option<(String, Option<Option<ImageHandle>>)>)>,
pub c_idle_exit_hint: Button<[Label; 1]>, pub c_idle_exit_hint: Button<[Label; 1]>,
c_top_label: AdvancedLabel, pub c_top_label: AdvancedLabel,
c_side1_label: AdvancedLabel, pub c_side1_label: AdvancedLabel,
c_side2_label: AdvancedLabel, pub c_side2_label: AdvancedLabel,
c_buttons: PlayPause, pub c_buttons: PlayPause,
cover_aspect_ratio: AnimationController<f32>, pub c_buttons_custom_pos: bool,
artist_image_aspect_ratio: AnimationController<f32>,
cover_left: f32, pub cover_aspect_ratio: AnimationController<f32>,
cover_top: f32, pub artist_image_aspect_ratio: AnimationController<f32>,
cover_bottom: f32,
pub cover_pos: Option<Rectangle>,
pub cover_left: f32,
pub cover_top: f32,
pub cover_bottom: f32,
pub artist_image_pos: Option<Rectangle>,
/// 0.0 -> same height as cover, /// 0.0 -> same height as cover,
/// 0.5 -> lower half of cover /// 0.5 -> lower half of cover
artist_image_top: f32, pub artist_image_top: f32,
artist_image_to_cover_margin: f32, pub artist_image_to_cover_margin: f32,
pub force_reset_texts: bool,
} }
impl IdleDisplay { impl IdleDisplay {
@ -60,6 +68,7 @@ impl IdleDisplay {
c_side1_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), 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_side2_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]),
c_buttons: PlayPause::new(GuiElemCfg::default()), c_buttons: PlayPause::new(GuiElemCfg::default()),
c_buttons_custom_pos: false,
cover_aspect_ratio: AnimationController::new( cover_aspect_ratio: AnimationController::new(
1.0, 1.0,
1.0, 1.0,
@ -78,11 +87,14 @@ impl IdleDisplay {
0.6, 0.6,
Instant::now(), Instant::now(),
), ),
cover_pos: None,
cover_left: 0.01, cover_left: 0.01,
cover_top: 0.21, cover_top: 0.21,
cover_bottom, cover_bottom,
artist_image_pos: None,
artist_image_top: 0.5, artist_image_top: 0.5,
artist_image_to_cover_margin: 0.01, artist_image_to_cover_margin: 0.01,
force_reset_texts: false,
} }
} }
} }
@ -108,7 +120,7 @@ impl GuiElem for IdleDisplay {
); );
// update current_info // update current_info
self.current_info.update(info, g); 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.current_info.new_song = false;
self.c_top_label.content = if let Some(song) = self.current_info.current_song { self.c_top_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
@ -207,6 +219,7 @@ impl GuiElem for IdleDisplay {
image_display( image_display(
g, g,
cover.as_ref(), 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().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_top,
info.pos.top_left().y + info.pos.height() * self.cover_bottom, info.pos.top_left().y + info.pos.height() * self.cover_bottom,
@ -220,6 +233,9 @@ impl GuiElem for IdleDisplay {
image_display( image_display(
g, g,
img.as_ref(), img.as_ref(),
self.artist_image_pos
.as_ref()
.map(|v| rect_from_rel(v, &info.pos)),
get_right_x( get_right_x(
info.pos.top_left().x + info.pos.height() * self.cover_left, info.pos.top_left().x + info.pos.height() * self.cover_left,
top, top,
@ -265,12 +281,14 @@ impl GuiElem for IdleDisplay {
let buttons_right_pos = 0.99; let buttons_right_pos = 0.99;
let buttons_width_max = info.pos.height() * 0.08 / 0.3 / info.pos.width(); let buttons_width_max = info.pos.height() * 0.08 / 0.3 / info.pos.width();
let buttons_width = buttons_width_max.min(0.2); let buttons_width = buttons_width_max.min(0.2);
if !self.c_buttons_custom_pos {
self.c_buttons.config_mut().pos = Rectangle::from_tuples( self.c_buttons.config_mut().pos = Rectangle::from_tuples(
(buttons_right_pos - buttons_width, 0.86), (buttons_right_pos - buttons_width, 0.86),
(buttons_right_pos, 0.94), (buttons_right_pos, 0.94),
); );
} }
} }
}
fn config(&self) -> &GuiElemCfg { fn config(&self) -> &GuiElemCfg {
&self.config &self.config
} }

View File

@ -123,6 +123,7 @@ impl LibraryBrowser {
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![], vec![],
vec![], vec![],
0.0,
); );
let (do_something_sender, do_something_receiver) = mpsc::channel(); let (do_something_sender, do_something_receiver) = mpsc::channel();
let search_settings_changed = Arc::new(AtomicBool::new(false)); let search_settings_changed = Arc::new(AtomicBool::new(false));
@ -936,7 +937,7 @@ impl GuiElem for ListArtist {
let selected = self.selected.clone(); let selected = self.selected.clone();
info.actions.push(GuiAction::Do(Box::new(move |gui| { info.actions.push(GuiAction::Do(Box::new(move |gui| {
let q = selected.as_queue( 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.database.lock().unwrap(),
); );
gui.exec_gui_action(GuiAction::SetDragging(Some(( gui.exec_gui_action(GuiAction::SetDragging(Some((
@ -1074,7 +1075,7 @@ impl GuiElem for ListAlbum {
let selected = self.selected.clone(); let selected = self.selected.clone();
info.actions.push(GuiAction::Do(Box::new(move |gui| { info.actions.push(GuiAction::Do(Box::new(move |gui| {
let q = selected.as_queue( 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.database.lock().unwrap(),
); );
gui.exec_gui_action(GuiAction::SetDragging(Some(( gui.exec_gui_action(GuiAction::SetDragging(Some((
@ -1208,7 +1209,7 @@ impl GuiElem for ListSong {
let selected = self.selected.clone(); let selected = self.selected.clone();
info.actions.push(GuiAction::Do(Box::new(move |gui| { info.actions.push(GuiAction::Do(Box::new(move |gui| {
let q = selected.as_queue( 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.database.lock().unwrap(),
); );
gui.exec_gui_action(GuiAction::SetDragging(Some(( gui.exec_gui_action(GuiAction::SetDragging(Some((
@ -1251,6 +1252,31 @@ impl GuiElem for ListSong {
} }
vec![] vec![]
} }
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
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 { struct FilterPanel {
@ -1484,24 +1510,28 @@ impl FilterPanel {
), ),
), ),
vec![0.0; 10], vec![0.0; 10],
0.0,
); );
let c_tab_filters_songs = ScrollBox::new( let c_tab_filters_songs = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))), GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))),
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
FilterTab::default(), FilterTab::default(),
vec![], vec![],
0.0,
); );
let c_tab_filters_albums = ScrollBox::new( let c_tab_filters_albums = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(), GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(),
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
FilterTab::default(), FilterTab::default(),
vec![], vec![],
0.0,
); );
let c_tab_filters_artists = ScrollBox::new( let c_tab_filters_artists = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(), GuiElemCfg::at(Rectangle::from_tuples((VSPLIT, HEIGHT), (1.0, 1.0))).disabled(),
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
FilterTab::default(), FilterTab::default(),
vec![], vec![],
0.0,
); );
let new_tab = Arc::new(AtomicUsize::new(0)); let new_tab = Arc::new(AtomicUsize::new(0));
let set_tab_1 = Arc::clone(&new_tab); let set_tab_1 = Arc::clone(&new_tab);

View File

@ -147,6 +147,7 @@ impl CurrentInfo {
pub fn image_display( pub fn image_display(
g: &mut speedy2d::Graphics2D, g: &mut speedy2d::Graphics2D,
img: Option<&ImageHandle>, img: Option<&ImageHandle>,
pos: Option<Rectangle>,
left: f32, left: f32,
top: f32, top: f32,
bottom: f32, bottom: f32,
@ -155,8 +156,12 @@ pub fn image_display(
if let Some(cover) = &img { if let Some(cover) = &img {
let cover_size = cover.size(); let cover_size = cover.size();
aspect_ratio.target = if cover_size.x > 0 && cover_size.y > 0 { aspect_ratio.target = if cover_size.x > 0 && cover_size.y > 0 {
let pos = if let Some(pos) = pos {
pos
} else {
let right_x = get_right_x(left, top, bottom, aspect_ratio.value); let right_x = get_right_x(left, top, bottom, aspect_ratio.value);
let pos = Rectangle::from_tuples((left, top), (right_x, bottom)); Rectangle::from_tuples((left, top), (right_x, bottom))
};
let aspect_ratio = cover_size.x as f32 / cover_size.y as f32; let aspect_ratio = cover_size.x as f32 / cover_size.y as f32;
g.draw_rectangle_image(pos, cover); g.draw_rectangle_image(pos, cover);
aspect_ratio aspect_ratio

View File

@ -96,6 +96,7 @@ impl QueueViewer {
crate::gui_base::ScrollBoxSizeUnit::Pixels, crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![], vec![],
vec![], vec![],
0.0,
), ),
c_empty_space_drag_handler: QueueEmptySpaceDragHandler::new(GuiElemCfg::at( c_empty_space_drag_handler: QueueEmptySpaceDragHandler::new(GuiElemCfg::at(
Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)), Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)),

View File

@ -4,9 +4,10 @@ use musicdb_lib::{data::queue::QueueContent, server::Command};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D}; use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D};
use crate::{ use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg}, gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemChildren},
gui_anim::AnimationController, gui_anim::AnimationController,
gui_base::{Button, Panel}, gui_base::{Button, Panel},
gui_edit_song::EditorForSongs,
gui_idle_display::IdleDisplay, gui_idle_display::IdleDisplay,
gui_library::LibraryBrowser, gui_library::LibraryBrowser,
gui_notif::NotifOverlay, gui_notif::NotifOverlay,
@ -37,17 +38,12 @@ pub fn transition(p: f32) -> f32 {
pub struct GuiScreen { pub struct GuiScreen {
config: GuiElemCfg, config: GuiElemCfg,
c_notif_overlay: NotifOverlay, pub c_notif_overlay: NotifOverlay,
c_idle_display: IdleDisplay, pub c_idle_display: IdleDisplay,
c_status_bar: StatusBar, pub c_editing_songs: Option<EditorForSongs>,
pub c_status_bar: StatusBar,
pub c_settings: Settings, pub c_settings: Settings,
pub c_main_view: Panel<( pub c_main_view: Panel<MainView>,
Button<[Label; 1]>,
Button<[Label; 1]>,
LibraryBrowser,
QueueViewer,
Button<[Label; 1]>,
)>,
pub c_context_menu: Option<Box<dyn GuiElem>>, pub c_context_menu: Option<Box<dyn GuiElem>>,
pub idle: AnimationController<f32>, pub idle: AnimationController<f32>,
// pub settings: (bool, Option<Instant>), // pub settings: (bool, Option<Instant>),
@ -57,6 +53,30 @@ pub struct GuiScreen {
pub prev_mouse_pos: Vec2, pub prev_mouse_pos: Vec2,
pub hotkey: Hotkey, 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<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
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 { impl GuiScreen {
pub fn new( pub fn new(
config: GuiElemCfg, config: GuiElemCfg,
@ -74,6 +94,7 @@ impl GuiScreen {
(0.0, 0.9), (0.0, 0.9),
(1.0, 1.0), (1.0, 1.0),
))), ))),
c_editing_songs: None,
c_idle_display: IdleDisplay::new(GuiElemCfg::default().disabled()), c_idle_display: IdleDisplay::new(GuiElemCfg::default().disabled()),
c_settings: Settings::new( c_settings: Settings::new(
GuiElemCfg::default().disabled(), GuiElemCfg::default().disabled(),
@ -85,38 +106,8 @@ impl GuiScreen {
), ),
c_main_view: Panel::new( c_main_view: Panel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))), GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
( MainView {
Button::new( button_clear_queue: 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(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))), GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.03))),
|_| { |_| {
vec![GuiAction::SendToServer( vec![GuiAction::SendToServer(
@ -139,7 +130,37 @@ impl GuiScreen {
Vec2::new(0.5, 0.5), 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, c_context_menu: None,
hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape), hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape),
@ -173,6 +194,9 @@ impl GuiScreen {
0.0 0.0
} }
} }
pub fn force_idle(&mut self) {
self.idle.target = 1.0;
}
pub fn not_idle(&mut self) { pub fn not_idle(&mut self) {
self.last_interaction = Instant::now(); self.last_interaction = Instant::now();
if self.idle.target > 0.0 { 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 { impl GuiElem for GuiScreen {
fn config(&self) -> &GuiElemCfg { fn config(&self) -> &GuiElemCfg {
@ -207,15 +237,19 @@ impl GuiElem for GuiScreen {
} }
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> { fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
Box::new( Box::new(
self.c_context_menu.iter_mut().map(|v| v.elem_mut()).chain(
[ [
self.c_notif_overlay.elem_mut(), self.c_notif_overlay.elem_mut(),
self.c_idle_display.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_status_bar.elem_mut(),
self.c_settings.elem_mut(), self.c_settings.elem_mut(),
self.c_main_view.elem_mut(), self.c_main_view.elem_mut(),
] ]),
.into_iter() ),
.chain(self.c_context_menu.as_mut().map(|v| v.elem_mut())),
) )
} }
fn any(&self) -> &dyn std::any::Any { fn any(&self) -> &dyn std::any::Any {
@ -301,9 +335,7 @@ impl GuiElem for GuiScreen {
// animations: idle // animations: idle
if idle_changed { if idle_changed {
let enable_normal_ui = self.idle.value < 1.0; let enable_normal_ui = self.idle.value < 1.0;
self.c_main_view.config_mut().enabled = enable_normal_ui; self.set_normal_ui_enabled(enable_normal_ui);
// self.c_settings.config_mut().enabled = enable_normal_ui;
self.c_status_bar.config_mut().enabled = enable_normal_ui;
if let Some(h) = &info.helper { if let Some(h) = &info.helper {
h.set_cursor_visible(enable_normal_ui); h.set_cursor_visible(enable_normal_ui);
} }

View File

@ -34,6 +34,7 @@ impl Settings {
scroll_sensitivity_pages, scroll_sensitivity_pages,
), ),
vec![], vec![],
0.0,
), ),
c_background: Panel::with_background(GuiElemCfg::default().w_mouse(), (), Color::BLACK), c_background: Panel::with_background(GuiElemCfg::default().w_mouse(), (), Color::BLACK),
} }

View File

@ -1,11 +1,10 @@
use std::time::Instant; use std::time::Instant;
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle}; use speedy2d::{dimen::Vec2, shape::Rectangle};
use crate::{ use crate::{
gui::{DrawInfo, GuiElem, GuiElemCfg}, gui::{DrawInfo, GuiElem, GuiElemCfg},
gui_anim::AnimationController, gui_anim::AnimationController,
gui_base::Panel,
gui_playback::{image_display, CurrentInfo}, gui_playback::{image_display, CurrentInfo},
gui_playpause::PlayPause, gui_playpause::PlayPause,
gui_text::AdvancedLabel, gui_text::AdvancedLabel,
@ -17,6 +16,7 @@ pub struct StatusBar {
current_info: CurrentInfo, current_info: CurrentInfo,
cover_aspect_ratio: AnimationController<f32>, cover_aspect_ratio: AnimationController<f32>,
c_song_label: AdvancedLabel, c_song_label: AdvancedLabel,
pub force_reset_texts: bool,
c_buttons: PlayPause, c_buttons: PlayPause,
} }
@ -36,6 +36,7 @@ impl StatusBar {
Instant::now(), Instant::now(),
), ),
c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]), c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]),
force_reset_texts: false,
c_buttons: PlayPause::new(GuiElemCfg::default()), 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) { fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
self.current_info.update(info, g); 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.current_info.new_song = false;
self.c_song_label.content = if let Some(song) = self.current_info.current_song { self.c_song_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config info.gui_config
@ -101,6 +102,7 @@ impl GuiElem for StatusBar {
image_display( image_display(
g, g,
cover.as_ref(), cover.as_ref(),
None,
info.pos.top_left().x + info.pos.height() * 0.05, 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.05,
info.pos.top_left().y + info.pos.height() * 0.95, info.pos.top_left().y + info.pos.height() * 0.95,

View File

@ -10,21 +10,24 @@ use std::{
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
use gui::GuiEvent; use gui::GuiEvent;
#[cfg(feature = "playback")]
use musicdb_lib::player::Player;
use musicdb_lib::{ use musicdb_lib::{
data::{ data::{
database::{ClientIo, Database}, database::{ClientIo, Database},
CoverId, SongId, CoverId, SongId,
}, },
load::ToFromBytes, load::ToFromBytes,
player::Player,
server::{get, Command}, server::{get, Command},
}; };
use speedy2d::color::Color;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui; mod gui;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_anim; mod gui_anim;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_base; mod gui_base;
mod gui_edit_song;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_idle_display; mod gui_idle_display;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
@ -47,6 +50,8 @@ mod gui_statusbar;
mod gui_text; mod gui_text;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_wrappers; mod gui_wrappers;
#[cfg(feature = "merscfg")]
mod merscfg;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod textcfg; mod textcfg;
@ -65,8 +70,10 @@ enum Mode {
/// graphical user interface /// graphical user interface
Gui, Gui,
/// play in sync with the server, but load the songs from a local copy of the lib-dir /// 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 }, 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 /// 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, SyncplayerNetwork,
} }
@ -97,23 +104,31 @@ fn main() {
let mut con = con.try_clone().unwrap(); let mut con = con.try_clone().unwrap();
// this is all you need to keep the db in sync // this is all you need to keep the db in sync
thread::spawn(move || { thread::spawn(move || {
#[cfg(feature = "playback")]
let mut player = let mut player =
if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) { if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) {
Some(Player::new().unwrap().without_sending_commands()) Some(Player::new().unwrap().without_sending_commands())
} else { } else {
None None
}; };
#[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 { if let Mode::SyncplayerLocal { lib_dir } = mode {
let mut db = database.lock().unwrap(); let mut db = database.lock().unwrap();
db.lib_directory = lib_dir; db.lib_directory = lib_dir;
} else { break 'ifstatementworkaround;
}
let mut db = database.lock().unwrap(); let mut db = database.lock().unwrap();
let client_con: Box<dyn ClientIo> = Box::new(TcpStream::connect(addr).unwrap()); let client_con: Box<dyn ClientIo> = Box::new(TcpStream::connect(addr).unwrap());
db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new( db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new(
musicdb_lib::server::get::Client::new(BufReader::new(client_con)).unwrap(), musicdb_lib::server::get::Client::new(BufReader::new(client_con)).unwrap(),
))); )));
}; }
loop { loop {
#[cfg(feature = "playback")]
if let Some(player) = &mut player { if let Some(player) = &mut player {
let mut db = database.lock().unwrap(); let mut db = database.lock().unwrap();
if db.is_client_init() { if db.is_client_init() {
@ -121,6 +136,7 @@ fn main() {
} }
} }
let update = Command::from_bytes(&mut con).unwrap(); let update = Command::from_bytes(&mut con).unwrap();
#[cfg(feature = "playback")]
if let Some(player) = &mut player { if let Some(player) = &mut player {
player.handle_command(&update); player.handle_command(&update);
} }
@ -154,6 +170,7 @@ fn main() {
) )
}; };
} }
#[cfg(feature = "playback")]
Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork => { Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork => {
con_thread.join().unwrap(); con_thread.join().unwrap();
} }
@ -180,3 +197,7 @@ fn get_cover(song: SongId, database: &Database) -> Option<CoverId> {
database.albums().get(song.album.as_ref()?)?.cover database.albums().get(song.album.as_ref()?)?.cover
} }
} }
pub(crate) fn color_scale(c: Color, r: f32, g: f32, b: f32, new_alpha: Option<f32>) -> Color {
Color::from_rgba(c.r() * r, c.g() * g, c.b() * b, new_alpha.unwrap_or(c.a()))
}

View File

@ -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<mers_lib::data::function::Function>);
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<RwLock<Data>>,
pub var_is_idle: Arc<RwLock<Data>>,
pub var_window_size_in_pixels: Arc<RwLock<Data>>,
pub var_idle_screen_cover_aspect_ratio: Arc<RwLock<Data>>,
// - - results from running functions - -
pub updated_playing_status: Arc<AtomicU8>,
pub updated_idle_status: Arc<AtomicU8>,
pub updated_idle_screen_cover_pos: Arc<Updatable<Option<Rectangle>>>,
pub updated_idle_screen_artist_image_pos: Arc<Updatable<Option<Rectangle>>>,
pub updated_idle_screen_top_text_pos: Arc<Updatable<Rectangle>>,
pub updated_idle_screen_side_text_1_pos: Arc<Updatable<Rectangle>>,
pub updated_idle_screen_side_text_2_pos: Arc<Updatable<Rectangle>>,
pub updated_idle_screen_playback_buttons_pos: Arc<Updatable<Rectangle>>,
pub updated_statusbar_text_format: Arc<Updatable<TextBuilder>>,
pub updated_idle_screen_top_text_format: Arc<Updatable<TextBuilder>>,
pub updated_idle_screen_side_text_1_format: Arc<Updatable<TextBuilder>>,
pub updated_idle_screen_side_text_2_format: Arc<Updatable<TextBuilder>>,
}
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<UserEventSender<GuiEvent>>,
notif_sender: Sender<
Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, 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::<mers_lib::data::tuple::Tuple>().unwrap().0;
let title = t[0].get().as_any().downcast_ref::<mers_lib::data::string::String>().unwrap().0.clone();
let text = t[1].get().as_any().downcast_ref::<mers_lib::data::string::String>().unwrap().0.clone();
let t = t[2].get();
let duration = t.as_any().downcast_ref::<mers_lib::data::int::Int>().map(|s| Duration::from_secs(s.0.max(0) as _)).unwrap_or_else(|| Duration::from_secs_f64(t.as_any().downcast_ref::<mers_lib::data::float::Float>().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::<mers_lib::data::tuple::Tuple>().unwrap().0.iter().map(|v| v.get().as_any().downcast_ref::<mers_lib::data::float::Float>().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::<mers_lib::data::tuple::Tuple>().unwrap().0.iter().map(|v| v.get().as_any().downcast_ref::<mers_lib::data::float::Float>().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::<mers_lib::data::string::String>().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::<mers_lib::data::string::String>().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::<mers_lib::data::string::String>().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::<mers_lib::data::string::String>().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<UserEventSender<GuiEvent>>,
notif_sender: Sender<
Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>,
>,
) -> std::io::Result<Result<Result<(), (String, Option<CheckError>)>, 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<UserEventSender<GuiEvent>>,
notif_sender: Sender<
Box<dyn FnOnce(&NotifOverlay) -> (Box<dyn GuiElem>, NotifInfo) + Send>,
>,
) -> Result<Result<(), (String, Option<CheckError>)>, 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<CheckError>)> {
if let Some(obj) = out
.get()
.as_any()
.downcast_ref::<mers_lib::data::object::Object>()
{
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<mers_lib::data::function::Function, (String, Option<CheckError>)> {
if let Some(func) = val
.get()
.as_any()
.downcast_ref::<mers_lib::data::function::Function>()
{
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<UserEventSender<GuiEvent>>,
update: Arc<Updatable<Rectangle>>,
) -> 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::<mers_lib::data::tuple::Tuple>()
.unwrap()
.0
.iter()
.map(|v| {
v.get()
.as_any()
.downcast_ref::<mers_lib::data::float::Float>()
.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<T> {
updated: AtomicBool,
value: Mutex<Option<T>>,
}
impl<T> Updatable<T> {
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<T> {
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<T> Updatable<T>
where
T: Default,
{
pub fn modify<R>(&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())
}
}

7
musicdb-lib/Cargo.toml Executable file → Normal file
View File

@ -6,8 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
awedio = "0.2.0" awedio = { version = "0.2.0", optional = true }
base64 = "0.21.2" base64 = "0.21.2"
rand = "0.8.5" rand = "0.8.5"
rc-u8-reader = "2.0.16" rc-u8-reader = "2.0.16"
tokio = "1.29.1" tokio = { version = "1.29.1", features = ["sync"] }
[features]
playback = ["awedio"]

View File

@ -1,4 +1,5 @@
pub mod data; pub mod data;
pub mod load; pub mod load;
#[cfg(feature = "playback")]
pub mod player; pub mod player;
pub mod server; pub mod server;

View File

@ -1,11 +1,8 @@
pub mod get; pub mod get;
use std::{ use std::{
io::{BufRead, BufReader, Read, Write}, io::{Read, Write},
net::{SocketAddr, TcpListener},
sync::{mpsc, Arc, Mutex}, sync::{mpsc, Arc, Mutex},
thread,
time::Duration,
}; };
use crate::{ use crate::{
@ -18,8 +15,15 @@ use crate::{
AlbumId, ArtistId, SongId, AlbumId, ArtistId, SongId,
}, },
load::ToFromBytes, 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)] #[derive(Clone, Debug)]
@ -84,6 +88,7 @@ impl Command {
/// a) initialize new connections using db.init_connection() to synchronize the new client /// a) initialize new connections using db.init_connection() to synchronize the new client
/// b) handle the decoding of messages using Command::from_bytes() /// 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. /// 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( pub fn run_server(
database: Arc<Mutex<Database>>, database: Arc<Mutex<Database>>,
addr_tcp: Option<SocketAddr>, addr_tcp: Option<SocketAddr>,

View File

@ -10,7 +10,7 @@ axum = { version = "0.6.19", features = ["headers"] }
clap = { version = "4.4.6", features = ["derive"] } clap = { version = "4.4.6", features = ["derive"] }
futures = "0.3.28" futures = "0.3.28"
headers = "0.3.8" 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 = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] } tokio = { version = "1.0", features = ["full"] }