This commit is contained in:
Mark 2023-08-13 23:58:53 +02:00
commit 922e4fcc00
43 changed files with 6822 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*/Cargo.lock
*/target

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# musicdb
custom music player running on my personal SBC which can be controlled from other WiFi devices (phone/pc)
should perform pretty well (it runs well on my Pine A64 with 10k+ songs)
## why???
#### server/client
allows you to play music on any device you want while controlling playback from anywhere.
you can either run the client and server on the same machine or connect via tcp.
if one client makes a change, all other clients will be notified of it and update almost instantly.
it is also possible for a fake "client" to mirror the main server's playback, so you could sync up your entire house if you wanted to.
#### complicated queue
- allows more customization of playback (loops, custom shuffles, etc.)
- is more organized (adding an album doesn't add 10-20 songs, it creates a folder so you can (re)move the entire album in/from the queue)
#### caching of songs
for (almost) gapless playback, even when the data is stored on a NAS or cloud
#### central database
when storing data on a cloud, it would take forever to load all songs and scan them for metadata.
you would also run into issues with different file formats and where to store the cover images.
a custom database speeds up server startup and allows for more features.

1
musicdb-client/.gitignore vendored Executable file
View File

@ -0,0 +1 @@
/target

14
musicdb-client/Cargo.toml Executable file
View File

@ -0,0 +1,14 @@
[package]
name = "musicdb-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
regex = "1.9.3"
speedy2d = { version = "1.12.0", optional = true }
[features]
default = ["speedy2d"]

880
musicdb-client/src/gui.rs Executable file
View File

@ -0,0 +1,880 @@
use std::{
any::Any,
eprintln,
net::TcpStream,
sync::{Arc, Mutex},
time::Instant,
usize,
};
use musicdb_lib::{
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
load::ToFromBytes,
server::Command,
};
use speedy2d::{
color::Color,
dimen::{UVec2, Vec2},
font::Font,
shape::Rectangle,
window::{
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper,
},
Graphics2D,
};
use crate::{gui_screen::GuiScreen, gui_wrappers::WithFocusHotkey};
pub enum GuiEvent {
Refresh,
UpdatedQueue,
UpdatedLibrary,
Exit,
}
pub fn main(
database: Arc<Mutex<Database>>,
connection: TcpStream,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
) {
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
"MusicDB Client",
WindowCreationOptions::new_fullscreen_borderless(),
)
.expect("couldn't open window");
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
let sender = window.create_user_event_sender();
window.run_loop(Gui::new(database, connection, event_sender_arc, sender));
}
pub struct Gui {
pub event_sender: UserEventSender<GuiEvent>,
pub database: Arc<Mutex<Database>>,
pub connection: TcpStream,
pub gui: GuiElem,
pub size: UVec2,
pub mouse_pos: Vec2,
pub font: Font,
pub last_draw: Instant,
pub modifiers: ModifiersState,
pub dragging: Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
pub line_height: f32,
pub last_height: f32,
pub scroll_pixels_multiplier: f64,
pub scroll_lines_multiplier: f64,
pub scroll_pages_multiplier: f64,
}
impl Gui {
fn new(
database: Arc<Mutex<Database>>,
connection: TcpStream,
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
event_sender: UserEventSender<GuiEvent>,
) -> Self {
database.lock().unwrap().update_endpoints.push(
musicdb_lib::data::database::UpdateEndpoint::Custom(Box::new(move |cmd| match cmd {
Command::Resume
| Command::Pause
| Command::Stop
| Command::Save
| Command::SetLibraryDirectory(..) => {}
Command::NextSong
| Command::QueueUpdate(..)
| Command::QueueAdd(..)
| Command::QueueInsert(..)
| Command::QueueRemove(..)
| Command::QueueGoto(..) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedQueue);
}
}
Command::SyncDatabase(..)
| Command::AddSong(_)
| Command::AddAlbum(_)
| Command::AddArtist(_)
| Command::ModifySong(_)
| Command::ModifyAlbum(_)
| Command::ModifyArtist(_) => {
if let Some(s) = &*event_sender_arc.lock().unwrap() {
_ = s.send_event(GuiEvent::UpdatedLibrary);
}
}
})),
);
let line_height = 32.0;
let scroll_pixels_multiplier = 1.0;
let scroll_lines_multiplier = 3.0;
let scroll_pages_multiplier = 0.75;
Gui {
event_sender,
database,
connection,
gui: GuiElem::new(WithFocusHotkey::new_noshift(
VirtualKeyCode::Escape,
GuiScreen::new(
GuiElemCfg::default(),
line_height,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
),
)),
size: UVec2::ZERO,
mouse_pos: Vec2::ZERO,
font: Font::new(include_bytes!(
"/usr/share/fonts/mozilla-fira/FiraSans-Regular.otf"
))
.unwrap(),
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
last_draw: Instant::now(),
modifiers: ModifiersState::default(),
dragging: None,
line_height,
last_height: 720.0,
scroll_pixels_multiplier,
scroll_lines_multiplier,
scroll_pages_multiplier,
}
}
}
/// the trait implemented by all Gui elements.
/// feel free to override the methods you wish to use.
#[allow(unused)]
pub trait GuiElemTrait {
fn config(&self) -> &GuiElemCfg;
fn config_mut(&mut self) -> &mut GuiElemCfg;
/// note: drawing happens from the last to the first element, while priority is from first to last.
/// if you wish to add a "high priority" child to a Vec<GuiElem> using push, .rev() the iterator in this method.
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_>;
fn any(&self) -> &dyn Any;
fn any_mut(&mut self) -> &mut dyn Any;
fn clone_gui(&self) -> Box<dyn GuiElemTrait>;
/// handles drawing.
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {}
/// an event that is invoked whenever a mouse button is pressed on the element.
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// an event that is invoked whenever a mouse button that was pressed on the element is released anywhere.
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// an event that is invoked after a mouse button was pressed and released on the same GUI element.
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_watch(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn key_focus(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<VirtualKeyCode>,
scan: KeyScancode,
) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
/// When something is dragged and released over this element
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
Vec::with_capacity(0)
}
fn updated_library(&mut self) {}
fn updated_queue(&mut self) {}
}
#[derive(Debug, Clone)]
pub struct GuiElemCfg {
pub enabled: bool,
/// if true, indicates that something (text size, screen size, ...) has changed
/// and you should probably relayout and redraw from scratch.
pub redraw: bool,
pub pos: Rectangle,
/// the pixel position after the last call to draw().
/// in draw, use info.pos instead, as pixel_pos is only updated *after* draw().
/// this can act like a "previous pos" field within draw.
pub pixel_pos: Rectangle,
pub mouse_down: (bool, bool, bool),
pub mouse_events: bool,
pub scroll_events: bool,
/// allows elements to watch all keyboard events, regardless of keyboard focus.
pub keyboard_events_watch: bool,
/// indicates that this element can have the keyboard focus
pub keyboard_events_focus: bool,
/// index of the child that has keyboard focus. if usize::MAX, `self` has focus.
/// will automatically be changed when Tab is pressed. [TODO]
pub keyboard_focus_index: usize,
pub request_keyboard_focus: bool,
pub drag_target: bool,
}
impl GuiElemCfg {
pub fn at(pos: Rectangle) -> Self {
Self {
pos,
..Default::default()
}
}
pub fn w_mouse(mut self) -> Self {
self.mouse_events = true;
self
}
pub fn w_scroll(mut self) -> Self {
self.scroll_events = true;
self
}
pub fn w_keyboard_watch(mut self) -> Self {
self.keyboard_events_watch = true;
self
}
pub fn w_keyboard_focus(mut self) -> Self {
self.keyboard_events_focus = true;
self
}
pub fn w_drag_target(mut self) -> Self {
self.drag_target = true;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
}
impl Default for GuiElemCfg {
fn default() -> Self {
Self {
enabled: true,
redraw: false,
pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
pixel_pos: Rectangle::ZERO,
mouse_down: (false, false, false),
mouse_events: false,
scroll_events: false,
keyboard_events_watch: false,
keyboard_events_focus: false,
keyboard_focus_index: usize::MAX,
request_keyboard_focus: false,
drag_target: false,
}
}
}
pub enum GuiAction {
OpenMain,
SetIdle(bool),
OpenSettings(bool),
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
SendToServer(Command),
/// unfocuses all gui elements, then assigns keyboard focus to one with config().request_keyboard_focus == true.
ResetKeyboardFocus,
SetDragging(
Option<(
Dragging,
Option<Box<dyn FnMut(&mut DrawInfo, &mut Graphics2D)>>,
)>,
),
SetLineHeight(f32),
Do(Box<dyn FnMut(&mut Gui)>),
Exit,
}
pub enum Dragging {
Artist(ArtistId),
Album(AlbumId),
Song(SongId),
Queue(Queue),
}
pub struct DrawInfo<'a> {
pub actions: Vec<GuiAction>,
pub pos: Rectangle,
pub database: &'a mut Database,
pub font: &'a Font,
/// absolute position of the mouse on the screen.
/// compare this to `pos` to find the mouse's relative position.
pub mouse_pos: Vec2,
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
pub has_keyboard_focus: bool,
pub child_has_keyboard_focus: bool,
/// the height of one line of text (in pixels)
pub line_height: f32,
}
pub struct GuiElem {
pub inner: Box<dyn GuiElemTrait>,
}
impl Clone for GuiElem {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone_gui(),
}
}
}
impl GuiElem {
pub fn new<T: GuiElemTrait + 'static>(inner: T) -> Self {
Self {
inner: Box::new(inner),
}
}
pub fn try_as<T: Any>(&self) -> Option<&T> {
self.inner.any().downcast_ref()
}
pub fn try_as_mut<T: Any>(&mut self) -> Option<&mut T> {
self.inner.any_mut().downcast_mut()
}
pub fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
if !self.inner.config_mut().enabled {
return;
}
// adjust info
let npos = adjust_area(&info.pos, &self.inner.config_mut().pos);
let ppos = std::mem::replace(&mut info.pos, npos);
if info.child_has_keyboard_focus {
if self.inner.config().keyboard_focus_index == usize::MAX {
info.has_keyboard_focus = true;
info.child_has_keyboard_focus = false;
}
}
// call trait's draw function
self.inner.draw(info, g);
// reset info
info.has_keyboard_focus = false;
let focus_path = info.child_has_keyboard_focus;
// children (in reverse order - first element has the highest priority)
let kbd_focus_index = self.inner.config().keyboard_focus_index;
for (i, c) in self
.inner
.children()
.collect::<Vec<_>>()
.into_iter()
.enumerate()
.rev()
{
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
c.draw(info, g);
}
// reset pt. 2
info.child_has_keyboard_focus = focus_path;
self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos);
}
/// recursively applies the function to all gui elements below and including this one
pub fn recursive_all<F: FnMut(&mut GuiElem)>(&mut self, f: &mut F) {
f(self);
for c in self.inner.children() {
c.recursive_all(f);
}
}
fn mouse_event<F: FnMut(&mut GuiElem) -> Option<Vec<GuiAction>>>(
&mut self,
condition: &mut F,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
for c in &mut self.inner.children() {
if c.inner.config().enabled {
if c.inner.config().pixel_pos.contains(pos) {
if let Some(v) = c.mouse_event(condition, pos) {
return Some(v);
}
}
}
}
condition(self)
}
fn release_drag(
&mut self,
dragged: &mut Option<Dragging>,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
self.mouse_event(
&mut |v| {
if v.inner.config().drag_target {
if let Some(d) = dragged.take() {
return Some(v.inner.dragged(d));
}
}
None
},
pos,
)
}
fn mouse_button(
&mut self,
button: MouseButton,
down: bool,
pos: Vec2,
) -> Option<Vec<GuiAction>> {
if down {
self.mouse_event(
&mut |v: &mut GuiElem| {
if v.inner.config().mouse_events {
match button {
MouseButton::Left => v.inner.config_mut().mouse_down.0 = true,
MouseButton::Middle => v.inner.config_mut().mouse_down.1 = true,
MouseButton::Right => v.inner.config_mut().mouse_down.2 = true,
MouseButton::Other(_) => {}
}
Some(v.inner.mouse_down(button))
} else {
None
}
},
pos,
)
} else {
let mut vec = vec![];
if let Some(a) = self.mouse_event(
&mut |v: &mut GuiElem| {
let down = v.inner.config().mouse_down;
if v.inner.config().mouse_events
&& ((button == MouseButton::Left && down.0)
|| (button == MouseButton::Middle && down.1)
|| (button == MouseButton::Right && down.2))
{
Some(v.inner.mouse_pressed(button))
} else {
None
}
},
pos,
) {
vec.extend(a);
};
self.recursive_all(&mut |v| {
if v.inner.config().mouse_events {
match button {
MouseButton::Left => v.inner.config_mut().mouse_down.0 = false,
MouseButton::Middle => v.inner.config_mut().mouse_down.1 = false,
MouseButton::Right => v.inner.config_mut().mouse_down.2 = false,
MouseButton::Other(_) => {}
}
vec.extend(v.inner.mouse_up(button));
}
});
Some(vec)
}
}
fn mouse_wheel(&mut self, diff: f32, pos: Vec2) -> Option<Vec<GuiAction>> {
self.mouse_event(
&mut |v| {
if v.inner.config().scroll_events {
Some(v.inner.mouse_wheel(diff))
} else {
None
}
},
pos,
)
}
fn keyboard_event<
F: FnOnce(&mut Self, &mut Vec<GuiAction>),
G: FnMut(&mut Self, &mut Vec<GuiAction>),
>(
&mut self,
f_focus: F,
mut f_watch: G,
) -> Vec<GuiAction> {
let mut o = vec![];
self.keyboard_event_inner(&mut Some(f_focus), &mut f_watch, &mut o, true);
o
}
fn keyboard_event_inner<
F: FnOnce(&mut Self, &mut Vec<GuiAction>),
G: FnMut(&mut Self, &mut Vec<GuiAction>),
>(
&mut self,
f_focus: &mut Option<F>,
f_watch: &mut G,
events: &mut Vec<GuiAction>,
focus: bool,
) {
f_watch(self, events);
let focus_index = self.inner.config().keyboard_focus_index;
for (i, child) in self.inner.children().enumerate() {
child.keyboard_event_inner(f_focus, f_watch, events, focus && i == focus_index);
}
if focus {
// we have focus and no child has consumed f_focus
if let Some(f) = f_focus.take() {
f(self, events)
}
}
}
fn keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool {
let mut focus_index = if refocus {
usize::MAX
} else {
self.inner.config().keyboard_focus_index
};
let allow_focus = self.inner.config().keyboard_events_focus;
let mut children = self.inner.children().collect::<Vec<_>>();
if focus_index == usize::MAX {
if decrement {
focus_index = children.len().saturating_sub(1);
} else {
focus_index = 0;
}
}
let mut changed = refocus;
let ok = loop {
if let Some(child) = children.get_mut(focus_index) {
if child.keyboard_move_focus(decrement, changed) {
break true;
} else {
changed = true;
if !decrement {
focus_index += 1;
} else {
focus_index = focus_index.wrapping_sub(1);
}
}
} else {
focus_index = usize::MAX;
break allow_focus && refocus;
}
};
self.inner.config_mut().keyboard_focus_index = focus_index;
ok
}
fn keyboard_reset_focus(&mut self) -> bool {
let mut index = usize::MAX;
for (i, c) in self.inner.children().enumerate() {
if c.keyboard_reset_focus() {
index = i;
break;
}
}
let wants = std::mem::replace(&mut self.inner.config_mut().request_keyboard_focus, false);
self.inner.config_mut().keyboard_focus_index = index;
index != usize::MAX || wants
}
}
pub fn adjust_area(outer: &Rectangle, rel_area: &Rectangle) -> Rectangle {
Rectangle::new(
adjust_pos(outer, rel_area.top_left()),
adjust_pos(outer, rel_area.bottom_right()),
)
}
pub fn adjust_pos(outer: &Rectangle, rel_pos: &Vec2) -> Vec2 {
Vec2::new(
outer.top_left().x + outer.width() * rel_pos.x,
outer.top_left().y + outer.height() * rel_pos.y,
)
}
impl Gui {
fn exec_gui_action(&mut self, action: GuiAction) {
match action {
GuiAction::Build(f) => {
let actions = f(&mut *self.database.lock().unwrap());
for action in actions {
self.exec_gui_action(action);
}
}
GuiAction::SendToServer(cmd) => {
if let Err(e) = cmd.to_bytes(&mut self.connection) {
eprintln!("Error sending command to server: {e}");
}
}
GuiAction::ResetKeyboardFocus => _ = self.gui.keyboard_reset_focus(),
GuiAction::SetDragging(d) => self.dragging = d,
GuiAction::SetLineHeight(h) => {
self.line_height = h;
self.gui
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
}
GuiAction::Do(mut f) => f(self),
GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
GuiAction::SetIdle(v) => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 != v {
gui.inner.idle = (v, Some(Instant::now()));
}
}
}
GuiAction::OpenSettings(v) => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 != v {
gui.inner.settings = (v, Some(Instant::now()));
}
}
}
GuiAction::OpenMain => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 {
gui.inner.settings = (false, Some(Instant::now()));
}
}
}
}
}
}
impl WindowHandler<GuiEvent> for Gui {
fn on_draw(&mut self, helper: &mut WindowHelper<GuiEvent>, graphics: &mut Graphics2D) {
let start = Instant::now();
graphics.draw_rectangle(
Rectangle::new(Vec2::ZERO, self.size.into_f32()),
Color::BLACK,
);
let mut dblock = self.database.lock().unwrap();
let mut info = DrawInfo {
actions: Vec::with_capacity(0),
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
database: &mut *dblock,
font: &self.font,
mouse_pos: self.mouse_pos,
helper: Some(helper),
has_keyboard_focus: false,
child_has_keyboard_focus: true,
line_height: self.line_height,
};
self.gui.draw(&mut info, graphics);
let actions = std::mem::replace(&mut info.actions, Vec::with_capacity(0));
if let Some((d, f)) = &mut self.dragging {
if let Some(f) = f {
f(&mut info, graphics);
} else {
match d {
Dragging::Artist(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Album(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Song(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(0, 100, 255, 100),
),
Dragging::Queue(_) => graphics.draw_circle(
self.mouse_pos,
25.0,
Color::from_int_rgba(100, 0, 255, 100),
),
}
}
}
drop(info);
drop(dblock);
for a in actions {
self.exec_gui_action(a);
}
// eprintln!(
// "fps <= {}",
// 1000 / self.last_draw.elapsed().as_millis().max(1)
// );
self.last_draw = start;
}
fn on_mouse_button_down(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if let Some(a) = self.gui.mouse_button(button, true, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_mouse_button_up(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if self.dragging.is_some() {
if let Some(a) = self.gui.release_drag(
&mut self.dragging.take().map(|v| v.0),
self.mouse_pos.clone(),
) {
for a in a {
self.exec_gui_action(a)
}
}
}
if let Some(a) = self.gui.mouse_button(button, false, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_mouse_wheel_scroll(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
distance: speedy2d::window::MouseScrollDistance,
) {
let dist = match distance {
MouseScrollDistance::Pixels { y, .. } => (self.scroll_pixels_multiplier * y) as f32,
MouseScrollDistance::Lines { y, .. } => {
(self.scroll_lines_multiplier * y) as f32 * self.line_height
}
MouseScrollDistance::Pages { y, .. } => {
(self.scroll_pages_multiplier * y) as f32 * self.last_height
}
};
if let Some(a) = self.gui.mouse_wheel(dist, self.mouse_pos.clone()) {
for a in a {
self.exec_gui_action(a)
}
}
helper.request_redraw();
}
fn on_keyboard_char(&mut self, helper: &mut WindowHelper<GuiEvent>, unicode_codepoint: char) {
helper.request_redraw();
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(
&mut e
.inner
.char_focus(self.modifiers.clone(), unicode_codepoint),
);
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(
&mut e
.inner
.char_watch(self.modifiers.clone(), unicode_codepoint),
);
}
},
) {
self.exec_gui_action(a);
}
}
fn on_key_down(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
virtual_key_code: Option<VirtualKeyCode>,
scancode: KeyScancode,
) {
helper.request_redraw();
if let Some(VirtualKeyCode::Tab) = virtual_key_code {
if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) {
self.gui.keyboard_move_focus(self.modifiers.shift(), false);
}
}
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(&mut e.inner.key_focus(
self.modifiers.clone(),
true,
virtual_key_code,
scancode,
));
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(&mut e.inner.key_watch(
self.modifiers.clone(),
true,
virtual_key_code,
scancode,
));
}
},
) {
self.exec_gui_action(a);
}
}
fn on_key_up(
&mut self,
helper: &mut WindowHelper<GuiEvent>,
virtual_key_code: Option<VirtualKeyCode>,
scancode: KeyScancode,
) {
helper.request_redraw();
for a in self.gui.keyboard_event(
|e, a| {
if e.inner.config().keyboard_events_focus {
a.append(&mut e.inner.key_focus(
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
|e, a| {
if e.inner.config().keyboard_events_watch {
a.append(&mut e.inner.key_watch(
self.modifiers.clone(),
false,
virtual_key_code,
scancode,
));
}
},
) {
self.exec_gui_action(a);
}
}
fn on_keyboard_modifiers_changed(
&mut self,
_helper: &mut WindowHelper<GuiEvent>,
state: ModifiersState,
) {
self.modifiers = state;
}
fn on_user_event(&mut self, helper: &mut WindowHelper<GuiEvent>, user_event: GuiEvent) {
match user_event {
GuiEvent::Refresh => helper.request_redraw(),
GuiEvent::UpdatedLibrary => {
self.gui.recursive_all(&mut |e| e.inner.updated_library());
helper.request_redraw();
}
GuiEvent::UpdatedQueue => {
self.gui.recursive_all(&mut |e| e.inner.updated_queue());
helper.request_redraw();
}
GuiEvent::Exit => helper.terminate_loop(),
}
}
fn on_mouse_move(&mut self, helper: &mut WindowHelper<GuiEvent>, position: Vec2) {
self.mouse_pos = position;
helper.request_redraw();
}
fn on_resize(&mut self, _helper: &mut WindowHelper<GuiEvent>, size_pixels: UVec2) {
self.size = size_pixels;
self.gui
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
}
}

459
musicdb-client/src/gui_base.rs Executable file
View File

@ -0,0 +1,459 @@
use std::{sync::Arc, time::Instant};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_text::Label,
};
/// A simple container for zero, one, or multiple child GuiElems. Can optionally fill the background with a color.
#[derive(Clone)]
pub struct Panel {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
pub background: Option<Color>,
}
impl Panel {
pub fn new(config: GuiElemCfg, children: Vec<GuiElem>) -> Self {
Self {
config,
children,
background: None,
}
}
pub fn with_background(config: GuiElemCfg, children: Vec<GuiElem>, background: Color) -> Self {
Self {
config,
children,
background: Some(background),
}
}
}
impl GuiElemTrait for Panel {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
if let Some(c) = self.background {
g.draw_rectangle(info.pos.clone(), c);
}
}
}
#[derive(Clone)]
pub struct ScrollBox {
config: GuiElemCfg,
pub children: Vec<(GuiElem, f32)>,
pub size_unit: ScrollBoxSizeUnit,
pub scroll_target: f32,
pub scroll_display: f32,
height_bottom: f32,
/// 0.max(height_bottom - 1)
max_scroll: f32,
last_height_px: f32,
}
#[derive(Clone)]
pub enum ScrollBoxSizeUnit {
Relative,
Pixels,
}
impl ScrollBox {
pub fn new(
mut config: GuiElemCfg,
size_unit: ScrollBoxSizeUnit,
children: Vec<(GuiElem, f32)>,
) -> Self {
// config.redraw = true;
Self {
config: config.w_scroll(),
children,
size_unit,
scroll_target: 0.0,
scroll_display: 0.0,
/// the y-position of the bottom edge of the last element (i.e. the total height)
height_bottom: 0.0,
max_scroll: 0.0,
last_height_px: 0.0,
}
}
}
impl GuiElemTrait for ScrollBox {
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.children
.iter_mut()
.rev()
.map(|(v, _)| v)
.skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0)
.take_while(|v| v.inner.config().pos.top_left().y < 1.0),
)
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.pixel_pos.size() != info.pos.size() {
self.config.redraw = true;
}
// smooth scrolling animation
if self.scroll_target > self.max_scroll {
self.scroll_target = self.max_scroll;
} else if self.scroll_target < 0.0 {
self.scroll_target = 0.0;
}
self.scroll_display = 0.2 * self.scroll_target + 0.8 * self.scroll_display;
if self.scroll_display != self.scroll_target {
self.config.redraw = true;
if (self.scroll_display - self.scroll_target).abs() < 1.0 / info.pos.height() {
self.scroll_display = self.scroll_target;
} else if let Some(h) = &info.helper {
h.request_redraw();
}
}
// recalculate positions
if self.config.redraw {
self.config.redraw = false;
let mut y_pos = -self.scroll_display;
for (e, h) in self.children.iter_mut() {
let h_rel = self.size_unit.to_rel(*h, info.pos.height());
let y_rel = self.size_unit.to_rel(y_pos, info.pos.height());
if y_rel + h_rel >= 0.0 && y_rel <= 1.0 {
let cfg = e.inner.config_mut();
cfg.enabled = true;
cfg.pos = Rectangle::new(
Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)),
Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)),
);
} else {
e.inner.config_mut().enabled = false;
}
y_pos += *h;
}
self.height_bottom = y_pos + self.scroll_display;
self.max_scroll =
0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height()));
}
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<crate::gui::GuiAction> {
self.scroll_target = (self.scroll_target
- self.size_unit.from_abs(diff as f32, self.last_height_px))
.max(0.0);
Vec::with_capacity(0)
}
}
impl ScrollBoxSizeUnit {
fn to_rel(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val,
Self::Pixels => val / draw_height,
}
}
fn from_rel(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val,
Self::Pixels => val * draw_height,
}
}
fn from_abs(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val / draw_height,
Self::Pixels => val,
}
}
}
#[derive(Clone)]
pub struct Button {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
action: Arc<dyn Fn(&Self) -> Vec<GuiAction> + 'static>,
}
impl Button {
/// automatically adds w_mouse to config
pub fn new<F: Fn(&Self) -> Vec<GuiAction> + 'static>(
config: GuiElemCfg,
action: F,
children: Vec<GuiElem>,
) -> Self {
Self {
config: config.w_mouse(),
children,
action: Arc::new(action),
}
}
}
impl GuiElemTrait for Button {
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.children.iter_mut())
}
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())
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
(self.action)(self)
} else {
vec![]
}
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
let mouse_down = self.config.mouse_down.0;
let contains = info.pos.contains(info.mouse_pos);
g.draw_rectangle(
info.pos.clone(),
if mouse_down && contains {
Color::from_rgb(0.25, 0.25, 0.25)
} else if contains || mouse_down {
Color::from_rgb(0.15, 0.15, 0.15)
} else {
Color::from_rgb(0.1, 0.1, 0.1)
},
);
}
}
#[derive(Clone)]
pub struct Slider {
pub config: GuiElemCfg,
pub children: Vec<GuiElem>,
pub slider_pos: Rectangle,
pub min: f64,
pub max: f64,
pub val: f64,
val_changed: bool,
pub val_changed_subs: Vec<bool>,
/// if true, the display should be visible.
pub display: bool,
/// if Some, the display is in a transition period.
/// you can set this to None to indicate that the transition has finished, but this is not required.
pub display_since: Option<Instant>,
pub on_update: Arc<dyn Fn(&mut Self, &mut DrawInfo)>,
}
impl Slider {
/// returns true if the value of the slider has changed since the last time this function was called.
/// this is usually used by the closure responsible for directly handling updates. if you wish to check for changes
/// from outside, push a `false` to `val_changed_subs` and remember your index.
/// when the value changes, this will be set to `true`. don't forget to reset it to `false` again if you find it set to `true`,
/// or your code will run every time.
pub fn val_changed(&mut self) -> bool {
if self.val_changed {
self.val_changed = false;
true
} else {
false
}
}
pub fn val_changed_peek(&self) -> bool {
self.val_changed
}
pub fn new<F: Fn(&mut Self, &mut DrawInfo) + 'static>(
config: GuiElemCfg,
slider_pos: Rectangle,
min: f64,
max: f64,
val: f64,
children: Vec<GuiElem>,
on_update: F,
) -> Self {
Self {
config: config.w_mouse().w_scroll(),
children,
slider_pos,
min,
max,
val,
val_changed: true,
val_changed_subs: vec![],
display: false,
display_since: None,
on_update: Arc::new(on_update),
}
}
pub fn new_labeled<F: Fn(&mut Self, &mut Label, &mut DrawInfo) + 'static>(
config: GuiElemCfg,
min: f64,
max: f64,
val: f64,
mktext: F,
) -> Self {
Self::new(
config,
Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
min,
max,
val,
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
String::new(),
Color::WHITE,
// Some(Color::from_int_rgba(0, 0, 0, 150)),
None,
Vec2::new(0.5, 1.0),
))],
move |s, i| {
if s.display || s.display_since.is_some() {
let mut label = s.children.pop().unwrap();
if let Some(l) = label.inner.any_mut().downcast_mut::<Label>() {
let display_state = if let Some(since) =
s.display_since.map(|v| v.elapsed().as_secs_f64() / 0.2)
{
if since >= 1.0 {
s.display_since = None;
if s.display {
1.0
} else {
0.0
}
} else {
if let Some(h) = &i.helper {
h.request_redraw();
}
s.config.redraw = true;
if s.display {
since
} else {
1.0 - since
}
}
} else {
1.0
};
let display_state =
(1.0 - (1.0 - display_state) * (1.0 - display_state)) as _;
if display_state == 0.0 {
l.config_mut().enabled = false;
} else {
l.pos.x = ((s.val - s.min) / (s.max - s.min)) as _;
*l.content.color() = Color::from_rgba(0.8, 0.8, 0.8, display_state);
let cfg = l.config_mut();
cfg.enabled = true;
let label_height = i.line_height / i.pos.height();
cfg.pos = Rectangle::from_tuples(
(0.05, 1.0 - label_height - display_state),
(0.95, 1.0 - display_state),
);
mktext(s, l, i);
}
}
s.children.push(label);
}
},
)
}
}
impl GuiElemTrait for Slider {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) {
self.display = !self.display;
self.display_since = Some(Instant::now());
self.config.redraw = true;
}
let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25);
let y_mid_line = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
let line_radius = dot_size * 0.25;
let line_pos = Rectangle::from_tuples(
(info.pos.top_left().x + dot_size, y_mid_line - line_radius),
(
info.pos.bottom_right().x - dot_size,
y_mid_line + line_radius,
),
);
let line_left = line_pos.top_left().x;
let line_width = line_pos.width();
if self.config.mouse_down.0 {
self.val = self.min
+ (self.max - self.min)
* 1.0f64.min(0.0f64.max(
(info.mouse_pos.x - line_pos.top_left().x) as f64 / line_pos.width() as f64,
));
self.val_changed = true;
for v in &mut self.val_changed_subs {
*v = true;
}
self.config.redraw = true;
}
let line_color = Color::from_int_rgb(50, 50, 100);
g.draw_circle(
Vec2::new(line_pos.top_left().x, y_mid_line),
line_radius,
line_color,
);
g.draw_circle(
Vec2::new(line_pos.bottom_right().x, y_mid_line),
line_radius,
line_color,
);
g.draw_rectangle(line_pos, line_color);
g.draw_circle(
Vec2::new(
line_left
+ (line_width as f64 * (self.val - self.min) / (self.max - self.min)) as f32,
y_mid_line,
),
0.5 * dot_size,
Color::CYAN,
);
if self.config.redraw {
self.config.redraw = false;
(Arc::clone(&self.on_update))(self, info);
}
}
}

