mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-03-10 05:43: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
|
||||
|
||||
[dependencies]
|
||||
directories = "5.0.1"
|
||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||
regex = "1.9.3"
|
||||
speedy2d = { version = "1.12.0", optional = true }
|
||||
|
@ -1,15 +1,17 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::HashMap,
|
||||
eprintln,
|
||||
io::{Read, Write},
|
||||
io::Cursor,
|
||||
net::TcpStream,
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
time::Instant,
|
||||
usize,
|
||||
};
|
||||
|
||||
use musicdb_lib::{
|
||||
data::{database::Database, queue::Queue, AlbumId, ArtistId, SongId},
|
||||
data::{database::Database, queue::Queue, AlbumId, ArtistId, CoverId, SongId},
|
||||
load::ToFromBytes,
|
||||
server::{get, Command},
|
||||
};
|
||||
@ -17,6 +19,7 @@ use speedy2d::{
|
||||
color::Color,
|
||||
dimen::{UVec2, Vec2},
|
||||
font::Font,
|
||||
image::ImageHandle,
|
||||
shape::Rectangle,
|
||||
window::{
|
||||
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
|
||||
@ -34,10 +37,10 @@ pub enum GuiEvent {
|
||||
Exit,
|
||||
}
|
||||
|
||||
pub fn main<T: Write + Read + 'static + Sync + Send>(
|
||||
pub fn main(
|
||||
database: Arc<Mutex<Database>>,
|
||||
connection: TcpStream,
|
||||
get_con: get::Client<T>,
|
||||
get_con: get::Client<TcpStream>,
|
||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||
) {
|
||||
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(
|
||||
"MusicDB Client",
|
||||
WindowCreationOptions::new_fullscreen_borderless(),
|
||||
WindowCreationOptions::new_windowed(
|
||||
speedy2d::window::WindowSize::MarginPhysicalPixels(0),
|
||||
None,
|
||||
),
|
||||
)
|
||||
.expect("couldn't open window");
|
||||
*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,
|
||||
database,
|
||||
connection,
|
||||
get_con,
|
||||
Arc::new(Mutex::new(get_con)),
|
||||
event_sender_arc,
|
||||
sender,
|
||||
line_height,
|
||||
@ -127,10 +133,12 @@ pub struct Gui {
|
||||
pub event_sender: UserEventSender<GuiEvent>,
|
||||
pub database: Arc<Mutex<Database>>,
|
||||
pub connection: TcpStream,
|
||||
pub get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||
pub gui: GuiElem,
|
||||
pub size: UVec2,
|
||||
pub mouse_pos: Vec2,
|
||||
pub font: Font,
|
||||
pub covers: Option<HashMap<CoverId, GuiCover>>,
|
||||
pub last_draw: Instant,
|
||||
pub modifiers: ModifiersState,
|
||||
pub dragging: Option<(
|
||||
@ -144,11 +152,11 @@ pub struct Gui {
|
||||
pub scroll_pages_multiplier: f64,
|
||||
}
|
||||
impl Gui {
|
||||
fn new<T: Read + Write + 'static + Sync + Send>(
|
||||
fn new(
|
||||
font: Font,
|
||||
database: Arc<Mutex<Database>>,
|
||||
connection: TcpStream,
|
||||
get_con: get::Client<T>,
|
||||
get_con: Arc<Mutex<get::Client<TcpStream>>>,
|
||||
event_sender_arc: Arc<Mutex<Option<UserEventSender<GuiEvent>>>>,
|
||||
event_sender: UserEventSender<GuiEvent>,
|
||||
line_height: f32,
|
||||
@ -192,11 +200,11 @@ impl Gui {
|
||||
event_sender,
|
||||
database,
|
||||
connection,
|
||||
get_con,
|
||||
gui: GuiElem::new(WithFocusHotkey::new_noshift(
|
||||
VirtualKeyCode::Escape,
|
||||
GuiScreen::new(
|
||||
GuiElemCfg::default(),
|
||||
get_con,
|
||||
line_height,
|
||||
scroll_pixels_multiplier,
|
||||
scroll_lines_multiplier,
|
||||
@ -206,6 +214,7 @@ impl Gui {
|
||||
size: UVec2::ZERO,
|
||||
mouse_pos: Vec2::ZERO,
|
||||
font,
|
||||
covers: Some(HashMap::new()),
|
||||
// font: Font::new(include_bytes!("/usr/share/fonts/TTF/FiraSans-Regular.ttf")).unwrap(),
|
||||
last_draw: Instant::now(),
|
||||
modifiers: ModifiersState::default(),
|
||||
@ -226,8 +235,12 @@ pub trait GuiElemTrait {
|
||||
fn config(&self) -> &GuiElemCfg;
|
||||
fn config_mut(&mut self) -> &mut GuiElemCfg;
|
||||
/// note: drawing happens from the last to the first element, while priority is from first to last.
|
||||
/// if you wish to add a "high priority" child to a Vec<GuiElem> using push, .rev() the iterator in this method.
|
||||
/// 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> + '_>;
|
||||
/// defaults to true.
|
||||
fn draw_rev(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn any(&self) -> &dyn Any;
|
||||
fn any_mut(&mut self) -> &mut dyn Any;
|
||||
fn clone_gui(&self) -> Box<dyn GuiElemTrait>;
|
||||
@ -381,6 +394,7 @@ pub enum GuiAction {
|
||||
)>,
|
||||
),
|
||||
SetLineHeight(f32),
|
||||
LoadCover(CoverId),
|
||||
/// Run a custom closure with mutable access to the Gui struct
|
||||
Do(Box<dyn FnMut(&mut Gui)>),
|
||||
Exit,
|
||||
@ -403,6 +417,8 @@ pub struct DrawInfo<'a> {
|
||||
/// compare this to `pos` to find the mouse's relative position.
|
||||
pub mouse_pos: Vec2,
|
||||
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 child_has_keyboard_focus: bool,
|
||||
/// the height of one line of text (in pixels)
|
||||
@ -456,16 +472,23 @@ impl GuiElem {
|
||||
let focus_path = info.child_has_keyboard_focus;
|
||||
// children (in reverse order - first element has the highest priority)
|
||||
let kbd_focus_index = self.inner.config().keyboard_focus_index;
|
||||
for (i, c) in self
|
||||
.inner
|
||||
.children()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
{
|
||||
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
|
||||
c.draw(info, g);
|
||||
if self.inner.draw_rev() {
|
||||
for (i, c) in self
|
||||
.inner
|
||||
.children()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
{
|
||||
info.child_has_keyboard_focus = focus_path && i == kbd_focus_index;
|
||||
c.draw(info, g);
|
||||
}
|
||||
} 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
|
||||
info.child_has_keyboard_focus = focus_path;
|
||||
@ -697,6 +720,12 @@ impl Gui {
|
||||
self.gui
|
||||
.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::Exit => _ = self.event_sender.send_event(GuiEvent::Exit),
|
||||
GuiAction::SetIdle(v) => {
|
||||
@ -784,12 +813,15 @@ impl WindowHandler<GuiEvent> for Gui {
|
||||
Color::BLACK,
|
||||
);
|
||||
let mut dblock = self.database.lock().unwrap();
|
||||
let mut covers = self.covers.take().unwrap();
|
||||
let mut info = DrawInfo {
|
||||
actions: Vec::with_capacity(0),
|
||||
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
|
||||
database: &mut *dblock,
|
||||
font: &self.font,
|
||||
mouse_pos: self.mouse_pos,
|
||||
get_con: Arc::clone(&self.get_con),
|
||||
covers: &mut covers,
|
||||
helper: Some(helper),
|
||||
has_keyboard_focus: false,
|
||||
child_has_keyboard_focus: true,
|
||||
@ -827,7 +859,9 @@ impl WindowHandler<GuiEvent> for Gui {
|
||||
}
|
||||
}
|
||||
}
|
||||
// cleanup
|
||||
drop(info);
|
||||
self.covers = Some(covers);
|
||||
drop(dblock);
|
||||
for a in actions {
|
||||
self.exec_gui_action(a);
|
||||
@ -1013,3 +1047,62 @@ impl WindowHandler<GuiEvent> for Gui {
|
||||
.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)
|
||||
max_scroll: f32,
|
||||
last_height_px: f32,
|
||||
mouse_in_scrollbar: bool,
|
||||
mouse_scrolling: bool,
|
||||
mouse_scroll_margin_right: f32,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub enum ScrollBoxSizeUnit {
|
||||
@ -131,13 +134,13 @@ pub enum ScrollBoxSizeUnit {
|
||||
}
|
||||
impl ScrollBox {
|
||||
pub fn new(
|
||||
mut config: GuiElemCfg,
|
||||
config: GuiElemCfg,
|
||||
size_unit: ScrollBoxSizeUnit,
|
||||
children: Vec<(GuiElem, f32)>,
|
||||
) -> Self {
|
||||
// config.redraw = true;
|
||||
Self {
|
||||
config: config.w_scroll(),
|
||||
config: config.w_scroll().w_mouse(),
|
||||
children,
|
||||
size_unit,
|
||||
scroll_target: 0.0,
|
||||
@ -146,6 +149,9 @@ impl ScrollBox {
|
||||
height_bottom: 0.0,
|
||||
max_scroll: 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(
|
||||
self.children
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.map(|(v, _)| v)
|
||||
.skip_while(|v| v.inner.config().pos.bottom_right().y < 0.0)
|
||||
.take_while(|v| v.inner.config().pos.top_left().y < 1.0),
|
||||
.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 {
|
||||
self
|
||||
}
|
||||
@ -196,6 +204,8 @@ impl GuiElemTrait for ScrollBox {
|
||||
}
|
||||
// recalculate positions
|
||||
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;
|
||||
let mut y_pos = -self.scroll_display;
|
||||
for (e, h) in self.children.iter_mut() {
|
||||
@ -206,7 +216,10 @@ impl GuiElemTrait for ScrollBox {
|
||||
cfg.enabled = true;
|
||||
cfg.pos = Rectangle::new(
|
||||
Vec2::new(cfg.pos.top_left().x, 0.0f32.max(y_rel)),
|
||||
Vec2::new(cfg.pos.bottom_right().x, 1.0f32.min(y_rel + h_rel)),
|
||||
Vec2::new(
|
||||
cfg.pos.bottom_right().x.min(max_x),
|
||||
1.0f32.min(y_rel + h_rel),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
e.inner.config_mut().enabled = false;
|
||||
@ -217,6 +230,39 @@ impl GuiElemTrait for ScrollBox {
|
||||
self.max_scroll =
|
||||
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> {
|
||||
self.scroll_target = (self.scroll_target
|
||||
@ -224,6 +270,18 @@ impl GuiElemTrait for ScrollBox {
|
||||
.max(0.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 {
|
||||
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::{
|
||||
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,
|
||||
};
|
||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
|
||||
|
||||
use crate::{
|
||||
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui_base::{Button, Panel, ScrollBox},
|
||||
gui_text::{Label, TextField},
|
||||
};
|
||||
@ -18,37 +24,73 @@ pub struct GuiEdit {
|
||||
editable: Editable,
|
||||
editing: Editing,
|
||||
reload: bool,
|
||||
rebuild_main: bool,
|
||||
rebuild_changes: bool,
|
||||
send: bool,
|
||||
apply_change: mpsc::Sender<Box<dyn FnOnce(&mut Self)>>,
|
||||
change_recv: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub enum Editable {
|
||||
Artist(ArtistId),
|
||||
Album(AlbumId),
|
||||
Song(SongId),
|
||||
Artist(Vec<ArtistId>),
|
||||
Album(Vec<AlbumId>),
|
||||
Song(Vec<SongId>),
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub enum Editing {
|
||||
NotLoaded,
|
||||
Artist(Artist),
|
||||
Album(Album),
|
||||
Song(Song),
|
||||
Artist(Vec<Artist>, Vec<ArtistChange>),
|
||||
Album(Vec<Album>, Vec<AlbumChange>),
|
||||
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 {
|
||||
pub fn new(config: GuiElemCfg, edit: Editable) -> Self {
|
||||
let (apply_change, change_recv) = mpsc::channel();
|
||||
let ac1 = apply_change.clone();
|
||||
let ac2 = apply_change.clone();
|
||||
Self {
|
||||
config,
|
||||
config: config.w_drag_target(),
|
||||
editable: edit,
|
||||
editing: Editing::NotLoaded,
|
||||
reload: true,
|
||||
rebuild_main: true,
|
||||
rebuild_changes: true,
|
||||
send: false,
|
||||
apply_change,
|
||||
change_recv,
|
||||
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(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))),
|
||||
|_| vec![GuiAction::CloseEditPanel],
|
||||
@ -123,181 +165,536 @@ impl GuiElemTrait for GuiEdit {
|
||||
self.send = false;
|
||||
match &self.editing {
|
||||
Editing::NotLoaded => {}
|
||||
Editing::Artist(v) => info
|
||||
.actions
|
||||
.push(GuiAction::SendToServer(Command::ModifyArtist(v.clone()))),
|
||||
Editing::Album(v) => info
|
||||
.actions
|
||||
.push(GuiAction::SendToServer(Command::ModifyAlbum(v.clone()))),
|
||||
Editing::Song(v) => info
|
||||
.actions
|
||||
.push(GuiAction::SendToServer(Command::ModifySong(v.clone()))),
|
||||
Editing::Artist(v, changes) => {
|
||||
for v in v {
|
||||
let mut v = v.clone();
|
||||
for change in changes.iter() {
|
||||
match change {
|
||||
ArtistChange::SetName(n) => v.name = n.clone(),
|
||||
ArtistChange::SetCover(c) => v.cover = c.clone(),
|
||||
ArtistChange::RemoveAlbum(id) => {
|
||||
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 {
|
||||
self.reload = false;
|
||||
let prev = std::mem::replace(&mut self.editing, Editing::NotLoaded);
|
||||
self.editing = match &self.editable {
|
||||
Editable::Artist(id) => {
|
||||
if let Some(v) = info.database.artists().get(id).cloned() {
|
||||
Editing::Artist(v)
|
||||
let v = id
|
||||
.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 {
|
||||
Editing::NotLoaded
|
||||
}
|
||||
}
|
||||
Editable::Album(id) => {
|
||||
if let Some(v) = info.database.albums().get(id).cloned() {
|
||||
Editing::Album(v)
|
||||
let v = id
|
||||
.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 {
|
||||
Editing::NotLoaded
|
||||
}
|
||||
}
|
||||
Editable::Song(id) => {
|
||||
if let Some(v) = info.database.songs().get(id).cloned() {
|
||||
Editing::Song(v)
|
||||
let v = id
|
||||
.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 {
|
||||
Editing::NotLoaded
|
||||
}
|
||||
}
|
||||
};
|
||||
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 {
|
||||
self.config.redraw = false;
|
||||
let scrollbox = if self.children.len() > 3 {
|
||||
let o = self.children.pop();
|
||||
while self.children.len() > 3 {
|
||||
self.children.pop();
|
||||
if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||
for c in sb.children.iter_mut() {
|
||||
c.1 = info.line_height;
|
||||
}
|
||||
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 dragged {
|
||||
Dragging::Artist(id) => {
|
||||
if let Editing::Artist(a, _) = &self.editing {
|
||||
self.editable = Editable::Artist(a.iter().map(|v| v.id).chain([id]).collect())
|
||||
}
|
||||
}
|
||||
Dragging::Album(id) => {
|
||||
if let Editing::Album(a, _) = &self.editing {
|
||||
self.editable = Editable::Album(a.iter().map(|v| v.id).chain([id]).collect())
|
||||
}
|
||||
}
|
||||
Dragging::Song(id) => {
|
||||
if let Editing::Song(a, _) = &self.editing {
|
||||
self.editable = Editable::Song(a.iter().map(|v| v.id).chain([id]).collect())
|
||||
}
|
||||
}
|
||||
Dragging::Queue(_) => return vec![],
|
||||
}
|
||||
self.reload = true;
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
impl GuiEdit {
|
||||
fn rebuild_main(&mut self, info: &mut DrawInfo) {
|
||||
if let Some(sb) = self.children[0].inner.any_mut().downcast_mut::<ScrollBox>() {
|
||||
sb.children.clear();
|
||||
sb.config_mut().redraw = true;
|
||||
match &self.editing {
|
||||
Editing::NotLoaded => {
|
||||
self.children.push(GuiElem::new(Label::new(
|
||||
GuiElemCfg::default(),
|
||||
"nothing here".to_string(),
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.5, 0.5),
|
||||
)));
|
||||
}
|
||||
Editing::Artist(artist) => {
|
||||
self.children.push(GuiElem::new(Label::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.8, 0.08))),
|
||||
artist.name.clone(),
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.1, 0.5),
|
||||
)));
|
||||
self.children.push(GuiElem::new(Label::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 0.04))),
|
||||
"Artist".to_string(),
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.8, 0.5),
|
||||
)));
|
||||
self.children.push(GuiElem::new(Label::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.04), (1.0, 0.08))),
|
||||
format!("#{}", artist.id),
|
||||
Color::WHITE,
|
||||
None,
|
||||
Vec2::new(0.8, 0.5),
|
||||
)));
|
||||
let mut elems = vec![];
|
||||
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(
|
||||
GuiElemCfg::default(),
|
||||
vec![
|
||||
GuiElem::new(Label::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.6, 1.0))),
|
||||
format!(
|
||||
"{} album{}",
|
||||
artist.albums.len(),
|
||||
if artist.albums.len() != 1 { "s" } else { "" }
|
||||
),
|
||||
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,
|
||||
)),
|
||||
GuiElem::new(Button::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 1.0))),
|
||||
{
|
||||
let apply_change = self.apply_change.clone();
|
||||
let my_index = elems.len();
|
||||
move |_| {
|
||||
_ = apply_change.send(Box::new(move |s| {
|
||||
s.config.redraw = true;
|
||||
if let Ok(id) = s
|
||||
.children
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<ScrollBox>()
|
||||
.unwrap()
|
||||
.children[my_index]
|
||||
.0
|
||||
.inner
|
||||
.children()
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.inner
|
||||
.children()
|
||||
.next()
|
||||
.unwrap()
|
||||
.inner
|
||||
.any()
|
||||
.downcast_ref::<Label>()
|
||||
.unwrap()
|
||||
.content
|
||||
.get_text()
|
||||
.parse::<AlbumId>()
|
||||
{
|
||||
if let Editing::Artist(artist) = &mut s.editing
|
||||
{
|
||||
artist.albums.push(id);
|
||||
}
|
||||
}
|
||||
}));
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
vec![GuiElem::new(Label::new(
|
||||
GuiElemCfg::default(),
|
||||
"add".to_string(),
|
||||
Color::LIGHT_GRAY,
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
))],
|
||||
)),
|
||||
],
|
||||
vec![GuiElem::new(TextField::new(
|
||||
GuiElemCfg::default(),
|
||||
name,
|
||||
Color::LIGHT_GRAY,
|
||||
Color::WHITE,
|
||||
))],
|
||||
)),
|
||||
info.line_height,
|
||||
));
|
||||
for &album in &artist.albums {
|
||||
elems.push((
|
||||
GuiElem::new(Button::new(
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
fn get_id(s: &mut GuiEdit) -> Option<AlbumId> {
|
||||
s.children[0]
|
||||
.inner
|
||||
.children()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<Panel>()
|
||||
.unwrap()
|
||||
.children()
|
||||
.next()
|
||||
.unwrap()
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<TextField>()
|
||||
.unwrap()
|
||||
.label_input()
|
||||
.content
|
||||
.get_text()
|
||||
.parse()
|
||||
.ok()
|
||||
}
|
||||
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 |_| {
|
||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||
GuiElemCfg::default(),
|
||||
Editable::Album(album),
|
||||
)))]
|
||||
_ = 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![GuiElem::new(Label::new(
|
||||
GuiElemCfg::default(),
|
||||
if let Some(a) = info.database.albums().get(&album) {
|
||||
format!("Album: {}", a.name)
|
||||
} else {
|
||||
format!("Album #{album}")
|
||||
},
|
||||
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 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(),
|
||||
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 |_| {
|
||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||
GuiElemCfg::default(),
|
||||
Editable::Album(vec![album_id]),
|
||||
)))]
|
||||
},
|
||||
vec![GuiElem::new(Label::new(
|
||||
GuiElemCfg::default(),
|
||||
if let Some(a) = album {
|
||||
a.name.clone()
|
||||
} else {
|
||||
format!("#{album_id}")
|
||||
},
|
||||
Color::WHITE,
|
||||
None,
|
||||
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),
|
||||
@ -306,48 +703,9 @@ impl GuiElemTrait for GuiEdit {
|
||||
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 {
|
||||
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),
|
||||
)));
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
};
|
||||
match self.editing {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,7 +350,7 @@ impl GuiElemTrait for ListArtist {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||
GuiElemCfg::default(),
|
||||
crate::gui_edit::Editable::Artist(self.id),
|
||||
crate::gui_edit::Editable::Artist(vec![self.id]),
|
||||
)))]
|
||||
} else {
|
||||
vec![]
|
||||
@ -444,7 +444,7 @@ impl GuiElemTrait for ListAlbum {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||
GuiElemCfg::default(),
|
||||
crate::gui_edit::Editable::Album(self.id),
|
||||
crate::gui_edit::Editable::Album(vec![self.id]),
|
||||
)))]
|
||||
} else {
|
||||
vec![]
|
||||
@ -538,7 +538,7 @@ impl GuiElemTrait for ListSong {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
|
||||
GuiElemCfg::default(),
|
||||
crate::gui_edit::Editable::Song(self.id),
|
||||
crate::gui_edit::Editable::Song(vec![self.id]),
|
||||
)))]
|
||||
} else {
|
||||
vec![]
|
||||
|
@ -1,18 +1,13 @@
|
||||
use std::{
|
||||
io::{Cursor, Read, Write},
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use std::{collections::VecDeque, sync::Arc, time::Instant};
|
||||
|
||||
use musicdb_lib::{
|
||||
data::{queue::QueueContent, CoverId, SongId},
|
||||
server::{get, Command},
|
||||
};
|
||||
use speedy2d::{
|
||||
color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle, window::MouseButton,
|
||||
data::{CoverId, SongId},
|
||||
server::Command,
|
||||
};
|
||||
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
|
||||
|
||||
use crate::{
|
||||
gui::{adjust_area, adjust_pos, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui::{adjust_area, adjust_pos, GuiAction, GuiCover, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
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,
|
||||
children: Vec<GuiElem>,
|
||||
get_con: Option<get::Client<T>>,
|
||||
prev_song: Option<SongId>,
|
||||
cover_pos: Rectangle,
|
||||
cover_id: Option<CoverId>,
|
||||
cover: Option<ImageHandle>,
|
||||
new_cover: Option<JoinHandle<(get::Client<T>, Option<Vec<u8>>)>>,
|
||||
covers: VecDeque<(CoverId, Option<(bool, Instant)>)>,
|
||||
}
|
||||
impl<T: Read + Write> Clone for CurrentSong<T> {
|
||||
fn clone(&self) -> 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 {
|
||||
impl CurrentSong {
|
||||
pub fn new(config: GuiElemCfg) -> Self {
|
||||
Self {
|
||||
config,
|
||||
children: vec![
|
||||
@ -67,16 +46,13 @@ impl<T: Read + Write + 'static + Sync + Send> CurrentSong<T> {
|
||||
Vec2::new(0.0, 0.0),
|
||||
)),
|
||||
],
|
||||
get_con: Some(get_con),
|
||||
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
||||
cover_id: None,
|
||||
covers: VecDeque::new(),
|
||||
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 {
|
||||
&self.config
|
||||
}
|
||||
@ -108,97 +84,43 @@ impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
|
||||
// no song, nothing in queue
|
||||
None
|
||||
} else {
|
||||
self.cover = None;
|
||||
// end of last song
|
||||
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 there is a new song:
|
||||
if self.prev_song != new_song {
|
||||
self.config.redraw = true;
|
||||
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 {
|
||||
self.config.redraw = false;
|
||||
let (name, subtext) = if let Some(song) = new_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 (
|
||||
song.artist
|
||||
.as_ref()
|
||||
@ -246,6 +168,107 @@ impl<T: Read + Write + 'static + Sync + Send> GuiElemTrait for CurrentSong<T> {
|
||||
.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()));
|
||||
}
|
||||
}
|
||||
pub fn new<T: Read + Write + 'static + Sync + Send>(
|
||||
pub fn new(
|
||||
config: GuiElemCfg,
|
||||
get_con: get::Client<T>,
|
||||
line_height: f32,
|
||||
scroll_sensitivity_pixels: f64,
|
||||
scroll_sensitivity_lines: f64,
|
||||
@ -84,7 +83,6 @@ impl GuiScreen {
|
||||
GuiElem::new(StatusBar::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.9), (1.0, 1.0))),
|
||||
true,
|
||||
get_con,
|
||||
)),
|
||||
GuiElem::new(Settings::new(
|
||||
GuiElemCfg::default().disabled(),
|
||||
@ -293,18 +291,14 @@ pub struct StatusBar {
|
||||
idle_mode: f32,
|
||||
}
|
||||
impl StatusBar {
|
||||
pub fn new<T: Read + Write + 'static + Sync + Send>(
|
||||
config: GuiElemCfg,
|
||||
playing: bool,
|
||||
get_con: get::Client<T>,
|
||||
) -> Self {
|
||||
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
|
||||
Self {
|
||||
config,
|
||||
children: vec![
|
||||
GuiElem::new(CurrentSong::new(
|
||||
GuiElemCfg::at(Rectangle::new(Vec2::ZERO, Vec2::new(0.8, 1.0))),
|
||||
get_con,
|
||||
)),
|
||||
GuiElem::new(CurrentSong::new(GuiElemCfg::at(Rectangle::new(
|
||||
Vec2::ZERO,
|
||||
Vec2::new(0.8, 1.0),
|
||||
)))),
|
||||
GuiElem::new(PlayPauseToggle::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.85, 0.0), (0.95, 1.0))),
|
||||
false,
|
||||
|
@ -46,6 +46,10 @@ impl Content {
|
||||
pub fn color(&mut self) -> &mut 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 {
|
||||
pub fn new(
|
||||
@ -148,11 +152,23 @@ impl TextField {
|
||||
hint,
|
||||
color_hint,
|
||||
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 {
|
||||
fn config(&self) -> &GuiElemCfg {
|
||||
|
@ -13,10 +13,10 @@ use musicdb_lib::{
|
||||
data::{
|
||||
album::Album,
|
||||
artist::Artist,
|
||||
database::{Cover, Database},
|
||||
database::{ClientIo, Cover, Database},
|
||||
queue::QueueContent,
|
||||
song::Song,
|
||||
DatabaseLocation, GeneralData,
|
||||
CoverId, DatabaseLocation, GeneralData, SongId,
|
||||
},
|
||||
load::ToFromBytes,
|
||||
player::Player,
|
||||
@ -43,27 +43,32 @@ mod gui_text;
|
||||
#[cfg(feature = "speedy2d")]
|
||||
mod gui_wrappers;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Mode {
|
||||
Cli,
|
||||
Gui,
|
||||
SyncPlayer,
|
||||
FillDb,
|
||||
SyncPlayerWithoutData,
|
||||
}
|
||||
|
||||
fn get_config_file_path() -> PathBuf {
|
||||
if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
|
||||
let mut config_home: PathBuf = config_home.into();
|
||||
config_home.push("musicdb-client");
|
||||
config_home
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
let mut config_home: PathBuf = home.into();
|
||||
config_home.push(".config");
|
||||
config_home.push("musicdb-client");
|
||||
config_home
|
||||
} else {
|
||||
eprintln!("No config directory!");
|
||||
std::process::exit(24);
|
||||
}
|
||||
directories::ProjectDirs::from("", "", "musicdb-client")
|
||||
.unwrap()
|
||||
.config_dir()
|
||||
.to_path_buf()
|
||||
// if let Ok(config_home) = std::env::var("XDG_CONFIG_HOME") {
|
||||
// let mut config_home: PathBuf = config_home.into();
|
||||
// config_home.push("musicdb-client");
|
||||
// config_home
|
||||
// } else if let Ok(home) = std::env::var("HOME") {
|
||||
// let mut config_home: PathBuf = home.into();
|
||||
// config_home.push(".config");
|
||||
// config_home.push("musicdb-client");
|
||||
// config_home
|
||||
// } else {
|
||||
// eprintln!("No config directory!");
|
||||
// std::process::exit(24);
|
||||
// }
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -72,9 +77,9 @@ fn main() {
|
||||
Some("cli") => Mode::Cli,
|
||||
Some("gui") => Mode::Gui,
|
||||
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;
|
||||
}
|
||||
};
|
||||
@ -88,17 +93,23 @@ fn main() {
|
||||
Arc::new(Mutex::new(None));
|
||||
#[cfg(feature = "speedy2d")]
|
||||
let sender = Arc::clone(&update_gui_sender);
|
||||
let wants_player = matches!(mode, Mode::SyncPlayer);
|
||||
let con_thread = {
|
||||
let database = Arc::clone(&database);
|
||||
let mut con = con.try_clone().unwrap();
|
||||
// this is all you need to keep the db in sync
|
||||
thread::spawn(move || {
|
||||
let mut player = if wants_player {
|
||||
let mut player = if matches!(mode, Mode::SyncPlayer | Mode::SyncPlayerWithoutData) {
|
||||
Some(Player::new().unwrap())
|
||||
} else {
|
||||
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 {
|
||||
if let Some(player) = &mut player {
|
||||
let mut db = database.lock().unwrap();
|
||||
@ -147,201 +158,9 @@ fn main() {
|
||||
)
|
||||
};
|
||||
}
|
||||
Mode::SyncPlayer => {
|
||||
Mode::SyncPlayer | Mode::SyncPlayerWithoutData => {
|
||||
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> {
|
||||
@ -506,3 +325,12 @@ pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
|
||||
}
|
||||
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::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs::{self, FileType},
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::Write,
|
||||
ops::IndexMut,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use id3::TagLike;
|
||||
use musicdb_lib::data::{
|
||||
album::{self, Album},
|
||||
album::Album,
|
||||
artist::Artist,
|
||||
database::{Cover, Database},
|
||||
song::Song,
|
||||
DatabaseLocation, GeneralData,
|
||||
CoverId, DatabaseLocation, GeneralData,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@ -63,7 +62,6 @@ fn main() {
|
||||
general: GeneralData::default(),
|
||||
});
|
||||
artists.insert(artist.to_string(), (artist_id, HashMap::new()));
|
||||
eprintln!("Artist #{artist_id}: {artist}");
|
||||
artist_id
|
||||
} else {
|
||||
artists.get(artist).unwrap().0
|
||||
@ -83,7 +81,6 @@ fn main() {
|
||||
album.to_string(),
|
||||
(album_id, song.0.parent().map(|dir| dir.to_path_buf())),
|
||||
);
|
||||
eprintln!("Album #{album_id}: {album}");
|
||||
album_id
|
||||
} else {
|
||||
let album = albums.get_mut(album).unwrap();
|
||||
@ -110,7 +107,7 @@ fn main() {
|
||||
.title()
|
||||
.map(|title| title.to_string())
|
||||
.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,
|
||||
title: title.clone(),
|
||||
location: DatabaseLocation {
|
||||
@ -123,51 +120,39 @@ fn main() {
|
||||
general: GeneralData::default(),
|
||||
cached_data: Arc::new(Mutex::new(None)),
|
||||
});
|
||||
eprintln!("Song #{song_id}: \"{title}\" @ {path:?}");
|
||||
}
|
||||
eprintln!("searching for covers...");
|
||||
for (artist, (_artist_id, albums)) in &artists {
|
||||
for (album, (album_id, album_dir)) in albums {
|
||||
let mut single_images = HashMap::new();
|
||||
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 {
|
||||
let mut cover = None;
|
||||
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:?}");
|
||||
if let Some(cover_id) = get_cover(&mut database, &lib_dir, album_dir) {
|
||||
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();
|
||||
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::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::{BufReader, Write},
|
||||
io::{BufReader, Read, Write},
|
||||
path::PathBuf,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
@ -36,7 +36,11 @@ pub struct Database {
|
||||
/// true if a song is/should be playing
|
||||
pub playing: bool,
|
||||
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)
|
||||
pub enum UpdateEndpoint {
|
||||
Bytes(Box<dyn Write + Sync + Send>),
|
||||
@ -53,6 +57,9 @@ impl Database {
|
||||
// exit
|
||||
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 {
|
||||
self.lib_directory.join(&location.rel_path)
|
||||
}
|
||||
@ -177,6 +184,28 @@ impl Database {
|
||||
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> {
|
||||
// TODO! this is slow because it clones everything - there has to be a better way...
|
||||
Command::SyncDatabase(
|
||||
@ -277,6 +306,15 @@ impl Database {
|
||||
Command::ModifyArtist(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) => {
|
||||
self.lib_directory = new_dir;
|
||||
}
|
||||
@ -303,6 +341,7 @@ impl Database {
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
}
|
||||
}
|
||||
pub fn new_empty(path: PathBuf, lib_dir: PathBuf) -> Self {
|
||||
@ -319,6 +358,7 @@ impl Database {
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
}
|
||||
}
|
||||
pub fn load_database(path: PathBuf) -> Result<Self, std::io::Error> {
|
||||
@ -339,6 +379,7 @@ impl Database {
|
||||
update_endpoints: vec![],
|
||||
playing: false,
|
||||
command_sender: None,
|
||||
remote_server_as_song_file_source: None,
|
||||
})
|
||||
}
|
||||
/// saves the database's contents. save path can be overridden
|
||||
|
@ -229,7 +229,7 @@ impl Queue {
|
||||
for action in actions {
|
||||
match action {
|
||||
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()) {
|
||||
db.apply_command(Command::QueueAdd(
|
||||
path,
|
||||
@ -239,7 +239,7 @@ impl Queue {
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
@ -9,7 +9,8 @@ use std::{
|
||||
use crate::load::ToFromBytes;
|
||||
|
||||
use super::{
|
||||
database::Database, AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
||||
database::{ClientIo, Database},
|
||||
AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -67,11 +68,13 @@ impl Song {
|
||||
Some(Err(_)) | Some(Ok(_)) => false,
|
||||
};
|
||||
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 || {
|
||||
eprintln!("[info] thread started");
|
||||
let data = Self::load_data(&path)?;
|
||||
eprintln!("[info] thread stopping after loading {path:?}");
|
||||
let data = Self::load_data(src)?;
|
||||
Some(Arc::new(data))
|
||||
})));
|
||||
true
|
||||
@ -97,7 +100,12 @@ impl Song {
|
||||
let mut cd = self.cached_data.lock().unwrap();
|
||||
*cd = match cd.take() {
|
||||
None => {
|
||||
if let Some(v) = Self::load_data(db.get_path(&self.location)) {
|
||||
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)))
|
||||
} else {
|
||||
None
|
||||
@ -113,16 +121,43 @@ impl Song {
|
||||
drop(cd);
|
||||
self.cached_data()
|
||||
}
|
||||
fn load_data<P: AsRef<Path>>(path: P) -> Option<Vec<u8>> {
|
||||
eprintln!("[info] loading song from {:?}", path.as_ref());
|
||||
match std::fs::read(&path) {
|
||||
Ok(v) => {
|
||||
eprintln!("[info] loaded song from {:?}", path.as_ref());
|
||||
Some(v)
|
||||
fn load_data(
|
||||
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) {
|
||||
Ok(v) => {
|
||||
eprintln!("[info] loaded song from {:?}", path);
|
||||
Some(v)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[info] error loading {:?}: {e:?}", path);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[info] error loading {:?}: {e:?}", path.as_ref());
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::data::{database::Database, CoverId};
|
||||
use crate::data::{database::Database, CoverId, SongId};
|
||||
|
||||
pub struct Client<T: Write + Read>(BufReader<T>);
|
||||
impl<T: Write + Read> Client<T> {
|
||||
@ -33,6 +33,34 @@ impl<T: Write + Read> Client<T> {
|
||||
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(
|
||||
@ -72,6 +100,40 @@ pub fn handle_one_connection_as_get(
|
||||
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},
|
||||
path::PathBuf,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
thread::{self, JoinHandle},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ use crate::{
|
||||
database::{Cover, Database, UpdateEndpoint},
|
||||
queue::Queue,
|
||||
song::Song,
|
||||
AlbumId, ArtistId, SongId,
|
||||
},
|
||||
load::ToFromBytes,
|
||||
player::Player,
|
||||
@ -46,6 +47,9 @@ pub enum Command {
|
||||
AddCover(Cover),
|
||||
ModifySong(Song),
|
||||
ModifyAlbum(Album),
|
||||
RemoveSong(SongId),
|
||||
RemoveAlbum(AlbumId),
|
||||
RemoveArtist(ArtistId),
|
||||
ModifyArtist(Artist),
|
||||
SetLibraryDirectory(PathBuf),
|
||||
}
|
||||
@ -262,6 +266,18 @@ impl ToFromBytes for Command {
|
||||
s.write_all(&[0b10011100])?;
|
||||
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) => {
|
||||
s.write_all(&[0b00110001])?;
|
||||
path.to_bytes(s)?;
|
||||
@ -308,6 +324,9 @@ impl ToFromBytes for Command {
|
||||
0b10010000 => Self::ModifySong(ToFromBytes::from_bytes(s)?),
|
||||
0b10010011 => Self::ModifyAlbum(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)?),
|
||||
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
|
||||
_ => {
|
||||
|
Loading…
Reference in New Issue
Block a user