mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 05:43:53 +01:00
init
This commit is contained in:
commit
922e4fcc00
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*/Cargo.lock
|
||||
*/target
|
31
README.md
Normal file
31
README.md
Normal 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
1
musicdb-client/.gitignore
vendored
Executable file
@ -0,0 +1 @@
|
||||
/target
|
14
musicdb-client/Cargo.toml
Executable file
14
musicdb-client/Cargo.toml
Executable 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
880
musicdb-client/src/gui.rs
Executable 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
459
musicdb-client/src/gui_base.rs
Executable 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
454
musicdb-client/src/gui_library.rs
Executable 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![]
|
||||
}
|
||||
}
|
||||
}
|
238
musicdb-client/src/gui_playback.rs
Executable file
238
musicdb-client/src/gui_playback.rs
Executable 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
689
musicdb-client/src/gui_queue.rs
Executable 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
290
musicdb-client/src/gui_screen.rs
Executable 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
316
musicdb-client/src/gui_settings.rs
Normal file
316
musicdb-client/src/gui_settings.rs
Normal 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
226
musicdb-client/src/gui_text.rs
Executable 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![]
|
||||
}
|
||||
}
|
147
musicdb-client/src/gui_wrappers.rs
Executable file
147
musicdb-client/src/gui_wrappers.rs
Executable 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
436
musicdb-client/src/main.rs
Executable 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
2
musicdb-lib/.gitignore
vendored
Executable file
@ -0,0 +1,2 @@
|
||||
/target
|
||||
/Cargo.lock
|
12
musicdb-lib/Cargo.toml
Executable file
12
musicdb-lib/Cargo.toml
Executable 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
43
musicdb-lib/src/data/album.rs
Executable 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
43
musicdb-lib/src/data/artist.rs
Executable 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
377
musicdb-lib/src/data/database.rs
Executable 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
73
musicdb-lib/src/data/mod.rs
Executable 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
286
musicdb-lib/src/data/queue.rs
Executable 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
166
musicdb-lib/src/data/song.rs
Executable 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
4
musicdb-lib/src/lib.rs
Executable 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
330
musicdb-lib/src/load/mod.rs
Executable 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
160
musicdb-lib/src/player/mod.rs
Executable 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
265
musicdb-lib/src/server/mod.rs
Executable 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
34
musicdb-lib/src/test.rs
Executable 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
1
musicdb-server/.gitignore
vendored
Executable file
@ -0,0 +1 @@
|
||||
/target
|
19
musicdb-server/Cargo.toml
Executable file
19
musicdb-server/Cargo.toml
Executable 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"
|
3
musicdb-server/assets/album-view.html
Normal file
3
musicdb-server/assets/album-view.html
Normal file
@ -0,0 +1,3 @@
|
||||
<h3>\:name</h3>
|
||||
<button hx-post="/queue/add-album/\:id" hx-swap="none">Queue</button>
|
||||
\:songs
|
1
musicdb-server/assets/albums_one.html
Normal file
1
musicdb-server/assets/albums_one.html
Normal file
@ -0,0 +1 @@
|
||||
<button hx-get="/album-view/\:id" hx-target="#album-view">\:name</button>
|
2
musicdb-server/assets/artist-view.html
Normal file
2
musicdb-server/assets/artist-view.html
Normal file
@ -0,0 +1,2 @@
|
||||
<h3>\:name</h3>
|
||||
\:albums
|
2
musicdb-server/assets/artists.html
Normal file
2
musicdb-server/assets/artists.html
Normal file
@ -0,0 +1,2 @@
|
||||
<h3>Artists</h3>
|
||||
\:artists
|
1
musicdb-server/assets/artists_one.html
Normal file
1
musicdb-server/assets/artists_one.html
Normal file
@ -0,0 +1 @@
|
||||
<button hx-get="/artist-view/\:id" hx-target="#artist-view">\:name</button>
|
3
musicdb-server/assets/queue.html
Normal file
3
musicdb-server/assets/queue.html
Normal file
@ -0,0 +1,3 @@
|
||||
<h2>Queue</h2>
|
||||
<div>Now Playing: <b>\:currentTitle</b></div>
|
||||
\:content
|
10
musicdb-server/assets/queue_folder.html
Normal file
10
musicdb-server/assets/queue_folder.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<small>>></small>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
<small>\:name</small>
|
||||
</div>
|
||||
\:content
|
||||
<div>
|
||||
<small><<</small>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
</div>
|
10
musicdb-server/assets/queue_folder_current.html
Normal file
10
musicdb-server/assets/queue_folder_current.html
Normal file
@ -0,0 +1,10 @@
|
||||
<div>
|
||||
<small>>></small>
|
||||
<button hx-post="/queue/goto/\:path" hx-swap="none">⏵</button>
|
||||
<small><b>\:name</b></small>
|
||||
</div>
|
||||
\:content
|
||||
<div>
|
||||
<small><<</small>
|
||||
<button hx-post="/queue/remove/\:path" hx-swap="none">x</button>
|
||||
</div>
|
5
musicdb-server/assets/queue_song.html
Normal file
5
musicdb-server/assets/queue_song.html
Normal 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">⏵</button>
|
||||
\:title
|
||||
</div>
|
5
musicdb-server/assets/queue_song_current.html
Normal file
5
musicdb-server/assets/queue_song_current.html
Normal 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">⏵</button>
|
||||
<b>\:title</b>
|
||||
</div>
|
24
musicdb-server/assets/root.html
Normal file
24
musicdb-server/assets/root.html
Normal 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">⏵</button>
|
||||
<button hx-post="/pause" hx-swap="none">⏸</button>
|
||||
<button hx-post="/stop" hx-swap="none">⏹</button>
|
||||
<button hx-post="/next" hx-swap="none">⏭</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>
|
1
musicdb-server/assets/songs_one.html
Normal file
1
musicdb-server/assets/songs_one.html
Normal file
@ -0,0 +1 @@
|
||||
<button hx-post="/queue/add-song/\:id" hx-swap="none">\:title</button>
|
184
musicdb-server/src/main.rs
Executable file
184
musicdb-server/src/main.rs
Executable 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
573
musicdb-server/src/web.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user