454
musicdb-client/src/gui_library.rs Executable file
View File

@ -0,0 +1,454 @@
use musicdb_lib::data::{database::Database, AlbumId, ArtistId, SongId};
use regex::{Regex, RegexBuilder};
use speedy2d::{
color::Color,
dimen::Vec2,
shape::Rectangle,
window::{MouseButton, VirtualKeyCode},
};
use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox,
gui_text::{Label, TextField},
gui_wrappers::WithFocusHotkey,
};
#[derive(Clone)]
pub struct LibraryBrowser {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
search_artist: String,
search_artist_regex: Option<Regex>,
search_album: String,
search_album_regex: Option<Regex>,
search_song: String,
search_song_regex: Option<Regex>,
}
fn search_regex_new(pat: &str) -> Result<Regex, regex::Error> {
RegexBuilder::new(pat)
.unicode(true)
.case_insensitive(true)
.build()
}
impl LibraryBrowser {
pub fn new(config: GuiElemCfg) -> Self {
let search_artist = TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.01), (0.45, 0.05))),
"artist".to_string(),
Color::GRAY,
Color::WHITE,
);
let search_album = TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.55, 0.01), (0.99, 0.05))),
"album".to_string(),
Color::GRAY,
Color::WHITE,
);
let search_song = WithFocusHotkey::new_ctrl(
VirtualKeyCode::F,
TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.01, 0.06), (0.99, 0.1))),
"song".to_string(),
Color::GRAY,
Color::WHITE,
),
);
let library_scroll_box = ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.1), (1.0, 1.0))),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![],
);
Self {
config,
children: vec![
GuiElem::new(search_artist),
GuiElem::new(search_album),
GuiElem::new(search_song),
GuiElem::new(library_scroll_box),
],
search_artist: String::new(),
search_artist_regex: None,
search_album: String::new(),
search_album_regex: None,
search_song: String::new(),
search_song_regex: None,
}
}
}
impl GuiElemTrait for LibraryBrowser {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
let mut search_changed = false;
{
let v = &mut self.children[0].try_as_mut::<TextField>().unwrap().children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_artist != *v.get_text() {
search_changed = true;
self.search_artist = v.get_text().clone();
self.search_artist_regex = search_regex_new(&self.search_artist).ok();
*v.color() = if self.search_artist_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
{
let v = &mut self.children[1].try_as_mut::<TextField>().unwrap().children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_album != *v.get_text() {
search_changed = true;
self.search_album = v.get_text().clone();
self.search_album_regex = search_regex_new(&self.search_album).ok();
*v.color() = if self.search_album_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
{
let v = &mut self.children[2]
.try_as_mut::<WithFocusHotkey<TextField>>()
.unwrap()
.inner
.children[0]
.try_as_mut::<Label>()
.unwrap()
.content;
if self.search_song != *v.get_text() {
search_changed = true;
self.search_song = v.get_text().clone();
self.search_song_regex = search_regex_new(&self.search_song).ok();
*v.color() = if self.search_song_regex.is_some() {
Color::WHITE
} else {
Color::RED
};
}
}
if self.config.redraw || search_changed || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
self.update_list(&info.database, info.line_height);
}
}
fn updated_library(&mut self) {
self.config.redraw = true;
}
}
impl LibraryBrowser {
fn update_list(&mut self, db: &Database, line_height: f32) {
let song_height = line_height;
let artist_height = song_height * 3.0;
let album_height = song_height * 2.0;
// sort artists by name
let mut artists = db.artists().iter().collect::<Vec<_>>();
artists.sort_by_key(|v| &v.1.name);
let mut gui_elements = vec![];
for (artist_id, artist) in artists {
if self.search_artist.is_empty()
|| self
.search_artist_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&artist.name))
{
let mut artist_gui = Some((
GuiElem::new(ListArtist::new(
GuiElemCfg::default(),
*artist_id,
artist.name.clone(),
)),
artist_height,
));
for album_id in &artist.albums {
if let Some(album) = db.albums().get(album_id) {
if self.search_album.is_empty()
|| self
.search_album_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&album.name))
{
let mut album_gui = Some((
GuiElem::new(ListAlbum::new(
GuiElemCfg::default(),
*album_id,
album.name.clone(),
)),
album_height,
));
for song_id in &album.songs {
if let Some(song) = db.songs().get(song_id) {
if self.search_song.is_empty()
|| self
.search_song_regex
.as_ref()
.is_some_and(|regex| regex.is_match(&song.title))
{
if let Some(g) = artist_gui.take() {
gui_elements.push(g);
}
if let Some(g) = album_gui.take() {
gui_elements.push(g);
}
gui_elements.push((
GuiElem::new(ListSong::new(
GuiElemCfg::default(),
*song_id,
song.title.clone(),
)),
song_height,
));
}
}
}
}
}
}
}
}
let scroll_box = self.children[3].try_as_mut::<ScrollBox>().unwrap();
scroll_box.children = gui_elements;
scroll_box.config_mut().redraw = true;
}
}
#[derive(Clone)]
struct ListArtist {
config: GuiElemCfg,
id: ArtistId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListArtist {
pub fn new(config: GuiElemCfg, id: ArtistId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(81, 24, 125),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListArtist {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Artist(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}
#[derive(Clone)]
struct ListAlbum {
config: GuiElemCfg,
id: AlbumId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListAlbum {
pub fn new(config: GuiElemCfg, id: AlbumId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(8, 61, 47),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListAlbum {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Album(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}
#[derive(Clone)]
struct ListSong {
config: GuiElemCfg,
id: SongId,
children: Vec<GuiElem>,
mouse_pos: Vec2,
}
impl ListSong {
pub fn new(config: GuiElemCfg, id: SongId, name: String) -> Self {
let label = Label::new(
GuiElemCfg::default(),
name,
Color::from_int_rgb(175, 175, 175),
None,
Vec2::new(0.0, 0.5),
);
Self {
config: config.w_mouse(),
id,
children: vec![GuiElem::new(label)],
mouse_pos: Vec2::ZERO,
}
}
}
impl GuiElemTrait for ListSong {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
vec![GuiAction::SetDragging(Some((
Dragging::Song(self.id),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
)))]
} else {
vec![]
}
}
}

View File

@ -0,0 +1,238 @@
use musicdb_lib::{
data::{queue::QueueContent, SongId},
server::Command,
};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
use crate::{
gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_text::Label,
};
#[derive(Clone)]
pub struct CurrentSong {
config: GuiElemCfg,
children: Vec<GuiElem>,
prev_song: Option<SongId>,
}
impl CurrentSong {
pub fn new(config: GuiElemCfg) -> Self {
Self {
config,
children: vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.5))),
"".to_owned(),
Color::from_int_rgb(180, 180, 210),
None,
Vec2::new(0.1, 1.0),
)),
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.5), (0.5, 1.0))),
"".to_owned(),
Color::from_int_rgb(120, 120, 120),
None,
Vec2::new(0.3, 0.0),
)),
],
prev_song: None,
}
}
}
impl GuiElemTrait for CurrentSong {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
let song = if let Some(v) = info.database.queue.get_current() {
if let QueueContent::Song(song) = v.content() {
if Some(*song) == self.prev_song {
// same song as before
return;
} else {
Some(*song)
}
} else if self.prev_song.is_none() {
// no song, nothing in queue
return;
} else {
None
}
} else if self.prev_song.is_none() {
// no song, nothing in queue
return;
} else {
None
};
if self.prev_song != song {
self.config.redraw = true;
self.prev_song = song;
}
if self.config.redraw {
self.config.redraw = false;
let (name, subtext) = if let Some(song) = song {
if let Some(song) = info.database.get_song(&song) {
let sub = match (
song.artist
.as_ref()
.and_then(|id| info.database.artists().get(id)),
song.album
.as_ref()
.and_then(|id| info.database.albums().get(id)),
) {
(None, None) => String::new(),
(Some(artist), None) => format!("by {}", artist.name),
(None, Some(album)) => {
if let Some(artist) = album
.artist
.as_ref()
.and_then(|id| info.database.artists().get(id))
{
format!("on {} by {}", album.name, artist.name)
} else {
format!("on {}", album.name)
}
}
(Some(artist), Some(album)) => {
format!("by {} on {}", artist.name, album.name)
}
};
(song.title.clone(), sub)
} else {
(
"< song not in db >".to_owned(),
"maybe restart the client to resync the database?".to_owned(),
)
}
} else {
(String::new(), String::new())
};
*self.children[0]
.try_as_mut::<Label>()
.unwrap()
.content
.text() = name;
*self.children[1]
.try_as_mut::<Label>()
.unwrap()
.content
.text() = subtext;
}
}
}
#[derive(Clone)]
pub struct PlayPauseToggle {
config: GuiElemCfg,
children: Vec<GuiElem>,
playing_target: bool,
playing_waiting_for_change: bool,
}
impl PlayPauseToggle {
/// automatically adds w_mouse to config
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
Self {
config: config.w_mouse(),
children: vec![],
playing_target: playing,
playing_waiting_for_change: false,
}
}
}
impl GuiElemTrait for PlayPauseToggle {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.playing_waiting_for_change {
if info.database.playing == self.playing_target {
self.playing_waiting_for_change = false;
}
} else {
// not waiting for change, update if the value changes
self.playing_target = info.database.playing;
}
let pos = if info.pos.width() > info.pos.height() {
let a = 0.5 * info.pos.height();
let m = 0.5 * (info.pos.top_left().x + info.pos.bottom_right().x);
Rectangle::new(
Vec2::new(m - a, info.pos.top_left().y),
Vec2::new(m + a, info.pos.bottom_right().y),
)
} else {
let a = 0.5 * info.pos.width();
let m = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
Rectangle::new(
Vec2::new(info.pos.top_left().x, m - a),
Vec2::new(info.pos.bottom_right().x, m + a),
)
};
if self.playing_target {
g.draw_triangle(
[
adjust_pos(&pos, &Vec2::new(0.25, 0.25)),
adjust_pos(&pos, &Vec2::new(0.75, 0.5)),
adjust_pos(&pos, &Vec2::new(0.25, 0.75)),
],
if self.playing_waiting_for_change {
Color::GRAY
} else {
Color::GREEN
},
)
} else {
g.draw_rectangle(
adjust_area(&pos, &Rectangle::from_tuples((0.25, 0.25), (0.75, 0.75))),
if self.playing_waiting_for_change {
Color::RED
} else {
Color::GRAY
},
);
}
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
if !self.playing_waiting_for_change {
self.playing_target = !self.playing_target;
self.playing_waiting_for_change = true;
vec![GuiAction::SendToServer(if self.playing_target {
Command::Resume
} else {
Command::Pause
})]
} else {
vec![]
}
}
}

