mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 14:13:53 +01:00
.
This commit is contained in:
parent
0e5e33367d
commit
ac16628c31
@ -6,6 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
directories = "5.0.1"
|
||||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||||
regex = "1.9.3"
|
regex = "1.9.3"
|
||||||
speedy2d = { version = "1.12.0", optional = true }
|
speedy2d = { version = "1.12.0", optional = true }
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
collections::HashMap,
|
||||||
eprintln,
|
eprintln,
|
||||||
io::{Read, Write},
|
io::Cursor,
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
thread::JoinHandle,
|
||||||
time::Instant,
|
time::Instant,
|
||||||
usize,
|
usize,
|
||||||
};
|
};
|
||||||
|
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
|
data::{database::Database, queue::Queue, AlbumId, ArtistId, CoverId, SongId},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
server::{get, Command},
|
server::{get, Command},
|
||||||
};
|
};
|
||||||
@ -17,6 +19,7 @@ use speedy2d::{
|
|||||||
color::Color,
|
color::Color,
|
||||||
dimen::{UVec2, Vec2},
|
dimen::{UVec2, Vec2},
|
||||||
font::Font,
|
font::Font,
|
||||||
|
image::ImageHandle,
|
||||||
shape::Rectangle,
|
shape::Rectangle,
|
||||||
window::{
|
window::{
|
||||||
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
|
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
|
||||||
@ -34,10 +37,10 @@ pub enum GuiEvent {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main<T: Write + Read + 'static + Sync + Send>(
|
pub fn main(
|
||||||
database: Arc<Mutex<Database>>,
|
database: Arc<Mutex<Database>>,
|
||||||
connection: TcpStream,
|
connection: TcpStream,
|
||||||
get_con: get::Client<T>,
|
get_con: get::Client<TcpStream>,
|
||||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||||
) {
|
) {
|
||||||
let mut config_file = super::get_config_file_path();
|
let mut config_file = super::get_config_file_path();
|
||||||
@ -104,7 +107,10 @@ pub fn main<T: Write + Read + 'static + Sync + Send>(
|
|||||||
|
|
||||||
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
|
let window = speedy2d::Window::<GuiEvent>::new_with_user_events(
|
||||||
"MusicDB Client",
|
"MusicDB Client",
|
||||||
WindowCreationOptions::new_fullscreen_borderless(),
|
WindowCreationOptions::new_windowed(
|
||||||
|
speedy2d::window::WindowSize::MarginPhysicalPixels(0),
|
||||||
|
None,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.expect("couldn't open window");
|
.expect("couldn't open window");
|
||||||
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
|
*event_sender_arc.lock().unwrap() = Some(window.create_user_event_sender());
|
||||||
@ -113,7 +119,7 @@ pub fn main<T: Write + Read + 'static + Sync + Send>(
|
|||||||
font,
|
font,
|
||||||
database,
|
database,
|
||||||
connection,
|
connection,
|
||||||
get_con,
|
Arc::new(Mutex::new(get_con)),
|
||||||
event_sender_arc,
|
event_sender_arc,
|
||||||
sender,
|
sender,
|
||||||
line_height,
|
line_height,
|
||||||
@ -127,10 +133,12 @@ pub struct Gui {
|
|||||||
pub event_sender: UserEventSender<GuiEvent>,
|
pub event_sender: UserEventSender<GuiEvent>,
|
||||||
pub database: Arc<Mutex<Database>>,
|
pub database: Arc<Mutex<Database>>,
|
||||||
pub connection: TcpStream,
|
pub connection: TcpStream,
|
||||||
|
pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||||
pub gui: GuiElem,
|
pub gui: GuiElem,
|
||||||
pub size: UVec2,
|
pub size: UVec2,
|
||||||
pub mouse_pos: Vec2,
|
pub mouse_pos: Vec2,
|
||||||
pub font: Font,
|
pub font: Font,
|
||||||
|
pub covers: Option<HashMap<CoverId, GuiCover>>,
|
||||||
pub last_draw: Instant,
|
pub last_draw: Instant,
|
||||||
pub modifiers: ModifiersState,
|
pub modifiers: ModifiersState,
|
||||||
pub dragging: Option<(
|
pub dragging: Option<(
|
||||||
@ -144,11 +152,11 @@ pub struct Gui {
|
|||||||
pub scroll_pages_multiplier: f64,
|
pub scroll_pages_multiplier: f64,
|
||||||
}
|
}
|
||||||
impl Gui {
|
impl Gui {
|
||||||
fn new<T: Read + Write + 'static + Sync + Send>(
|
fn new(
|
||||||
font: Font,
|
font: Font,
|
||||||
database: Arc<Mutex<Database>>,
|
database: Arc<Mutex<Database>>,
|
||||||
connection: TcpStream,
|
connection: TcpStream,
|
||||||
get_con: get::Client<T>,
|
get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||||
event_sender: UserEventSender<GuiEvent>,
|
event_sender: UserEventSender<GuiEvent>,
|
||||||
line_height: f32,
|
line_height: f32,
|
||||||
@ -192,11 +200,11 @@ impl Gui {
|
|||||||
event_sender,
|
event_sender,
|
||||||
database,
|
database,
|
||||||
connection,
|
connection,
|
||||||
|
get_con,
|
||||||
gui: GuiElem::new(WithFocusHotkey::new_noshift(
|
gui: GuiElem::new(WithFocusHotkey::new_noshift(
|
||||||
VirtualKeyCode::Escape,
|
VirtualKeyCode::Escape,
|
||||||
GuiScreen::new(
|
GuiScreen::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
get_con,
|
|
||||||
line_height,
|
line_height,
|
||||||
scroll_pixels_multiplier,
|
scroll_pixels_multiplier,
|
||||||
scroll_lines_multiplier,
|
scroll_lines_multiplier,
|
||||||
@ -206,6 +214,7 @@ impl Gui {
|
|||||||
size: UVec2::ZERO,
|
size: UVec2::ZERO,
|
||||||
mouse_pos: Vec2::ZERO,
|
mouse_pos: Vec2::ZERO,
|
||||||
font,
|
font,
|
||||||
|
covers: Some(HashMap::new()),
|
||||||
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
|
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
|
||||||
last_draw: Instant::now(),
|
last_draw: Instant::now(),
|
||||||
modifiers: ModifiersState::default(),
|
modifiers: ModifiersState::default(),
|
||||||
@ -226,8 +235,12 @@ pub trait GuiElemTrait {
|
|||||||
fn config(&self) -> &GuiElemCfg;
|
fn config(&self) -> &GuiElemCfg;
|
||||||
fn config_mut(&mut self) -> &mut 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.
|
/// 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.
|
/// if you wish to add a "high priority" child to a Vec<GuiElem> using push, .rev() the iterator in this method and change draw_rev to false.
|
||||||
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_>;
|
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_>;
|
||||||
|
/// defaults to true.
|
||||||
|
fn draw_rev(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
fn any(&self) -> &dyn Any;
|
fn any(&self) -> &dyn Any;
|
||||||
fn any_mut(&mut self) -> &mut dyn Any;
|
fn any_mut(&mut self) -> &mut dyn Any;
|
||||||
fn clone_gui(&self) -> Box<dyn GuiElemTrait>;
|
fn clone_gui(&self) -> Box<dyn GuiElemTrait>;
|
||||||
@ -381,6 +394,7 @@ pub enum GuiAction {
|
|||||||
)>,
|
)>,
|
||||||
),
|
),
|
||||||
SetLineHeight(f32),
|
SetLineHeight(f32),
|
||||||
|
LoadCover(CoverId),
|
||||||
/// Run a custom closure with mutable access to the Gui struct
|
/// Run a custom closure with mutable access to the Gui struct
|
||||||
Do(Box<dyn FnMut(&mut Gui)>),
|
Do(Box<dyn FnMut(&mut Gui)>),
|
||||||
Exit,
|
Exit,
|
||||||
@ -403,6 +417,8 @@ pub struct DrawInfo<'a> {
|
|||||||
/// compare this to `pos` to find the mouse's relative position.
|
/// compare this to `pos` to find the mouse's relative position.
|
||||||
pub mouse_pos: Vec2,
|
pub mouse_pos: Vec2,
|
||||||
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
|
pub helper: Option<&'a mut WindowHelper<GuiEvent>>,
|
||||||
|
pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||||
|
pub covers: &'a mut HashMap<CoverId, GuiCover>,
|
||||||
pub has_keyboard_focus: bool,
|
pub has_keyboard_focus: bool,
|
||||||
pub child_has_keyboard_focus: bool,
|
pub child_has_keyboard_focus: bool,
|
||||||
/// the height of one line of text (in pixels)
|
/// the height of one line of text (in pixels)
|
||||||
@ -456,6 +472,7 @@ impl GuiElem {
|
|||||||
let focus_path = info.child_has_keyboard_focus;
|
let focus_path = info.child_has_keyboard_focus;
|
||||||
// children (in reverse order - first element has the highest priority)
|
// children (in reverse order - first element has the highest priority)
|
||||||
let kbd_focus_index = self.inner.config().keyboard_focus_index;
|
let kbd_focus_index = self.inner.config().keyboard_focus_index;
|
||||||
|
if self.inner.draw_rev() {
|
||||||
for (i, c) in self
|
for (i, c) in self
|
||||||
.inner
|
.inner
|
||||||
.children()
|
.children()
|
||||||
@ -467,6 +484,12 @@ impl GuiElem {
|
|||||||
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
|
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
|
||||||
c.draw(info, g);
|
c.draw(info, g);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (i, c) in self.inner.children().enumerate() {
|
||||||
|
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
|
||||||
|
c.draw(info, g);
|
||||||
|
}
|
||||||
|
}
|
||||||
// reset pt. 2
|
// reset pt. 2
|
||||||
info.child_has_keyboard_focus = focus_path;
|
info.child_has_keyboard_focus = focus_path;
|
||||||
self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos);
|
self.inner.config_mut().pixel_pos = std::mem::replace(&mut info.pos, ppos);
|
||||||
@ -697,6 +720,12 @@ impl Gui {
|
|||||||
self.gui
|
self.gui
|
||||||
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
|
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
|
||||||
}
|
}
|
||||||
|
GuiAction::LoadCover(id) => {
|
||||||
|
self.covers
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.insert(id, GuiCover::new(id, Arc::clone(&self.get_con)));
|
||||||
|
}
|
||||||
GuiAction::Do(mut f) => f(self),
|
GuiAction::Do(mut f) => f(self),
|
||||||
GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
|
GuiAction::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
|
||||||
GuiAction::SetIdle(v) => {
|
GuiAction::SetIdle(v) => {
|
||||||
@ -784,12 +813,15 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
Color::BLACK,
|
Color::BLACK,
|
||||||
);
|
);
|
||||||
let mut dblock = self.database.lock().unwrap();
|
let mut dblock = self.database.lock().unwrap();
|
||||||
|
let mut covers = self.covers.take().unwrap();
|
||||||
let mut info = DrawInfo {
|
let mut info = DrawInfo {
|
||||||
actions: Vec::with_capacity(0),
|
actions: Vec::with_capacity(0),
|
||||||
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
|
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
|
||||||
database: &mut *dblock,
|
database: &mut *dblock,
|
||||||
font: &self.font,
|
font: &self.font,
|
||||||
mouse_pos: self.mouse_pos,
|
mouse_pos: self.mouse_pos,
|
||||||
|
get_con: Arc::clone(&self.get_con),
|
||||||
|
covers: &mut covers,
|
||||||
helper: Some(helper),
|
helper: Some(helper),
|
||||||
has_keyboard_focus: false,
|
has_keyboard_focus: false,
|
||||||
child_has_keyboard_focus: true,
|
child_has_keyboard_focus: true,
|
||||||
@ -827,7 +859,9 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// cleanup
|
||||||
drop(info);
|
drop(info);
|
||||||
|
self.covers = Some(covers);
|
||||||
drop(dblock);
|
drop(dblock);
|
||||||
for a in actions {
|
for a in actions {
|
||||||
self.exec_gui_action(a);
|
self.exec_gui_action(a);
|
||||||
@ -1013,3 +1047,62 @@ impl WindowHandler<GuiEvent> for Gui {
|
|||||||
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
|
.recursive_all(&mut |e| e.inner.config_mut().redraw = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum GuiCover {
|
||||||
|
Loading(JoinHandle<Option<Vec<u8>>>),
|
||||||
|
Loaded(ImageHandle),
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
impl GuiCover {
|
||||||
|
pub fn new(id: CoverId, get_con: Arc<Mutex<get::Client<TcpStream>>>) -> Self {
|
||||||
|
Self::Loading(std::thread::spawn(move || {
|
||||||
|
get_con
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.cover_bytes(id)
|
||||||
|
.ok()
|
||||||
|
.and_then(|v| v.ok())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
pub fn get(&self) -> Option<ImageHandle> {
|
||||||
|
match self {
|
||||||
|
Self::Loaded(handle) => Some(handle.clone()),
|
||||||
|
Self::Loading(_) | Self::Error => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_init(&mut self, g: &mut Graphics2D) -> Option<ImageHandle> {
|
||||||
|
match self {
|
||||||
|
Self::Loaded(handle) => Some(handle.clone()),
|
||||||
|
Self::Error => None,
|
||||||
|
Self::Loading(t) => {
|
||||||
|
if t.is_finished() {
|
||||||
|
let s = std::mem::replace(self, Self::Error);
|
||||||
|
if let Self::Loading(t) = s {
|
||||||
|
match t.join().unwrap() {
|
||||||
|
Some(bytes) => match g.create_image_from_file_bytes(
|
||||||
|
None,
|
||||||
|
speedy2d::image::ImageSmoothingMode::Linear,
|
||||||
|
Cursor::new(bytes),
|
||||||
|
) {
|
||||||
|
Ok(handle) => {
|
||||||
|
*self = Self::Loaded(handle.clone());
|
||||||
|
Some(handle)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[info] couldn't load cover from bytes: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*self = s;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -123,6 +123,9 @@ pub struct ScrollBox {
|
|||||||
/// 0.max(height_bottom - 1)
|
/// 0.max(height_bottom - 1)
|
||||||
max_scroll: f32,
|
max_scroll: f32,
|
||||||
last_height_px: f32,
|
last_height_px: f32,
|
||||||
|
mouse_in_scrollbar: bool,
|
||||||
|
mouse_scrolling: bool,
|
||||||
|
mouse_scroll_margin_right: f32,
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ScrollBoxSizeUnit {
|
pub enum ScrollBoxSizeUnit {
|
||||||
@ -131,13 +134,13 @@ pub enum ScrollBoxSizeUnit {
|
|||||||
}
|
}
|
||||||
impl ScrollBox {
|
impl ScrollBox {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mut config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
size_unit: ScrollBoxSizeUnit,
|
size_unit: ScrollBoxSizeUnit,
|
||||||
children: Vec<(GuiElem, f32)>,
|
children: Vec<(GuiElem, f32)>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// config.redraw = true;
|
// config.redraw = true;
|
||||||
Self {
|
Self {
|
||||||
config: config.w_scroll(),
|
config: config.w_scroll().w_mouse(),
|
||||||
children,
|
children,
|
||||||
size_unit,
|
size_unit,
|
||||||
scroll_target: 0.0,
|
scroll_target: 0.0,
|
||||||
@ -146,6 +149,9 @@ impl ScrollBox {
|
|||||||
height_bottom: 0.0,
|
height_bottom: 0.0,
|
||||||
max_scroll: 0.0,
|
max_scroll: 0.0,
|
||||||
last_height_px: 0.0,
|
last_height_px: 0.0,
|
||||||
|
mouse_in_scrollbar: false,
|
||||||
|
mouse_scrolling: false,
|
||||||
|
mouse_scroll_margin_right: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,12 +166,14 @@ impl GuiElemTrait for ScrollBox {
|
|||||||
Box::new(
|
Box::new(
|
||||||
self.children
|
self.children
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.rev()
|
|
||||||
.map(|(v, _)| v)
|
.map(|(v, _)| v)
|
||||||
.skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0)
|
.skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0)
|
||||||
.take_while(|v| v.inner.config().pos.top_left().y < 1.0),
|
.take_while(|v| v.inner.config().pos.top_left().y <= 1.0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn draw_rev(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn any(&self) -> &dyn std::any::Any {
|
fn any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -196,6 +204,8 @@ impl GuiElemTrait for ScrollBox {
|
|||||||
}
|
}
|
||||||
// recalculate positions
|
// recalculate positions
|
||||||
if self.config.redraw {
|
if self.config.redraw {
|
||||||
|
self.mouse_scroll_margin_right = info.line_height * 0.2;
|
||||||
|
let max_x = 1.0 - self.mouse_scroll_margin_right / info.pos.width();
|
||||||
self.config.redraw = false;
|
self.config.redraw = false;
|
||||||
let mut y_pos = -self.scroll_display;
|
let mut y_pos = -self.scroll_display;
|
||||||
for (e, h) in self.children.iter_mut() {
|
for (e, h) in self.children.iter_mut() {
|
||||||
@ -206,7 +216,10 @@ impl GuiElemTrait for ScrollBox {
|
|||||||
cfg.enabled = true;
|
cfg.enabled = true;
|
||||||
cfg.pos = Rectangle::new(
|
cfg.pos = Rectangle::new(
|
||||||
Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)),
|
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)),
|
Vec2::new(
|
||||||
|
cfg.pos.bottom_right().x.min(max_x),
|
||||||
|
1.0f32.min(y_rel + h_rel),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
e.inner.config_mut().enabled = false;
|
e.inner.config_mut().enabled = false;
|
||||||
@ -217,6 +230,39 @@ impl GuiElemTrait for ScrollBox {
|
|||||||
self.max_scroll =
|
self.max_scroll =
|
||||||
0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height()));
|
0.0f32.max(self.height_bottom - self.size_unit.from_rel(0.75, info.pos.height()));
|
||||||
}
|
}
|
||||||
|
// scroll bar
|
||||||
|
self.mouse_in_scrollbar = info.mouse_pos.y >= info.pos.top_left().y
|
||||||
|
&& info.mouse_pos.y <= info.pos.bottom_right().y
|
||||||
|
&& info.mouse_pos.x <= info.pos.bottom_right().x
|
||||||
|
&& info.mouse_pos.x >= (info.pos.bottom_right().x - self.mouse_scroll_margin_right);
|
||||||
|
if self.mouse_scrolling {
|
||||||
|
self.scroll_target = (self.max_scroll * (info.mouse_pos.y - info.pos.top_left().y)
|
||||||
|
/ info.pos.height())
|
||||||
|
.max(0.0)
|
||||||
|
.min(self.max_scroll);
|
||||||
|
}
|
||||||
|
if self.mouse_in_scrollbar
|
||||||
|
|| self.mouse_scrolling
|
||||||
|
|| (self.scroll_display - self.scroll_target).abs()
|
||||||
|
> self.size_unit.from_abs(1.0, info.pos.height())
|
||||||
|
{
|
||||||
|
let y1 = info.pos.top_left().y
|
||||||
|
+ info.pos.height() * self.scroll_display.min(self.scroll_target) / self.max_scroll
|
||||||
|
- 1.0;
|
||||||
|
let y2 = info.pos.top_left().y
|
||||||
|
+ info.pos.height() * self.scroll_display.max(self.scroll_target) / self.max_scroll
|
||||||
|
+ 1.0;
|
||||||
|
g.draw_rectangle(
|
||||||
|
Rectangle::from_tuples(
|
||||||
|
(
|
||||||
|
info.pos.bottom_right().x - self.mouse_scroll_margin_right,
|
||||||
|
y1,
|
||||||
|
),
|
||||||
|
(info.pos.bottom_right().x, y2),
|
||||||
|
),
|
||||||
|
Color::WHITE,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn mouse_wheel(&mut self, diff: f32) -> Vec<crate::gui::GuiAction> {
|
fn mouse_wheel(&mut self, diff: f32) -> Vec<crate::gui::GuiAction> {
|
||||||
self.scroll_target = (self.scroll_target
|
self.scroll_target = (self.scroll_target
|
||||||
@ -224,6 +270,18 @@ impl GuiElemTrait for ScrollBox {
|
|||||||
.max(0.0);
|
.max(0.0);
|
||||||
Vec::with_capacity(0)
|
Vec::with_capacity(0)
|
||||||
}
|
}
|
||||||
|
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||||
|
if button == MouseButton::Left && self.mouse_in_scrollbar {
|
||||||
|
self.mouse_scrolling = true;
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||||
|
if button == MouseButton::Left {
|
||||||
|
self.mouse_scrolling = false;
|
||||||
|
}
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl ScrollBoxSizeUnit {
|
impl ScrollBoxSizeUnit {
|
||||||
fn to_rel(&self, val: f32, draw_height: f32) -> f32 {
|
fn to_rel(&self, val: f32, draw_height: f32) -> f32 {
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
use std::sync::{atomic::AtomicBool, mpsc, Arc};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::{atomic::AtomicBool, mpsc, Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{album::Album, artist::Artist, song::Song, AlbumId, ArtistId, SongId},
|
data::{
|
||||||
|
album::Album, artist::Artist, queue::QueueContent, song::Song, AlbumId, ArtistId, CoverId,
|
||||||
|
SongId,
|
||||||
|
},
|
||||||
server::Command,
|
server::Command,
|
||||||
};
|
};
|
||||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||||
gui_base::{Button, Panel, ScrollBox},
|
gui_base::{Button, Panel, ScrollBox},
|
||||||
gui_text::{Label, TextField},
|
gui_text::{Label, TextField},
|
||||||
};
|
};
|
||||||
@ -18,37 +24,73 @@ pub struct GuiEdit {
|
|||||||
editable: Editable,
|
editable: Editable,
|
||||||
editing: Editing,
|
editing: Editing,
|
||||||
reload: bool,
|
reload: bool,
|
||||||
|
rebuild_main: bool,
|
||||||
|
rebuild_changes: bool,
|
||||||
send: bool,
|
send: bool,
|
||||||
apply_change: mpsc::Sender<Box<dyn FnOnce(&mut Self)>>,
|
apply_change: mpsc::Sender<Box<dyn FnOnce(&mut Self)>>,
|
||||||
change_recv: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
change_recv: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Editable {
|
pub enum Editable {
|
||||||
Artist(ArtistId),
|
Artist(Vec<ArtistId>),
|
||||||
Album(AlbumId),
|
Album(Vec<AlbumId>),
|
||||||
Song(SongId),
|
Song(Vec<SongId>),
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum Editing {
|
pub enum Editing {
|
||||||
NotLoaded,
|
NotLoaded,
|
||||||
Artist(Artist),
|
Artist(Vec<Artist>, Vec<ArtistChange>),
|
||||||
Album(Album),
|
Album(Vec<Album>, Vec<AlbumChange>),
|
||||||
Song(Song),
|
Song(Vec<Song>, Vec<SongChange>),
|
||||||
}
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ArtistChange {
|
||||||
|
SetName(String),
|
||||||
|
SetCover(Option<ArtistId>),
|
||||||
|
RemoveAlbum(AlbumId),
|
||||||
|
AddAlbum(AlbumId),
|
||||||
|
RemoveSong(SongId),
|
||||||
|
AddSong(SongId),
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum AlbumChange {
|
||||||
|
SetName(String),
|
||||||
|
SetCover(Option<ArtistId>),
|
||||||
|
RemoveSong(SongId),
|
||||||
|
AddSong(SongId),
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum SongChange {
|
||||||
|
SetTitle(String),
|
||||||
|
SetCover(Option<ArtistId>),
|
||||||
|
}
|
||||||
|
|
||||||
impl GuiEdit {
|
impl GuiEdit {
|
||||||
pub fn new(config: GuiElemCfg, edit: Editable) -> Self {
|
pub fn new(config: GuiElemCfg, edit: Editable) -> Self {
|
||||||
let (apply_change, change_recv) = mpsc::channel();
|
let (apply_change, change_recv) = mpsc::channel();
|
||||||
let ac1 = apply_change.clone();
|
let ac1 = apply_change.clone();
|
||||||
let ac2 = apply_change.clone();
|
let ac2 = apply_change.clone();
|
||||||
Self {
|
Self {
|
||||||
config,
|
config: config.w_drag_target(),
|
||||||
editable: edit,
|
editable: edit,
|
||||||
editing: Editing::NotLoaded,
|
editing: Editing::NotLoaded,
|
||||||
reload: true,
|
reload: true,
|
||||||
|
rebuild_main: true,
|
||||||
|
rebuild_changes: true,
|
||||||
send: false,
|
send: false,
|
||||||
apply_change,
|
apply_change,
|
||||||
change_recv,
|
change_recv,
|
||||||
children: vec![
|
children: vec![
|
||||||
|
GuiElem::new(ScrollBox::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.6))),
|
||||||
|
crate::gui_base::ScrollBoxSizeUnit::Pixels,
|
||||||
|
vec![],
|
||||||
|
)),
|
||||||
|
GuiElem::new(ScrollBox::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.6), (1.0, 0.9))),
|
||||||
|
crate::gui_base::ScrollBoxSizeUnit::Pixels,
|
||||||
|
vec![],
|
||||||
|
)),
|
||||||
GuiElem::new(Button::new(
|
GuiElem::new(Button::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))),
|
||||||
|_| vec![GuiAction::CloseEditPanel],
|
|_| vec![GuiAction::CloseEditPanel],
|
||||||
@ -123,231 +165,547 @@ impl GuiElemTrait for GuiEdit {
|
|||||||
self.send = false;
|
self.send = false;
|
||||||
match &self.editing {
|
match &self.editing {
|
||||||
Editing::NotLoaded => {}
|
Editing::NotLoaded => {}
|
||||||
Editing::Artist(v) => info
|
Editing::Artist(v, changes) => {
|
||||||
.actions
|
for v in v {
|
||||||
.push(GuiAction::SendToServer(Command::ModifyArtist(v.clone()))),
|
let mut v = v.clone();
|
||||||
Editing::Album(v) => info
|
for change in changes.iter() {
|
||||||
.actions
|
match change {
|
||||||
.push(GuiAction::SendToServer(Command::ModifyAlbum(v.clone()))),
|
ArtistChange::SetName(n) => v.name = n.clone(),
|
||||||
Editing::Song(v) => info
|
ArtistChange::SetCover(c) => v.cover = c.clone(),
|
||||||
.actions
|
ArtistChange::RemoveAlbum(id) => {
|
||||||
.push(GuiAction::SendToServer(Command::ModifySong(v.clone()))),
|
if let Some(i) = v.albums.iter().position(|id| id == id) {
|
||||||
|
v.albums.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArtistChange::AddAlbum(id) => {
|
||||||
|
if !v.albums.contains(id) {
|
||||||
|
v.albums.push(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArtistChange::RemoveSong(id) => {
|
||||||
|
if let Some(i) = v.singles.iter().position(|id| id == id) {
|
||||||
|
v.singles.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArtistChange::AddSong(id) => {
|
||||||
|
if !v.singles.contains(id) {
|
||||||
|
v.singles.push(*id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info.actions
|
||||||
|
.push(GuiAction::SendToServer(Command::ModifyArtist(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Editing::Album(v, changes) => {
|
||||||
|
for v in v {
|
||||||
|
let mut v = v.clone();
|
||||||
|
for change in changes.iter() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
info.actions
|
||||||
|
.push(GuiAction::SendToServer(Command::ModifyAlbum(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Editing::Song(v, changes) => {
|
||||||
|
for v in v {
|
||||||
|
let mut v = v.clone();
|
||||||
|
for change in changes.iter() {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
info.actions
|
||||||
|
.push(GuiAction::SendToServer(Command::ModifySong(v)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.reload {
|
if self.reload {
|
||||||
self.reload = false;
|
self.reload = false;
|
||||||
|
let prev = std::mem::replace(&mut self.editing, Editing::NotLoaded);
|
||||||
self.editing = match &self.editable {
|
self.editing = match &self.editable {
|
||||||
Editable::Artist(id) => {
|
Editable::Artist(id) => {
|
||||||
if let Some(v) = info.database.artists().get(id).cloned() {
|
let v = id
|
||||||
Editing::Artist(v)
|
.iter()
|
||||||
|
.filter_map(|id| info.database.artists().get(id).cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !v.is_empty() {
|
||||||
|
Editing::Artist(
|
||||||
|
v,
|
||||||
|
if let Editing::Artist(_, c) = prev {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Editing::NotLoaded
|
Editing::NotLoaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Editable::Album(id) => {
|
Editable::Album(id) => {
|
||||||
if let Some(v) = info.database.albums().get(id).cloned() {
|
let v = id
|
||||||
Editing::Album(v)
|
.iter()
|
||||||
|
.filter_map(|id| info.database.albums().get(id).cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !v.is_empty() {
|
||||||
|
Editing::Album(
|
||||||
|
v,
|
||||||
|
if let Editing::Album(_, c) = prev {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Editing::NotLoaded
|
Editing::NotLoaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Editable::Song(id) => {
|
Editable::Song(id) => {
|
||||||
if let Some(v) = info.database.songs().get(id).cloned() {
|
let v = id
|
||||||
Editing::Song(v)
|
.iter()
|
||||||
|
.filter_map(|id| info.database.songs().get(id).cloned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !v.is_empty() {
|
||||||
|
Editing::Song(
|
||||||
|
v,
|
||||||
|
if let Editing::Song(_, c) = prev {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Editing::NotLoaded
|
Editing::NotLoaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.config.redraw = true;
|
self.config.redraw = true;
|
||||||
|
self.rebuild_main = true;
|
||||||
|
self.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||||
|
for (c, _) in sb.children.iter() {
|
||||||
|
if let Some(p) = c
|
||||||
|
.inner
|
||||||
|
.any()
|
||||||
|
.downcast_ref::<Panel>()
|
||||||
|
.and_then(|p| p.children.get(0))
|
||||||
|
.and_then(|e| e.inner.any().downcast_ref::<TextField>())
|
||||||
|
{
|
||||||
|
if p.label_input().content.will_redraw() {
|
||||||
|
if let Some((key, _)) = p.label_hint().content.get_text().split_once(':') {
|
||||||
|
match (&mut self.editing, key) {
|
||||||
|
(Editing::Artist(_, changes), "name") => {
|
||||||
|
let mut c = changes.iter_mut();
|
||||||
|
loop {
|
||||||
|
if let Some(c) = c.next() {
|
||||||
|
if let ArtistChange::SetName(n) = c {
|
||||||
|
*n = p.label_input().content.get_text().clone();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changes.push(ArtistChange::SetName(
|
||||||
|
p.label_input().content.get_text().clone(),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
(Editing::Artist(_, changes), "cover") => {
|
||||||
|
let mut c = changes.iter_mut();
|
||||||
|
loop {
|
||||||
|
if let Some(c) = c.next() {
|
||||||
|
if let ArtistChange::SetCover(n) = c {
|
||||||
|
*n =
|
||||||
|
p.label_input().content.get_text().parse().ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changes.push(ArtistChange::SetCover(
|
||||||
|
p.label_input().content.get_text().parse().ok(),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.rebuild_main {
|
||||||
|
self.rebuild_main = false;
|
||||||
|
self.rebuild_main(info);
|
||||||
|
}
|
||||||
|
if self.rebuild_changes {
|
||||||
|
self.rebuild_changes = false;
|
||||||
|
self.rebuild_changes(info);
|
||||||
}
|
}
|
||||||
if self.config.redraw {
|
if self.config.redraw {
|
||||||
self.config.redraw = false;
|
self.config.redraw = false;
|
||||||
let scrollbox = if self.children.len() > 3 {
|
if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||||
let o = self.children.pop();
|
for c in sb.children.iter_mut() {
|
||||||
while self.children.len() > 3 {
|
c.1 = info.line_height;
|
||||||
self.children.pop();
|
|
||||||
}
|
}
|
||||||
o
|
}
|
||||||
} else {
|
}
|
||||||
None
|
}
|
||||||
|
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
|
||||||
|
let dragged = match dragged {
|
||||||
|
Dragging::Artist(_) | Dragging::Album(_) | Dragging::Song(_) => dragged,
|
||||||
|
Dragging::Queue(q) => match q.content() {
|
||||||
|
QueueContent::Song(id) => Dragging::Song(*id),
|
||||||
|
_ => Dragging::Queue(q),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
match &self.editing {
|
match dragged {
|
||||||
Editing::NotLoaded => {
|
Dragging::Artist(id) => {
|
||||||
self.children.push(GuiElem::new(Label::new(
|
if let Editing::Artist(a, _) = &self.editing {
|
||||||
GuiElemCfg::default(),
|
self.editable = Editable::Artist(a.iter().map(|v| v.id).chain([id]).collect())
|
||||||
"nothing here".to_string(),
|
|
||||||
Color::WHITE,
|
|
||||||
None,
|
|
||||||
Vec2::new(0.5, 0.5),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
Editing::Artist(artist) => {
|
}
|
||||||
self.children.push(GuiElem::new(Label::new(
|
Dragging::Album(id) => {
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.8, 0.08))),
|
if let Editing::Album(a, _) = &self.editing {
|
||||||
artist.name.clone(),
|
self.editable = Editable::Album(a.iter().map(|v| v.id).chain([id]).collect())
|
||||||
Color::WHITE,
|
}
|
||||||
None,
|
}
|
||||||
Vec2::new(0.1, 0.5),
|
Dragging::Song(id) => {
|
||||||
)));
|
if let Editing::Song(a, _) = &self.editing {
|
||||||
self.children.push(GuiElem::new(Label::new(
|
self.editable = Editable::Song(a.iter().map(|v| v.id).chain([id]).collect())
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 0.04))),
|
}
|
||||||
"Artist".to_string(),
|
}
|
||||||
Color::WHITE,
|
Dragging::Queue(_) => return vec![],
|
||||||
None,
|
}
|
||||||
Vec2::new(0.8, 0.5),
|
self.reload = true;
|
||||||
)));
|
vec![]
|
||||||
self.children.push(GuiElem::new(Label::new(
|
}
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.04), (1.0, 0.08))),
|
}
|
||||||
format!("#{}", artist.id),
|
impl GuiEdit {
|
||||||
Color::WHITE,
|
fn rebuild_main(&mut self, info: &mut DrawInfo) {
|
||||||
None,
|
if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||||
Vec2::new(0.8, 0.5),
|
sb.children.clear();
|
||||||
)));
|
sb.config_mut().redraw = true;
|
||||||
let mut elems = vec![];
|
match &self.editing {
|
||||||
elems.push((
|
Editing::NotLoaded => {}
|
||||||
|
Editing::Artist(v, _) => {
|
||||||
|
// name
|
||||||
|
let mut names = v
|
||||||
|
.iter()
|
||||||
|
.map(|v| &v.name)
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
names.sort_unstable();
|
||||||
|
let name = if names.len() == 1 {
|
||||||
|
format!("name: {}", names[0])
|
||||||
|
} else {
|
||||||
|
let mut name = format!("name: {}", names[0]);
|
||||||
|
for n in names.iter().skip(1) {
|
||||||
|
name.push_str(" / ");
|
||||||
|
name.push_str(n);
|
||||||
|
}
|
||||||
|
name
|
||||||
|
};
|
||||||
|
sb.children.push((
|
||||||
GuiElem::new(Panel::new(
|
GuiElem::new(Panel::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
vec![
|
vec![GuiElem::new(TextField::new(
|
||||||
GuiElem::new(Label::new(
|
GuiElemCfg::default(),
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.6, 1.0))),
|
name,
|
||||||
format!(
|
|
||||||
"{} album{}",
|
|
||||||
artist.albums.len(),
|
|
||||||
if artist.albums.len() != 1 { "s" } else { "" }
|
|
||||||
),
|
|
||||||
Color::LIGHT_GRAY,
|
Color::LIGHT_GRAY,
|
||||||
None,
|
|
||||||
Vec2::new(0.0, 0.5),
|
|
||||||
)),
|
|
||||||
GuiElem::new(TextField::new(
|
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.6, 0.0), (0.8, 1.0))),
|
|
||||||
"id".to_string(),
|
|
||||||
Color::DARK_GRAY,
|
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
|
))],
|
||||||
)),
|
)),
|
||||||
GuiElem::new(Button::new(
|
info.line_height,
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 1.0))),
|
));
|
||||||
|
// cover
|
||||||
|
let covers = v.iter().filter_map(|v| v.cover).collect::<Vec<_>>();
|
||||||
|
let cover = if covers.is_empty() {
|
||||||
|
format!("cover: None")
|
||||||
|
} else {
|
||||||
|
let mut cover = format!("cover: {}", covers[0]);
|
||||||
|
for c in covers.iter().skip(1) {
|
||||||
|
cover.push('/');
|
||||||
|
cover.push_str(&format!("{c}"));
|
||||||
|
}
|
||||||
|
cover
|
||||||
|
};
|
||||||
|
sb.children.push((
|
||||||
|
GuiElem::new(Panel::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
vec![GuiElem::new(TextField::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
cover,
|
||||||
|
Color::LIGHT_GRAY,
|
||||||
|
Color::WHITE,
|
||||||
|
))],
|
||||||
|
)),
|
||||||
|
info.line_height,
|
||||||
|
));
|
||||||
|
// albums
|
||||||
|
let mut albums = HashMap::new();
|
||||||
|
for v in v {
|
||||||
|
for album in &v.albums {
|
||||||
|
if let Some(count) = albums.get_mut(album) {
|
||||||
|
*count += 1;
|
||||||
|
} else {
|
||||||
|
albums.insert(*album, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
let apply_change = self.apply_change.clone();
|
fn get_id(s: &mut GuiEdit) -> Option<AlbumId> {
|
||||||
let my_index = elems.len();
|
s.children[0]
|
||||||
move |_| {
|
.inner
|
||||||
_ = apply_change.send(Box::new(move |s| {
|
.children()
|
||||||
s.config.redraw = true;
|
.collect::<Vec<_>>()
|
||||||
if let Ok(id) = s
|
.into_iter()
|
||||||
.children
|
.rev()
|
||||||
.last_mut()
|
.nth(2)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.inner
|
.inner
|
||||||
.any_mut()
|
.any_mut()
|
||||||
.downcast_mut::<ScrollBox>()
|
.downcast_mut::<Panel>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.children[my_index]
|
|
||||||
.0
|
|
||||||
.inner
|
|
||||||
.children()
|
|
||||||
.nth(1)
|
|
||||||
.unwrap()
|
|
||||||
.inner
|
|
||||||
.children()
|
.children()
|
||||||
.next()
|
.next()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.inner
|
.inner
|
||||||
.any()
|
.any_mut()
|
||||||
.downcast_ref::<Label>()
|
.downcast_mut::<TextField>()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.label_input()
|
||||||
.content
|
.content
|
||||||
.get_text()
|
.get_text()
|
||||||
.parse::<AlbumId>()
|
.parse()
|
||||||
{
|
.ok()
|
||||||
if let Editing::Artist(artist) = &mut s.editing
|
|
||||||
{
|
|
||||||
artist.albums.push(id);
|
|
||||||
}
|
}
|
||||||
|
let add_button = {
|
||||||
|
let apply_change = self.apply_change.clone();
|
||||||
|
GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (0.9, 1.0))),
|
||||||
|
move |_| {
|
||||||
|
_ = apply_change.send(Box::new(move |s| {
|
||||||
|
if let Some(album_id) = get_id(s) {
|
||||||
|
if let Editing::Artist(_, c) = &mut s.editing {
|
||||||
|
if let Some(i) = c.iter().position(|c| {
|
||||||
|
matches!(c, ArtistChange::AddAlbum(id) if *id == album_id)
|
||||||
|
}) {
|
||||||
|
c.remove(i);
|
||||||
|
}
|
||||||
|
c.push(ArtistChange::AddAlbum(album_id));
|
||||||
|
s.rebuild_changes = true;}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
vec![]
|
vec![]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
vec![GuiElem::new(Label::new(
|
vec![GuiElem::new(Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
"add".to_string(),
|
format!("add"),
|
||||||
Color::LIGHT_GRAY,
|
Color::GREEN,
|
||||||
None,
|
None,
|
||||||
Vec2::new(0.0, 0.5),
|
Vec2::new(0.5, 0.5),
|
||||||
))],
|
))],
|
||||||
)),
|
))
|
||||||
],
|
};
|
||||||
)),
|
let remove_button = {
|
||||||
info.line_height,
|
let apply_change = self.apply_change.clone();
|
||||||
));
|
|
||||||
for &album in &artist.albums {
|
|
||||||
elems.push((
|
|
||||||
GuiElem::new(Button::new(
|
GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.9, 0.0), (1.0, 1.0))),
|
||||||
|
move |_| {
|
||||||
|
_ = apply_change.send(Box::new(move |s| {
|
||||||
|
if let Some(album_id) = get_id(s) {
|
||||||
|
if let Editing::Artist(_, c) = &mut s.editing {
|
||||||
|
if let Some(i) = c.iter().position(|c| {
|
||||||
|
matches!(c, ArtistChange::RemoveAlbum(id) if *id == album_id)
|
||||||
|
}) {
|
||||||
|
c.remove(i);
|
||||||
|
}
|
||||||
|
c.push(ArtistChange::RemoveAlbum(album_id));
|
||||||
|
s.rebuild_changes = true;}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
vec![GuiElem::new(Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
|
format!("remove"),
|
||||||
|
Color::RED,
|
||||||
|
None,
|
||||||
|
Vec2::new(0.5, 0.5),
|
||||||
|
))],
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let name = GuiElem::new(TextField::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.8, 1.0))),
|
||||||
|
"add or remove album by id".to_string(),
|
||||||
|
Color::LIGHT_GRAY,
|
||||||
|
Color::WHITE,
|
||||||
|
));
|
||||||
|
sb.children.push((
|
||||||
|
GuiElem::new(Panel::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
vec![name, add_button, remove_button],
|
||||||
|
)),
|
||||||
|
info.line_height * 2.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for (album_id, count) in albums {
|
||||||
|
let album = info.database.albums().get(&album_id);
|
||||||
|
let add_button = if count == v.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let apply_change = self.apply_change.clone();
|
||||||
|
Some(GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (0.9, 1.0))),
|
||||||
|
move |_| {
|
||||||
|
_ = apply_change.send(Box::new(move |s| {
|
||||||
|
if let Editing::Artist(_, c) = &mut s.editing {
|
||||||
|
if let Some(i) = c.iter().position(|c| {
|
||||||
|
matches!(c, ArtistChange::AddAlbum(id) if *id == album_id)
|
||||||
|
}) {
|
||||||
|
c.remove(i);
|
||||||
|
}
|
||||||
|
c.push(ArtistChange::AddAlbum(album_id));
|
||||||
|
s.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
vec![GuiElem::new(Label::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
format!("add"),
|
||||||
|
Color::GREEN,
|
||||||
|
None,
|
||||||
|
Vec2::new(0.5, 0.5),
|
||||||
|
))],
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
let remove_button = {
|
||||||
|
let apply_change = self.apply_change.clone();
|
||||||
|
GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples((0.9, 0.0), (1.0, 1.0))),
|
||||||
|
move |_| {
|
||||||
|
_ = apply_change.send(Box::new(move |s| {
|
||||||
|
if let Editing::Artist(_, c) = &mut s.editing {
|
||||||
|
if let Some(i) = c.iter().position(|c| {
|
||||||
|
matches!(c, ArtistChange::RemoveAlbum(id) if *id == album_id)
|
||||||
|
}) {
|
||||||
|
c.remove(i);
|
||||||
|
}
|
||||||
|
c.push(ArtistChange::RemoveAlbum(album_id));
|
||||||
|
s.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
vec![GuiElem::new(Label::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
format!("remove"),
|
||||||
|
Color::RED,
|
||||||
|
None,
|
||||||
|
Vec2::new(0.5, 0.5),
|
||||||
|
))],
|
||||||
|
))
|
||||||
|
};
|
||||||
|
let name = GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::at(Rectangle::from_tuples(
|
||||||
|
(0.0, 0.0),
|
||||||
|
(if add_button.is_some() { 0.8 } else { 0.9 }, 1.0),
|
||||||
|
)),
|
||||||
move |_| {
|
move |_| {
|
||||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
Editable::Album(album),
|
Editable::Album(vec![album_id]),
|
||||||
)))]
|
)))]
|
||||||
},
|
},
|
||||||
vec![GuiElem::new(Label::new(
|
vec![GuiElem::new(Label::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
if let Some(a) = info.database.albums().get(&album) {
|
if let Some(a) = album {
|
||||||
format!("Album: {}", a.name)
|
a.name.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("Album #{album}")
|
format!("#{album_id}")
|
||||||
},
|
},
|
||||||
Color::WHITE,
|
Color::WHITE,
|
||||||
None,
|
None,
|
||||||
Vec2::new(0.0, 0.5),
|
Vec2::new(0.0, 0.5),
|
||||||
))],
|
))],
|
||||||
|
));
|
||||||
|
sb.children.push((
|
||||||
|
GuiElem::new(Panel::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
if let Some(add_button) = add_button {
|
||||||
|
vec![name, add_button, remove_button]
|
||||||
|
} else {
|
||||||
|
vec![name, remove_button]
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
info.line_height,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Editing::Album(v, _) => {}
|
||||||
|
Editing::Song(v, _) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn rebuild_changes(&mut self, info: &mut DrawInfo) {
|
||||||
|
if let Some(sb) = self.children[1].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||||
|
sb.children.clear();
|
||||||
|
sb.config_mut().redraw = true;
|
||||||
|
match &self.editing {
|
||||||
|
Editing::NotLoaded => {}
|
||||||
|
Editing::Artist(_, a) => {
|
||||||
|
for (i, v) in a.iter().enumerate() {
|
||||||
|
let text = match v {
|
||||||
|
ArtistChange::SetName(v) => format!("set name to \"{v}\""),
|
||||||
|
ArtistChange::SetCover(c) => {
|
||||||
|
if let Some(c) = c {
|
||||||
|
format!("set cover to {c}")
|
||||||
|
} else {
|
||||||
|
"remove cover".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArtistChange::RemoveAlbum(v) => format!("remove album {v}"),
|
||||||
|
ArtistChange::AddAlbum(v) => format!("add album {v}"),
|
||||||
|
ArtistChange::RemoveSong(v) => format!("remove song {v}"),
|
||||||
|
ArtistChange::AddSong(v) => format!("add song {v}"),
|
||||||
|
};
|
||||||
|
let s = self.apply_change.clone();
|
||||||
|
sb.children.push((
|
||||||
|
GuiElem::new(Button::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
move |_| {
|
||||||
|
_ = s.send(Box::new(move |s| {
|
||||||
|
if !s.rebuild_changes {
|
||||||
|
if let Editing::Artist(_, v) = &mut s.editing {
|
||||||
|
if i < v.len() {
|
||||||
|
v.remove(i);
|
||||||
|
}
|
||||||
|
s.rebuild_changes = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
vec![GuiElem::new(Label::new(
|
||||||
|
GuiElemCfg::default(),
|
||||||
|
text,
|
||||||
|
Color::WHITE,
|
||||||
|
None,
|
||||||
|
Vec2::new(0.0, 0.5),
|
||||||
|
))],
|
||||||
)),
|
)),
|
||||||
info.line_height,
|
info.line_height,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.children.push(if let Some(mut sb) = scrollbox {
|
|
||||||
if let Some(s) = sb.inner.any_mut().downcast_mut::<ScrollBox>() {
|
|
||||||
s.children = elems;
|
|
||||||
s.config_mut().redraw = true;
|
|
||||||
sb
|
|
||||||
} else {
|
|
||||||
GuiElem::new(ScrollBox::new(
|
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.08), (1.0, 1.0))),
|
|
||||||
crate::gui_base::ScrollBoxSizeUnit::Pixels,
|
|
||||||
elems,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
} else {
|
_ => todo!(),
|
||||||
GuiElem::new(ScrollBox::new(
|
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.08), (1.0, 1.0))),
|
|
||||||
crate::gui_base::ScrollBoxSizeUnit::Pixels,
|
|
||||||
elems,
|
|
||||||
))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Editing::Album(album) => {
|
|
||||||
self.children.push(GuiElem::new(Label::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
format!("Album: {}", album.name),
|
|
||||||
Color::WHITE,
|
|
||||||
None,
|
|
||||||
Vec2::new(0.5, 0.5),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Editing::Song(song) => {
|
|
||||||
self.children.push(GuiElem::new(Label::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
format!("Song: {}", song.title),
|
|
||||||
Color::WHITE,
|
|
||||||
None,
|
|
||||||
Vec2::new(0.5, 0.5),
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match self.editing {
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,7 +350,7 @@ impl GuiElemTrait for ListArtist {
|
|||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
crate::gui_edit::Editable::Artist(self.id),
|
crate::gui_edit::Editable::Artist(vec![self.id]),
|
||||||
)))]
|
)))]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
@ -444,7 +444,7 @@ impl GuiElemTrait for ListAlbum {
|
|||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
crate::gui_edit::Editable::Album(self.id),
|
crate::gui_edit::Editable::Album(vec![self.id]),
|
||||||
)))]
|
)))]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
@ -538,7 +538,7 @@ impl GuiElemTrait for ListSong {
|
|||||||
self.mouse = false;
|
self.mouse = false;
|
||||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
crate::gui_edit::Editable::Song(self.id),
|
crate::gui_edit::Editable::Song(vec![self.id]),
|
||||||
)))]
|
)))]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
use std::{
|
use std::{collections::VecDeque, sync::Arc, time::Instant};
|
||||||
io::{Cursor, Read, Write},
|
|
||||||
thread::{self, JoinHandle},
|
|
||||||
};
|
|
||||||
|
|
||||||
use musicdb_lib::{
|
use musicdb_lib::{
|
||||||
data::{queue::QueueContent, CoverId, SongId},
|
data::{CoverId, SongId},
|
||||||
server::{get, Command},
|
server::Command,
|
||||||
};
|
|
||||||
use speedy2d::{
|
|
||||||
color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle, window::MouseButton,
|
|
||||||
};
|
};
|
||||||
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
gui::{adjust_area, adjust_pos, GuiAction, GuiCover, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||||
gui_text::Label,
|
gui_text::Label,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -23,32 +18,16 @@ This file could probably have a better name.
|
|||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
pub struct CurrentSong<T: Read + Write> {
|
#[derive(Clone)]
|
||||||
|
pub struct CurrentSong {
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
children: Vec<GuiElem>,
|
children: Vec<GuiElem>,
|
||||||
get_con: Option<get::Client<T>>,
|
|
||||||
prev_song: Option<SongId>,
|
prev_song: Option<SongId>,
|
||||||
cover_pos: Rectangle,
|
cover_pos: Rectangle,
|
||||||
cover_id: Option<CoverId>,
|
covers: VecDeque<(CoverId, Option<(bool, Instant)>)>,
|
||||||
cover: Option<ImageHandle>,
|
|
||||||
new_cover: Option<JoinHandle<(get::Client<T>, Option<Vec<u8>>)>>,
|
|
||||||
}
|
}
|
||||||
impl<T: Read + Write> Clone for CurrentSong<T> {
|
impl CurrentSong {
|
||||||
fn clone(&self) -> Self {
|
pub fn new(config: GuiElemCfg) -> Self {
|
||||||
Self {
|
|
||||||
config: self.config.clone(),
|
|
||||||
children: self.children.clone(),
|
|
||||||
get_con: None,
|
|
||||||
prev_song: None,
|
|
||||||
cover_pos: self.cover_pos.clone(),
|
|
||||||
cover_id: None,
|
|
||||||
cover: None,
|
|
||||||
new_cover: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: Read + Write + 'static + Sync + Send> CurrentSong<T> {
|
|
||||||
pub fn new(config: GuiElemCfg, get_con: get::Client<T>) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![
|
children: vec![
|
||||||
@ -67,16 +46,13 @@ impl<T: Read + Write + 'static + Sync + Send> CurrentSong<T> {
|
|||||||
Vec2::new(0.0, 0.0),
|
Vec2::new(0.0, 0.0),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
get_con: Some(get_con),
|
|
||||||
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
||||||
cover_id: None,
|
covers: VecDeque::new(),
|
||||||
prev_song: None,
|
prev_song: None,
|
||||||
cover: None,
|
|
||||||
new_cover: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
|
impl GuiElemTrait for CurrentSong {
|
||||||
fn config(&self) -> &GuiElemCfg {
|
fn config(&self) -> &GuiElemCfg {
|
||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
@ -108,97 +84,43 @@ impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
|
|||||||
// no song, nothing in queue
|
// no song, nothing in queue
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
self.cover = None;
|
// end of last song
|
||||||
Some(None)
|
Some(None)
|
||||||
};
|
};
|
||||||
// drawing stuff
|
|
||||||
if self.config.pixel_pos.size() != info.pos.size() {
|
|
||||||
let leftright = 0.05;
|
|
||||||
let topbottom = 0.05;
|
|
||||||
let mut width = 0.3;
|
|
||||||
let mut height = 1.0 - topbottom * 2.0;
|
|
||||||
if width * info.pos.width() < height * info.pos.height() {
|
|
||||||
height = width * info.pos.width() / info.pos.height();
|
|
||||||
} else {
|
|
||||||
width = height * info.pos.height() / info.pos.width();
|
|
||||||
}
|
|
||||||
let right = leftright + width + leftright;
|
|
||||||
self.cover_pos = Rectangle::from_tuples(
|
|
||||||
(leftright, 0.5 - 0.5 * height),
|
|
||||||
(leftright + width, 0.5 + 0.5 * height),
|
|
||||||
);
|
|
||||||
for el in self.children.iter_mut().take(2) {
|
|
||||||
let pos = &mut el.inner.config_mut().pos;
|
|
||||||
*pos = Rectangle::new(Vec2::new(right, pos.top_left().y), *pos.bottom_right());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.new_cover.as_ref().is_some_and(|v| v.is_finished()) {
|
|
||||||
let (get_con, cover) = self.new_cover.take().unwrap().join().unwrap();
|
|
||||||
self.get_con = Some(get_con);
|
|
||||||
if let Some(cover) = cover {
|
|
||||||
self.cover = g
|
|
||||||
.create_image_from_file_bytes(
|
|
||||||
None,
|
|
||||||
speedy2d::image::ImageSmoothingMode::Linear,
|
|
||||||
Cursor::new(cover),
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(cover) = &self.cover {
|
|
||||||
g.draw_rectangle_image(
|
|
||||||
Rectangle::new(
|
|
||||||
Vec2::new(
|
|
||||||
info.pos.top_left().x + info.pos.width() * self.cover_pos.top_left().x,
|
|
||||||
info.pos.top_left().y + info.pos.height() * self.cover_pos.top_left().y,
|
|
||||||
),
|
|
||||||
Vec2::new(
|
|
||||||
info.pos.top_left().x + info.pos.width() * self.cover_pos.bottom_right().x,
|
|
||||||
info.pos.top_left().y + info.pos.height() * self.cover_pos.bottom_right().y,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
cover,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(new_song) = new_song {
|
if let Some(new_song) = new_song {
|
||||||
// if there is a new song:
|
// if there is a new song:
|
||||||
if self.prev_song != new_song {
|
if self.prev_song != new_song {
|
||||||
self.config.redraw = true;
|
self.config.redraw = true;
|
||||||
self.prev_song = new_song;
|
self.prev_song = new_song;
|
||||||
}
|
}
|
||||||
|
// get cover
|
||||||
|
let get_cover = |song: Option<u64>| crate::get_cover(song?, info.database);
|
||||||
|
let cover = get_cover(new_song);
|
||||||
|
// fade out all covers
|
||||||
|
for (_, t) in &mut self.covers {
|
||||||
|
if !t.is_some_and(|t| t.0) {
|
||||||
|
// not fading out yet, so make it start
|
||||||
|
*t = Some((true, Instant::now()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cover fades in now.
|
||||||
|
if let Some(cover) = cover {
|
||||||
|
self.covers
|
||||||
|
.push_back((cover, Some((false, Instant::now()))));
|
||||||
|
}
|
||||||
|
if let Some(next_cover) = get_cover(info.database.queue.get_next_song().cloned()) {
|
||||||
|
if !info.covers.contains_key(&next_cover) {
|
||||||
|
info.covers.insert(
|
||||||
|
next_cover,
|
||||||
|
GuiCover::new(next_cover, Arc::clone(&info.get_con)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// redraw
|
||||||
if self.config.redraw {
|
if self.config.redraw {
|
||||||
self.config.redraw = false;
|
self.config.redraw = false;
|
||||||
let (name, subtext) = if let Some(song) = new_song {
|
let (name, subtext) = if let Some(song) = new_song {
|
||||||
if let Some(song) = info.database.get_song(&song) {
|
if let Some(song) = info.database.get_song(&song) {
|
||||||
let cover = if let Some(v) = song.cover {
|
|
||||||
Some(v)
|
|
||||||
} else if let Some(v) = song
|
|
||||||
.album
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|id| info.database.albums().get(id))
|
|
||||||
.and_then(|album| album.cover)
|
|
||||||
{
|
|
||||||
Some(v)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if cover != self.cover_id {
|
|
||||||
self.cover = None;
|
|
||||||
if let Some(cover) = cover {
|
|
||||||
if let Some(mut get_con) = self.get_con.take() {
|
|
||||||
self.new_cover = Some(thread::spawn(move || {
|
|
||||||
match get_con.cover_bytes(cover).unwrap() {
|
|
||||||
Ok(v) => (get_con, Some(v)),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("couldn't get cover (response: {e})");
|
|
||||||
(get_con, None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.cover_id = cover;
|
|
||||||
}
|
|
||||||
let sub = match (
|
let sub = match (
|
||||||
song.artist
|
song.artist
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -246,6 +168,107 @@ impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
|
|||||||
.text() = subtext;
|
.text() = subtext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// drawing stuff
|
||||||
|
if self.config.pixel_pos.size() != info.pos.size() {
|
||||||
|
let leftright = 0.05;
|
||||||
|
let topbottom = 0.05;
|
||||||
|
let mut width = 0.3;
|
||||||
|
let mut height = 1.0 - topbottom * 2.0;
|
||||||
|
if width * info.pos.width() < height * info.pos.height() {
|
||||||
|
height = width * info.pos.width() / info.pos.height();
|
||||||
|
} else {
|
||||||
|
width = height * info.pos.height() / info.pos.width();
|
||||||
|
}
|
||||||
|
let right = leftright + width + leftright;
|
||||||
|
self.cover_pos = Rectangle::from_tuples(
|
||||||
|
(leftright, 0.5 - 0.5 * height),
|
||||||
|
(leftright + width, 0.5 + 0.5 * height),
|
||||||
|
);
|
||||||
|
for el in self.children.iter_mut().take(2) {
|
||||||
|
let pos = &mut el.inner.config_mut().pos;
|
||||||
|
*pos = Rectangle::new(Vec2::new(right, pos.top_left().y), *pos.bottom_right());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut cover_to_remove = None;
|
||||||
|
for (cover_index, (cover_id, time)) in self.covers.iter_mut().enumerate() {
|
||||||
|
let pos = match time {
|
||||||
|
None => 1.0,
|
||||||
|
Some((false, t)) => {
|
||||||
|
let el = t.elapsed().as_secs_f32();
|
||||||
|
if el >= 1.0 {
|
||||||
|
*time = None;
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
if let Some(h) = &info.helper {
|
||||||
|
h.request_redraw();
|
||||||
|
}
|
||||||
|
el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((true, t)) => {
|
||||||
|
let el = t.elapsed().as_secs_f32();
|
||||||
|
if el >= 1.0 {
|
||||||
|
cover_to_remove = Some(cover_index);
|
||||||
|
2.0
|
||||||
|
} else {
|
||||||
|
if let Some(h) = &info.helper {
|
||||||
|
h.request_redraw();
|
||||||
|
}
|
||||||
|
1.0 + el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(cover) = info.covers.get_mut(cover_id) {
|
||||||
|
if let Some(cover) = cover.get_init(g) {
|
||||||
|
let rect = Rectangle::new(
|
||||||
|
Vec2::new(
|
||||||
|
info.pos.top_left().x + info.pos.width() * self.cover_pos.top_left().x,
|
||||||
|
info.pos.top_left().y + info.pos.height() * self.cover_pos.top_left().y,
|
||||||
|
),
|
||||||
|
Vec2::new(
|
||||||
|
info.pos.top_left().x
|
||||||
|
+ info.pos.width() * self.cover_pos.bottom_right().x,
|
||||||
|
info.pos.top_left().y
|
||||||
|
+ info.pos.height() * self.cover_pos.bottom_right().y,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if pos == 1.0 {
|
||||||
|
g.draw_rectangle_image(rect, &cover);
|
||||||
|
} else {
|
||||||
|
let prog = (pos - 1.0).abs();
|
||||||
|
// shrink to half (0.5x0.5) size while moving left and fading out
|
||||||
|
let lx = rect.top_left().x + rect.width() * prog * 0.25;
|
||||||
|
let rx = rect.bottom_right().x - rect.width() * prog * 0.25;
|
||||||
|
let ty = rect.top_left().y + rect.height() * prog * 0.25;
|
||||||
|
let by = rect.bottom_right().y - rect.height() * prog * 0.25;
|
||||||
|
let mut moved = rect.width() * prog * prog;
|
||||||
|
if pos > 1.0 {
|
||||||
|
moved = -moved;
|
||||||
|
}
|
||||||
|
g.draw_rectangle_image_tinted(
|
||||||
|
Rectangle::from_tuples((lx + moved, ty), (rx + moved, by)),
|
||||||
|
Color::from_rgba(
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
if pos > 1.0 { 2.0 - pos } else { pos },
|
||||||
|
),
|
||||||
|
&cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cover still loading, just wait
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// cover not loading or loaded, start loading!
|
||||||
|
info.covers
|
||||||
|
.insert(*cover_id, GuiCover::new(*cover_id, info.get_con.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// removing one cover per frame is good enough
|
||||||
|
if let Some(index) = cover_to_remove {
|
||||||
|
self.covers.remove(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,9 +70,8 @@ impl GuiScreen {
|
|||||||
self.edit_panel = (false, Some(Instant::now()));
|
self.edit_panel = (false, Some(Instant::now()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn new<T: Read + Write + 'static + Sync + Send>(
|
pub fn new(
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
get_con: get::Client<T>,
|
|
||||||
line_height: f32,
|
line_height: f32,
|
||||||
scroll_sensitivity_pixels: f64,
|
scroll_sensitivity_pixels: f64,
|
||||||
scroll_sensitivity_lines: f64,
|
scroll_sensitivity_lines: f64,
|
||||||
@ -84,7 +83,6 @@ impl GuiScreen {
|
|||||||
GuiElem::new(StatusBar::new(
|
GuiElem::new(StatusBar::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
|
||||||
true,
|
true,
|
||||||
get_con,
|
|
||||||
)),
|
)),
|
||||||
GuiElem::new(Settings::new(
|
GuiElem::new(Settings::new(
|
||||||
GuiElemCfg::default().disabled(),
|
GuiElemCfg::default().disabled(),
|
||||||
@ -293,18 +291,14 @@ pub struct StatusBar {
|
|||||||
idle_mode: f32,
|
idle_mode: f32,
|
||||||
}
|
}
|
||||||
impl StatusBar {
|
impl StatusBar {
|
||||||
pub fn new<T: Read + Write + 'static + Sync + Send>(
|
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
|
||||||
config: GuiElemCfg,
|
|
||||||
playing: bool,
|
|
||||||
get_con: get::Client<T>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
children: vec![
|
children: vec![
|
||||||
GuiElem::new(CurrentSong::new(
|
GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
|
||||||
GuiElemCfg::at(Rectangle::new(Vec2::ZERO, Vec2::new(0.8, 1.0))),
|
Vec2::ZERO,
|
||||||
get_con,
|
Vec2::new(0.8, 1.0),
|
||||||
)),
|
)))),
|
||||||
GuiElem::new(PlayPauseToggle::new(
|
GuiElem::new(PlayPauseToggle::new(
|
||||||
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
|
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
|
||||||
false,
|
false,
|
||||||
|
@ -46,6 +46,10 @@ impl Content {
|
|||||||
pub fn color(&mut self) -> &mut Color {
|
pub fn color(&mut self) -> &mut Color {
|
||||||
&mut self.color
|
&mut self.color
|
||||||
}
|
}
|
||||||
|
/// returns true if the text needs to be redrawn, probably because it was changed.
|
||||||
|
pub fn will_redraw(&self) -> bool {
|
||||||
|
self.formatted.is_none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Label {
|
impl Label {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@ -148,11 +152,23 @@ impl TextField {
|
|||||||
hint,
|
hint,
|
||||||
color_hint,
|
color_hint,
|
||||||
None,
|
None,
|
||||||
Vec2::new(0.5, 0.5),
|
Vec2::new(0.0, 0.5),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn label_input(&self) -> &Label {
|
||||||
|
self.children[0].inner.any().downcast_ref().unwrap()
|
||||||
|
}
|
||||||
|
pub fn label_input_mut(&mut self) -> &mut Label {
|
||||||
|
self.children[0].inner.any_mut().downcast_mut().unwrap()
|
||||||
|
}
|
||||||
|
pub fn label_hint(&self) -> &Label {
|
||||||
|
self.children[1].inner.any().downcast_ref().unwrap()
|
||||||
|
}
|
||||||
|
pub fn label_hint_mut(&mut self) -> &mut Label {
|
||||||
|
self.children[1].inner.any_mut().downcast_mut().unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl GuiElemTrait for TextField {
|
impl GuiElemTrait for TextField {
|
||||||
fn config(&self) -> &GuiElemCfg {
|
fn config(&self) -> &GuiElemCfg {
|
||||||
|
@ -13,10 +13,10 @@ use musicdb_lib::{
|
|||||||
data::{
|
data::{
|
||||||
album::Album,
|
album::Album,
|
||||||
artist::Artist,
|
artist::Artist,
|
||||||
database::{Cover, Database},
|
database::{ClientIo, Cover, Database},
|
||||||
queue::QueueContent,
|
queue::QueueContent,
|
||||||
song::Song,
|
song::Song,
|
||||||
DatabaseLocation, GeneralData,
|
CoverId, DatabaseLocation, GeneralData, SongId,
|
||||||
},
|
},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
player::Player,
|
player::Player,
|
||||||
@ -43,27 +43,32 @@ mod gui_text;
|
|||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
mod gui_wrappers;
|
mod gui_wrappers;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Cli,
|
Cli,
|
||||||
Gui,
|
Gui,
|
||||||
SyncPlayer,
|
SyncPlayer,
|
||||||
FillDb,
|
SyncPlayerWithoutData,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_config_file_path() -> PathBuf {
|
fn get_config_file_path() -> PathBuf {
|
||||||
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
|
directories::ProjectDirs::from("", "", "musicdb-client")
|
||||||
let mut config_home: PathBuf = config_home.into();
|
.unwrap()
|
||||||
config_home.push("musicdb-client");
|
.config_dir()
|
||||||
config_home
|
.to_path_buf()
|
||||||
} else if let Ok(home) = std::env::var("HOME") {
|
// if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
|
||||||
let mut config_home: PathBuf = home.into();
|
// let mut config_home: PathBuf = config_home.into();
|
||||||
config_home.push(".config");
|
// config_home.push("musicdb-client");
|
||||||
config_home.push("musicdb-client");
|
// config_home
|
||||||
config_home
|
// } else if let Ok(home) = std::env::var("HOME") {
|
||||||
} else {
|
// let mut config_home: PathBuf = home.into();
|
||||||
eprintln!("No config directory!");
|
// config_home.push(".config");
|
||||||
std::process::exit(24);
|
// config_home.push("musicdb-client");
|
||||||
}
|
// config_home
|
||||||
|
// } else {
|
||||||
|
// eprintln!("No config directory!");
|
||||||
|
// std::process::exit(24);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -72,9 +77,9 @@ fn main() {
|
|||||||
Some("cli") => Mode::Cli,
|
Some("cli") => Mode::Cli,
|
||||||
Some("gui") => Mode::Gui,
|
Some("gui") => Mode::Gui,
|
||||||
Some("syncplayer") => Mode::SyncPlayer,
|
Some("syncplayer") => Mode::SyncPlayer,
|
||||||
Some("filldb") => Mode::FillDb,
|
Some("syncplayernd") => Mode::SyncPlayerWithoutData,
|
||||||
_ => {
|
_ => {
|
||||||
println!("Run with argument <cli/gui/syncplayer/filldb>!");
|
println!("Run with argument <cli/gui/syncplayer/syncplayernd>!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -88,17 +93,23 @@ fn main() {
|
|||||||
Arc::new(Mutex::new(None));
|
Arc::new(Mutex::new(None));
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
let sender = Arc::clone(&update_gui_sender);
|
let sender = Arc::clone(&update_gui_sender);
|
||||||
let wants_player = matches!(mode, Mode::SyncPlayer);
|
|
||||||
let con_thread = {
|
let con_thread = {
|
||||||
let database = Arc::clone(&database);
|
let database = Arc::clone(&database);
|
||||||
let mut con = con.try_clone().unwrap();
|
let mut con = con.try_clone().unwrap();
|
||||||
// this is all you need to keep the db in sync
|
// this is all you need to keep the db in sync
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut player = if wants_player {
|
let mut player = if matches!(mode, Mode::SyncPlayer | Mode::SyncPlayerWithoutData) {
|
||||||
Some(Player::new().unwrap())
|
Some(Player::new().unwrap())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
if matches!(mode, Mode::SyncPlayerWithoutData) {
|
||||||
|
let mut db = database.lock().unwrap();
|
||||||
|
let client_con: Box<dyn ClientIo> = Box::new(TcpStream::connect(addr).unwrap());
|
||||||
|
db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new(
|
||||||
|
musicdb_lib::server::get::Client::new(BufReader::new(client_con)).unwrap(),
|
||||||
|
)));
|
||||||
|
};
|
||||||
loop {
|
loop {
|
||||||
if let Some(player) = &mut player {
|
if let Some(player) = &mut player {
|
||||||
let mut db = database.lock().unwrap();
|
let mut db = database.lock().unwrap();
|
||||||
@ -147,201 +158,9 @@ fn main() {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Mode::SyncPlayer => {
|
Mode::SyncPlayer | Mode::SyncPlayerWithoutData => {
|
||||||
con_thread.join().unwrap();
|
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" {
|
|
||||||
let mut covers = 0;
|
|
||||||
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());
|
|
||||||
let cover = songs.iter().map(|entry| entry.path()).find(|path| {
|
|
||||||
path.extension().is_some_and(|ext| {
|
|
||||||
ext.to_str().is_some_and(|ext| {
|
|
||||||
matches!(
|
|
||||||
ext.to_lowercase().trim(),
|
|
||||||
"png" | "jpg" | "jpeg"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
let cover = if let Some(cover) = &cover
|
|
||||||
{
|
|
||||||
eprintln!("Adding cover {cover:?}");
|
|
||||||
Command::AddCover(Cover {
|
|
||||||
location: DatabaseLocation {
|
|
||||||
rel_path: PathBuf::from(
|
|
||||||
artist.file_name(),
|
|
||||||
)
|
|
||||||
.join(album.file_name())
|
|
||||||
.join(
|
|
||||||
cover
|
|
||||||
.file_name()
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
data: Arc::new(Mutex::new((
|
|
||||||
false, None,
|
|
||||||
))),
|
|
||||||
})
|
|
||||||
.to_bytes(&mut con)
|
|
||||||
.expect(
|
|
||||||
"sending AddCover to db failed",
|
|
||||||
);
|
|
||||||
covers += 1;
|
|
||||||
Some(covers - 1)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Command::AddAlbum(Album {
|
|
||||||
id: 0,
|
|
||||||
name: album_name.clone(),
|
|
||||||
artist: Some(artist_id),
|
|
||||||
cover,
|
|
||||||
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> {
|
struct Looper<'a> {
|
||||||
@ -506,3 +325,12 @@ pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
|
|||||||
}
|
}
|
||||||
o
|
o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_cover(song: SongId, database: &Database) -> Option<CoverId> {
|
||||||
|
let song = database.get_song(&song)?;
|
||||||
|
if let Some(v) = song.cover {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
database.albums().get(song.album.as_ref()?)?.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Binary file not shown.
@ -1,19 +1,18 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::HashMap,
|
||||||
fs::{self, FileType},
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
ops::IndexMut,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use id3::TagLike;
|
use id3::TagLike;
|
||||||
use musicdb_lib::data::{
|
use musicdb_lib::data::{
|
||||||
album::{self, Album},
|
album::Album,
|
||||||
artist::Artist,
|
artist::Artist,
|
||||||
database::{Cover, Database},
|
database::{Cover, Database},
|
||||||
song::Song,
|
song::Song,
|
||||||
DatabaseLocation, GeneralData,
|
CoverId, DatabaseLocation, GeneralData,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -63,7 +62,6 @@ fn main() {
|
|||||||
general: GeneralData::default(),
|
general: GeneralData::default(),
|
||||||
});
|
});
|
||||||
artists.insert(artist.to_string(), (artist_id, HashMap::new()));
|
artists.insert(artist.to_string(), (artist_id, HashMap::new()));
|
||||||
eprintln!("Artist #{artist_id}: {artist}");
|
|
||||||
artist_id
|
artist_id
|
||||||
} else {
|
} else {
|
||||||
artists.get(artist).unwrap().0
|
artists.get(artist).unwrap().0
|
||||||
@ -83,7 +81,6 @@ fn main() {
|
|||||||
album.to_string(),
|
album.to_string(),
|
||||||
(album_id, song.0.parent().map(|dir| dir.to_path_buf())),
|
(album_id, song.0.parent().map(|dir| dir.to_path_buf())),
|
||||||
);
|
);
|
||||||
eprintln!("Album #{album_id}: {album}");
|
|
||||||
album_id
|
album_id
|
||||||
} else {
|
} else {
|
||||||
let album = albums.get_mut(album).unwrap();
|
let album = albums.get_mut(album).unwrap();
|
||||||
@ -110,7 +107,7 @@ fn main() {
|
|||||||
.title()
|
.title()
|
||||||
.map(|title| title.to_string())
|
.map(|title| title.to_string())
|
||||||
.unwrap_or_else(|| song.0.file_stem().unwrap().to_string_lossy().into_owned());
|
.unwrap_or_else(|| song.0.file_stem().unwrap().to_string_lossy().into_owned());
|
||||||
let song_id = database.add_song_new(Song {
|
database.add_song_new(Song {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: title.clone(),
|
title: title.clone(),
|
||||||
location: DatabaseLocation {
|
location: DatabaseLocation {
|
||||||
@ -123,51 +120,39 @@ fn main() {
|
|||||||
general: GeneralData::default(),
|
general: GeneralData::default(),
|
||||||
cached_data: Arc::new(Mutex::new(None)),
|
cached_data: Arc::new(Mutex::new(None)),
|
||||||
});
|
});
|
||||||
eprintln!("Song #{song_id}: \"{title}\" @ {path:?}");
|
|
||||||
}
|
}
|
||||||
eprintln!("searching for covers...");
|
eprintln!("searching for covers...");
|
||||||
for (artist, (_artist_id, albums)) in &artists {
|
let mut single_images = HashMap::new();
|
||||||
for (album, (album_id, album_dir)) in albums {
|
for (i1, (_artist, (artist_id, albums))) in artists.iter().enumerate() {
|
||||||
|
eprint!("\rartist {}/{}", i1 + 1, artists.len());
|
||||||
|
for (_album, (album_id, album_dir)) in albums {
|
||||||
if let Some(album_dir) = album_dir {
|
if let Some(album_dir) = album_dir {
|
||||||
let mut cover = None;
|
if let Some(cover_id) = get_cover(&mut database, &lib_dir, album_dir) {
|
||||||
if let Ok(files) = fs::read_dir(album_dir) {
|
|
||||||
for file in files {
|
|
||||||
if let Ok(file) = file {
|
|
||||||
if let Ok(metadata) = file.metadata() {
|
|
||||||
if metadata.is_file() {
|
|
||||||
let path = file.path();
|
|
||||||
if matches!(
|
|
||||||
path.extension().and_then(|v| v.to_str()),
|
|
||||||
Some("png" | "jpg" | "jpeg")
|
|
||||||
) {
|
|
||||||
if cover.is_none()
|
|
||||||
|| cover
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|(_, size)| *size < metadata.len())
|
|
||||||
{
|
|
||||||
cover = Some((path, metadata.len()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some((path, _)) = cover {
|
|
||||||
let rel_path = path.strip_prefix(&lib_dir).unwrap().to_path_buf();
|
|
||||||
let cover_id = database.add_cover_new(Cover {
|
|
||||||
location: DatabaseLocation {
|
|
||||||
rel_path: rel_path.clone(),
|
|
||||||
},
|
|
||||||
data: Arc::new(Mutex::new((false, None))),
|
|
||||||
});
|
|
||||||
eprintln!("Cover #{cover_id}: {artist} - {album} -> {rel_path:?}");
|
|
||||||
database.albums_mut().get_mut(album_id).unwrap().cover = Some(cover_id);
|
database.albums_mut().get_mut(album_id).unwrap().cover = Some(cover_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(artist) = database.artists().get(artist_id) {
|
||||||
|
for song in artist.singles.clone() {
|
||||||
|
if let Some(dir) = AsRef::<Path>::as_ref(&lib_dir)
|
||||||
|
.join(&database.songs().get(&song).unwrap().location.rel_path)
|
||||||
|
.parent()
|
||||||
|
{
|
||||||
|
let cover_id = if let Some(cover_id) = single_images.get(dir) {
|
||||||
|
Some(*cover_id)
|
||||||
|
} else if let Some(cover_id) = get_cover(&mut database, &lib_dir, dir) {
|
||||||
|
single_images.insert(dir.to_owned(), cover_id);
|
||||||
|
Some(cover_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let song = database.songs_mut().get_mut(&song).unwrap();
|
||||||
|
song.cover = cover_id;
|
||||||
}
|
}
|
||||||
eprintln!("saving dbfile...");
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("\nsaving dbfile...");
|
||||||
database.save_database(None).unwrap();
|
database.save_database(None).unwrap();
|
||||||
eprintln!("done!");
|
eprintln!("done!");
|
||||||
}
|
}
|
||||||
@ -200,3 +185,40 @@ impl OnceNewline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_cover(database: &mut Database, lib_dir: &str, abs_dir: impl AsRef<Path>) -> Option<CoverId> {
|
||||||
|
let mut cover = None;
|
||||||
|
if let Ok(files) = fs::read_dir(abs_dir) {
|
||||||
|
for file in files {
|
||||||
|
if let Ok(file) = file {
|
||||||
|
if let Ok(metadata) = file.metadata() {
|
||||||
|
if metadata.is_file() {
|
||||||
|
let path = file.path();
|
||||||
|
if path.extension().and_then(|v| v.to_str()).is_some_and(|v| {
|
||||||
|
matches!(v.to_lowercase().as_str(), "png" | "jpg" | "jpeg")
|
||||||
|
}) {
|
||||||
|
if cover.is_none()
|
||||||
|
|| cover
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|(_, size)| *size < metadata.len())
|
||||||
|
{
|
||||||
|
cover = Some((path, metadata.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some((path, _)) = cover {
|
||||||
|
let rel_path = path.strip_prefix(&lib_dir).unwrap().to_path_buf();
|
||||||
|
Some(database.add_cover_new(Cover {
|
||||||
|
location: DatabaseLocation {
|
||||||
|
rel_path: rel_path.clone(),
|
||||||
|
},
|
||||||
|
data: Arc::new(Mutex::new((false, None))),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::{BufReader, Write},
|
io::{BufReader, Read, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{mpsc, Arc, Mutex},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@ -36,7 +36,11 @@ pub struct Database {
|
|||||||
/// true if a song is/should be playing
|
/// true if a song is/should be playing
|
||||||
pub playing: bool,
|
pub playing: bool,
|
||||||
pub command_sender: Option<mpsc::Sender<Command>>,
|
pub command_sender: Option<mpsc::Sender<Command>>,
|
||||||
|
pub remote_server_as_song_file_source:
|
||||||
|
Option<Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>>,
|
||||||
}
|
}
|
||||||
|
pub trait ClientIo: Read + Write + Send {}
|
||||||
|
impl<T: Read + Write + Send> ClientIo for T {}
|
||||||
// for custom server implementations, this enum should allow you to deal with updates from any context (writers such as tcp streams, sync/async mpsc senders, or via closure as a fallback)
|
// for custom server implementations, this enum should allow you to deal with updates from any context (writers such as tcp streams, sync/async mpsc senders, or via closure as a fallback)
|
||||||
pub enum UpdateEndpoint {
|
pub enum UpdateEndpoint {
|
||||||
Bytes(Box<dyn Write + Sync + Send>),
|
Bytes(Box<dyn Write + Sync + Send>),
|
||||||
@ -53,6 +57,9 @@ impl Database {
|
|||||||
// exit
|
// exit
|
||||||
panic!("DatabasePanic: {msg}");
|
panic!("DatabasePanic: {msg}");
|
||||||
}
|
}
|
||||||
|
pub fn is_client(&self) -> bool {
|
||||||
|
self.db_file.as_os_str().is_empty()
|
||||||
|
}
|
||||||
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
pub fn get_path(&self, location: &DatabaseLocation) -> PathBuf {
|
||||||
self.lib_directory.join(&location.rel_path)
|
self.lib_directory.join(&location.rel_path)
|
||||||
}
|
}
|
||||||
@ -177,6 +184,28 @@ impl Database {
|
|||||||
self.songs.insert(song.id, song)
|
self.songs.insert(song.id, song)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_song(&mut self, song: SongId) -> Option<Song> {
|
||||||
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
Some(removed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn remove_album(&mut self, song: SongId) -> Option<Song> {
|
||||||
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
Some(removed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn remove_artist(&mut self, song: SongId) -> Option<Song> {
|
||||||
|
if let Some(removed) = self.songs.remove(&song) {
|
||||||
|
Some(removed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_connection<T: Write>(&self, con: &mut T) -> Result<(), std::io::Error> {
|
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...
|
// TODO! this is slow because it clones everything - there has to be a better way...
|
||||||
Command::SyncDatabase(
|
Command::SyncDatabase(
|
||||||
@ -277,6 +306,15 @@ impl Database {
|
|||||||
Command::ModifyArtist(artist) => {
|
Command::ModifyArtist(artist) => {
|
||||||
_ = self.update_artist(artist);
|
_ = self.update_artist(artist);
|
||||||
}
|
}
|
||||||
|
Command::RemoveSong(song) => {
|
||||||
|
_ = self.remove_song(song);
|
||||||
|
}
|
||||||
|
Command::RemoveAlbum(album) => {
|
||||||
|
_ = self.remove_album(album);
|
||||||
|
}
|
||||||
|
Command::RemoveArtist(artist) => {
|
||||||
|
_ = self.remove_artist(artist);
|
||||||
|
}
|
||||||
Command::SetLibraryDirectory(new_dir) => {
|
Command::SetLibraryDirectory(new_dir) => {
|
||||||
self.lib_directory = new_dir;
|
self.lib_directory = new_dir;
|
||||||
}
|
}
|
||||||
@ -303,6 +341,7 @@ impl Database {
|
|||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
|
remote_server_as_song_file_source: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
|
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
|
||||||
@ -319,6 +358,7 @@ impl Database {
|
|||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
|
remote_server_as_song_file_source: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> {
|
pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> {
|
||||||
@ -339,6 +379,7 @@ impl Database {
|
|||||||
update_endpoints: vec![],
|
update_endpoints: vec![],
|
||||||
playing: false,
|
playing: false,
|
||||||
command_sender: None,
|
command_sender: None,
|
||||||
|
remote_server_as_song_file_source: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
/// saves the database's contents. save path can be overridden
|
/// saves the database's contents. save path can be overridden
|
||||||
|
@ -229,7 +229,7 @@ impl Queue {
|
|||||||
for action in actions {
|
for action in actions {
|
||||||
match action {
|
match action {
|
||||||
QueueAction::AddRandomSong(path) => {
|
QueueAction::AddRandomSong(path) => {
|
||||||
if !db.db_file.as_os_str().is_empty() {
|
if !db.is_client() {
|
||||||
if let Some(song) = db.songs().keys().choose(&mut rand::thread_rng()) {
|
if let Some(song) = db.songs().keys().choose(&mut rand::thread_rng()) {
|
||||||
db.apply_command(Command::QueueAdd(
|
db.apply_command(Command::QueueAdd(
|
||||||
path,
|
path,
|
||||||
@ -239,7 +239,7 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueueAction::SetShuffle(path, shuf, next) => {
|
QueueAction::SetShuffle(path, shuf, next) => {
|
||||||
if !db.db_file.as_os_str().is_empty() {
|
if !db.is_client() {
|
||||||
db.apply_command(Command::QueueSetShuffle(path, shuf, next));
|
db.apply_command(Command::QueueSetShuffle(path, shuf, next));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
thread::JoinHandle,
|
thread::JoinHandle,
|
||||||
};
|
};
|
||||||
@ -9,7 +9,8 @@ use std::{
|
|||||||
use crate::load::ToFromBytes;
|
use crate::load::ToFromBytes;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
database::Database, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
database::{ClientIo, Database},
|
||||||
|
AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -67,11 +68,13 @@ impl Song {
|
|||||||
Some(Err(_)) | Some(Ok(_)) => false,
|
Some(Err(_)) | Some(Ok(_)) => false,
|
||||||
};
|
};
|
||||||
if start_thread {
|
if start_thread {
|
||||||
let path = db.get_path(&self.location);
|
let src = if let Some(dlcon) = &db.remote_server_as_song_file_source {
|
||||||
|
Err((self.id, Arc::clone(dlcon)))
|
||||||
|
} else {
|
||||||
|
Ok(db.get_path(&self.location))
|
||||||
|
};
|
||||||
*cd = Some(Err(std::thread::spawn(move || {
|
*cd = Some(Err(std::thread::spawn(move || {
|
||||||
eprintln!("[info] thread started");
|
let data = Self::load_data(src)?;
|
||||||
let data = Self::load_data(&path)?;
|
|
||||||
eprintln!("[info] thread stopping after loading {path:?}");
|
|
||||||
Some(Arc::new(data))
|
Some(Arc::new(data))
|
||||||
})));
|
})));
|
||||||
true
|
true
|
||||||
@ -97,7 +100,12 @@ impl Song {
|
|||||||
let mut cd = self.cached_data.lock().unwrap();
|
let mut cd = self.cached_data.lock().unwrap();
|
||||||
*cd = match cd.take() {
|
*cd = match cd.take() {
|
||||||
None => {
|
None => {
|
||||||
if let Some(v) = Self::load_data(db.get_path(&self.location)) {
|
let src = if let Some(dlcon) = &db.remote_server_as_song_file_source {
|
||||||
|
Err((self.id, Arc::clone(dlcon)))
|
||||||
|
} else {
|
||||||
|
Ok(db.get_path(&self.location))
|
||||||
|
};
|
||||||
|
if let Some(v) = Self::load_data(src) {
|
||||||
Some(Ok(Arc::new(v)))
|
Some(Ok(Arc::new(v)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@ -113,19 +121,46 @@ impl Song {
|
|||||||
drop(cd);
|
drop(cd);
|
||||||
self.cached_data()
|
self.cached_data()
|
||||||
}
|
}
|
||||||
fn load_data<P: AsRef<Path>>(path: P) -> Option<Vec<u8>> {
|
fn load_data(
|
||||||
eprintln!("[info] loading song from {:?}", path.as_ref());
|
src: Result<
|
||||||
|
PathBuf,
|
||||||
|
(
|
||||||
|
SongId,
|
||||||
|
Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>,
|
||||||
|
),
|
||||||
|
>,
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
match src {
|
||||||
|
Ok(path) => {
|
||||||
|
eprintln!("[info] loading song from {:?}", path);
|
||||||
match std::fs::read(&path) {
|
match std::fs::read(&path) {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
eprintln!("[info] loaded song from {:?}", path.as_ref());
|
eprintln!("[info] loaded song from {:?}", path);
|
||||||
Some(v)
|
Some(v)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[info] error loading {:?}: {e:?}", path.as_ref());
|
eprintln!("[info] error loading {:?}: {e:?}", path);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err((id, dlcon)) => {
|
||||||
|
eprintln!("[info] loading song {id}");
|
||||||
|
match dlcon
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.song_file(id, true)
|
||||||
|
.expect("problem with downloader connection...")
|
||||||
|
{
|
||||||
|
Ok(data) => Some(data),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[info] error loading song {id}: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Display for Song {
|
impl Display for Song {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
@ -4,7 +4,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::data::{database::Database, CoverId};
|
use crate::data::{database::Database, CoverId, SongId};
|
||||||
|
|
||||||
pub struct Client<T: Write + Read>(BufReader<T>);
|
pub struct Client<T: Write + Read>(BufReader<T>);
|
||||||
impl<T: Write + Read> Client<T> {
|
impl<T: Write + Read> Client<T> {
|
||||||
@ -33,6 +33,34 @@ impl<T: Write + Read> Client<T> {
|
|||||||
Ok(Err(response))
|
Ok(Err(response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn song_file(
|
||||||
|
&mut self,
|
||||||
|
id: SongId,
|
||||||
|
blocking: bool,
|
||||||
|
) -> Result<Result<Vec<u8>, String>, std::io::Error> {
|
||||||
|
writeln!(
|
||||||
|
self.0.get_mut(),
|
||||||
|
"{}",
|
||||||
|
con_get_encode_string(&format!(
|
||||||
|
"song-file{}\n{id}",
|
||||||
|
if blocking { "-blocking" } else { "" }
|
||||||
|
))
|
||||||
|
)?;
|
||||||
|
let mut response = String::new();
|
||||||
|
self.0.read_line(&mut response)?;
|
||||||
|
let response = con_get_decode_line(&response);
|
||||||
|
if response.starts_with("len: ") {
|
||||||
|
if let Ok(len) = response[4..].trim().parse() {
|
||||||
|
let mut bytes = vec![0; len];
|
||||||
|
self.0.read_exact(&mut bytes)?;
|
||||||
|
Ok(Ok(bytes))
|
||||||
|
} else {
|
||||||
|
Ok(Err(response))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Err(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_one_connection_as_get(
|
pub fn handle_one_connection_as_get(
|
||||||
@ -72,6 +100,40 @@ pub fn handle_one_connection_as_get(
|
|||||||
writeln!(connection.get_mut(), "no cover")?;
|
writeln!(connection.get_mut(), "no cover")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"song-file" => {
|
||||||
|
if let Some(bytes) =
|
||||||
|
request
|
||||||
|
.next()
|
||||||
|
.and_then(|id| id.parse().ok())
|
||||||
|
.and_then(|id| {
|
||||||
|
db.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get_song(&id)
|
||||||
|
.and_then(|song| song.cached_data())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
writeln!(connection.get_mut(), "len: {}", bytes.len())?;
|
||||||
|
connection.get_mut().write_all(&bytes)?;
|
||||||
|
} else {
|
||||||
|
writeln!(connection.get_mut(), "no data")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"song-file-blocking" => {
|
||||||
|
if let Some(bytes) =
|
||||||
|
request
|
||||||
|
.next()
|
||||||
|
.and_then(|id| id.parse().ok())
|
||||||
|
.and_then(|id| {
|
||||||
|
let db = db.lock().unwrap();
|
||||||
|
db.get_song(&id).and_then(|song| song.cached_data_now(&db))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
writeln!(connection.get_mut(), "len: {}", bytes.len())?;
|
||||||
|
connection.get_mut().write_all(&bytes)?;
|
||||||
|
} else {
|
||||||
|
writeln!(connection.get_mut(), "no data")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use std::{
|
|||||||
net::{SocketAddr, TcpListener},
|
net::{SocketAddr, TcpListener},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{mpsc, Arc, Mutex},
|
sync::{mpsc, Arc, Mutex},
|
||||||
thread::{self, JoinHandle},
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ use crate::{
|
|||||||
database::{Cover, Database, UpdateEndpoint},
|
database::{Cover, Database, UpdateEndpoint},
|
||||||
queue::Queue,
|
queue::Queue,
|
||||||
song::Song,
|
song::Song,
|
||||||
|
AlbumId, ArtistId, SongId,
|
||||||
},
|
},
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
player::Player,
|
player::Player,
|
||||||
@ -46,6 +47,9 @@ pub enum Command {
|
|||||||
AddCover(Cover),
|
AddCover(Cover),
|
||||||
ModifySong(Song),
|
ModifySong(Song),
|
||||||
ModifyAlbum(Album),
|
ModifyAlbum(Album),
|
||||||
|
RemoveSong(SongId),
|
||||||
|
RemoveAlbum(AlbumId),
|
||||||
|
RemoveArtist(ArtistId),
|
||||||
ModifyArtist(Artist),
|
ModifyArtist(Artist),
|
||||||
SetLibraryDirectory(PathBuf),
|
SetLibraryDirectory(PathBuf),
|
||||||
}
|
}
|
||||||
@ -262,6 +266,18 @@ impl ToFromBytes for Command {
|
|||||||
s.write_all(&[0b10011100])?;
|
s.write_all(&[0b10011100])?;
|
||||||
artist.to_bytes(s)?;
|
artist.to_bytes(s)?;
|
||||||
}
|
}
|
||||||
|
Self::RemoveSong(song) => {
|
||||||
|
s.write_all(&[0b11010000])?;
|
||||||
|
song.to_bytes(s)?;
|
||||||
|
}
|
||||||
|
Self::RemoveAlbum(album) => {
|
||||||
|
s.write_all(&[0b11010011])?;
|
||||||
|
album.to_bytes(s)?;
|
||||||
|
}
|
||||||
|
Self::RemoveArtist(artist) => {
|
||||||
|
s.write_all(&[0b11011100])?;
|
||||||
|
artist.to_bytes(s)?;
|
||||||
|
}
|
||||||
Self::SetLibraryDirectory(path) => {
|
Self::SetLibraryDirectory(path) => {
|
||||||
s.write_all(&[0b00110001])?;
|
s.write_all(&[0b00110001])?;
|
||||||
path.to_bytes(s)?;
|
path.to_bytes(s)?;
|
||||||
@ -308,6 +324,9 @@ impl ToFromBytes for Command {
|
|||||||
0b10010000 => Self::ModifySong(ToFromBytes::from_bytes(s)?),
|
0b10010000 => Self::ModifySong(ToFromBytes::from_bytes(s)?),
|
||||||
0b10010011 => Self::ModifyAlbum(ToFromBytes::from_bytes(s)?),
|
0b10010011 => Self::ModifyAlbum(ToFromBytes::from_bytes(s)?),
|
||||||
0b10011100 => Self::ModifyArtist(ToFromBytes::from_bytes(s)?),
|
0b10011100 => Self::ModifyArtist(ToFromBytes::from_bytes(s)?),
|
||||||
|
0b11010000 => Self::RemoveSong(ToFromBytes::from_bytes(s)?),
|
||||||
|
0b11010011 => Self::RemoveAlbum(ToFromBytes::from_bytes(s)?),
|
||||||
|
0b11011100 => Self::RemoveArtist(ToFromBytes::from_bytes(s)?),
|
||||||
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
|
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
|
||||||
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
|
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
|
||||||
_ => {
|
_ => {
|
||||||
|
Loading…
Reference in New Issue
Block a user