This commit is contained in:
Mark 2023-09-07 21:53:05 +02:00
parent 0e5e33367d
commit ac16628c31
16 changed files with 1173 additions and 623 deletions

View File

@ -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 }

View File

@ -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
}
}
}
}
}

View File

@ -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 {

View File

@ -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 {
_ => {}
} }
} }
} }

View File

@ -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![]

View File

@ -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);
}
} }
} }

View File

@ -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,

View File

@ -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 {

View File

@ -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.

View File

@ -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
}
}

View File

@ -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

View File

@ -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));
} }
} }

View File

@ -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 {

View File

@ -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")?;
}
}
_ => {} _ => {}
} }
} }

View File

@ -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)?),
_ => { _ => {