689
musicdb-client/src/gui_queue.rs Executable file
View File

@ -0,0 +1,689 @@
use musicdb_lib::{
data::{
database::Database,
queue::{Queue, QueueContent},
song::Song,
AlbumId, ArtistId,
},
server::Command,
};
use speedy2d::{
color::Color,
dimen::Vec2,
shape::Rectangle,
window::{ModifiersState, MouseButton, VirtualKeyCode},
};
use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox,
gui_text::Label,
};
#[derive(Clone)]
pub struct QueueViewer {
config: GuiElemCfg,
children: Vec<GuiElem>,
}
impl QueueViewer {
pub fn new(config: GuiElemCfg) -> Self {
let queue_scroll_box = ScrollBox::new(
GuiElemCfg::default(),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![(
GuiElem::new(Label::new(
GuiElemCfg::default(),
"loading...".to_string(),
Color::DARK_GRAY,
None,
Vec2::new(0.5, 0.5),
)),
1.0,
)],
);
Self {
config,
children: vec![
GuiElem::new(queue_scroll_box),
GuiElem::new(QueueEmptySpaceDragHandler::new(GuiElemCfg::default())),
],
}
}
}
impl GuiElemTrait for QueueViewer {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
let mut c = vec![];
queue_gui(
&info.database.queue,
&info.database,
0.0,
0.02,
info.line_height,
&mut c,
vec![],
true,
);
let mut scroll_box = self.children[0].try_as_mut::<ScrollBox>().unwrap();
scroll_box.children = c;
scroll_box.config_mut().redraw = true;
}
}
fn updated_queue(&mut self) {
self.config.redraw = true;
}
}
fn queue_gui(
queue: &Queue,
db: &Database,
depth: f32,
depth_inc_by: f32,
line_height: f32,
target: &mut Vec<(GuiElem, f32)>,
path: Vec<usize>,
current: bool,
) {
let cfg = GuiElemCfg::at(Rectangle::from_tuples((depth, 0.0), (1.0, 1.0)));
match queue.content() {
QueueContent::Song(id) => {
if let Some(s) = db.songs().get(id) {
target.push((
GuiElem::new(QueueSong::new(cfg, path, s.clone(), current)),
line_height,
));
}
}
QueueContent::Folder(ia, q, _) => {
target.push((
GuiElem::new(QueueFolder::new(cfg, path.clone(), queue.clone(), current)),
line_height * 0.67,
));
for (i, q) in q.iter().enumerate() {
let mut p = path.clone();
p.push(i);
queue_gui(
q,
db,
depth + depth_inc_by,
depth_inc_by,
line_height,
target,
p,
current && *ia == i,
);
}
}
}
}
#[derive(Clone)]
struct QueueEmptySpaceDragHandler {
config: GuiElemCfg,
children: Vec<GuiElem>,
}
impl QueueEmptySpaceDragHandler {
pub fn new(config: GuiElemCfg) -> Self {
Self {
config: config.w_drag_target(),
children: vec![],
}
}
}
impl GuiElemTrait for QueueEmptySpaceDragHandler {
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.children.iter_mut())
}
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())
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
dragged_add_to_queue(dragged, |q| Command::QueueAdd(vec![], q))
}
}
fn generic_queue_draw(
info: &mut DrawInfo,
path: &Vec<usize>,
mouse: &mut bool,
copy_on_mouse_down: bool,
) -> bool {
if *mouse && !info.pos.contains(info.mouse_pos) {
*mouse = false;
if !copy_on_mouse_down {
info.actions
.push(GuiAction::SendToServer(Command::QueueRemove(path.clone())));
}
true
} else {
false
}
}
#[derive(Clone)]
struct QueueSong {
config: GuiElemCfg,
children: Vec<GuiElem>,
path: Vec<usize>,
song: Song,
current: bool,
mouse: bool,
mouse_pos: Vec2,
copy: bool,
copy_on_mouse_down: bool,
}
impl QueueSong {
pub fn new(config: GuiElemCfg, path: Vec<usize>, song: Song, current: bool) -> Self {
Self {
config: config.w_mouse().w_keyboard_watch().w_drag_target(),
children: vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
song.title.clone(),
if current {
Color::from_int_rgb(194, 76, 178)
} else {
Color::from_int_rgb(120, 76, 194)
},
None,
Vec2::new(0.0, 0.5),
))],
path,
song,
current,
mouse: false,
mouse_pos: Vec2::ZERO,
copy: false,
copy_on_mouse_down: false,
}
}
}
impl GuiElemTrait for QueueSong {
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.children.iter_mut())
}
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())
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
self.mouse = true;
self.copy_on_mouse_down = self.copy;
}
vec![]
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(),
))]
} else {
vec![]
}
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if !self.mouse {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
info.actions.push(GuiAction::SetDragging(Some((
Dragging::Queue(QueueContent::Song(self.song.id).into()),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
))));
}
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
_down: bool,
_key: Option<VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.copy = modifiers.ctrl();
vec![]
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
let mut p = self.path.clone();
dragged_add_to_queue(dragged, move |q| {
if let Some(i) = p.pop() {
Command::QueueInsert(p, i, q)
} else {
Command::QueueAdd(p, q)
}
})
}
}
#[derive(Clone)]
struct QueueFolder {
config: GuiElemCfg,
children: Vec<GuiElem>,
path: Vec<usize>,
queue: Queue,
current: bool,
mouse: bool,
mouse_pos: Vec2,
copy: bool,
copy_on_mouse_down: bool,
}
impl QueueFolder {
pub fn new(config: GuiElemCfg, path: Vec<usize>, queue: Queue, current: bool) -> Self {
Self {
config: if path.is_empty() {
config
} else {
config.w_mouse().w_keyboard_watch()
}
.w_drag_target(),
children: vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
match queue.content() {
QueueContent::Folder(_, q, n) => format!(
"{} ({})",
if path.is_empty() && n.is_empty() {
"Queue"
} else {
n
},
q.len()
),
_ => "[???]".to_string(),
},
Color::from_int_rgb(52, 132, 50),
None,
Vec2::new(0.0, 0.5),
))],
path,
queue,
current,
mouse: false,
mouse_pos: Vec2::ZERO,
copy: false,
copy_on_mouse_down: false,
}
}
}
impl GuiElemTrait for QueueFolder {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if !self.mouse {
self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y,
);
}
if generic_queue_draw(info, &self.path, &mut self.mouse, self.copy_on_mouse_down) {
let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height();
let mut el = GuiElem::new(self.clone());
info.actions.push(GuiAction::SetDragging(Some((
Dragging::Queue(self.queue.clone()),
Some(Box::new(move |i, g| {
let sw = i.pos.width();
let sh = i.pos.height();
let x = (i.mouse_pos.x - mouse_pos.x) / sw;
let y = (i.mouse_pos.y - mouse_pos.y) / sh;
el.inner.config_mut().pos =
Rectangle::from_tuples((x, y), (x + w / sw, y + h / sh));
el.draw(i, g)
})),
))));
}
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left {
self.mouse = true;
self.copy_on_mouse_down = self.copy;
}
vec![]
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::SendToServer(Command::QueueGoto(
self.path.clone(),
))]
} else {
vec![]
}
}
fn key_watch(
&mut self,
modifiers: ModifiersState,
_down: bool,
_key: Option<VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.copy = modifiers.ctrl();
vec![]
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
let p = self.path.clone();
dragged_add_to_queue(dragged, move |q| Command::QueueAdd(p, q))
}
}
fn dragged_add_to_queue<F: FnOnce(Queue) -> Command + 'static>(
dragged: Dragging,
f: F,
) -> Vec<GuiAction> {
match dragged {
Dragging::Artist(id) => {
vec![GuiAction::Build(Box::new(move |db| {
if let Some(q) = add_to_queue_artist_by_id(id, db) {
vec![GuiAction::SendToServer(f(q))]
} else {
vec![]
}
}))]
}
Dragging::Album(id) => {
vec![GuiAction::Build(Box::new(move |db| {
if let Some(q) = add_to_queue_album_by_id(id, db) {
vec![GuiAction::SendToServer(f(q))]
} else {
vec![]
}
}))]
}
Dragging::Song(id) => {
let q = QueueContent::Song(id).into();
vec![GuiAction::SendToServer(f(q))]
}
Dragging::Queue(q) => {
vec![GuiAction::SendToServer(f(q))]
}
}
}
fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
if let Some(album) = db.albums().get(&id) {
Some(
QueueContent::Folder(
0,
album
.songs
.iter()
.map(|id| QueueContent::Song(*id).into())
.collect(),
album.name.clone(),
)
.into(),
)
} else {
None
}
}
fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
if let Some(artist) = db.artists().get(&id) {
Some(
QueueContent::Folder(
0,
artist
.albums
.iter()
.filter_map(|id| add_to_queue_album_by_id(*id, db))
.collect(),
artist.name.clone(),
)
.into(),
)
} else {
None
}
}
// use musicdb_lib::{
// data::{
// database::Database,
// queue::{Queue, QueueContent},
// AlbumId, ArtistId,
// },
// server::Command,
// };
// use speedy2d::{
// color::Color,
// dimen::Vec2,
// font::{TextLayout, TextOptions},
// };
// use crate::gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
// pub struct QueueViewer {
// config: GuiElemCfg,
// children: Vec<GuiElem>,
// /// 0.0 = bottom
// scroll: f32,
// }
// impl QueueViewer {
// pub fn new(config: GuiElemCfg) -> Self {
// Self {
// config: config.w_drag_target(),
// children: vec![],
// scroll: 0.0,
// }
// }
// }
// impl GuiElemTrait for QueueViewer {
// 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.children.iter_mut())
// }
// fn draw(&mut self, info: &DrawInfo, g: &mut speedy2d::Graphics2D) {
// g.draw_rectangle(info.pos.clone(), Color::from_rgb(0.0, 0.1, 0.0));
// let queue_height = info.database.queue.len();
// let start_y_pos = info.pos.bottom_right().y
// + (self.scroll - queue_height as f32) * info.queue_song_height;
// let mut skip = 0;
// let limit = queue_height.saturating_sub(self.scroll.floor() as usize + skip);
// self.draw_queue(
// &info.database.queue,
// &mut skip,
// &mut 0,
// limit,
// &mut Vec2::new(info.pos.top_left().x, start_y_pos),
// info.pos.width(),
// info,
// g,
// );
// }
// fn dragged(&mut self, dragged: Dragging) -> Vec<crate::gui::GuiAction> {
// match dragged {
// Dragging::Song(id) => vec![GuiAction::SendToServer(Command::QueueAdd(
// vec![],
// QueueContent::Song(id).into(),
// ))],
// Dragging::Album(id) => vec![GuiAction::Build(Box::new(move |db| {
// if let Some(q) = Self::add_to_queue_album_by_id(id, db) {
// vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
// } else {
// vec![]
// }
// }))],
// Dragging::Artist(id) => vec![GuiAction::Build(Box::new(move |db| {
// if let Some(q) = Self::add_to_queue_artist_by_id(id, db) {
// vec![GuiAction::SendToServer(Command::QueueAdd(vec![], q))]
// } else {
// vec![]
// }
// }))],
// _ => vec![],
// }
// }
// }
// impl QueueViewer {
// fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
// if let Some(album) = db.albums().get(&id) {
// Some(
// QueueContent::Folder(
// 0,
// album
// .songs
// .iter()
// .map(|id| QueueContent::Song(*id).into())
// .collect(),
// album.name.clone(),
// )
// .into(),
// )
// } else {
// None
// }
// }
// fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
// if let Some(artist) = db.artists().get(&id) {
// Some(
// QueueContent::Folder(
// 0,
// artist
// .albums
// .iter()
// .filter_map(|id| Self::add_to_queue_album_by_id(*id, db))
// .collect(),
// artist.name.clone(),
// )
// .into(),
// )
// } else {
// None
// }
// }
// }
// const INDENT_PX: f32 = 8.0;
// impl QueueViewer {
// fn draw_queue(
// &mut self,
// queue: &Queue,
// skip: &mut usize,
// drawn: &mut usize,
// limit: usize,
// top_left: &mut Vec2,
// width: f32,
// info: &DrawInfo,
// g: &mut speedy2d::Graphics2D,
// ) {
// // eprintln!("[queue: {} : {}/{}]", *skip, *drawn, limit);
// match queue.content() {
// QueueContent::Song(id) => {
// if *skip == 0 {
// if *drawn < limit {
// *drawn += 1;
// let text = if let Some(song) = info.database.get_song(id) {
// song.title.clone()
// } else {
// format!("< {id} >")
// };
// let height = info
// .font
// .layout_text(&text, 1.0, TextOptions::new())
// .height();
// g.draw_text_cropped(
// top_left.clone(),
// info.pos.clone(),
// Color::from_int_rgb(112, 41, 99),
// &info.font.layout_text(
// &text,
// 0.75 * info.queue_song_height / height,
// TextOptions::new(),
// ),
// );
// top_left.y += info.queue_song_height;
// }
// } else {
// *skip -= 1;
// }
// }
// QueueContent::Folder(index, vec, _name) => {
// top_left.x += INDENT_PX;
// for v in vec {
// self.draw_queue(v, skip, drawn, limit, top_left, width - INDENT_PX, info, g);
// }
// top_left.x -= INDENT_PX;
// }
// }
// }
// }

290
musicdb-client/src/gui_screen.rs Executable file
View File

@ -0,0 +1,290 @@
use std::time::{Duration, Instant};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel},
gui_library::LibraryBrowser,
gui_playback::{CurrentSong, PlayPauseToggle},
gui_queue::QueueViewer,
gui_settings::Settings,
gui_text::Label,
};
/// calculates f(p) (f(x) = 3x^2 - 2x^3)):
/// f(0) = 0
/// f(0.5) = 0.5
/// f(1) = 1
/// f'(0) = f'(1) = 0
/// -> smooth animation, fast to calculate
pub fn transition(p: f32) -> f32 {
3.0 * p * p - 2.0 * p * p * p
}
#[derive(Clone)]
pub struct GuiScreen {
config: GuiElemCfg,
/// 0: StatusBar / Idle display
/// 1: Settings
/// 2: Panel for Main view
children: Vec<GuiElem>,
pub idle: (bool, Option<Instant>),
pub settings: (bool, Option<Instant>),
pub last_interaction: Instant,
idle_timeout: Option<f64>,
pub prev_mouse_pos: Vec2,
}
impl GuiScreen {
fn i_statusbar() -> usize {
0
}
pub fn new(
config: GuiElemCfg,
line_height: f32,
scroll_sensitivity_pixels: f64,
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
Self {
config: config.w_keyboard_watch(),
children: vec![
GuiElem::new(StatusBar::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
true,
)),
GuiElem::new(Settings::new(
GuiElemCfg::default().disabled(),
line_height,
scroll_sensitivity_pixels,
scroll_sensitivity_lines,
scroll_sensitivity_pages,
)),
GuiElem::new(Panel::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.9))),
vec![
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.0), (0.75, 0.1))),
|_| vec![GuiAction::OpenSettings(true)],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Settings".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 0.1))),
|_| vec![GuiAction::Exit],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Exit".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(LibraryBrowser::new(GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.5, 1.0),
)))),
GuiElem::new(QueueViewer::new(GuiElemCfg::at(Rectangle::from_tuples(
(0.5, 0.1),
(1.0, 1.0),
)))),
],
)),
],
idle: (false, None),
settings: (false, None),
last_interaction: Instant::now(),
idle_timeout: Some(60.0),
prev_mouse_pos: Vec2::ZERO,
}
}
fn get_prog(v: &mut (bool, Option<Instant>), seconds: f32) -> f32 {
if let Some(since) = &mut v.1 {
let prog = since.elapsed().as_secs_f32() / seconds;
if prog >= 1.0 {
v.1 = None;
if v.0 {
1.0
} else {
0.0
}
} else {
if v.0 {
prog
} else {
1.0 - prog
}
}
} else if v.0 {
1.0
} else {
0.0
}
}
fn not_idle(&mut self) {
self.last_interaction = Instant::now();
if self.idle.0 {
self.idle = (false, Some(Instant::now()));
}
}
fn idle_check(&mut self) {
if !self.idle.0 {
if let Some(dur) = &self.idle_timeout {
if self.last_interaction.elapsed().as_secs_f64() > *dur {
self.idle = (true, Some(Instant::now()));
}
}
}
}
}
impl GuiElemTrait for GuiScreen {
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.children.iter_mut())
}
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())
}
fn key_watch(
&mut self,
_modifiers: speedy2d::window::ModifiersState,
_down: bool,
_key: Option<speedy2d::window::VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.not_idle();
vec![]
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
// idle stuff
if self.prev_mouse_pos != info.mouse_pos {
self.prev_mouse_pos = info.mouse_pos;
self.not_idle();
} else if !self.idle.0 && self.config.pixel_pos.size() != info.pos.size() {
// resizing prevents idle, but doesn't un-idle
self.not_idle();
}
self.idle_check();
// request_redraw for animations
if self.idle.1.is_some() | self.settings.1.is_some() {
if let Some(h) = &info.helper {
h.request_redraw()
}
}
// animations: idle
if self.idle.1.is_some() {
let seconds = if self.idle.0 { 2.0 } else { 0.5 };
let p1 = Self::get_prog(&mut self.idle, seconds);
if !self.idle.0 || self.idle.1.is_none() {
if let Some(h) = &info.helper {
h.set_cursor_visible(!self.idle.0);
for el in self.children.iter_mut().skip(1) {
el.inner.config_mut().enabled = !self.idle.0;
}
}
}
let p = transition(p1);
self.children[0].inner.config_mut().pos =
Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 1.0));
self.children[0]
.inner
.any_mut()
.downcast_mut::<StatusBar>()
.unwrap()
.idle_mode = p1;
}
// animations: settings
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();
cfg.enabled = p > 0.0;
cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 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]
.inner
.any()
.downcast_ref::<Settings>()
.unwrap()
.get_timeout_val();
}
}
}
#[derive(Clone)]
pub struct StatusBar {
config: GuiElemCfg,
children: Vec<GuiElem>,
idle_mode: f32,
}
impl StatusBar {
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
Self {
config,
children: vec![
GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
Vec2::ZERO,
Vec2::new(0.8, 1.0),
)))),
GuiElem::new(PlayPauseToggle::new(
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
false,
)),
GuiElem::new(Panel::with_background(
GuiElemCfg::default(),
vec![],
Color::BLACK,
)),
],
idle_mode: 0.0,
}
}
}
impl GuiElemTrait for StatusBar {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
if self.idle_mode < 1.0 {
g.draw_line(
info.pos.top_left(),
info.pos.top_right(),
2.0,
Color::from_rgba(1.0, 1.0, 1.0, 1.0 - self.idle_mode),
);
}
}
}

View File

@ -0,0 +1,316 @@
use std::{ops::DerefMut, time::Duration};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, Graphics2D};
use crate::{
gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel, ScrollBox, Slider},
gui_text::Label,
};
#[derive(Clone)]
pub struct Settings {
pub config: GuiElemCfg,
pub children: Vec<GuiElem>,
}
impl Settings {
pub fn new(
mut config: GuiElemCfg,
line_height: f32,
scroll_sensitivity_pixels: f64,
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
config.redraw = true;
Self {
config,
children: vec![
GuiElem::new(ScrollBox::new(
GuiElemCfg::default(),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
vec![
(
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.75, 0.0), (1.0, 1.0))),
|btn| vec![GuiAction::OpenSettings(false)],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Back".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Settings panel opacity".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new({
let mut s = Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
1.0,
1.0,
|slider, label, _info| {
if slider.val_changed() {
*label.content.text() =
format!("{:.0}%", slider.val * 100.0);
}
},
);
s.val_changed_subs.push(false);
s
}),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Line Height / Text Size".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
16.0,
80.0,
line_height as _,
|slider, label, info| {
if slider.val_changed() {
*label.content.text() =
format!("line height: {:.0}", slider.val);
let h = slider.val as _;
info.actions.push(GuiAction::SetLineHeight(h));
}
},
)),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Scroll Sensitivity (lines)".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
12.0,
scroll_sensitivity_lines,
|slider, label, info| {
if slider.val_changed() {
*label.content.text() =
format!("{:.1}", slider.val);
let h = slider.val as _;
info.actions.push(GuiAction::Do(Box::new(
move |gui| gui.scroll_lines_multiplier = h,
)));
}
},
)),
],
)),
0.0,
),
(
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples(
(0.0, 0.0),
(0.33, 1.0),
)),
"Idle time".to_string(),
Color::WHITE,
None,
Vec2::new(0.9, 0.5),
)),
GuiElem::new(Slider::new_labeled(
GuiElemCfg::at(Rectangle::from_tuples(
(0.33, 0.0),
(1.0, 1.0),
)),
0.0,
(60.0f64 * 60.0 * 6.0).sqrt(),
60.0f64.sqrt(),
|slider, label, info| {
if slider.val_changed() {
*label.content.text() = if slider.val > 0.0 {
let mut s = String::new();
let seconds = (slider.val * slider.val) as u64;
let hours = seconds / 3600;
let seconds = seconds % 3600;
let minutes = seconds / 60;
let seconds = seconds % 60;
if hours > 0 {
s = hours.to_string();
s.push_str("h ");
}
if minutes > 0 || hours > 0 && seconds > 0 {
s.push_str(&minutes.to_string());
s.push_str("m ");
}
if hours == 0
&& minutes < 10
&& (seconds > 0 || minutes == 0)
{
s.push_str(&seconds.to_string());
s.push_str("s");
} else if s.ends_with(" ") {
s.pop();
}
s
} else {
"no timeout".to_string()
}
};
let h = slider.val as _;
if slider.val_changed() {
info.actions.push(GuiAction::Do(Box::new(
move |gui| gui.scroll_lines_multiplier = h,
)));
}
},
)),
],
)),
0.0,
),
],
)),
GuiElem::new(Panel::with_background(
GuiElemCfg::default().w_mouse(),
vec![],
Color::BLACK,
)),
],
}
}
pub fn get_timeout_val(&self) -> Option<f64> {
let v = self.children[0]
.inner
.any()
.downcast_ref::<ScrollBox>()
.unwrap()
.children[4]
.0
.inner
.any()
.downcast_ref::<Panel>()
.unwrap()
.children[1]
.inner
.any()
.downcast_ref::<Slider>()
.unwrap()
.val;
if v > 0.0 {
Some(v * v)
} else {
None
}
}
}
impl GuiElemTrait for Settings {
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.children.iter_mut())
}
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) {
let (rest, background) = self.children.split_at_mut(1);
let scrollbox = rest[0].inner.any_mut().downcast_mut::<ScrollBox>().unwrap();
let settings_opacity_slider = scrollbox.children[1]
.0
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.children[1]
.inner
.any_mut()
.downcast_mut::<Slider>()
.unwrap();
if settings_opacity_slider.val_changed_subs[0] {
settings_opacity_slider.val_changed_subs[0] = false;
let color = background[0]
.inner
.any_mut()
.downcast_mut::<Panel>()
.unwrap()
.background
.as_mut()
.unwrap();
*color = Color::from_rgba(
color.r(),
color.g(),
color.b(),
settings_opacity_slider.val as _,
);
}
if self.config.redraw {
self.config.redraw = false;
for (i, (_, h)) in scrollbox.children.iter_mut().enumerate() {
*h = if i == 0 {
info.line_height * 2.0
} else {
info.line_height
};
}
}
}
}

226
musicdb-client/src/gui_text.rs Executable file
View File

@ -0,0 +1,226 @@
use std::rc::Rc;
use speedy2d::{
color::Color,
dimen::Vec2,
font::{FormattedTextBlock, TextLayout, TextOptions},
shape::Rectangle,
window::{ModifiersState, MouseButton},
};
use crate::gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
#[derive(Clone)]
pub struct Label {
config: GuiElemCfg,
children: Vec<GuiElem>,
pub content: Content,
pub pos: Vec2,
}
#[derive(Clone)]
pub struct Content {
text: String,
color: Color,
background: Option<Color>,
formatted: Option<Rc<FormattedTextBlock>>,
}
impl Content {
pub fn get_text(&self) -> &String {
&self.text
}
pub fn get_color(&self) -> &Color {
&self.color
}
/// causes text layout reset
pub fn text(&mut self) -> &mut String {
self.formatted = None;
&mut self.text
}
pub fn color(&mut self) -> &mut Color {
&mut self.color
}
}
impl Label {
pub fn new(
config: GuiElemCfg,
text: String,
color: Color,
background: Option<Color>,
pos: Vec2,
) -> Self {
Self {
config,
children: vec![],
content: Content {
text,
color,
background,
formatted: None,
},
pos,
}
}
}
impl GuiElemTrait for Label {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.pixel_pos.size() != info.pos.size() {
// resize
self.content.formatted = None;
}
let text = if let Some(text) = &self.content.formatted {
text
} else {
let l = info
.font
.layout_text(&self.content.text, 1.0, TextOptions::new());
let l = info.font.layout_text(
&self.content.text,
(info.pos.width() / l.width()).min(info.pos.height() / l.height()),
TextOptions::new(),
);
self.content.formatted = Some(l);
self.content.formatted.as_ref().unwrap()
};
let top_left = Vec2::new(
info.pos.top_left().x + self.pos.x * (info.pos.width() - text.width()),
info.pos.top_left().y + self.pos.y * (info.pos.height() - text.height()),
);
if let Some(bg) = self.content.background {
g.draw_rectangle(
Rectangle::new(
top_left,
Vec2::new(top_left.x + text.width(), top_left.y + text.height()),
),
bg,
);
}
g.draw_text(top_left, self.content.color, text);
}
}
// TODO! this, but requires keyboard events first
/// a single-line text fields for users to type text into.
#[derive(Clone)]
pub struct TextField {
config: GuiElemCfg,
pub children: Vec<GuiElem>,
}
impl TextField {
pub fn new(config: GuiElemCfg, hint: String, color_hint: Color, color_input: Color) -> Self {
Self {
config: config.w_mouse().w_keyboard_focus(),
children: vec![
GuiElem::new(Label::new(
GuiElemCfg::default(),
String::new(),
color_input,
None,
Vec2::new(0.0, 0.5),
)),
GuiElem::new(Label::new(
GuiElemCfg::default(),
hint,
color_hint,
None,
Vec2::new(0.0, 0.5),
)),
],
}
}
}
impl GuiElemTrait for TextField {
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.children.iter_mut())
}
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())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
let (t, c) = if info.has_keyboard_focus {
(3.0, Color::WHITE)
} else {
(1.0, Color::GRAY)
};
g.draw_line(info.pos.top_left(), info.pos.top_right(), t, c);
g.draw_line(info.pos.bottom_left(), info.pos.bottom_right(), t, c);
g.draw_line(info.pos.top_left(), info.pos.bottom_left(), t, c);
g.draw_line(info.pos.top_right(), info.pos.bottom_right(), t, c);
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.config.request_keyboard_focus = true;
vec![GuiAction::ResetKeyboardFocus]
}
fn char_focus(&mut self, modifiers: ModifiersState, key: char) -> Vec<GuiAction> {
if !(modifiers.ctrl() || modifiers.alt() || modifiers.logo()) && !key.is_control() {
let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content;
let was_empty = content.get_text().is_empty();
content.text().push(key);
if was_empty {
self.children[1].inner.config_mut().enabled = false;
}
}
vec![]
}
fn key_focus(
&mut self,
modifiers: ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
_scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
if down
&& !(modifiers.alt() || modifiers.logo())
&& key == Some(speedy2d::window::VirtualKeyCode::Backspace)
{
let content = &mut self.children[0].try_as_mut::<Label>().unwrap().content;
if !content.get_text().is_empty() {
if modifiers.ctrl() {
for s in [true, false, true] {
while !content.get_text().is_empty()
&& content.get_text().ends_with(' ') == s
{
content.text().pop();
}
}
} else {
content.text().pop();
}
if content.get_text().is_empty() {
self.children[1].inner.config_mut().enabled = true;
}
}
}
vec![]
}
}

View File

@ -0,0 +1,147 @@
use speedy2d::{
window::{MouseButton, VirtualKeyCode},
Graphics2D,
};
use crate::gui::{DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait};
#[derive(Clone)]
pub struct WithFocusHotkey<T: GuiElemTrait + Clone> {
pub inner: T,
/// 4 * (ignore, pressed): 10 or 11 -> doesn't matter, 01 -> must be pressed, 00 -> must not be pressed
/// logo alt shift ctrl
pub modifiers: u8,
pub key: VirtualKeyCode,
}
impl<T: GuiElemTrait + Clone> WithFocusHotkey<T> {
/// unlike noshift, this ignores the shift modifier
pub fn new_key(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b1000, key, inner)
}
/// requires the key to be pressed without any modifiers
pub fn new_noshift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0, key, inner)
}
pub fn new_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b0100, key, inner)
}
pub fn new_ctrl(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b01, key, inner)
}
pub fn new_ctrl_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b0101, key, inner)
}
pub fn new_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010000, key, inner)
}
pub fn new_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010100, key, inner)
}
pub fn new_ctrl_alt(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010001, key, inner)
}
pub fn new_ctrl_alt_shift(key: VirtualKeyCode, inner: T) -> WithFocusHotkey<T> {
Self::new(0b010101, key, inner)
}
pub fn new(modifiers: u8, key: VirtualKeyCode, mut inner: T) -> WithFocusHotkey<T> {
inner.config_mut().keyboard_events_watch = true;
WithFocusHotkey {
inner,
modifiers,
key,
}
}
}
impl<T: Clone + 'static> GuiElemTrait for WithFocusHotkey<T>
where
T: GuiElemTrait,
{
fn config(&self) -> &GuiElemCfg {
self.inner.config()
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
self.inner.config_mut()
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
self.inner.children()
}
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())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
self.inner.draw(info, g)
}
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_down(button)
}
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_up(button)
}
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
self.inner.mouse_pressed(button)
}
fn mouse_wheel(&mut self, diff: f32) -> Vec<GuiAction> {
self.inner.mouse_wheel(diff)
}
fn char_watch(
&mut self,
modifiers: speedy2d::window::ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.inner.char_watch(modifiers, key)
}
fn char_focus(
&mut self,
modifiers: speedy2d::window::ModifiersState,
key: char,
) -> Vec<GuiAction> {
self.inner.char_focus(modifiers, key)
}
fn key_watch(
&mut self,
modifiers: speedy2d::window::ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
let hotkey = down == false
&& key.is_some_and(|v| v == self.key)
&& (self.modifiers & 0b10 == 1 || (self.modifiers & 0b01 == 1) == modifiers.ctrl())
&& (self.modifiers & 0b1000 == 1
|| (self.modifiers & 0b0100 == 1) == modifiers.shift())
&& (self.modifiers & 0b100000 == 1
|| (self.modifiers & 0b010000 == 1) == modifiers.alt())
&& (self.modifiers & 0b10000000 == 1
|| (self.modifiers & 0b01000000 == 1) == modifiers.logo());
let mut o = self.inner.key_watch(modifiers, down, key, scan);
if hotkey {
self.config_mut().request_keyboard_focus = true;
o.push(GuiAction::ResetKeyboardFocus);
}
o
}
fn key_focus(
&mut self,
modifiers: speedy2d::window::ModifiersState,
down: bool,
key: Option<speedy2d::window::VirtualKeyCode>,
scan: speedy2d::window::KeyScancode,
) -> Vec<GuiAction> {
self.inner.key_focus(modifiers, down, key, scan)
}
fn dragged(&mut self, dragged: crate::gui::Dragging) -> Vec<GuiAction> {
self.inner.dragged(dragged)
}
fn updated_library(&mut self) {
self.inner.updated_library()
}
fn updated_queue(&mut self) {
self.inner.updated_queue()
}
}

436
musicdb-client/src/main.rs Executable file
View File

@ -0,0 +1,436 @@
use std::{
eprintln, fs,
net::{SocketAddr, TcpStream},
path::PathBuf,
sync::{Arc, Mutex},
thread,
time::Duration,
};
use gui::GuiEvent;
use musicdb_lib::{
data::{
album::Album, artist::Artist, database::Database, queue::QueueContent, song::Song,
DatabaseLocation, GeneralData,
},
load::ToFromBytes,
player::Player,
server::Command,
};
#[cfg(feature = "speedy2d")]
mod gui;
#[cfg(feature = "speedy2d")]
mod gui_base;
#[cfg(feature = "speedy2d")]
mod gui_library;
#[cfg(feature = "speedy2d")]
mod gui_playback;
#[cfg(feature = "speedy2d")]
mod gui_queue;
#[cfg(feature = "speedy2d")]
mod gui_screen;
#[cfg(feature = "speedy2d")]
mod gui_settings;
#[cfg(feature = "speedy2d")]
mod gui_text;
#[cfg(feature = "speedy2d")]
mod gui_wrappers;
enum Mode {
Cli,
Gui,
SyncPlayer,
FillDb,
}
fn main() {
let mut args = std::env::args().skip(1);
let mode = match args.next().as_ref().map(|v| v.trim()) {
Some("cli") => Mode::Cli,
Some("gui") => Mode::Gui,
Some("syncplayer") => Mode::SyncPlayer,
Some("filldb") => Mode::FillDb,
_ => {
println!("Run with argument <cli/gui/syncplayer/filldb>!");
return;
}
};
let addr = args.next().unwrap_or("127.0.0.1:26314".to_string());
let mut con = TcpStream::connect(addr.parse::<SocketAddr>().unwrap()).unwrap();
let database = Arc::new(Mutex::new(Database::new_clientside()));
#[cfg(feature = "speedy2d")]
let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
Arc::new(Mutex::new(None));
#[cfg(feature = "speedy2d")]
let sender = Arc::clone(&update_gui_sender);
let wants_player = matches!(mode, Mode::SyncPlayer);
let con_thread = {
let database = Arc::clone(&database);
let mut con = con.try_clone().unwrap();
// this is all you need to keep the db in sync
thread::spawn(move || {
let mut player = if wants_player {
Some(Player::new().unwrap())
} else {
None
};
loop {
if let Some(player) = &mut player {
let mut db = database.lock().unwrap();
if !db.lib_directory.as_os_str().is_empty() {
player.update(&mut db);
}
}
let update = Command::from_bytes(&mut con).unwrap();
if let Some(player) = &mut player {
player.handle_command(&update);
}
database.lock().unwrap().apply_command(update);
#[cfg(feature = "speedy2d")]
if let Some(v) = &*update_gui_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
}
}
})
};
match mode {
Mode::Cli => {
Looper {
con: &mut con,
database: &database,
}
.cmd_loop();
}
Mode::Gui => {
#[cfg(feature = "speedy2d")]
{
let occasional_refresh_sender = Arc::clone(&sender);
thread::spawn(move || loop {
std::thread::sleep(Duration::from_secs(1));
if let Some(v) = &*occasional_refresh_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
}
});
gui::main(database, con, sender)
};
}
Mode::SyncPlayer => {
con_thread.join().unwrap();
}
Mode::FillDb => {
// wait for init
let dir = loop {
let db = database.lock().unwrap();
if !db.lib_directory.as_os_str().is_empty() {
break db.lib_directory.clone();
}
drop(db);
std::thread::sleep(Duration::from_millis(300));
};
eprintln!("
WARN: This will add all audio files in the lib-dir to the library, even if they were already added!
lib-dir: {:?}
If you really want to continue, type Yes.", dir);
let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
if line.trim().to_lowercase() == "yes" {
for artist in fs::read_dir(&dir)
.expect("reading lib-dir")
.filter_map(|v| v.ok())
{
if let Ok(albums) = fs::read_dir(artist.path()) {
let artist_name = artist.file_name().to_string_lossy().into_owned();
let mut artist_id = None;
for album in albums.filter_map(|v| v.ok()) {
if let Ok(songs) = fs::read_dir(album.path()) {
let album_name = album.file_name().to_string_lossy().into_owned();
let mut album_id = None;
let mut songs: Vec<_> = songs.filter_map(|v| v.ok()).collect();
songs.sort_unstable_by_key(|v| v.file_name());
for song in songs {
match song.path().extension().map(|v| v.to_str()) {
Some(Some(
"mp3" | "wav" | "wma" | "aac" | "flac" | "m4a" | "m4p"
| "ogg" | "oga" | "mogg" | "opus" | "tta",
)) => {
println!("> {:?}", song.path());
let song_name =
song.file_name().to_string_lossy().into_owned();
println!(
" {} - {} - {}",
song_name, artist_name, album_name
);
// get artist id
let artist_id = if let Some(v) = artist_id {
v
} else {
let mut adding_artist = false;
loop {
let db = database.lock().unwrap();
let artists = db
.artists()
.iter()
.filter(|(_, v)| v.name == artist_name)
.collect::<Vec<_>>();
if artists.len() > 1 {
eprintln!("Choosing the first of {} artists named {}.", artists.len(), artist_name);
}
if let Some((id, _)) = artists.first() {
artist_id = Some(**id);
break **id;
} else {
drop(db);
if !adding_artist {
adding_artist = true;
Command::AddArtist(Artist {
id: 0,
name: artist_name.clone(),
cover: None,
albums: vec![],
singles: vec![],
general: GeneralData::default(),
})
.to_bytes(&mut con)
.expect(
"sending AddArtist to db failed",
);
}
std::thread::sleep(Duration::from_millis(
300,
));
};
}
};
// get album id
let album_id = if let Some(v) = album_id {
v
} else {
let mut adding_album = false;
loop {
let db = database.lock().unwrap();
let albums = db
.artists()
.get(&artist_id)
.expect("artist_id not valid (bug)")
.albums
.iter()
.filter_map(|v| {
Some((v, db.albums().get(&v)?))
})
.filter(|(_, v)| v.name == album_name)
.collect::<Vec<_>>();
if albums.len() > 1 {
eprintln!("Choosing the first of {} albums named {} by the artist {}.", albums.len(), album_name, artist_name);
}
if let Some((id, _)) = albums.first() {
album_id = Some(**id);
break **id;
} else {
drop(db);
if !adding_album {
adding_album = true;
Command::AddAlbum(Album {
id: 0,
name: album_name.clone(),
artist: Some(artist_id),
cover: None,
songs: vec![],
general: GeneralData::default(),
})
.to_bytes(&mut con)
.expect("sending AddAlbum to db failed");
}
std::thread::sleep(Duration::from_millis(
300,
));
};
}
};
Command::AddSong(Song::new(
DatabaseLocation {
rel_path: PathBuf::from(artist.file_name())
.join(album.file_name())
.join(song.file_name()),
},
song_name,
Some(album_id),
Some(artist_id),
vec![],
None,
))
.to_bytes(&mut con)
.expect("sending AddSong to db failed");
}
_ => {}
}
}
}
}
}
}
}
}
}
}
struct Looper<'a> {
pub con: &'a mut TcpStream,
pub database: &'a Arc<Mutex<Database>>,
}
impl<'a> Looper<'a> {
pub fn cmd_loop(&mut self) {
loop {
println!();
let line = self.read_line(" > enter a command (help for help)");
let line = line.trim();
match line {
"resume" => Command::Resume,
"pause" => Command::Pause,
"stop" => Command::Stop,
"next" => Command::NextSong,
"set-lib-dir" => {
let line = self.read_line("Enter the new (absolute) library directory, or leave empty to abort");
if !line.is_empty() {
Command::SetLibraryDirectory(line.into())
} else {
continue;
}
},
"add-song" => {
let song = Song {
id: 0,
location: self.read_line("The songs file is located, relative to the library root, at...").into(),
title: self.read_line("The songs title is..."),
album: self.read_line_ido("The song is part of the album with the id... (empty for None)"),
artist: self.read_line_ido("The song is made by the artist with the id... (empty for None)"),
more_artists: accumulate(|| self.read_line_ido("The song is made with support by other artist, one of which has the id... (will ask repeatedly; leave empty once done)")),
cover: self.read_line_ido("The song should use the cover with the id... (empty for None - will default to album or artist cover, if available)"),
general: GeneralData::default(),
cached_data: Arc::new(Mutex::new(None)),
};
println!("You are about to add the following song to the database:");
println!(" + {song}");
if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" {
Command::AddSong(song)
} else {
println!("[-] Aborted - no event will be sent to the database.");
continue;
}
},
"update-song" => {
let song_id = self.read_line_id("The ID of the song is...");
if let Some(mut song) = self.database.lock().unwrap().get_song(&song_id).cloned() {
println!("You are now editing the song {song}.");
loop {
match self.read_line("What do you want to edit? (title/album/artist/location or done)").to_lowercase().trim() {
"done" => break,
"title" => {
println!("prev: '{}'", song.title);
song.title = self.read_line("");
}
"album" => {
println!("prev: '{}'", song.album.map_or(String::new(), |v| v.to_string()));
song.album = self.read_line_ido("");
}
"artist" => {
println!("prev: '{}'", song.artist.map_or(String::new(), |v| v.to_string()));
song.artist = self.read_line_ido("");
}
"location" => {
println!("prev: '{:?}'", song.location);
song.location = self.read_line("").into();
}
_ => println!("[-] must be title/album/artist/location or done"),
}
}
println!("You are about to update the song:");
println!(" + {song}");
if self.read_line("Are you sure? (type 'yes' to continue)").to_lowercase().trim() == "yes" {
Command::ModifySong(song)
} else {
println!("[-] Aborted - no event will be sent to the database.");
continue;
}
} else {
println!("[-] No song with that ID found, aborting.");
continue;
}
}
"queue-clear" => Command::QueueUpdate(vec![], QueueContent::Folder(0, vec![], String::new()).into()),
"queue-add-to-end" => Command::QueueAdd(vec![], QueueContent::Song(self.read_line_id("The ID of the song that should be added to the end of the queue is...")).into()),
"save" => Command::Save,
"status" => {
let db = self.database.lock().unwrap();
println!("DB contains {} songs:", db.songs().len());
for song in db.songs().values() {
println!("> [{}]: {}", song.id, song);
}
println!("Queue: {:?}, then {:?}", db.queue.get_current(), db.queue.get_next());
continue;
}
"exit" => {
println!("<< goodbye");
break;
}
_ => {
println!("Type 'exit' to exit, 'status' to see the db, 'resume', 'pause', 'stop', 'next', 'queue-clear', 'queue-add-to-end', 'add-song', 'add-album', 'add-artist', 'update-song', 'update-album', 'update-artist', 'set-lib-dir', or 'save' to control playback or update the db.");
continue;
}
}
.to_bytes(self.con)
.unwrap();
}
}
pub fn read_line(&mut self, q: &str) -> String {
loop {
if !q.is_empty() {
println!("{q}");
}
let mut line = String::new();
std::io::stdin().read_line(&mut line).unwrap();
while line.ends_with('\n') || line.ends_with('\r') {
line.pop();
}
if line.trim() == "#" {
self.cmd_loop();
} else {
return line;
}
}
}
pub fn read_line_id(&mut self, q: &str) -> u64 {
loop {
if let Ok(v) = self.read_line(q).trim().parse() {
return v;
} else {
println!("[-] Must be a positive integer.");
}
}
}
pub fn read_line_ido(&mut self, q: &str) -> Option<u64> {
loop {
let line = self.read_line(q);
let line = line.trim();
if line.is_empty() {
return None;
}
if let Ok(v) = line.parse() {
return Some(v);
} else {
println!("[-] Must be a positive integer or nothing for None.");
}
}
}
}
pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
let mut o = vec![];
loop {
if let Some(v) = f() {
o.push(v);
} else {
break;
}
}
o
}

2
musicdb-lib/.gitignore vendored Executable file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

12
musicdb-lib/Cargo.toml Executable file
View File

@ -0,0 +1,12 @@
[package]
name = "musicdb-lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
awedio = "0.2.0"
base64 = "0.21.2"
rc-u8-reader = "2.0.16"
tokio = "1.29.1"

43
musicdb-lib/src/data/album.rs Executable file
View File

@ -0,0 +1,43 @@
use std::io::{Read, Write};
use crate::load::ToFromBytes;
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
#[derive(Clone, Debug)]
pub struct Album {
pub id: AlbumId,
pub name: String,
pub artist: Option<ArtistId>,
pub cover: Option<CoverId>,
pub songs: Vec<SongId>,
pub general: GeneralData,
}
impl ToFromBytes for Album {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.id.to_bytes(s)?;
self.name.to_bytes(s)?;
self.artist.to_bytes(s)?;
self.songs.to_bytes(s)?;
self.cover.to_bytes(s)?;
self.general.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self {
id: ToFromBytes::from_bytes(s)?,
name: ToFromBytes::from_bytes(s)?,
artist: ToFromBytes::from_bytes(s)?,
songs: ToFromBytes::from_bytes(s)?,
cover: ToFromBytes::from_bytes(s)?,
general: ToFromBytes::from_bytes(s)?,
})
}
}

43
musicdb-lib/src/data/artist.rs Executable file
View File

@ -0,0 +1,43 @@
use std::io::{Read, Write};
use crate::load::ToFromBytes;
use super::{AlbumId, ArtistId, CoverId, GeneralData, SongId};
#[derive(Clone, Debug)]
pub struct Artist {
pub id: ArtistId,
pub name: String,
pub cover: Option<CoverId>,
pub albums: Vec<AlbumId>,
pub singles: Vec<SongId>,
pub general: GeneralData,
}
impl ToFromBytes for Artist {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.id.to_bytes(s)?;
self.name.to_bytes(s)?;
self.albums.to_bytes(s)?;
self.singles.to_bytes(s)?;
self.cover.to_bytes(s)?;
self.general.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self {
id: ToFromBytes::from_bytes(s)?,
name: ToFromBytes::from_bytes(s)?,
albums: ToFromBytes::from_bytes(s)?,
singles: ToFromBytes::from_bytes(s)?,
cover: ToFromBytes::from_bytes(s)?,
general: ToFromBytes::from_bytes(s)?,
})
}
}

377
musicdb-lib/src/data/database.rs Executable file
View File

@ -0,0 +1,377 @@
use std::{
collections::HashMap,
fs::{self, File},
io::{BufReader, Write},
path::PathBuf,
sync::{mpsc, Arc},
time::Instant,
};
use crate::{load::ToFromBytes, server::Command};
use super::{
album::Album,
artist::Artist,
queue::{Queue, QueueContent},
song::Song,
AlbumId, ArtistId, CoverId, DatabaseLocation, SongId,
};
pub struct Database {
db_file: PathBuf,
pub lib_directory: PathBuf,
artists: HashMap<ArtistId, Artist>,
albums: HashMap<AlbumId, Album>,
songs: HashMap<SongId, Song>,
covers: HashMap<CoverId, DatabaseLocation>,
// TODO! make sure this works out for the server AND clients
// cover_cache: HashMap<CoverId, Vec<u8>>,
db_data_file_change_first: Option<Instant>,
db_data_file_change_last: Option<Instant>,
pub queue: Queue,
pub update_endpoints: Vec<UpdateEndpoint>,
pub playing: bool,
pub command_sender: Option<mpsc::Sender<Command>>,
}
pub enum UpdateEndpoint {
Bytes(Box<dyn Write + Sync + Send>),
CmdChannel(mpsc::Sender<Arc<Command>>),
CmdChannelTokio(tokio::sync::mpsc::UnboundedSender<Arc<Command>>),
Custom(Box<dyn FnMut(&Command) + Send>),
}
impl Database {
fn panic(&self, msg: &str) -> ! {
// custom panic handler
// make a backup
// exit
panic!("DatabasePanic: {msg}");
}
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
self.lib_directory.join(&location.rel_path)
}
pub fn get_song(&self, song: &SongId) -> Option<&Song> {
self.songs.get(song)
}
pub fn get_song_mut(&mut self, song: &SongId) -> Option<&mut Song> {
self.songs.get_mut(song)
}
/// adds a song to the database.
/// ignores song.id and just assigns a new id, which it then returns.
/// this function also adds a reference to the new song to the album (or artist.singles, if no album)
pub fn add_song_new(&mut self, song: Song) -> SongId {
let album = song.album.clone();
let artist = song.artist.clone();
let id = self.add_song_new_nomagic(song);
if let Some(Some(album)) = album.map(|v| self.albums.get_mut(&v)) {
album.songs.push(id);
} else {
if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) {
artist.singles.push(id);
}
}
id
}
pub fn add_song_new_nomagic(&mut self, mut song: Song) -> SongId {
for key in 0.. {
if !self.songs.contains_key(&key) {
song.id = key;
self.songs.insert(key, song);
return key;
}
}
self.panic("database.songs all keys used - no more capacity for new songs!");
}
/// adds an artist to the database.
/// ignores artist.id and just assigns a new id, which it then returns.
/// this function does nothing special.
pub fn add_artist_new(&mut self, artist: Artist) -> ArtistId {
let id = self.add_artist_new_nomagic(artist);
id
}
fn add_artist_new_nomagic(&mut self, mut artist: Artist) -> ArtistId {
for key in 0.. {
if !self.artists.contains_key(&key) {
artist.id = key;
self.artists.insert(key, artist);
return key;
}
}
self.panic("database.artists all keys used - no more capacity for new artists!");
}
/// adds an album to the database.
/// ignores album.id and just assigns a new id, which it then returns.
/// this function also adds a reference to the new album to the artist
pub fn add_album_new(&mut self, album: Album) -> AlbumId {
let artist = album.artist.clone();
let id = self.add_album_new_nomagic(album);
if let Some(Some(artist)) = artist.map(|v| self.artists.get_mut(&v)) {
artist.albums.push(id);
}
id
}
fn add_album_new_nomagic(&mut self, mut album: Album) -> AlbumId {
for key in 0.. {
if !self.albums.contains_key(&key) {
album.id = key;
self.albums.insert(key, album);
return key;
}
}
self.panic("database.artists all keys used - no more capacity for new artists!");
}
/// updates an existing song in the database with the new value.
/// uses song.id to find the correct song.
/// if the id doesn't exist in the db, Err(()) is returned.
/// Otherwise Some(old_data) is returned.
pub fn update_song(&mut self, song: Song) -> Result<Song, ()> {
if let Some(prev_song) = self.songs.get_mut(&song.id) {
Ok(std::mem::replace(prev_song, song))
} else {
Err(())
}
}
pub fn update_album(&mut self, album: Album) -> Result<Album, ()> {
if let Some(prev_album) = self.albums.get_mut(&album.id) {
Ok(std::mem::replace(prev_album, album))
} else {
Err(())
}
}
pub fn update_artist(&mut self, artist: Artist) -> Result<Artist, ()> {
if let Some(prev_artist) = self.artists.get_mut(&artist.id) {
Ok(std::mem::replace(prev_artist, artist))
} else {
Err(())
}
}
/// [NOT RECOMMENDED - use add_song_new or update_song instead!] inserts the song into the database.
/// uses song.id. If another song with that ID exists, it is replaced and Some(other_song) is returned.
/// If no other song exists, the song will be added to the database with the given ID and None is returned.
pub fn update_or_add_song(&mut self, song: Song) -> Option<Song> {
self.songs.insert(song.id, song)
}
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
// TODO! this is slow because it clones everything - there has to be a better way...
Command::SyncDatabase(
self.artists().iter().map(|v| v.1.clone()).collect(),
self.albums().iter().map(|v| v.1.clone()).collect(),
self.songs().iter().map(|v| v.1.clone()).collect(),
)
.to_bytes(con)?;
Command::QueueUpdate(vec![], self.queue.clone()).to_bytes(con)?;
if self.playing {
Command::Resume.to_bytes(con)?;
}
// since this is so easy to check for, it comes last.
// this allows clients to find out when init_connection is done.
Command::SetLibraryDirectory(self.lib_directory.clone()).to_bytes(con)?;
// is initialized now - client can receive updates after this point.
// NOTE: Don't write to connection anymore - the db will dispatch updates on its own.
// we just need to handle commands (receive from the connection).
Ok(())
}
pub fn apply_command(&mut self, command: Command) {
// since db.update_endpoints is empty for clients, this won't cause unwanted back and forth
self.broadcast_update(&command);
match command {
Command::Resume => self.playing = true,
Command::Pause => self.playing = false,
Command::Stop => self.playing = false,
Command::NextSong => {
self.queue.advance_index();
}
Command::Save => {
if let Err(e) = self.save_database(None) {
eprintln!("Couldn't save: {e}");
}
}
Command::SyncDatabase(a, b, c) => self.sync(a, b, c),
Command::QueueUpdate(index, new_data) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
*v = new_data;
}
}
Command::QueueAdd(mut index, new_data) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
v.add_to_end(new_data);
}
}
Command::QueueInsert(mut index, pos, new_data) => {
if let Some(v) = self.queue.get_item_at_index_mut(&index, 0) {
v.insert(new_data, pos);
}
}
Command::QueueRemove(index) => {
self.queue.remove_by_index(&index, 0);
}
Command::QueueGoto(index) => self.queue.set_index(&index, 0),
Command::AddSong(song) => {
self.add_song_new(song);
}
Command::AddAlbum(album) => {
self.add_album_new(album);
}
Command::AddArtist(artist) => {
self.add_artist_new(artist);
}
Command::ModifySong(song) => {
_ = self.update_song(song);
}
Command::ModifyAlbum(album) => {
_ = self.update_album(album);
}
Command::ModifyArtist(artist) => {
_ = self.update_artist(artist);
}
Command::SetLibraryDirectory(new_dir) => {
self.lib_directory = new_dir;
}
}
}
}
// file saving/loading
impl Database {
/// Database is also used for clients, to keep things consistent.
/// A client database doesn't need any storage paths and won't perform autosaves.
pub fn new_clientside() -> Self {
Self {
db_file: PathBuf::new(),
lib_directory: PathBuf::new(),
artists: HashMap::new(),
albums: HashMap::new(),
songs: HashMap::new(),
covers: HashMap::new(),
db_data_file_change_first: None,
db_data_file_change_last: None,
queue: QueueContent::Folder(0, vec![], String::new()).into(),
update_endpoints: vec![],
playing: false,
command_sender: None,
}
}
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
Self {
db_file: path,
lib_directory: lib_dir,
artists: HashMap::new(),
albums: HashMap::new(),
songs: HashMap::new(),
covers: HashMap::new(),
db_data_file_change_first: None,
db_data_file_change_last: None,
queue: QueueContent::Folder(0, vec![], String::new()).into(),
update_endpoints: vec![],
playing: false,
command_sender: None,
}
}
pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> {
let mut file = BufReader::new(File::open(&path)?);
eprintln!("[info] loading library from {file:?}");
let lib_directory = ToFromBytes::from_bytes(&mut file)?;
eprintln!("[info] library directory is {lib_directory:?}");
Ok(Self {
db_file: path,
lib_directory,
artists: ToFromBytes::from_bytes(&mut file)?,
albums: ToFromBytes::from_bytes(&mut file)?,
songs: ToFromBytes::from_bytes(&mut file)?,
covers: ToFromBytes::from_bytes(&mut file)?,
db_data_file_change_first: None,
db_data_file_change_last: None,
queue: QueueContent::Folder(0, vec![], String::new()).into(),
update_endpoints: vec![],
playing: false,
command_sender: None,
})
}
pub fn save_database(&self, path: Option<PathBuf>) -> Result<PathBuf, std::io::Error> {
let path = if let Some(p) = path {
p
} else {
self.db_file.clone()
};
// if no path is set (client mode), do nothing
if path.as_os_str().is_empty() {
return Ok(path);
}
eprintln!("[info] saving db to {path:?}.");
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(&path)?;
self.lib_directory.to_bytes(&mut file)?;
self.artists.to_bytes(&mut file)?;
self.albums.to_bytes(&mut file)?;
self.songs.to_bytes(&mut file)?;
self.covers.to_bytes(&mut file)?;
Ok(path)
}
pub fn broadcast_update(&mut self, update: &Command) {
let mut remove = vec![];
let mut bytes = None;
let mut arc = None;
for (i, udep) in self.update_endpoints.iter_mut().enumerate() {
match udep {
UpdateEndpoint::Bytes(writer) => {
if bytes.is_none() {
bytes = Some(update.to_bytes_vec());
}
if writer.write_all(bytes.as_ref().unwrap()).is_err() {
remove.push(i);
}
}
UpdateEndpoint::CmdChannel(sender) => {
if arc.is_none() {
arc = Some(Arc::new(update.clone()));
}
if sender.send(arc.clone().unwrap()).is_err() {
remove.push(i);
}
}
UpdateEndpoint::CmdChannelTokio(sender) => {
if arc.is_none() {
arc = Some(Arc::new(update.clone()));
}
if sender.send(arc.clone().unwrap()).is_err() {
remove.push(i);
}
}
UpdateEndpoint::Custom(func) => func(update),
}
}
if !remove.is_empty() {
eprintln!(
"[info] closing {} connections, {} are still active",
remove.len(),
self.update_endpoints.len() - remove.len()
);
for i in remove.into_iter().rev() {
self.update_endpoints.remove(i);
}
}
}
pub fn sync(&mut self, artists: Vec<Artist>, albums: Vec<Album>, songs: Vec<Song>) {
self.artists = artists.iter().map(|v| (v.id, v.clone())).collect();
self.albums = albums.iter().map(|v| (v.id, v.clone())).collect();
self.songs = songs.iter().map(|v| (v.id, v.clone())).collect();
}
}
impl Database {
pub fn songs(&self) -> &HashMap<SongId, Song> {
&self.songs
}
pub fn albums(&self) -> &HashMap<AlbumId, Album> {
&self.albums
}
pub fn artists(&self) -> &HashMap<ArtistId, Artist> {
&self.artists
}
}

73
musicdb-lib/src/data/mod.rs Executable file
View File

@ -0,0 +1,73 @@
use std::{
io::{Read, Write},
path::PathBuf,
};
use crate::load::ToFromBytes;
pub mod album;
pub mod artist;
pub mod database;
pub mod queue;
pub mod song;
pub type SongId = u64;
pub type AlbumId = u64;
pub type ArtistId = u64;
pub type CoverId = u64;
#[derive(Clone, Default, Debug)]
pub struct GeneralData {
pub tags: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct DatabaseLocation {
pub rel_path: PathBuf,
}
impl ToFromBytes for DatabaseLocation {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.rel_path.to_bytes(s)
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self {
rel_path: ToFromBytes::from_bytes(s)?,
})
}
}
impl<P> From<P> for DatabaseLocation
where
P: Into<PathBuf>,
{
fn from(value: P) -> Self {
Self {
rel_path: value.into(),
}
}
}
impl ToFromBytes for GeneralData {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.tags.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self {
tags: ToFromBytes::from_bytes(s)?,
})
}
}

286
musicdb-lib/src/data/queue.rs Executable file
View File

@ -0,0 +1,286 @@
use crate::load::ToFromBytes;
use super::SongId;
#[derive(Clone, Debug)]
pub struct Queue {
enabled: bool,
content: QueueContent,
}
#[derive(Clone, Debug)]
pub enum QueueContent {
Song(SongId),
Folder(usize, Vec<Queue>, String),
}
impl Queue {
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn content(&self) -> &QueueContent {
&self.content
}
pub fn add_to_end(&mut self, v: Self) -> bool {
match &mut self.content {
QueueContent::Song(_) => false,
QueueContent::Folder(_, vec, _) => {
vec.push(v);
true
}
}
}
pub fn insert(&mut self, v: Self, index: usize) -> bool {
match &mut self.content {
QueueContent::Song(_) => false,
QueueContent::Folder(_, vec, _) => {
if index <= vec.len() {
vec.insert(index, v);
true
} else {
false
}
}
}
}
pub fn len(&self) -> usize {
if !self.enabled {
return 0;
}
match &self.content {
QueueContent::Song(_) => 1,
QueueContent::Folder(_, v, _) => v.iter().map(|v| v.len()).sum(),
}
}
/// recursively descends the queue until the current active element is found, then returns it.
pub fn get_current(&self) -> Option<&Self> {
match &self.content {
QueueContent::Folder(i, v, _) => {
let i = *i;
if let Some(v) = v.get(i) {
v.get_current()
} else {
None
}
}
QueueContent::Song(_) => Some(self),
}
}
pub fn get_current_song(&self) -> Option<&SongId> {
if let QueueContent::Song(id) = self.get_current()?.content() {
Some(id)
} else {
None
}
}
pub fn get_next_song(&self) -> Option<&SongId> {
if let QueueContent::Song(id) = self.get_next()?.content() {
Some(id)
} else {
None
}
}
pub fn get_next(&self) -> Option<&Self> {
match &self.content {
QueueContent::Folder(i, vec, _) => {
let i = *i;
if let Some(v) = vec.get(i) {
if let Some(v) = v.get_next() {
Some(v)
} else {
if let Some(v) = vec.get(i + 1) {
v.get_current()
} else {
None
}
}
} else {
None
}
}
QueueContent::Song(_) => None,
}
}
pub fn advance_index(&mut self) -> bool {
match &mut self.content {
QueueContent::Song(_) => false,
QueueContent::Folder(index, contents, _) => {
if let Some(c) = contents.get_mut(*index) {
// inner value could advance index, do nothing.
if c.advance_index() {
true
} else {
loop {
if *index + 1 < contents.len() {
// can advance
*index += 1;
if contents[*index].enabled {
break true;
}
} else {
// can't advance: index would be out of bounds
*index = 0;
break false;
}
}
}
} else {
*index = 0;
false
}
}
}
}
pub fn set_index(&mut self, index: &Vec<usize>, depth: usize) {
let i = index.get(depth).map(|v| *v).unwrap_or(0);
match &mut self.content {
QueueContent::Song(_) => {}
QueueContent::Folder(idx, contents, _) => {
*idx = i;
for (i2, c) in contents.iter_mut().enumerate() {
if i2 != i {
c.set_index(&vec![], 0)
}
}
if let Some(c) = contents.get_mut(i) {
c.set_index(index, depth + 1);
}
}
}
}
pub fn get_item_at_index(&self, index: &Vec<usize>, depth: usize) -> Option<&Self> {
if let Some(i) = index.get(depth) {
match &self.content {
QueueContent::Song(_) => None,
QueueContent::Folder(_, v, _) => {
if let Some(v) = v.get(*i) {
v.get_item_at_index(index, depth + 1)
} else {
None
}
}
}
} else {
Some(self)
}
}
pub fn get_item_at_index_mut(&mut self, index: &Vec<usize>, depth: usize) -> Option<&mut Self> {
if let Some(i) = index.get(depth) {
match &mut self.content {
QueueContent::Song(_) => None,
QueueContent::Folder(_, v, _) => {
if let Some(v) = v.get_mut(*i) {
v.get_item_at_index_mut(index, depth + 1)
} else {
None
}
}
}
} else {
Some(self)
}
}
pub fn remove_by_index(&mut self, index: &Vec<usize>, depth: usize) -> Option<Self> {
if let Some(i) = index.get(depth) {
match &mut self.content {
QueueContent::Song(_) => None,
QueueContent::Folder(ci, v, _) => {
if depth + 1 < index.len() {
if let Some(v) = v.get_mut(*i) {
v.remove_by_index(index, depth + 1)
} else {
None
}
} else {
if *i < v.len() {
// if current playback is past this point,
// reduce the index by 1 so that it still points to the same element
if *ci > *i {
*ci -= 1;
}
Some(v.remove(*i))
} else {
None
}
}
}
}
} else {
None
}
}
}
impl From<QueueContent> for Queue {
fn from(value: QueueContent) -> Self {
Self {
enabled: true,
content: value,
}
}
}
impl ToFromBytes for Queue {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: std::io::Write,
{
s.write_all(&[if self.enabled { 0b11111111 } else { 0b00000000 }])?;
self.content.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: std::io::Read,
{
let mut enabled = [0];
s.read_exact(&mut enabled)?;
Ok(Self {
enabled: enabled[0].count_ones() >= 4,
content: ToFromBytes::from_bytes(s)?,
})
}
}
impl ToFromBytes for QueueContent {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: std::io::Write,
{
match self {
Self::Song(id) => {
s.write_all(&[0b11111111])?;
id.to_bytes(s)?;
}
Self::Folder(index, contents, name) => {
s.write_all(&[0b00000000])?;
index.to_bytes(s)?;
contents.to_bytes(s)?;
name.to_bytes(s)?;
}
}
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: std::io::Read,
{
let mut switch_on = [0];
s.read_exact(&mut switch_on)?;
Ok(if switch_on[0].count_ones() > 4 {
Self::Song(ToFromBytes::from_bytes(s)?)
} else {
Self::Folder(
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
)
})
}
}

166
musicdb-lib/src/data/song.rs Executable file
View File

@ -0,0 +1,166 @@
use std::{
fmt::Display,
io::{Read, Write},
path::Path,
sync::{Arc, Mutex},
thread::JoinHandle,
};
use crate::load::ToFromBytes;
use super::{
database::Database, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
};
#[derive(Clone, Debug)]
pub struct Song {
pub id: SongId,
pub location: DatabaseLocation,
pub title: String,
pub album: Option<AlbumId>,
pub artist: Option<ArtistId>,
pub more_artists: Vec<ArtistId>,
pub cover: Option<CoverId>,
pub general: GeneralData,
/// None => No cached data
/// Some(Err) => No cached data yet, but a thread is working on loading it.
/// Some(Ok(data)) => Cached data is available.
pub cached_data: Arc<Mutex<Option<Result<Arc<Vec<u8>>, JoinHandle<Option<Arc<Vec<u8>>>>>>>>,
}
impl Song {
pub fn new(
location: DatabaseLocation,
title: String,
album: Option<AlbumId>,
artist: Option<ArtistId>,
more_artists: Vec<ArtistId>,
cover: Option<CoverId>,
) -> Self {
Self {
id: 0,
location,
title,
album,
artist,
more_artists,
cover,
general: GeneralData::default(),
cached_data: Arc::new(Mutex::new(None)),
}
}
pub fn uncache_data(&self) {
*self.cached_data.lock().unwrap() = None;
}
/// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
pub fn cache_data_start_thread(&self, db: &Database) -> bool {
let mut cd = self.cached_data.lock().unwrap();
let start_thread = match cd.as_ref() {
None => true,
Some(Err(_)) | Some(Ok(_)) => false,
};
if start_thread {
let path = db.get_path(&self.location);
*cd = Some(Err(std::thread::spawn(move || {
eprintln!("[info] thread started");
let data = Self::load_data(&path)?;
eprintln!("[info] thread stopping after loading {path:?}");
Some(Arc::new(data))
})));
true
} else {
false
}
}
/// Gets the cached data, if available.
/// If a thread is running to load the data, it is not awaited.
/// This function doesn't block.
pub fn cached_data(&self) -> Option<Arc<Vec<u8>>> {
if let Some(Ok(v)) = self.cached_data.lock().unwrap().as_ref() {
Some(Arc::clone(v))
} else {
None
}
}
/// Gets the cached data, if available.
/// If a thread is running to load the data, it *is* awaited.
/// This function will block until the data is loaded.
/// If it still returns none, some error must have occured.
pub fn cached_data_now(&self, db: &Database) -> Option<Arc<Vec<u8>>> {
let mut cd = self.cached_data.lock().unwrap();
*cd = match cd.take() {
None => {
if let Some(v) = Self::load_data(db.get_path(&self.location)) {
Some(Ok(Arc::new(v)))
} else {
None
}
}
Some(Err(t)) => match t.join() {
Err(_e) => None,
Ok(Some(v)) => Some(Ok(v)),
Ok(None) => None,
},
Some(Ok(v)) => Some(Ok(v)),
};
drop(cd);
self.cached_data()
}
fn load_data<P: AsRef<Path>>(path: P) -> Option<Vec<u8>> {
eprintln!("[info] loading song from {:?}", path.as_ref());
match std::fs::read(&path) {
Ok(v) => {
eprintln!("[info] loaded song from {:?}", path.as_ref());
Some(v)
}
Err(e) => {
eprintln!("[info] error loading {:?}: {e:?}", path.as_ref());
None
}
}
}
}
impl Display for Song {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title)?;
match (self.artist, self.album) {
(Some(artist), Some(album)) => write!(f, " (by {artist} on {album})")?,
(None, Some(album)) => write!(f, " (on {album})")?,
(Some(artist), None) => write!(f, " (by {artist})")?,
(None, None) => {}
}
Ok(())
}
}
impl ToFromBytes for Song {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.id.to_bytes(s)?;
self.location.to_bytes(s)?;
self.title.to_bytes(s)?;
self.album.to_bytes(s)?;
self.artist.to_bytes(s)?;
self.more_artists.to_bytes(s)?;
self.cover.to_bytes(s)?;
self.general.to_bytes(s)?;
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(Self {
id: ToFromBytes::from_bytes(s)?,
location: ToFromBytes::from_bytes(s)?,
title: ToFromBytes::from_bytes(s)?,
album: ToFromBytes::from_bytes(s)?,
artist: ToFromBytes::from_bytes(s)?,
more_artists: ToFromBytes::from_bytes(s)?,
cover: ToFromBytes::from_bytes(s)?,
general: ToFromBytes::from_bytes(s)?,
cached_data: Arc::new(Mutex::new(None)),
})
}
}

4
musicdb-lib/src/lib.rs Executable file
View File

@ -0,0 +1,4 @@
pub mod data;
pub mod load;
pub mod player;
pub mod server;

330
musicdb-lib/src/load/mod.rs Executable file
View File

@ -0,0 +1,330 @@
use std::{
collections::HashMap,
io::{Read, Write},
path::PathBuf,
};
pub trait ToFromBytes: Sized {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write;
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read;
fn to_bytes_vec(&self) -> Vec<u8> {
let mut b = Vec::new();
_ = self.to_bytes(&mut b);
b
}
}
// impl ToFromBytes
// common types (String, Vec, ...)
impl ToFromBytes for String {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.len().to_bytes(s)?;
s.write_all(self.as_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let len = ToFromBytes::from_bytes(s)?;
let mut buf = vec![0; len];
s.read_exact(&mut buf)?;
Ok(String::from_utf8_lossy(&buf).into_owned())
}
}
impl ToFromBytes for PathBuf {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.to_string_lossy().into_owned().to_bytes(s)
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(String::from_bytes(s)?.into())
}
}
impl<C> ToFromBytes for Vec<C>
where
C: ToFromBytes,
{
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.len().to_bytes(s)?;
for elem in self {
elem.to_bytes(s)?;
}
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let len = ToFromBytes::from_bytes(s)?;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
buf.push(ToFromBytes::from_bytes(s)?);
}
Ok(buf)
}
}
impl<A> ToFromBytes for Option<A>
where
A: ToFromBytes,
{
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
match self {
None => s.write_all(&[0b11001100]),
Some(v) => {
s.write_all(&[0b00111010])?;
v.to_bytes(s)
}
}
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0u8];
s.read_exact(&mut b)?;
match b[0] {
0b00111010 => Ok(Some(ToFromBytes::from_bytes(s)?)),
_ => Ok(None),
}
}
}
impl<K, V> ToFromBytes for HashMap<K, V>
where
K: ToFromBytes + std::cmp::Eq + std::hash::Hash,
V: ToFromBytes,
{
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
self.len().to_bytes(s)?;
for (key, val) in self.iter() {
key.to_bytes(s)?;
val.to_bytes(s)?;
}
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let len = ToFromBytes::from_bytes(s)?;
let mut o = Self::with_capacity(len);
for _ in 0..len {
o.insert(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?);
}
Ok(o)
}
}
// - for (i/u)(size/8/16/32/64/128)
impl ToFromBytes for usize {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
(*self as u64).to_bytes(s)
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(u64::from_bytes(s)? as _)
}
}
impl ToFromBytes for isize {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
(*self as i64).to_bytes(s)
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
Ok(i64::from_bytes(s)? as _)
}
}
impl ToFromBytes for u8 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&[*self])
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 1];
s.read_exact(&mut b)?;
Ok(b[0])
}
}
impl ToFromBytes for i8 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 1];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for u16 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 2];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for i16 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 2];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for u32 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 4];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for i32 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 4];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for u64 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 8];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for i64 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 8];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for u128 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 16];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}
impl ToFromBytes for i128 {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
s.write_all(&self.to_be_bytes())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: Read,
{
let mut b = [0; 16];
s.read_exact(&mut b)?;
Ok(Self::from_be_bytes(b))
}
}

160
musicdb-lib/src/player/mod.rs Executable file
View File

@ -0,0 +1,160 @@
use std::sync::Arc;
use awedio::{
backends::CpalBackend,
manager::Manager,
sounds::wrappers::{AsyncCompletionNotifier, Controller, Pausable},
Sound,
};
use rc_u8_reader::ArcU8Reader;
use crate::{
data::{database::Database, SongId},
server::Command,
};
pub struct Player {
/// can be unused, but must be present otherwise audio playback breaks
#[allow(unused)]
backend: CpalBackend,
source: Option<(
Controller<AsyncCompletionNotifier<Pausable<Box<dyn Sound>>>>,
tokio::sync::oneshot::Receiver<()>,
)>,
manager: Manager,
current_song_id: SongOpt,
}
pub enum SongOpt {
None,
Some(SongId),
/// Will be set to Some or None once handeled
New(Option<SongId>),
}
impl Player {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let (manager, backend) = awedio::start()?;
Ok(Self {
manager,
backend,
source: None,
current_song_id: SongOpt::None,
})
}
pub fn handle_command(&mut self, command: &Command) {
match command {
Command::Resume => self.resume(),
Command::Pause => self.pause(),
Command::Stop => self.stop(),
_ => {}
}
}
pub fn pause(&mut self) {
if let Some((source, _notif)) = &mut self.source {
source.set_paused(true);
}
}
pub fn resume(&mut self) {
if let Some((source, _notif)) = &mut self.source {
source.set_paused(false);
} else if let SongOpt::Some(id) = &self.current_song_id {
// there is no source to resume playback on, but there is a current song
self.current_song_id = SongOpt::New(Some(*id));
}
}
pub fn stop(&mut self) {
if let Some((source, _notif)) = &mut self.source {
source.set_paused(true);
}
self.current_song_id = SongOpt::New(None);
}
pub fn update(&mut self, db: &mut Database) {
if db.playing && self.source.is_none() {
if let Some(song) = db.queue.get_current_song() {
// db playing, but no source - initialize a source (via SongOpt::New)
self.current_song_id = SongOpt::New(Some(*song));
} else {
// db.playing, but no song in queue...
}
} else if let Some((_source, notif)) = &mut self.source {
if let Ok(()) = notif.try_recv() {
// song has finished playing
db.apply_command(Command::NextSong);
self.current_song_id = SongOpt::New(db.queue.get_current_song().cloned());
}
}
// check the queue's current index
if let SongOpt::None = self.current_song_id {
if let Some(id) = db.queue.get_current_song() {
self.current_song_id = SongOpt::New(Some(*id));
}
} else if let SongOpt::Some(l_id) = &self.current_song_id {
if let Some(id) = db.queue.get_current_song() {
if *id != *l_id {
self.current_song_id = SongOpt::New(Some(*id));
}
} else {
self.current_song_id = SongOpt::New(None);
}
}
// new current song
if let SongOpt::New(song_opt) = &self.current_song_id {
// stop playback
eprintln!("[play] stopping playback");
self.manager.clear();
if let Some(song_id) = song_opt {
if db.playing {
// start playback again
if let Some(song) = db.get_song(song_id) {
eprintln!("[play] starting playback...");
// add our song
let ext = match &song.location.rel_path.extension() {
Some(s) => s.to_str().unwrap_or(""),
None => "",
};
let (sound, notif) = Self::sound_from_bytes(
ext,
song.cached_data_now(db).expect("no cached data"),
)
.unwrap()
.pausable()
.with_async_completion_notifier();
// add it
let (sound, controller) = sound.controllable();
self.source = Some((controller, notif));
// and play it
self.manager.play(Box::new(sound));
eprintln!("[play] started playback");
} else {
panic!("invalid song ID: current_song_id not found in DB!");
}
}
self.current_song_id = SongOpt::Some(*song_id);
} else {
self.current_song_id = SongOpt::None;
}
if let Some(Some(song)) = db.queue.get_next_song().map(|v| db.get_song(v)) {
song.cache_data_start_thread(&db);
}
}
}
/// partly identical to awedio/src/sounds/open_file.rs open_file_with_reader(), which is a private function I can't access
fn sound_from_bytes(
extension: &str,
bytes: Arc<Vec<u8>>,
) -> Result<Box<dyn Sound>, std::io::Error> {
let reader = ArcU8Reader::new(bytes);
Ok(match extension {
"wav" => Box::new(
awedio::sounds::decoders::WavDecoder::new(reader)
.map_err(|_e| std::io::Error::from(std::io::ErrorKind::InvalidData))?,
),
"mp3" => Box::new(awedio::sounds::decoders::Mp3Decoder::new(reader)),
_ => return Err(std::io::Error::from(std::io::ErrorKind::Unsupported)),
})
}
}

265
musicdb-lib/src/server/mod.rs Executable file
View File

@ -0,0 +1,265 @@
use std::{
eprintln,
io::Write,
net::{SocketAddr, TcpListener},
path::PathBuf,
sync::{mpsc, Arc, Mutex},
thread::{self, JoinHandle},
time::Duration,
};
use crate::{
data::{
album::Album,
artist::Artist,
database::{Database, UpdateEndpoint},
queue::Queue,
song::Song,
},
load::ToFromBytes,
player::Player,
};
#[derive(Clone, Debug)]
pub enum Command {
Resume,
Pause,
Stop,
Save,
NextSong,
SyncDatabase(Vec<Artist>, Vec<Album>, Vec<Song>),
QueueUpdate(Vec<usize>, Queue),
QueueAdd(Vec<usize>, Queue),
QueueInsert(Vec<usize>, usize, Queue),
QueueRemove(Vec<usize>),
QueueGoto(Vec<usize>),
/// .id field is ignored!
AddSong(Song),
/// .id field is ignored!
AddAlbum(Album),
/// .id field is ignored!
AddArtist(Artist),
ModifySong(Song),
ModifyAlbum(Album),
ModifyArtist(Artist),
SetLibraryDirectory(PathBuf),
}
impl Command {
pub fn send_to_server(self, db: &Database) -> Result<(), Self> {
if let Some(sender) = &db.command_sender {
sender.send(self).unwrap();
Ok(())
} else {
Err(self)
}
}
pub fn send_to_server_or_apply(self, db: &mut Database) {
if let Some(sender) = &db.command_sender {
sender.send(self).unwrap();
} else {
db.apply_command(self);
}
}
}
/// starts handling database.command_sender events and optionally spawns a tcp server.
/// this function creates a new command_sender.
/// if you wish to implement your own server, set db.command_sender to None,
/// start a new thread running this function,
/// wait for db.command_sender to be Some,
/// then start your server.
/// for tcp-like protocols, you only need to
/// a) sync and register new connections using db.init_connection and db.update_endpoints.push
/// b) handle the decoding of messages using Command::from_bytes(), then send them to the db using db.command_sender.
/// for other protocols (like http + sse)
/// a) initialize new connections using db.init_connection() to synchronize the new client
/// b) handle the decoding of messages using Command::from_bytes()
/// c) re-encode all received messages using Command::to_bytes_vec(), send them to the db, and send them to all your clients.
pub fn run_server(
database: Arc<Mutex<Database>>,
addr_tcp: Option<SocketAddr>,
sender_sender: Option<tokio::sync::mpsc::Sender<mpsc::Sender<Command>>>,
) {
let mut player = Player::new().unwrap();
let (command_sender, command_receiver) = mpsc::channel();
if let Some(s) = sender_sender {
s.blocking_send(command_sender.clone()).unwrap();
}
database.lock().unwrap().command_sender = Some(command_sender.clone());
if let Some(addr) = addr_tcp {
match TcpListener::bind(addr) {
Ok(v) => {
let command_sender = command_sender.clone();
let db = Arc::clone(&database);
thread::spawn(move || loop {
if let Ok((mut connection, con_addr)) = v.accept() {
eprintln!("[info] TCP connection accepted from {con_addr}.");
let command_sender = command_sender.clone();
let db = Arc::clone(&db);
thread::spawn(move || {
// sync database
let mut db = db.lock().unwrap();
db.init_connection(&mut connection)?;
db.update_endpoints.push(UpdateEndpoint::Bytes(Box::new(
connection.try_clone().unwrap(),
)));
drop(db);
loop {
if let Ok(command) = Command::from_bytes(&mut connection) {
command_sender.send(command).unwrap();
} else {
break;
}
}
Ok::<(), std::io::Error>(())
});
}
});
}
Err(e) => {
eprintln!("[WARN] Couldn't start TCP listener: {e}");
}
}
}
let dur = Duration::from_secs_f32(0.1);
loop {
player.update(&mut database.lock().unwrap());
if let Ok(command) = command_receiver.recv_timeout(dur) {
player.handle_command(&command);
database.lock().unwrap().apply_command(command);
}
}
}
pub trait Connection: Sized + Send + 'static {
type SendError: Send;
fn send_command(&mut self, command: Command) -> Result<(), Self::SendError>;
fn receive_updates(&mut self) -> Result<Vec<Command>, Self::SendError>;
fn receive_update_blocking(&mut self) -> Result<Command, Self::SendError>;
fn move_to_thread<F: FnMut(&mut Self, Command) -> bool + Send + 'static>(
mut self,
mut handler: F,
) -> JoinHandle<Result<Self, Self::SendError>> {
std::thread::spawn(move || loop {
let update = self.receive_update_blocking()?;
if handler(&mut self, update) {
return Ok(self);
}
})
}
}
impl ToFromBytes for Command {
fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
where
T: Write,
{
match self {
Self::Resume => s.write_all(&[0b11000000])?,
Self::Pause => s.write_all(&[0b00110000])?,
Self::Stop => s.write_all(&[0b11110000])?,
Self::Save => s.write_all(&[0b11110011])?,
Self::NextSong => s.write_all(&[0b11110010])?,
Self::SyncDatabase(a, b, c) => {
s.write_all(&[0b01011000])?;
a.to_bytes(s)?;
b.to_bytes(s)?;
c.to_bytes(s)?;
}
Self::QueueUpdate(index, new_data) => {
s.write_all(&[0b00011100])?;
index.to_bytes(s)?;
new_data.to_bytes(s)?;
}
Self::QueueAdd(index, new_data) => {
s.write_all(&[0b00011010])?;
index.to_bytes(s)?;
new_data.to_bytes(s)?;
}
Self::QueueInsert(index, pos, new_data) => {
s.write_all(&[0b00011110])?;
index.to_bytes(s)?;
pos.to_bytes(s)?;
new_data.to_bytes(s)?;
}
Self::QueueRemove(index) => {
s.write_all(&[0b00011001])?;
index.to_bytes(s)?;
}
Self::QueueGoto(index) => {
s.write_all(&[0b00011011])?;
index.to_bytes(s)?;
}
Self::AddSong(song) => {
s.write_all(&[0b01010000])?;
song.to_bytes(s)?;
}
Self::AddAlbum(album) => {
s.write_all(&[0b01010011])?;
album.to_bytes(s)?;
}
Self::AddArtist(artist) => {
s.write_all(&[0b01011100])?;
artist.to_bytes(s)?;
}
Self::ModifySong(song) => {
s.write_all(&[0b10010000])?;
song.to_bytes(s)?;
}
Self::ModifyAlbum(album) => {
s.write_all(&[0b10010011])?;
album.to_bytes(s)?;
}
Self::ModifyArtist(artist) => {
s.write_all(&[0b10011100])?;
artist.to_bytes(s)?;
}
Self::SetLibraryDirectory(path) => {
s.write_all(&[0b00110001])?;
path.to_bytes(s)?;
}
}
Ok(())
}
fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
where
T: std::io::Read,
{
let mut kind = [0];
s.read_exact(&mut kind)?;
Ok(match kind[0] {
0b11000000 => Self::Resume,
0b00110000 => Self::Pause,
0b11110000 => Self::Stop,
0b11110011 => Self::Save,
0b11110010 => Self::NextSong,
0b01011000 => Self::SyncDatabase(
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
),
0b00011100 => {
Self::QueueUpdate(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?)
}
0b00011010 => Self::QueueAdd(ToFromBytes::from_bytes(s)?, ToFromBytes::from_bytes(s)?),
0b00011110 => Self::QueueInsert(
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
ToFromBytes::from_bytes(s)?,
),
0b00011001 => Self::QueueRemove(ToFromBytes::from_bytes(s)?),
0b00011011 => Self::QueueGoto(ToFromBytes::from_bytes(s)?),
0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
0b10011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
_ => {
eprintln!("unexpected byte when reading command; stopping playback.");
Self::Stop
}
})
}
}

34
musicdb-lib/src/test.rs Executable file
View File

@ -0,0 +1,34 @@
#![cfg(test)]
use std::{assert_eq, path::PathBuf};
use crate::load::ToFromBytes;
#[test]
fn string() {
for v in ["dskjh2d89dnas2d90", "aosu 89d 89a 89", "a/b/c/12"] {
let v = v.to_owned();
assert_eq!(v, String::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap());
let v = PathBuf::from(v);
assert_eq!(v, PathBuf::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap());
}
}
#[test]
fn vec() {
for v in [vec!["asdad".to_owned(), "dsnakf".to_owned()], vec![]] {
assert_eq!(
v,
Vec::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()
)
}
}
#[test]
fn option() {
for v in [None, Some("value".to_owned())] {
assert_eq!(
v,
Option::<String>::from_bytes(&mut &v.to_bytes_vec()[..]).unwrap()
)
}
}

1
musicdb-server/.gitignore vendored Executable file
View File

@ -0,0 +1 @@
/target

19
musicdb-server/Cargo.toml Executable file
View File

@ -0,0 +1,19 @@
[package]
name = "musicdb-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = { version = "0.6.19", features = ["headers"] }
futures = "0.3.28"
headers = "0.3.8"
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }
tokio-stream = "0.1.14"
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.4.0", features = ["fs", "trace"] }
trace = "0.1.7"

View File

@ -0,0 +1,3 @@
<h3>\:name</h3>
<button hx-post="/queue/add-album/\:id" hx-swap="none">Queue</button>
\:songs

View File

@ -0,0 +1 @@
<button hx-get="/album-view/\:id" hx-target="#album-view">\:name</button>

View File

@ -0,0 +1,2 @@
<h3>\:name</h3>
\:albums

View File

@ -0,0 +1,2 @@
<h3>Artists</h3>
\:artists

View File

@ -0,0 +1 @@
<button hx-get="/artist-view/\:id" hx-target="#artist-view">\:name</button>

View File

@ -0,0 +1,3 @@
<h2>Queue</h2>
<div>Now Playing: <b>\:currentTitle</b></div>
\:content

View File

@ -0,0 +1,10 @@
<div>
<small>&gt;&gt;</small>
<button hx-post="/queue/goto/\:path" hx-swap="none">&#x23F5;</button>
<small>\:name</small>
</div>
\:content
<div>
<small>&lt;&lt;</small>
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
</div>

View File

@ -0,0 +1,10 @@
<div>
<small>&gt;&gt;</small>
<button hx-post="/queue/goto/\:path" hx-swap="none">&#x23F5;</button>
<small><b>\:name</b></small>
</div>
\:content
<div>
<small>&lt;&lt;</small>
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
</div>

View File

@ -0,0 +1,5 @@
<div>
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
<button hx-post="/queue/goto/\:path" hx-swap="none">&#x23F5;</button>
\:title
</div>

View File

@ -0,0 +1,5 @@
<div>
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
<button hx-post="/queue/goto/\:path" hx-swap="none">&#x23F5;</button>
<b>\:title</b>
</div>

View File

@ -0,0 +1,24 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="https://unpkg.com/htmx.org@1.9.3"></script>
<title>MusicDb</title>
</head>
<body>
<div hx-sse="connect:/sse">
<div hx-sse="swap:playing">(loading)</div>
<button hx-post="/resume" hx-swap="none">&#x23F5;</button>
<button hx-post="/pause" hx-swap="none">&#x23F8;</button>
<button hx-post="/stop" hx-swap="none">&#x23F9;</button>
<button hx-post="/next" hx-swap="none">&#x23ED;</button>
<button hx-post="/queue/clear" hx-swap="none">-</button>
<div hx-sse="swap:queue">(loading)</div>
<div hx-sse="swap:artists">(loading)</div>
<div id="artist-view"></div>
<div id="album-view"></div>
</div>
</body>
</html>

View File

@ -0,0 +1 @@
<button hx-post="/queue/add-song/\:id" hx-swap="none">\:title</button>

184
musicdb-server/src/main.rs Executable file
View File

@ -0,0 +1,184 @@
mod web;
use std::{
path::PathBuf,
process::exit,
sync::{Arc, Mutex},
thread,
};
use musicdb_lib::server::{run_server, Command};
use musicdb_lib::data::database::Database;
/*
# Exit codes
0 => exited as requested by the user
1 => exit after printing help message
3 => error parsing cli arguments
10 => tried to start with a path that caused some io::Error
11 => tried to start with a path that does not exist (--init prevents this)
*/
#[tokio::main]
async fn main() {
let mut args = std::env::args().skip(1);
let mut tcp_addr = None;
let mut web_addr = None;
let mut lib_dir_for_init = None;
let database = if let Some(path_s) = args.next() {
loop {
if let Some(arg) = args.next() {
if arg.starts_with("--") {
match &arg[2..] {
"init" => {
if let Some(lib_dir) = args.next() {
lib_dir_for_init = Some(lib_dir);
} else {
eprintln!(
"[EXIT]
missing argument: --init <lib path>"
);
exit(3);
}
}
"tcp" => {
if let Some(addr) = args.next() {
if let Ok(addr) = addr.parse() {
tcp_addr = Some(addr)
} else {
eprintln!(
"[EXIT]
bad argument: --tcp <addr:port>: couldn't parse <addr:port>"
);
exit(3);
}
} else {
eprintln!(
"[EXIT]
missing argument: --tcp <addr:port>"
);
exit(3);
}
}
"web" => {
if let Some(addr) = args.next() {
if let Ok(addr) = addr.parse() {
web_addr = Some(addr)
} else {
eprintln!(
"[EXIT]
bad argument: --web <addr:port>: couldn't parse <addr:port>"
);
exit(3);
}
} else {
eprintln!(
"[EXIT]
missing argument: --web <addr:port>"
);
exit(3);
}
}
o => {
eprintln!(
"[EXIT]
Unknown long argument --{o}"
);
exit(3);
}
}
} else if arg.starts_with("-") {
match &arg[1..] {
o => {
eprintln!(
"[EXIT]
Unknown short argument -{o}"
);
exit(3);
}
}
} else {
eprintln!(
"[EXIT]
Argument didn't start with - or -- ({arg})."
);
exit(3);
}
} else {
break;
}
}
let path = PathBuf::from(&path_s);
match path.try_exists() {
Ok(exists) => {
if let Some(lib_directory) = lib_dir_for_init {
Database::new_empty(path, lib_directory.into())
} else if exists {
Database::load_database(path).unwrap()
} else {
eprintln!(
"[EXIT]
The provided path does not exist."
);
exit(11);
}
}
Err(e) => {
eprintln!(
"[EXIT]
Error getting information about the provided path '{path_s}': {e}"
);
exit(10);
}
}
} else {
eprintln!(
"[EXIT]
musicdb - help
musicdb <path to database file> <options> <options> <...>
options:
--init <lib directory>
--tcp <addr:port>
--web <addr:port>
this help was shown because no arguments were provided."
);
exit(1);
};
// database.add_song_new(Song::new(
// "Amaranthe/Manifest/02 Make It Better.mp3".into(),
// "Make It Better".to_owned(),
// None,
// None,
// vec![],
// None,
// ));
// let mut player = Player::new();
// eprintln!("[info] database.songs: {:?}", database.songs());
// database.save_database(Some("/tmp/dbfile".into())).unwrap();
// eprintln!("{}", database.get_song(&0).unwrap());
// database.queue.add_to_end(QueueContent::Song(1).into());
// player.update_and_restart_playing_song(&database);
let database = Arc::new(Mutex::new(database));
if tcp_addr.is_some() || web_addr.is_some() {
if let Some(addr) = web_addr {
let (s, mut r) = tokio::sync::mpsc::channel(2);
let db = Arc::clone(&database);
thread::spawn(move || run_server(database, tcp_addr, Some(s)));
if let Some(sender) = r.recv().await {
web::main(db, sender, addr).await;
}
} else {
run_server(database, tcp_addr, None);
}
} else {
eprintln!("nothing to do, not starting the server.");
}
// std::io::stdin().read_line(&mut String::new()).unwrap();
// dbg!(Update::from_bytes(&mut BufReader::new(
// TcpStream::connect("127.0.0.1:26314".parse::<SocketAddr>().unwrap()).unwrap()
// )));
}

573
musicdb-server/src/web.rs Normal file
View File

@ -0,0 +1,573 @@
use std::convert::Infallible;
use std::mem;
use std::net::SocketAddr;
use std::sync::{mpsc, Arc, Mutex};
use std::task::Poll;
use std::time::Duration;
use axum::extract::{Path, State};
use axum::response::sse::Event;
use axum::response::{Html, Sse};
use axum::routing::{get, post};
use axum::{Router, TypedHeader};
use futures::{stream, Stream};
use musicdb_lib::data::database::{Database, UpdateEndpoint};
use musicdb_lib::data::queue::{Queue, QueueContent};
use musicdb_lib::server::Command;
use tokio_stream::StreamExt as _;
/*
23E9 fast forward
23EA rewind, fast backwards
23EB fast increase
23EC fast decrease
23ED skip to end, next
23EE skip to start, previous
23EF play/pause toggle
23F1 stopwatch
23F2 timer clock
23F3 hourglass
23F4 reverse, back
23F5 forward, next, play
23F6 increase
23F7 decrease
23F8 pause
23F9 stop
23FA record
*/
#[derive(Clone)]
pub struct AppState {
db: Arc<Mutex<Database>>,
html: Arc<AppHtml>,
}
#[derive(Debug)]
pub struct AppHtml {
/// /
/// can use:
root: Vec<HtmlPart>,
/// sse:artists
/// can use: artists (0+ repeats of artists_one)
artists: Vec<HtmlPart>,
/// can use: id, name
artists_one: Vec<HtmlPart>,
/// /artist-view/:artist-id
/// can use: albums (0+ repeats of albums_one)
artist_view: Vec<HtmlPart>,
/// can use: name
albums_one: Vec<HtmlPart>,
/// /album-view/:album-id
/// can use: id, name, songs (0+ repeats of songs_one)
album_view: Vec<HtmlPart>,
/// can use: title
songs_one: Vec<HtmlPart>,
/// /queue
/// can use: currentTitle, nextTitle, content
queue: Vec<HtmlPart>,
/// can use: path, title
queue_song: Vec<HtmlPart>,
/// can use: path, title
queue_song_current: Vec<HtmlPart>,
/// can use: path, content, name
queue_folder: Vec<HtmlPart>,
/// can use: path, content, name
queue_folder_current: Vec<HtmlPart>,
}
impl AppHtml {
pub fn from_dir<P: AsRef<std::path::Path>>(dir: P) -> std::io::Result<Self> {
let dir = dir.as_ref();
Ok(Self {
root: Self::parse(&std::fs::read_to_string(dir.join("root.html"))?),
artists: Self::parse(&std::fs::read_to_string(dir.join("artists.html"))?),
artists_one: Self::parse(&std::fs::read_to_string(dir.join("artists_one.html"))?),
artist_view: Self::parse(&std::fs::read_to_string(dir.join("artist-view.html"))?),
albums_one: Self::parse(&std::fs::read_to_string(dir.join("albums_one.html"))?),
album_view: Self::parse(&std::fs::read_to_string(dir.join("album-view.html"))?),
songs_one: Self::parse(&std::fs::read_to_string(dir.join("songs_one.html"))?),
queue: Self::parse(&std::fs::read_to_string(dir.join("queue.html"))?),
queue_song: Self::parse(&std::fs::read_to_string(dir.join("queue_song.html"))?),
queue_song_current: Self::parse(&std::fs::read_to_string(
dir.join("queue_song_current.html"),
)?),
queue_folder: Self::parse(&std::fs::read_to_string(dir.join("queue_folder.html"))?),
queue_folder_current: Self::parse(&std::fs::read_to_string(
dir.join("queue_folder_current.html"),
)?),
})
}
pub fn parse(s: &str) -> Vec<HtmlPart> {
let mut o = Vec::new();
let mut c = String::new();
let mut chars = s.chars().peekable();
loop {
if let Some(ch) = chars.next() {
if ch == '\\' && chars.peek().is_some_and(|ch| *ch == ':') {
chars.next();
o.push(HtmlPart::Plain(mem::replace(&mut c, String::new())));
loop {
if let Some(ch) = chars.peek() {
if !ch.is_ascii_alphabetic() {
o.push(HtmlPart::Insert(mem::replace(&mut c, String::new())));
break;
} else {
c.push(*ch);
chars.next();
}
} else {
if c.len() > 0 {
o.push(HtmlPart::Insert(c));
}
return o;
}
}
} else {
c.push(ch);
}
} else {
if c.len() > 0 {
o.push(HtmlPart::Plain(c));
}
return o;
}
}
}
}
#[derive(Debug)]
pub enum HtmlPart {
/// text as plain html
Plain(String),
/// insert some value depending on context and key
Insert(String),
}
pub async fn main(db: Arc<Mutex<Database>>, sender: mpsc::Sender<Command>, addr: SocketAddr) {
let db1 = Arc::clone(&db);
let state = AppState {
db,
html: Arc::new(AppHtml::from_dir("assets").unwrap()),
};
let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = (
sender.clone(),
sender.clone(),
sender.clone(),
sender.clone(),
sender.clone(),
sender.clone(),
sender.clone(),
sender.clone(),
sender,
);
let state1 = state.clone();
let app = Router::new()
// root
.nest_service(
"/",
get(move || async move {
Html(
state1
.html
.root
.iter()
.map(|v| match v {
HtmlPart::Plain(v) => v,
HtmlPart::Insert(_) => "",
})
.collect::<String>(),
)
}),
)
// server-sent events
.route("/sse", get(sse_handler))
// inner views (embedded in root)
.route("/artist-view/:artist-id", get(artist_view_handler))
.route("/album-view/:album-id", get(album_view_handler))
// handle POST requests via the mpsc::Sender instead of locking the db.
.route(
"/pause",
post(move || async move {
_ = s1.send(Command::Pause);
}),
)
.route(
"/resume",
post(move || async move {
_ = s2.send(Command::Resume);
}),
)
.route(
"/stop",
post(move || async move {
_ = s3.send(Command::Stop);
}),
)
.route(
"/next",
post(move || async move {
_ = s4.send(Command::NextSong);
}),
)
.route(
"/queue/clear",
post(move || async move {
_ = s5.send(Command::QueueUpdate(
vec![],
QueueContent::Folder(0, vec![], String::new()).into(),
));
}),
)
.route(
"/queue/remove/:i",
post(move |Path(i): Path<String>| async move {
let mut ids = vec![];
for id in i.split('-') {
if let Ok(n) = id.parse() {
ids.push(n);
} else {
return;
}
}
_ = s8.send(Command::QueueRemove(ids));
}),
)
.route(
"/queue/goto/:i",
post(move |Path(i): Path<String>| async move {
let mut ids = vec![];
for id in i.split('-') {
if let Ok(n) = id.parse() {
ids.push(n);
} else {
return;
}
}
_ = s9.send(Command::QueueGoto(ids));
}),
)
.route(
"/queue/add-song/:song-id",
post(move |Path(song_id)| async move {
_ = s6.send(Command::QueueAdd(
vec![],
QueueContent::Song(song_id).into(),
));
}),
)
.route(
"/queue/add-album/:album-id",
post(move |Path(album_id)| async move {
if let Some(album) = db1.lock().unwrap().albums().get(&album_id) {
_ = s7.send(Command::QueueAdd(
vec![],
QueueContent::Folder(
0,
album
.songs
.iter()
.map(|id| QueueContent::Song(*id).into())
.collect(),
album.name.clone(),
)
.into(),
));
}
}),
)
.with_state(state);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn sse_handler(
TypedHeader(user_agent): TypedHeader<headers::UserAgent>,
State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
println!("`{}` connected", user_agent.as_str());
let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
let mut db = state.db.lock().unwrap();
_ = sender.send(Arc::new(Command::SyncDatabase(vec![], vec![], vec![])));
_ = sender.send(Arc::new(Command::NextSong));
_ = sender.send(Arc::new(if db.playing {
Command::Resume
} else {
Command::Pause
}));
db.update_endpoints
.push(UpdateEndpoint::CmdChannelTokio(sender));
drop(db);
let stream = stream::poll_fn(move |_ctx| {
if let Ok(cmd) = receiver.try_recv() {
Poll::Ready(Some(match cmd.as_ref() {
Command::Resume => Event::default().event("playing").data("playing"),
Command::Pause => Event::default().event("playing").data("paused"),
Command::Stop => Event::default().event("playing").data("stopped"),
Command::SyncDatabase(..)
| Command::ModifySong(..)
| Command::ModifyAlbum(..)
| Command::ModifyArtist(..)
| Command::AddSong(..)
| Command::AddAlbum(..)
| Command::AddArtist(..) => Event::default().event("artists").data({
let db = state.db.lock().unwrap();
let mut a = db.artists().iter().collect::<Vec<_>>();
a.sort_unstable_by_key(|(_id, artist)| &artist.name);
let mut artists = String::new();
for (id, artist) in a {
for v in &state.html.artists_one {
match v {
HtmlPart::Plain(v) => artists.push_str(v),
HtmlPart::Insert(key) => match key.as_str() {
"id" => artists.push_str(&id.to_string()),
"name" => artists.push_str(&artist.name),
_ => {}
},
}
}
}
state
.html
.artists
.iter()
.map(|v| match v {
HtmlPart::Plain(v) => v,
HtmlPart::Insert(key) => match key.as_str() {
"artists" => &artists,
_ => "",
},
})
.collect::<String>()
}),
Command::NextSong
| Command::QueueUpdate(..)
| Command::QueueAdd(..)
| Command::QueueInsert(..)
| Command::QueueRemove(..)
| Command::QueueGoto(..) => {
let db = state.db.lock().unwrap();
let current = db
.queue
.get_current_song()
.map_or(None, |id| db.songs().get(id));
let next = db
.queue
.get_next_song()
.map_or(None, |id| db.songs().get(id));
let mut content = String::new();
build_queue_content_build(
&db,
&state,
&mut content,
&db.queue,
String::new(),
true,
);
Event::default().event("queue").data(
state
.html
.queue
.iter()
.map(|v| match v {
HtmlPart::Plain(v) => v,
HtmlPart::Insert(key) => match key.as_str() {
"currentTitle" => {
if let Some(s) = current {
&s.title
} else {
""
}
}
"nextTitle" => {
if let Some(s) = next {
&s.title
} else {
""
}
}
"content" => &content,
_ => "",
},
})
.collect::<String>(),
)
}
Command::Save | Command::SetLibraryDirectory(_) => return Poll::Pending,
}))
} else {
return Poll::Pending;
}
})
.map(Ok);
// .throttle(Duration::from_millis(100));
Sse::new(stream)
.keep_alive(axum::response::sse::KeepAlive::new().interval(Duration::from_millis(250)))
}
async fn artist_view_handler(
State(state): State<AppState>,
Path(artist_id): Path<u64>,
) -> Html<String> {
let db = state.db.lock().unwrap();
if let Some(artist) = db.artists().get(&artist_id) {
let mut albums = String::new();
for id in artist.albums.iter() {
if let Some(album) = db.albums().get(id) {
for v in &state.html.albums_one {
match v {
HtmlPart::Plain(v) => albums.push_str(v),
HtmlPart::Insert(key) => match key.as_str() {
"id" => albums.push_str(&id.to_string()),
"name" => albums.push_str(&album.name),
_ => {}
},
}
}
}
}
let id = artist_id.to_string();
Html(
state
.html
.artist_view
.iter()
.map(|v| match v {
HtmlPart::Plain(v) => v,
HtmlPart::Insert(key) => match key.as_str() {
"id" => &id,
"name" => &artist.name,
"albums" => &albums,
_ => "",
},
})
.collect(),
)
} else {
Html(format!(
"<h1>Bad ID</h1><p>There is no artist with the id {artist_id} in the database</p>"
))
}
}
async fn album_view_handler(
State(state): State<AppState>,
Path(album_id): Path<u64>,
) -> Html<String> {
let db = state.db.lock().unwrap();
if let Some(album) = db.albums().get(&album_id) {
let mut songs = String::new();
for id in album.songs.iter() {
if let Some(song) = db.songs().get(id) {
for v in &state.html.songs_one {
match v {
HtmlPart::Plain(v) => songs.push_str(v),
HtmlPart::Insert(key) => match key.as_str() {
"id" => songs.push_str(&id.to_string()),
"title" => songs.push_str(&song.title),
_ => {}
},
}
}
}
}
let id = album_id.to_string();
Html(
state
.html
.album_view
.iter()
.map(|v| match v {
HtmlPart::Plain(v) => v,
HtmlPart::Insert(key) => match key.as_str() {
"id" => &id,
"name" => &album.name,
"songs" => &songs,
_ => "",
},
})
.collect(),
)
} else {
Html(format!(
"<h1>Bad ID</h1><p>There is no album with the id {album_id} in the database</p>"
))
}
}
fn build_queue_content_build(
db: &Database,
state: &AppState,
html: &mut String,
queue: &Queue,
path: String,
current: bool,
) {
// TODO: Do something for disabled ones too (they shouldn't just be hidden)
if queue.enabled() {
match queue.content() {
QueueContent::Song(id) => {
if let Some(song) = db.songs().get(id) {
for v in if current {
&state.html.queue_song_current
} else {
&state.html.queue_song
} {
match v {
HtmlPart::Plain(v) => html.push_str(v),
HtmlPart::Insert(key) => match key.as_str() {
"path" => html.push_str(&path),
"title" => html.push_str(&song.title),
_ => {}
},
}
}
}
}
QueueContent::Folder(ci, c, name) => {
if path.is_empty() {
for (i, c) in c.iter().enumerate() {
let current = current && *ci == i;
build_queue_content_build(db, state, html, c, i.to_string(), current)
}
} else {
for v in if current {
&state.html.queue_folder_current
} else {
&state.html.queue_folder
} {
match v {
HtmlPart::Plain(v) => html.push_str(v),
HtmlPart::Insert(key) => match key.as_str() {
"path" => html.push_str(&path),
"name" => html.push_str(name),
"content" => {
for (i, c) in c.iter().enumerate() {
let current = current && *ci == i;
build_queue_content_build(
db,
state,
html,
c,
format!("{path}-{i}"),
current,
)
}
}
_ => {}
},
}
}
}
}
}
}
}