mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-09-13 15:16:14 +02:00
Compare commits
No commits in common. "11383651803edf66d717acdcadb182fe4f1e86fd" and "bf100f38e37a125d2a099d888b6f4a23a749f037" have entirely different histories.
1138365180
...
bf100f38e3
@ -7,11 +7,11 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
musicdb-lib = { path = "../musicdb-lib", default-features = false }
|
musicdb-lib = { path = "../musicdb-lib", default-features = false }
|
||||||
clap = { version = "4.5.45", features = ["derive"] }
|
clap = { version = "4.4.6", features = ["derive"] }
|
||||||
directories = "6.0.0"
|
directories = "5.0.1"
|
||||||
regex = "1.11.1"
|
regex = "1.9.3"
|
||||||
speedy2d = { version = "2.1.0", optional = true }
|
speedy2d = { version = "1.12.0", optional = true }
|
||||||
toml = "0.9.5"
|
toml = "0.7.6"
|
||||||
# musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
|
# musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
|
||||||
uianimator = "0.1.1"
|
uianimator = "0.1.1"
|
||||||
|
|
||||||
@ -27,9 +27,7 @@ default = ["gui", "default-playback"]
|
|||||||
# enables syncplayer modes, where the client mirrors the server's playback
|
# enables syncplayer modes, where the client mirrors the server's playback
|
||||||
gui = ["speedy2d"]
|
gui = ["speedy2d"]
|
||||||
# merscfg = ["mers", "gui"]
|
# merscfg = ["mers", "gui"]
|
||||||
merscfg = []
|
|
||||||
# mers = ["musicdb-mers"]
|
# mers = ["musicdb-mers"]
|
||||||
mers = []
|
|
||||||
playback = []
|
playback = []
|
||||||
default-playback = ["playback", "musicdb-lib/default-playback"]
|
default-playback = ["playback", "musicdb-lib/default-playback"]
|
||||||
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
|
playback-via-playback-rs = ["playback", "musicdb-lib/playback-via-playback-rs"]
|
||||||
|
@ -1258,8 +1258,8 @@ pub enum GuiAction {
|
|||||||
Do(Box<dyn FnOnce(&mut Gui)>),
|
Do(Box<dyn FnOnce(&mut Gui)>),
|
||||||
Exit,
|
Exit,
|
||||||
EditSongs(Vec<Song>),
|
EditSongs(Vec<Song>),
|
||||||
// EditAlbums(Vec<Album>, bool),
|
// EditAlbums(Vec<Album>),
|
||||||
// EditArtists(Vec<Artist>, bool),
|
// EditArtists(Vec<Artist>),
|
||||||
OpenAddSongsMenu,
|
OpenAddSongsMenu,
|
||||||
CloseAddSongsMenu,
|
CloseAddSongsMenu,
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,130 @@
|
|||||||
use std::time::Instant;
|
use std::{
|
||||||
|
ops::{Add, AddAssign, Mul, MulAssign, Sub},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator};
|
pub struct AnimationController<F> {
|
||||||
|
pub last_updated: Instant,
|
||||||
pub struct AnimationController {
|
pub value: F,
|
||||||
speed: f64,
|
pub speed: F,
|
||||||
anim: DefaultAnimatorF64Quadratic,
|
pub max_speed: F,
|
||||||
|
pub target: F,
|
||||||
|
/// while the time remaining to finish the animation is above this, we accelerate (higher -> stop acceleration earlier)
|
||||||
|
pub accel_until: F,
|
||||||
|
/// if the time remaining to finish the animation drops below this, we decelerate (higher -> start decelerating earlier)
|
||||||
|
pub decel_while: F,
|
||||||
|
pub acceleration: F,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimationController {
|
pub trait Float:
|
||||||
pub fn new(value: f64, target: f64, speed: f64) -> Self {
|
Sized
|
||||||
let mut anim = DefaultAnimatorF64Quadratic::new(value, speed);
|
+ Clone
|
||||||
if value != target {
|
+ Copy
|
||||||
anim.set_target(target, Instant::now());
|
+ Add<Self, Output = Self>
|
||||||
}
|
+ Sub<Self, Output = Self>
|
||||||
AnimationController { speed, anim }
|
+ std::ops::Neg<Output = Self>
|
||||||
}
|
+ Mul<Self, Output = Self>
|
||||||
pub fn target(&self) -> f64 {
|
+ MulAssign<Self>
|
||||||
self.anim.target()
|
+ AddAssign<Self>
|
||||||
}
|
+ PartialOrd<Self>
|
||||||
pub fn set_target(&mut self, now: Instant, target: f64) {
|
{
|
||||||
self.anim.set_target(target, now);
|
fn zero() -> Self;
|
||||||
}
|
/// 1/1000
|
||||||
pub fn update(&mut self, now: Instant, instant: bool) -> Result<f64, f64> {
|
fn milli() -> Self;
|
||||||
if self.anim.target() != self.anim.get_value(now) {
|
fn duration_secs(d: Duration) -> Self;
|
||||||
if instant {
|
fn abs(self) -> Self {
|
||||||
let target = self.anim.target();
|
if self < Self::zero() {
|
||||||
self.anim = DefaultAnimatorF64Quadratic::new(target, self.speed);
|
-self
|
||||||
Ok(target)
|
|
||||||
} else {
|
|
||||||
Ok(self.anim.get_value(now))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Err(self.anim.target())
|
self
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn value(&mut self, now: Instant) -> f64 {
|
|
||||||
match self.update(now, false) {
|
|
||||||
Ok(v) | Err(v) => v,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<F: Float> AnimationController<F> {
|
||||||
|
pub fn new(
|
||||||
|
value: F,
|
||||||
|
target: F,
|
||||||
|
acceleration: F,
|
||||||
|
max_speed: F,
|
||||||
|
accel_until: F,
|
||||||
|
decel_while: F,
|
||||||
|
now: Instant,
|
||||||
|
) -> Self {
|
||||||
|
AnimationController {
|
||||||
|
last_updated: now,
|
||||||
|
value,
|
||||||
|
speed: F::zero(),
|
||||||
|
max_speed,
|
||||||
|
target,
|
||||||
|
accel_until,
|
||||||
|
decel_while,
|
||||||
|
acceleration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn ignore_elapsed_time(&mut self, now: Instant) {
|
||||||
|
self.last_updated = now;
|
||||||
|
}
|
||||||
|
pub fn update(&mut self, now: Instant, instant: bool) -> bool {
|
||||||
|
let changed = if self.target != self.value {
|
||||||
|
if instant {
|
||||||
|
self.value = self.target;
|
||||||
|
} else {
|
||||||
|
let inc = self.target > self.value;
|
||||||
|
let seconds = F::duration_secs(now.duration_since(self.last_updated));
|
||||||
|
let ref1 = self.value + self.speed * self.accel_until;
|
||||||
|
let ref2 = self.value + self.speed * self.decel_while;
|
||||||
|
let speed_diff = match (ref1 < self.target, ref2 > self.target) {
|
||||||
|
(true, false) => self.acceleration,
|
||||||
|
(false, true) => -self.acceleration,
|
||||||
|
(true, true) | (false, false) => F::zero(),
|
||||||
|
};
|
||||||
|
self.speed += speed_diff;
|
||||||
|
if self.speed.abs() > self.max_speed {
|
||||||
|
if self.speed < F::zero() {
|
||||||
|
self.speed = -self.max_speed;
|
||||||
|
} else {
|
||||||
|
self.speed = self.max_speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.value += self.speed * seconds;
|
||||||
|
self.speed += speed_diff;
|
||||||
|
if (self.target - self.value).abs() < self.speed * F::milli()
|
||||||
|
|| inc != (self.target > self.value)
|
||||||
|
{
|
||||||
|
// overshoot or target reached
|
||||||
|
self.value = self.target;
|
||||||
|
self.speed = F::zero();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
self.last_updated = now;
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Float for f32 {
|
||||||
|
fn zero() -> Self {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
fn milli() -> Self {
|
||||||
|
0.001
|
||||||
|
}
|
||||||
|
fn duration_secs(d: Duration) -> Self {
|
||||||
|
d.as_secs_f32().min(0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Float for f64 {
|
||||||
|
fn zero() -> Self {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
fn milli() -> Self {
|
||||||
|
0.001
|
||||||
|
}
|
||||||
|
fn duration_secs(d: Duration) -> Self {
|
||||||
|
d.as_secs_f64().min(0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -355,14 +355,6 @@ impl<C: GuiElemChildren> Button<C> {
|
|||||||
action: Arc::new(action),
|
action: Arc::new(action),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn disable(&mut self) {
|
|
||||||
self.config.mouse_events = false;
|
|
||||||
self.config.keyboard_events_focus = false;
|
|
||||||
}
|
|
||||||
pub fn enable(&mut self) {
|
|
||||||
self.config.mouse_events = true;
|
|
||||||
self.config.keyboard_events_focus = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl<C: GuiElemChildren + 'static> GuiElem for Button<C> {
|
impl<C: GuiElemChildren + 'static> GuiElem for Button<C> {
|
||||||
fn config(&self) -> &GuiElemCfg {
|
fn config(&self) -> &GuiElemCfg {
|
||||||
@ -612,7 +604,7 @@ impl GuiElem for Slider {
|
|||||||
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
|
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
|
||||||
if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) {
|
if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) {
|
||||||
self.display = !self.display;
|
self.display = !self.display;
|
||||||
self.display_since = Some(info.time);
|
self.display_since = Some(Instant::now());
|
||||||
self.config.redraw = true;
|
self.config.redraw = true;
|
||||||
}
|
}
|
||||||
let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25);
|
let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25);
|
||||||
|
@ -1,315 +0,0 @@
|
|||||||
use std::{collections::BTreeSet, time::Instant};
|
|
||||||
|
|
||||||
use speedy2d::{
|
|
||||||
color::Color,
|
|
||||||
dimen::{Vec2, Vector2},
|
|
||||||
shape::Rectangle,
|
|
||||||
Graphics2D,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
gui::{DrawInfo, GuiElem, GuiElemCfg},
|
|
||||||
gui_anim::AnimationController,
|
|
||||||
gui_base::{Button, ScrollBox},
|
|
||||||
gui_text::{Label, TextField},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const ELEM_HEIGHT: f32 = 32.0;
|
|
||||||
|
|
||||||
pub enum Event {
|
|
||||||
RemoveTag(String),
|
|
||||||
AddTag(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditorForAnyTagInList {
|
|
||||||
config: GuiElemCfg,
|
|
||||||
pub tag: String,
|
|
||||||
label: Label,
|
|
||||||
rm_button: Button<[IconDelete; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EditorForAnyTagInList {
|
|
||||||
pub fn new<T: From<Event> + 'static>(
|
|
||||||
tag: String,
|
|
||||||
sender: std::sync::mpsc::Sender<T>,
|
|
||||||
config: GuiElemCfg,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
tag: tag.clone(),
|
|
||||||
label: Label::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
tag.clone(),
|
|
||||||
Color::WHITE,
|
|
||||||
None,
|
|
||||||
Vector2::new(0.0, 0.5),
|
|
||||||
),
|
|
||||||
rm_button: Button::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
move |btn| {
|
|
||||||
btn.disable();
|
|
||||||
sender.send(Event::RemoveTag(tag.clone()).into()).unwrap();
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
[IconDelete::new(GuiElemCfg::default())],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GuiElem for EditorForAnyTagInList {
|
|
||||||
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
|
|
||||||
let rm_button_size = (info.pos.height() * 0.8).min(info.pos.width() * 0.33);
|
|
||||||
let rm_button_padding = (info.pos.height() - rm_button_size) / 2.0;
|
|
||||||
let label_padding = info.pos.height() * 0.05;
|
|
||||||
let x_split = (info.pos.width() - rm_button_size) / info.pos.width();
|
|
||||||
self.rm_button.config_mut().pos = Rectangle::from_tuples(
|
|
||||||
(x_split, rm_button_padding / info.pos.height()),
|
|
||||||
(1.0, 1.0 - rm_button_padding / info.pos.height()),
|
|
||||||
);
|
|
||||||
self.label.config_mut().pos = Rectangle::from_tuples(
|
|
||||||
(0.0, label_padding / info.pos.height()),
|
|
||||||
(x_split, 1.0 - label_padding / info.pos.height()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fn config(&self) -> &GuiElemCfg {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
fn config_mut(&mut self) -> &mut GuiElemCfg {
|
|
||||||
&mut self.config
|
|
||||||
}
|
|
||||||
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
||||||
Box::new([self.label.elem_mut(), self.rm_button.elem_mut()].into_iter())
|
|
||||||
}
|
|
||||||
fn any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem(&self) -> &dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem_mut(&mut self) -> &mut dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IconDelete {
|
|
||||||
config: GuiElemCfg,
|
|
||||||
}
|
|
||||||
impl IconDelete {
|
|
||||||
pub fn new(config: GuiElemCfg) -> Self {
|
|
||||||
Self { config }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl GuiElem for IconDelete {
|
|
||||||
fn draw(&mut self, info: &mut DrawInfo, g: &mut Graphics2D) {
|
|
||||||
let thickness = (info.pos.height() * 0.01).max(1.0);
|
|
||||||
g.draw_line(
|
|
||||||
*info.pos.top_left(),
|
|
||||||
*info.pos.bottom_right(),
|
|
||||||
thickness,
|
|
||||||
Color::GRAY,
|
|
||||||
);
|
|
||||||
g.draw_line(
|
|
||||||
info.pos.top_right(),
|
|
||||||
info.pos.bottom_left(),
|
|
||||||
thickness,
|
|
||||||
Color::GRAY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fn config(&self) -> &GuiElemCfg {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
fn config_mut(&mut self) -> &mut GuiElemCfg {
|
|
||||||
&mut self.config
|
|
||||||
}
|
|
||||||
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
||||||
Box::new([].into_iter())
|
|
||||||
}
|
|
||||||
fn any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem(&self) -> &dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem_mut(&mut self) -> &mut dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditorForAnyTagAdder<T: From<Event>> {
|
|
||||||
config: GuiElemCfg,
|
|
||||||
event_sender: std::sync::mpsc::Sender<T>,
|
|
||||||
/// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6)
|
|
||||||
pub open_prog: AnimationController,
|
|
||||||
expand_to: f32,
|
|
||||||
c_value: TextField,
|
|
||||||
c_picker: ScrollBox<Vec<Button<[Label; 1]>>>,
|
|
||||||
last_search: String,
|
|
||||||
}
|
|
||||||
impl<T: From<Event> + 'static> EditorForAnyTagAdder<T> {
|
|
||||||
pub fn new(event_sender: std::sync::mpsc::Sender<T>) -> Self {
|
|
||||||
let expand_to = 7.0;
|
|
||||||
Self {
|
|
||||||
config: GuiElemCfg::default(),
|
|
||||||
event_sender,
|
|
||||||
open_prog: AnimationController::new(1.0, 1.0, 4.0),
|
|
||||||
expand_to,
|
|
||||||
c_value: TextField::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
"artist".to_owned(),
|
|
||||||
Color::DARK_GRAY,
|
|
||||||
Color::WHITE,
|
|
||||||
),
|
|
||||||
c_picker: ScrollBox::new(
|
|
||||||
GuiElemCfg::default().disabled(),
|
|
||||||
crate::gui_base::ScrollBoxSizeUnit::Pixels,
|
|
||||||
vec![],
|
|
||||||
vec![],
|
|
||||||
ELEM_HEIGHT,
|
|
||||||
),
|
|
||||||
last_search: String::from("\n"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn clear(&mut self, now: Instant) {
|
|
||||||
self.last_search = "\n".to_owned();
|
|
||||||
self.c_value.c_input.content.text().clear();
|
|
||||||
self.open_prog.set_target(now, 1.0);
|
|
||||||
self.config_mut().redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T: From<Event> + 'static> GuiElem for EditorForAnyTagAdder<T> {
|
|
||||||
fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
|
||||||
let picker_enabled = self.open_prog.value(info.time) > 1.0;
|
|
||||||
self.c_picker.config_mut().enabled = picker_enabled;
|
|
||||||
if picker_enabled {
|
|
||||||
let split = 1.0 / self.open_prog.value(info.time) as f32;
|
|
||||||
self.c_value.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split));
|
|
||||||
self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0));
|
|
||||||
} else {
|
|
||||||
self.c_value.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, 1.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
let search = self.c_value.c_input.content.get_text().to_lowercase();
|
|
||||||
let search_changed = &self.last_search != &search;
|
|
||||||
if self.config.redraw || search_changed {
|
|
||||||
*self.c_value.c_input.content.color() = Color::WHITE;
|
|
||||||
if search_changed {
|
|
||||||
if search.is_empty() {
|
|
||||||
self.open_prog.set_target(info.time, 1.0);
|
|
||||||
} else {
|
|
||||||
self.open_prog.set_target(info.time, self.expand_to as f64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut tags = info
|
|
||||||
.database
|
|
||||||
.songs()
|
|
||||||
.values()
|
|
||||||
.flat_map(|s| s.general.tags.iter())
|
|
||||||
.chain(
|
|
||||||
info.database
|
|
||||||
.songs()
|
|
||||||
.values()
|
|
||||||
.flat_map(|s| s.general.tags.iter()),
|
|
||||||
)
|
|
||||||
.chain(
|
|
||||||
info.database
|
|
||||||
.songs()
|
|
||||||
.values()
|
|
||||||
.flat_map(|s| s.general.tags.iter()),
|
|
||||||
)
|
|
||||||
.filter(|tag| tag.to_lowercase().contains(&search))
|
|
||||||
.map(|tag| tag.clone())
|
|
||||||
.collect::<BTreeSet<_>>();
|
|
||||||
if !tags.contains(self.c_value.c_input.content.get_text()) {
|
|
||||||
tags.insert(self.c_value.c_input.content.get_text().clone());
|
|
||||||
}
|
|
||||||
self.c_picker.children = tags
|
|
||||||
.iter()
|
|
||||||
.map(|tag| {
|
|
||||||
let sender = self.event_sender.clone();
|
|
||||||
Button::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
{
|
|
||||||
let tag = tag.clone();
|
|
||||||
move |_| {
|
|
||||||
sender.send(Event::AddTag(tag.clone()).into()).unwrap();
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[Label::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
tag.clone(),
|
|
||||||
Color::LIGHT_GRAY,
|
|
||||||
None,
|
|
||||||
Vec2::new(0.0, 0.5),
|
|
||||||
)],
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.c_picker.config_mut().redraw = true;
|
|
||||||
self.last_search = search;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn config(&self) -> &GuiElemCfg {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
fn config_mut(&mut self) -> &mut GuiElemCfg {
|
|
||||||
&mut self.config
|
|
||||||
}
|
|
||||||
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
||||||
Box::new([self.c_value.elem_mut(), self.c_picker.elem_mut()].into_iter())
|
|
||||||
}
|
|
||||||
fn any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem(&self) -> &dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem_mut(&mut self) -> &mut dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SpacerForScrollBox {
|
|
||||||
config: GuiElemCfg,
|
|
||||||
}
|
|
||||||
impl SpacerForScrollBox {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
config: GuiElemCfg::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl GuiElem for SpacerForScrollBox {
|
|
||||||
fn draw(&mut self, _info: &mut DrawInfo, _g: &mut Graphics2D) {}
|
|
||||||
fn config(&self) -> &GuiElemCfg {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
fn config_mut(&mut self) -> &mut GuiElemCfg {
|
|
||||||
&mut self.config
|
|
||||||
}
|
|
||||||
fn children(&mut self) -> Box<dyn Iterator<Item = &mut dyn GuiElem> + '_> {
|
|
||||||
Box::new([].into_iter())
|
|
||||||
}
|
|
||||||
fn any(&self) -> &dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem(&self) -> &dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
fn elem_mut(&mut self) -> &mut dyn GuiElem {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,12 +11,13 @@ use crate::{
|
|||||||
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren},
|
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren},
|
||||||
gui_anim::AnimationController,
|
gui_anim::AnimationController,
|
||||||
gui_base::{Button, Panel, ScrollBox},
|
gui_base::{Button, Panel, ScrollBox},
|
||||||
gui_edit_any::{EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox, ELEM_HEIGHT},
|
|
||||||
gui_text::{Label, TextField},
|
gui_text::{Label, TextField},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Fix bug where after selecting an artist you can't mouse-click the buttons anymore (to change it)
|
// TODO: Fix bug where after selecting an artist you can't mouse-click the buttons anymore (to change it)
|
||||||
|
|
||||||
|
const ELEM_HEIGHT: f32 = 32.0;
|
||||||
|
|
||||||
pub struct EditorForSongs {
|
pub struct EditorForSongs {
|
||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
songs: Vec<Song>,
|
songs: Vec<Song>,
|
||||||
@ -32,21 +33,11 @@ pub enum Event {
|
|||||||
Close,
|
Close,
|
||||||
Apply,
|
Apply,
|
||||||
SetArtist(String, Option<ArtistId>),
|
SetArtist(String, Option<ArtistId>),
|
||||||
GeneralEvent(super::gui_edit_any::Event),
|
|
||||||
}
|
}
|
||||||
impl From<super::gui_edit_any::Event> for Event {
|
|
||||||
fn from(value: super::gui_edit_any::Event) -> Self {
|
|
||||||
Self::GeneralEvent(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditorForSongElems {
|
pub struct EditorForSongElems {
|
||||||
c_title: TextField,
|
c_title: TextField,
|
||||||
c_artist: EditorForSongArtistChooser,
|
c_artist: EditorForSongArtistChooser,
|
||||||
c_album: Label,
|
c_album: Label,
|
||||||
c_tags: Vec<EditorForAnyTagInList>,
|
|
||||||
c_new_tag: EditorForAnyTagAdder<Event>,
|
|
||||||
c_spacers: [SpacerForScrollBox; 4],
|
|
||||||
}
|
}
|
||||||
impl GuiElemChildren for EditorForSongElems {
|
impl GuiElemChildren for EditorForSongElems {
|
||||||
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn crate::gui::GuiElem> + '_> {
|
fn iter(&mut self) -> Box<dyn Iterator<Item = &mut dyn crate::gui::GuiElem> + '_> {
|
||||||
@ -56,13 +47,11 @@ impl GuiElemChildren for EditorForSongElems {
|
|||||||
self.c_artist.elem_mut(),
|
self.c_artist.elem_mut(),
|
||||||
self.c_album.elem_mut(),
|
self.c_album.elem_mut(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter(),
|
||||||
.chain(self.c_tags.iter_mut().map(|e| e.elem_mut()))
|
|
||||||
.chain(std::iter::once(self.c_new_tag.elem_mut())),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
3 + self.c_tags.len() + 1 + self.c_spacers.len()
|
3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,32 +96,6 @@ impl EditorForSongs {
|
|||||||
None,
|
None,
|
||||||
Vec2::new(0.0, 0.5),
|
Vec2::new(0.0, 0.5),
|
||||||
),
|
),
|
||||||
c_tags: {
|
|
||||||
let mut tags = Vec::new();
|
|
||||||
for song in songs.iter() {
|
|
||||||
for tag in song.general.tags.iter() {
|
|
||||||
if !tags.contains(&tag.as_str()) {
|
|
||||||
tags.push(tag.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tags.into_iter()
|
|
||||||
.map(|tag| {
|
|
||||||
EditorForAnyTagInList::new(
|
|
||||||
tag.to_owned(),
|
|
||||||
sender.clone(),
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
c_new_tag: EditorForAnyTagAdder::new(sender.clone()),
|
|
||||||
c_spacers: [
|
|
||||||
SpacerForScrollBox::new(),
|
|
||||||
SpacerForScrollBox::new(),
|
|
||||||
SpacerForScrollBox::new(),
|
|
||||||
SpacerForScrollBox::new(),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
vec![],
|
vec![],
|
||||||
ELEM_HEIGHT,
|
ELEM_HEIGHT,
|
||||||
@ -206,8 +169,7 @@ impl GuiElem for EditorForSongs {
|
|||||||
gui.gui.set_normal_ui_enabled(true);
|
gui.gui.set_normal_ui_enabled(true);
|
||||||
}))),
|
}))),
|
||||||
Event::Apply => {
|
Event::Apply => {
|
||||||
let mut actions = Vec::new();
|
for song in &self.songs {
|
||||||
for song in self.songs.iter() {
|
|
||||||
let mut song = song.clone();
|
let mut song = song.clone();
|
||||||
|
|
||||||
let new_title = self
|
let new_title = self
|
||||||
@ -226,24 +188,17 @@ impl GuiElem for EditorForSongs {
|
|||||||
song.artist = artist_id;
|
song.artist = artist_id;
|
||||||
song.album = None;
|
song.album = None;
|
||||||
}
|
}
|
||||||
actions.push(Action::ModifySong(song, Req::none()));
|
|
||||||
}
|
|
||||||
if actions.len() == 1 {
|
|
||||||
info.actions
|
info.actions
|
||||||
.push(GuiAction::SendToServer(actions.pop().unwrap()));
|
.push(GuiAction::SendToServer(Action::ModifySong(
|
||||||
} else if actions.len() > 1 {
|
song,
|
||||||
info.actions
|
Req::none(),
|
||||||
.push(GuiAction::SendToServer(Action::Multiple(actions)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::SetArtist(name, id) => {
|
Event::SetArtist(name, id) => {
|
||||||
self.c_scrollbox.children.c_artist.chosen_id = id;
|
self.c_scrollbox.children.c_artist.chosen_id = id;
|
||||||
self.c_scrollbox.children.c_artist.last_search = name.to_lowercase();
|
self.c_scrollbox.children.c_artist.last_search = name.to_lowercase();
|
||||||
self.c_scrollbox
|
self.c_scrollbox.children.c_artist.open_prog.target = 1.0;
|
||||||
.children
|
|
||||||
.c_artist
|
|
||||||
.open_prog
|
|
||||||
.set_target(info.time, 1.0);
|
|
||||||
*self
|
*self
|
||||||
.c_scrollbox
|
.c_scrollbox
|
||||||
.children
|
.children
|
||||||
@ -254,52 +209,6 @@ impl GuiElem for EditorForSongs {
|
|||||||
.text() = name;
|
.text() = name;
|
||||||
self.c_scrollbox.children.c_artist.config_mut().redraw = true;
|
self.c_scrollbox.children.c_artist.config_mut().redraw = true;
|
||||||
}
|
}
|
||||||
Event::GeneralEvent(e) => {
|
|
||||||
use super::gui_edit_any::Event as GeneralEvent;
|
|
||||||
match e {
|
|
||||||
GeneralEvent::RemoveTag(tag) => {
|
|
||||||
for song in self.songs.iter_mut() {
|
|
||||||
if let Some(i) =
|
|
||||||
song.general.tags.iter().position(|t| *t == tag)
|
|
||||||
{
|
|
||||||
song.general.tags.remove(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(i) = (&self.c_scrollbox.children.c_tags)
|
|
||||||
.into_iter()
|
|
||||||
.position(|e| e.tag == tag)
|
|
||||||
{
|
|
||||||
self.c_scrollbox.children.c_tags.remove(i);
|
|
||||||
self.c_scrollbox.config_mut().redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GeneralEvent::AddTag(tag) => {
|
|
||||||
self.c_scrollbox.children.c_new_tag.clear(info.time);
|
|
||||||
for song in self.songs.iter_mut() {
|
|
||||||
if !song.general.tags.contains(&tag) {
|
|
||||||
song.general.tags.push(tag.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !(&self.c_scrollbox.children.c_tags)
|
|
||||||
.into_iter()
|
|
||||||
.any(|e| e.tag == tag)
|
|
||||||
{
|
|
||||||
self.c_scrollbox.children_heights.insert(
|
|
||||||
3 + self.c_scrollbox.children.c_tags.len(),
|
|
||||||
ELEM_HEIGHT,
|
|
||||||
);
|
|
||||||
self.c_scrollbox.children.c_tags.push(
|
|
||||||
EditorForAnyTagInList::new(
|
|
||||||
tag,
|
|
||||||
self.event_sender.clone(),
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.c_scrollbox.config_mut().redraw = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
}
|
}
|
||||||
@ -323,34 +232,15 @@ impl GuiElem for EditorForSongs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// artist sel
|
// artist sel
|
||||||
if let Ok(val) = self
|
if self
|
||||||
.c_scrollbox
|
.c_scrollbox
|
||||||
.children
|
.children
|
||||||
.c_artist
|
.c_artist
|
||||||
.open_prog
|
.open_prog
|
||||||
.update(info.time, false)
|
.update(Instant::now(), false)
|
||||||
{
|
{
|
||||||
if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) {
|
if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) {
|
||||||
*v = ELEM_HEIGHT * val as f32;
|
*v = ELEM_HEIGHT * self.c_scrollbox.children.c_artist.open_prog.value;
|
||||||
self.c_scrollbox.config_mut().redraw = true;
|
|
||||||
}
|
|
||||||
if let Some(h) = &info.helper {
|
|
||||||
h.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(val) = self
|
|
||||||
.c_scrollbox
|
|
||||||
.children
|
|
||||||
.c_new_tag
|
|
||||||
.open_prog
|
|
||||||
.update(info.time, false)
|
|
||||||
{
|
|
||||||
if let Some(v) = self
|
|
||||||
.c_scrollbox
|
|
||||||
.children_heights
|
|
||||||
.get_mut(3 + self.c_scrollbox.children.c_tags.len())
|
|
||||||
{
|
|
||||||
*v = ELEM_HEIGHT * val as f32;
|
|
||||||
self.c_scrollbox.config_mut().redraw = true;
|
self.c_scrollbox.config_mut().redraw = true;
|
||||||
}
|
}
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
@ -382,7 +272,7 @@ pub struct EditorForSongArtistChooser {
|
|||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
event_sender: std::sync::mpsc::Sender<Event>,
|
event_sender: std::sync::mpsc::Sender<Event>,
|
||||||
/// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6)
|
/// `1.0` = collapsed, `self.expand_to` = expanded (shows `c_picker` of height 7-1=6)
|
||||||
open_prog: AnimationController,
|
open_prog: AnimationController<f32>,
|
||||||
expand_to: f32,
|
expand_to: f32,
|
||||||
chosen_id: Option<ArtistId>,
|
chosen_id: Option<ArtistId>,
|
||||||
c_name: TextField,
|
c_name: TextField,
|
||||||
@ -395,7 +285,7 @@ impl EditorForSongArtistChooser {
|
|||||||
Self {
|
Self {
|
||||||
config: GuiElemCfg::default(),
|
config: GuiElemCfg::default(),
|
||||||
event_sender,
|
event_sender,
|
||||||
open_prog: AnimationController::new(1.0, 1.0, 4.0),
|
open_prog: AnimationController::new(1.0, 1.0, 0.3, 8.0, 0.5, 0.6, Instant::now()),
|
||||||
expand_to,
|
expand_to,
|
||||||
chosen_id: None,
|
chosen_id: None,
|
||||||
c_name: TextField::new(
|
c_name: TextField::new(
|
||||||
@ -417,10 +307,10 @@ impl EditorForSongArtistChooser {
|
|||||||
}
|
}
|
||||||
impl GuiElem for EditorForSongArtistChooser {
|
impl GuiElem for EditorForSongArtistChooser {
|
||||||
fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
fn draw(&mut self, info: &mut crate::gui::DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
||||||
let picker_enabled = self.open_prog.value(info.time) > 1.0;
|
let picker_enabled = self.open_prog.value > 1.0;
|
||||||
self.c_picker.config_mut().enabled = picker_enabled;
|
self.c_picker.config_mut().enabled = picker_enabled;
|
||||||
if picker_enabled {
|
if picker_enabled {
|
||||||
let split = 1.0 / self.open_prog.value(info.time) as f32;
|
let split = 1.0 / self.open_prog.value;
|
||||||
self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split));
|
self.c_name.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (1.0, split));
|
||||||
self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0));
|
self.c_picker.config_mut().pos = Rectangle::from_tuples((0.0, split), (1.0, 1.0));
|
||||||
} else {
|
} else {
|
||||||
@ -437,10 +327,9 @@ impl GuiElem for EditorForSongArtistChooser {
|
|||||||
};
|
};
|
||||||
if search_changed {
|
if search_changed {
|
||||||
self.chosen_id = None;
|
self.chosen_id = None;
|
||||||
|
self.open_prog.target = self.expand_to;
|
||||||
if search.is_empty() {
|
if search.is_empty() {
|
||||||
self.open_prog.set_target(info.time, 1.0);
|
self.open_prog.target = 1.0;
|
||||||
} else {
|
|
||||||
self.open_prog.set_target(info.time, self.expand_to as f64);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let artists = info
|
let artists = info
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::{
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
use musicdb_lib::data::ArtistId;
|
use musicdb_lib::data::ArtistId;
|
||||||
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
|
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
|
||||||
@ -24,8 +27,8 @@ pub struct IdleDisplay {
|
|||||||
pub c_buttons: PlayPause,
|
pub c_buttons: PlayPause,
|
||||||
pub c_buttons_custom_pos: bool,
|
pub c_buttons_custom_pos: bool,
|
||||||
|
|
||||||
pub cover_aspect_ratio: AnimationController,
|
pub cover_aspect_ratio: AnimationController<f32>,
|
||||||
pub artist_image_aspect_ratio: AnimationController,
|
pub artist_image_aspect_ratio: AnimationController<f32>,
|
||||||
|
|
||||||
pub cover_pos: Option<Rectangle>,
|
pub cover_pos: Option<Rectangle>,
|
||||||
pub cover_left: f32,
|
pub cover_left: f32,
|
||||||
@ -73,8 +76,24 @@ impl IdleDisplay {
|
|||||||
is_fav: (false, Arc::clone(&is_fav)),
|
is_fav: (false, Arc::clone(&is_fav)),
|
||||||
c_buttons: PlayPause::new(GuiElemCfg::default(), is_fav),
|
c_buttons: PlayPause::new(GuiElemCfg::default(), is_fav),
|
||||||
c_buttons_custom_pos: false,
|
c_buttons_custom_pos: false,
|
||||||
cover_aspect_ratio: AnimationController::new(1.0, 1.0, 1.0),
|
cover_aspect_ratio: AnimationController::new(
|
||||||
artist_image_aspect_ratio: AnimationController::new(0.0, 0.0, 1.0),
|
1.0,
|
||||||
|
1.0,
|
||||||
|
0.01,
|
||||||
|
1.0,
|
||||||
|
0.8,
|
||||||
|
0.6,
|
||||||
|
Instant::now(),
|
||||||
|
),
|
||||||
|
artist_image_aspect_ratio: AnimationController::new(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.01,
|
||||||
|
1.0,
|
||||||
|
0.8,
|
||||||
|
0.6,
|
||||||
|
Instant::now(),
|
||||||
|
),
|
||||||
cover_pos: None,
|
cover_pos: None,
|
||||||
cover_left: 0.01,
|
cover_left: 0.01,
|
||||||
cover_top: 0.21,
|
cover_top: 0.21,
|
||||||
@ -162,7 +181,7 @@ impl GuiElem for IdleDisplay {
|
|||||||
.is_some_and(|(a, _)| *a != artist_id)
|
.is_some_and(|(a, _)| *a != artist_id)
|
||||||
{
|
{
|
||||||
self.current_artist_image = Some((artist_id, None));
|
self.current_artist_image = Some((artist_id, None));
|
||||||
self.artist_image_aspect_ratio.set_target(info.time, 0.0);
|
self.artist_image_aspect_ratio.target = 0.0;
|
||||||
if let Some(artist) = info.database.artists().get(&artist_id) {
|
if let Some(artist) = info.database.artists().get(&artist_id) {
|
||||||
for tag in &artist.general.tags {
|
for tag in &artist.general.tags {
|
||||||
if tag.starts_with("ImageExt=") {
|
if tag.starts_with("ImageExt=") {
|
||||||
@ -186,7 +205,7 @@ impl GuiElem for IdleDisplay {
|
|||||||
} else {
|
} else {
|
||||||
if self.current_artist_image.is_some() {
|
if self.current_artist_image.is_some() {
|
||||||
self.current_artist_image = None;
|
self.current_artist_image = None;
|
||||||
self.artist_image_aspect_ratio.set_target(info.time, 0.0);
|
self.artist_image_aspect_ratio.target = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,7 +213,7 @@ impl GuiElem for IdleDisplay {
|
|||||||
self.current_info.new_cover = false;
|
self.current_info.new_cover = false;
|
||||||
match self.current_info.current_cover {
|
match self.current_info.current_cover {
|
||||||
None | Some((_, Some(None))) => {
|
None | Some((_, Some(None))) => {
|
||||||
self.cover_aspect_ratio.set_target(info.time, 0.0);
|
self.cover_aspect_ratio.target = 0.0;
|
||||||
}
|
}
|
||||||
Some((_, None)) | Some((_, Some(Some(_)))) => {}
|
Some((_, None)) | Some((_, Some(Some(_)))) => {}
|
||||||
}
|
}
|
||||||
@ -224,7 +243,6 @@ impl GuiElem for IdleDisplay {
|
|||||||
info.pos.top_left().x + info.pos.height() * self.cover_left,
|
info.pos.top_left().x + info.pos.height() * self.cover_left,
|
||||||
info.pos.top_left().y + info.pos.height() * self.cover_top,
|
info.pos.top_left().y + info.pos.height() * self.cover_top,
|
||||||
info.pos.top_left().y + info.pos.height() * self.cover_bottom,
|
info.pos.top_left().y + info.pos.height() * self.cover_bottom,
|
||||||
info.time,
|
|
||||||
&mut self.cover_aspect_ratio,
|
&mut self.cover_aspect_ratio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -242,23 +260,20 @@ impl GuiElem for IdleDisplay {
|
|||||||
info.pos.top_left().x + info.pos.height() * self.cover_left,
|
info.pos.top_left().x + info.pos.height() * self.cover_left,
|
||||||
top,
|
top,
|
||||||
bottom,
|
bottom,
|
||||||
self.cover_aspect_ratio.value(info.time) as f32,
|
self.cover_aspect_ratio.value,
|
||||||
) + info.pos.height() * self.artist_image_to_cover_margin,
|
) + info.pos.height() * self.artist_image_to_cover_margin,
|
||||||
top + (bottom - top) * self.artist_image_top,
|
top + (bottom - top) * self.artist_image_top,
|
||||||
bottom,
|
bottom,
|
||||||
info.time,
|
|
||||||
&mut self.artist_image_aspect_ratio,
|
&mut self.artist_image_aspect_ratio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// move children to make space for cover
|
// move children to make space for cover
|
||||||
let ar_updated = self
|
let ar_updated = self
|
||||||
.cover_aspect_ratio
|
.cover_aspect_ratio
|
||||||
.update(info.time, info.high_performance)
|
.update(info.time.clone(), info.high_performance)
|
||||||
.is_ok()
|
|
||||||
| self
|
| self
|
||||||
.artist_image_aspect_ratio
|
.artist_image_aspect_ratio
|
||||||
.update(info.time, info.high_performance)
|
.update(info.time.clone(), info.high_performance);
|
||||||
.is_ok();
|
|
||||||
if ar_updated || info.pos.size() != self.config.pixel_pos.size() {
|
if ar_updated || info.pos.size() != self.config.pixel_pos.size() {
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
h.request_redraw();
|
h.request_redraw();
|
||||||
@ -266,12 +281,8 @@ impl GuiElem for IdleDisplay {
|
|||||||
// make thing be relative to width instead of to height by multiplying with this
|
// make thing be relative to width instead of to height by multiplying with this
|
||||||
let top = self.cover_top;
|
let top = self.cover_top;
|
||||||
let bottom = self.cover_bottom;
|
let bottom = self.cover_bottom;
|
||||||
let left = (get_right_x(
|
let left = (get_right_x(self.cover_left, top, bottom, self.cover_aspect_ratio.value)
|
||||||
self.cover_left,
|
+ self.artist_image_to_cover_margin)
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
self.cover_aspect_ratio.value(info.time) as f32,
|
|
||||||
) + self.artist_image_to_cover_margin)
|
|
||||||
* info.pos.height()
|
* info.pos.height()
|
||||||
/ info.pos.width();
|
/ info.pos.width();
|
||||||
let ai_top = top + (bottom - top) * self.artist_image_top;
|
let ai_top = top + (bottom - top) * self.artist_image_top;
|
||||||
@ -282,7 +293,7 @@ impl GuiElem for IdleDisplay {
|
|||||||
left,
|
left,
|
||||||
ai_top * info.pos.height() / info.pos.width(),
|
ai_top * info.pos.height() / info.pos.width(),
|
||||||
bottom * info.pos.height() / info.pos.width(),
|
bottom * info.pos.height() / info.pos.width(),
|
||||||
self.artist_image_aspect_ratio.value(info.time) as f32,
|
self.artist_image_aspect_ratio.value,
|
||||||
);
|
);
|
||||||
self.c_side2_label.config_mut().pos =
|
self.c_side2_label.config_mut().pos =
|
||||||
Rectangle::from_tuples((left, ai_top), (max_right, bottom));
|
Rectangle::from_tuples((left, ai_top), (max_right, bottom));
|
||||||
|
@ -6,6 +6,7 @@ use std::{
|
|||||||
atomic::{AtomicBool, AtomicUsize},
|
atomic::{AtomicBool, AtomicUsize},
|
||||||
mpsc, Mutex,
|
mpsc, Mutex,
|
||||||
},
|
},
|
||||||
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use musicdb_lib::data::{
|
use musicdb_lib::data::{
|
||||||
@ -69,7 +70,7 @@ pub struct LibraryBrowser {
|
|||||||
search_song: String,
|
search_song: String,
|
||||||
search_song_regex: Option<Regex>,
|
search_song_regex: Option<Regex>,
|
||||||
filter_target_state: Arc<AtomicBool>,
|
filter_target_state: Arc<AtomicBool>,
|
||||||
filter_state: AnimationController,
|
filter_state: AnimationController<f32>,
|
||||||
library_updated: bool,
|
library_updated: bool,
|
||||||
search_settings_changed: Arc<AtomicBool>,
|
search_settings_changed: Arc<AtomicBool>,
|
||||||
search_is_case_sensitive: Arc<AtomicBool>,
|
search_is_case_sensitive: Arc<AtomicBool>,
|
||||||
@ -202,7 +203,7 @@ impl LibraryBrowser {
|
|||||||
search_song: String::new(),
|
search_song: String::new(),
|
||||||
search_song_regex: None,
|
search_song_regex: None,
|
||||||
filter_target_state,
|
filter_target_state,
|
||||||
filter_state: AnimationController::new(0.0, 0.0, 4.0),
|
filter_state: AnimationController::new(0.0, 0.0, 0.25, 25.0, 0.1, 0.2, Instant::now()),
|
||||||
library_updated: true,
|
library_updated: true,
|
||||||
search_settings_changed,
|
search_settings_changed,
|
||||||
search_is_case_sensitive,
|
search_is_case_sensitive,
|
||||||
@ -379,19 +380,18 @@ impl GuiElem for LibraryBrowser {
|
|||||||
let filter_target_state = self
|
let filter_target_state = self
|
||||||
.filter_target_state
|
.filter_target_state
|
||||||
.load(std::sync::atomic::Ordering::Relaxed);
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
self.filter_state
|
self.filter_state.target = if filter_target_state { 1.0 } else { 0.0 };
|
||||||
.set_target(info.time, if filter_target_state { 1.0 } else { 0.0 });
|
if self.filter_state.update(info.time, info.high_performance) {
|
||||||
if let Ok(val) = self.filter_state.update(info.time, info.high_performance) {
|
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
h.request_redraw();
|
h.request_redraw();
|
||||||
}
|
}
|
||||||
let y = LP_LIB1 + (LP_LIB1S - LP_LIB1) * val as f32;
|
let y = LP_LIB1 + (LP_LIB1S - LP_LIB1) * self.filter_state.value;
|
||||||
self.c_scroll_box.config_mut().pos =
|
self.c_scroll_box.config_mut().pos =
|
||||||
Rectangle::new(Vec2::new(0.0, y), Vec2::new(1.0, LP_LIB2));
|
Rectangle::new(Vec2::new(0.0, y), Vec2::new(1.0, LP_LIB2));
|
||||||
let filter_panel = &mut self.c_filter_panel;
|
let filter_panel = &mut self.c_filter_panel;
|
||||||
filter_panel.config_mut().pos =
|
filter_panel.config_mut().pos =
|
||||||
Rectangle::new(Vec2::new(0.0, LP_LIB1), Vec2::new(1.0, y));
|
Rectangle::new(Vec2::new(0.0, LP_LIB1), Vec2::new(1.0, y));
|
||||||
filter_panel.config.enabled = val > 0.0;
|
filter_panel.config.enabled = self.filter_state.value > 0.0;
|
||||||
}
|
}
|
||||||
// -
|
// -
|
||||||
if self.library_updated {
|
if self.library_updated {
|
||||||
@ -1275,54 +1275,24 @@ impl GuiElem for ListSong {
|
|||||||
fn mouse_pressed(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
|
fn mouse_pressed(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
|
||||||
if button == MouseButton::Right && e.take() {
|
if button == MouseButton::Right && e.take() {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let mut menu_actions: Vec<Box<dyn GuiElem + 'static>> = vec![Box::new(Button::new(
|
vec![GuiAction::Build(Box::new(move |db| {
|
||||||
GuiElemCfg::default(),
|
if let Some(me) = db.songs().get(&id) {
|
||||||
move |_| {
|
let me = me.clone();
|
||||||
vec![GuiAction::Build(Box::new(move |db| {
|
vec![GuiAction::ContextMenu(Some(vec![Box::new(Button::new(
|
||||||
if let Some(me) = db.get_song(&id) {
|
|
||||||
vec![GuiAction::EditSongs(vec![me.clone()])]
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}))]
|
|
||||||
},
|
|
||||||
[Label::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
format!("Edit this song"),
|
|
||||||
Color::WHITE,
|
|
||||||
None,
|
|
||||||
Vec2::new_y(0.5),
|
|
||||||
)],
|
|
||||||
))];
|
|
||||||
if self.selected.contains_song(&id) {
|
|
||||||
menu_actions.push(Box::new(Button::new(
|
|
||||||
GuiElemCfg::default(),
|
|
||||||
{
|
|
||||||
let selected = self.selected.clone();
|
|
||||||
move |_| {
|
|
||||||
let selected = selected.clone();
|
|
||||||
vec![GuiAction::Build(Box::new(move |db| {
|
|
||||||
vec![GuiAction::EditSongs(selected.view(
|
|
||||||
|(artists, albums, songs)| {
|
|
||||||
songs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|id| db.get_song(id).cloned())
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
))]
|
|
||||||
}))]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[Label::new(
|
|
||||||
GuiElemCfg::default(),
|
GuiElemCfg::default(),
|
||||||
format!("Edit selected songs"),
|
move |_| vec![GuiAction::EditSongs(vec![me.clone()])],
|
||||||
Color::WHITE,
|
[Label::new(
|
||||||
None,
|
GuiElemCfg::default(),
|
||||||
Vec2::new_y(0.5),
|
format!("Edit"),
|
||||||
)],
|
Color::WHITE,
|
||||||
)));
|
None,
|
||||||
}
|
Vec2::new_y(0.5),
|
||||||
vec![GuiAction::ContextMenu(Some(menu_actions))]
|
)],
|
||||||
|
))]))]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}))]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use std::{
|
use std::{sync::Arc, time::Duration};
|
||||||
sync::Arc,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use musicdb_lib::data::{CoverId, SongId};
|
use musicdb_lib::data::{CoverId, SongId};
|
||||||
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
|
use speedy2d::{color::Color, dimen::Vec2, image::ImageHandle, shape::Rectangle};
|
||||||
@ -154,29 +151,25 @@ pub fn image_display(
|
|||||||
left: f32,
|
left: f32,
|
||||||
top: f32,
|
top: f32,
|
||||||
bottom: f32,
|
bottom: f32,
|
||||||
now: Instant,
|
aspect_ratio: &mut AnimationController<f32>,
|
||||||
aspect_ratio: &mut AnimationController,
|
|
||||||
) {
|
) {
|
||||||
if let Some(cover) = &img {
|
if let Some(cover) = &img {
|
||||||
let cover_size = cover.size();
|
let cover_size = cover.size();
|
||||||
let pos = if let Some(pos) = pos {
|
aspect_ratio.target = if cover_size.x > 0 && cover_size.y > 0 {
|
||||||
pos
|
let pos = if let Some(pos) = pos {
|
||||||
} else {
|
pos
|
||||||
let right_x = get_right_x(left, top, bottom, aspect_ratio.value(now) as f32);
|
|
||||||
Rectangle::from_tuples((left, top), (right_x, bottom))
|
|
||||||
};
|
|
||||||
g.draw_rectangle_image(pos, cover);
|
|
||||||
aspect_ratio.set_target(
|
|
||||||
now,
|
|
||||||
if cover_size.x > 0 && cover_size.y > 0 {
|
|
||||||
let aspect_ratio = cover_size.x as f32 / cover_size.y as f32;
|
|
||||||
aspect_ratio as f64
|
|
||||||
} else {
|
} else {
|
||||||
0.0
|
let right_x = get_right_x(left, top, bottom, aspect_ratio.value);
|
||||||
},
|
Rectangle::from_tuples((left, top), (right_x, bottom))
|
||||||
);
|
};
|
||||||
|
let aspect_ratio = cover_size.x as f32 / cover_size.y as f32;
|
||||||
|
g.draw_rectangle_image(pos, cover);
|
||||||
|
aspect_ratio
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
aspect_ratio.set_target(now, 0.0);
|
aspect_ratio.target = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_right_x(left: f32, top: f32, bottom: f32, aspect_ratio: f32) -> f32 {
|
pub fn get_right_x(left: f32, top: f32, bottom: f32, aspect_ratio: f32) -> f32 {
|
||||||
|
@ -403,7 +403,7 @@ impl GuiElem for GuiScreen {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
// request_redraw for animations
|
// request_redraw for animations
|
||||||
let idle_value = self.idle.get_value(info.time) as f32;
|
let idle_value = self.idle.get_value(Instant::now()) as f32;
|
||||||
let idle_changed = self.idle_prev_val != idle_value;
|
let idle_changed = self.idle_prev_val != idle_value;
|
||||||
if idle_changed || idle_exit_anim || self.settings.1.is_some() {
|
if idle_changed || idle_exit_anim || self.settings.1.is_some() {
|
||||||
self.idle_prev_val = idle_value;
|
self.idle_prev_val = idle_value;
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
use std::sync::{atomic::AtomicBool, Arc};
|
use std::{
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
use speedy2d::{dimen::Vec2, shape::Rectangle};
|
use speedy2d::{dimen::Vec2, shape::Rectangle};
|
||||||
|
|
||||||
@ -14,7 +17,7 @@ pub struct StatusBar {
|
|||||||
config: GuiElemCfg,
|
config: GuiElemCfg,
|
||||||
pub idle_mode: f32,
|
pub idle_mode: f32,
|
||||||
current_info: CurrentInfo,
|
current_info: CurrentInfo,
|
||||||
cover_aspect_ratio: AnimationController,
|
cover_aspect_ratio: AnimationController<f32>,
|
||||||
c_song_label: AdvancedLabel,
|
c_song_label: AdvancedLabel,
|
||||||
pub force_reset_texts: bool,
|
pub force_reset_texts: bool,
|
||||||
c_buttons: PlayPause,
|
c_buttons: PlayPause,
|
||||||
@ -28,7 +31,15 @@ impl StatusBar {
|
|||||||
config,
|
config,
|
||||||
idle_mode: 0.0,
|
idle_mode: 0.0,
|
||||||
current_info: CurrentInfo::new(),
|
current_info: CurrentInfo::new(),
|
||||||
cover_aspect_ratio: AnimationController::new(0.0, 0.0, 1.0),
|
cover_aspect_ratio: AnimationController::new(
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.01,
|
||||||
|
1.0,
|
||||||
|
0.8,
|
||||||
|
0.6,
|
||||||
|
Instant::now(),
|
||||||
|
),
|
||||||
c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]),
|
c_song_label: AdvancedLabel::new(GuiElemCfg::default(), Vec2::new(0.0, 0.5), vec![]),
|
||||||
force_reset_texts: false,
|
force_reset_texts: false,
|
||||||
is_fav: (false, Arc::clone(&is_fav)),
|
is_fav: (false, Arc::clone(&is_fav)),
|
||||||
@ -71,7 +82,7 @@ impl GuiElem for StatusBar {
|
|||||||
self.current_info.new_cover = false;
|
self.current_info.new_cover = false;
|
||||||
match self.current_info.current_cover {
|
match self.current_info.current_cover {
|
||||||
None | Some((_, Some(None))) => {
|
None | Some((_, Some(None))) => {
|
||||||
self.cover_aspect_ratio.set_target(info.time, 0.0);
|
self.cover_aspect_ratio.target = 0.0;
|
||||||
}
|
}
|
||||||
Some((_, None)) | Some((_, Some(Some(_)))) => {}
|
Some((_, None)) | Some((_, Some(Some(_)))) => {}
|
||||||
}
|
}
|
||||||
@ -79,8 +90,7 @@ impl GuiElem for StatusBar {
|
|||||||
// move children to make space for cover
|
// move children to make space for cover
|
||||||
let ar_updated = self
|
let ar_updated = self
|
||||||
.cover_aspect_ratio
|
.cover_aspect_ratio
|
||||||
.update(info.time, info.high_performance)
|
.update(info.time.clone(), info.high_performance);
|
||||||
.is_ok();
|
|
||||||
if ar_updated || info.pos.size() != self.config.pixel_pos.size() {
|
if ar_updated || info.pos.size() != self.config.pixel_pos.size() {
|
||||||
if let Some(h) = &info.helper {
|
if let Some(h) = &info.helper {
|
||||||
h.request_redraw();
|
h.request_redraw();
|
||||||
@ -95,8 +105,7 @@ impl GuiElem for StatusBar {
|
|||||||
);
|
);
|
||||||
self.c_song_label.config_mut().pos = Rectangle::from_tuples(
|
self.c_song_label.config_mut().pos = Rectangle::from_tuples(
|
||||||
(
|
(
|
||||||
self.cover_aspect_ratio.value(info.time) as f32 * info.pos.height()
|
self.cover_aspect_ratio.value * info.pos.height() / info.pos.width(),
|
||||||
/ info.pos.width(),
|
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
(buttons_right_pos - buttons_width, 1.0),
|
(buttons_right_pos - buttons_width, 1.0),
|
||||||
@ -116,7 +125,6 @@ impl GuiElem for StatusBar {
|
|||||||
info.pos.top_left().x + info.pos.height() * 0.05,
|
info.pos.top_left().x + info.pos.height() * 0.05,
|
||||||
info.pos.top_left().y + info.pos.height() * 0.05,
|
info.pos.top_left().y + info.pos.height() * 0.05,
|
||||||
info.pos.top_left().y + info.pos.height() * 0.95,
|
info.pos.top_left().y + info.pos.height() * 0.95,
|
||||||
info.time,
|
|
||||||
&mut self.cover_aspect_ratio,
|
&mut self.cover_aspect_ratio,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{fmt::Display, sync::Arc};
|
use std::{fmt::Display, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use musicdb_lib::data::CoverId;
|
use musicdb_lib::data::CoverId;
|
||||||
use speedy2d::{
|
use speedy2d::{
|
||||||
@ -30,7 +30,7 @@ pub struct Content {
|
|||||||
text: String,
|
text: String,
|
||||||
color: Color,
|
color: Color,
|
||||||
background: Option<Color>,
|
background: Option<Color>,
|
||||||
formatted: Option<FormattedTextBlock>,
|
formatted: Option<Rc<FormattedTextBlock>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#![allow(dead_code)]
|
// #![allow(unused)]
|
||||||
#![allow(unused_variables)]
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufReader, Write},
|
io::{BufReader, Write},
|
||||||
@ -32,7 +31,6 @@ mod gui;
|
|||||||
mod gui_anim;
|
mod gui_anim;
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
mod gui_base;
|
mod gui_base;
|
||||||
pub mod gui_edit_any;
|
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
mod gui_edit_song;
|
mod gui_edit_song;
|
||||||
#[cfg(feature = "speedy2d")]
|
#[cfg(feature = "speedy2d")]
|
||||||
@ -148,6 +146,7 @@ fn main() {
|
|||||||
| Mode::GuiSyncplayerNetwork
|
| Mode::GuiSyncplayerNetwork
|
||||||
);
|
);
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
|
#[allow(unused)]
|
||||||
let mut cache_manager = None;
|
let mut cache_manager = None;
|
||||||
#[cfg(feature = "playback")]
|
#[cfg(feature = "playback")]
|
||||||
let mut player = if is_syncplayer {
|
let mut player = if is_syncplayer {
|
||||||
@ -161,12 +160,6 @@ fn main() {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
// prevent unused assignment warning, we might
|
|
||||||
// need cache manager at some point -_-
|
|
||||||
#[cfg(feature = "playback")]
|
|
||||||
if false {
|
|
||||||
drop(cache_manager);
|
|
||||||
}
|
|
||||||
#[allow(unused_labels)]
|
#[allow(unused_labels)]
|
||||||
'ifstatementworkaround: {
|
'ifstatementworkaround: {
|
||||||
// use if+break instead of if-else because we can't #[cfg(feature)] the if statement,
|
// use if+break instead of if-else because we can't #[cfg(feature)] the if statement,
|
||||||
|
@ -6,6 +6,6 @@ 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]
|
||||||
id3 = "1.16.3"
|
id3 = "1.16.0"
|
||||||
mp3-duration = "0.1.10"
|
mp3-duration = "0.1.10"
|
||||||
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }
|
||||||
|
@ -4,8 +4,7 @@ use std::{
|
|||||||
fs,
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex}, time::SystemTime,
|
||||||
time::SystemTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use id3::TagLike;
|
use id3::TagLike;
|
||||||
@ -27,31 +26,21 @@ fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
};
|
};
|
||||||
let mut bad_arg = false;
|
let mut bad_arg = false;
|
||||||
let mut dbdir = ".".to_owned();
|
|
||||||
let mut skip_duration = false;
|
let mut skip_duration = false;
|
||||||
let mut custom_files = None;
|
let mut custom_files = None;
|
||||||
|
let mut artist_txt = false;
|
||||||
let mut artist_img = false;
|
let mut artist_img = false;
|
||||||
let mut export_custom_files = None;
|
|
||||||
loop {
|
loop {
|
||||||
match args.next() {
|
match args.next() {
|
||||||
None => break,
|
None => break,
|
||||||
Some(arg) => match arg.as_str() {
|
Some(arg) => match arg.as_str() {
|
||||||
"--help" => {
|
"--help" => {
|
||||||
eprintln!("--dbdir <path>: Save dbfile in the <path> directory (default: `.`)");
|
|
||||||
eprintln!("--skip-duration: Don't try to figure out the songs duration from file contents. This means mp3 files with the Duration field unset will have a duration of 0.");
|
eprintln!("--skip-duration: Don't try to figure out the songs duration from file contents. This means mp3 files with the Duration field unset will have a duration of 0.");
|
||||||
eprintln!("--custom-files <path>: server will use <path> as its custom-files directory. Additional data is loaded from here.");
|
eprintln!("--custom-files <path>: server will use <path> as its custom-files directory.");
|
||||||
|
eprintln!("--cf-artist-txt: For each artist, check for an <artist>.txt file. If it exists, add each line as a tag to that artist.");
|
||||||
eprintln!("--cf-artist-img: For each artist, check for an <artist>.{{jpg,png,...}} file. If it exists, add ImageExt=<extension> tag to the artist, so the image can be loaded by clients later.");
|
eprintln!("--cf-artist-img: For each artist, check for an <artist>.{{jpg,png,...}} file. If it exists, add ImageExt=<extension> tag to the artist, so the image can be loaded by clients later.");
|
||||||
eprintln!("--export-custom-files <path>: Create <path> as a directory containing metadata from the *existing* dbfile, so that it can be loaded again using --custom-files <same-path>.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
"--dbdir" => {
|
|
||||||
if let Some(dir) = args.next() {
|
|
||||||
dbdir = dir;
|
|
||||||
} else {
|
|
||||||
bad_arg = true;
|
|
||||||
eprintln!("--dbdir <path> :: missing <path>!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"--skip-duration" => skip_duration = true,
|
"--skip-duration" => skip_duration = true,
|
||||||
"--custom-files" => {
|
"--custom-files" => {
|
||||||
if let Some(path) = args.next() {
|
if let Some(path) = args.next() {
|
||||||
@ -61,15 +50,8 @@ fn main() {
|
|||||||
eprintln!("--custom-files <path> :: missing <path>!");
|
eprintln!("--custom-files <path> :: missing <path>!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--cf-artist-txt" => artist_txt = true,
|
||||||
"--cf-artist-img" => artist_img = true,
|
"--cf-artist-img" => artist_img = true,
|
||||||
"--export-custom-files" => {
|
|
||||||
if let Some(path) = args.next() {
|
|
||||||
export_custom_files = Some(PathBuf::from(path));
|
|
||||||
} else {
|
|
||||||
bad_arg = true;
|
|
||||||
eprintln!("--export-custom-files <path> :: missing <path>!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
arg => {
|
arg => {
|
||||||
bad_arg = true;
|
bad_arg = true;
|
||||||
eprintln!("Unknown argument: {arg}");
|
eprintln!("Unknown argument: {arg}");
|
||||||
@ -77,19 +59,9 @@ fn main() {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if export_custom_files.is_some() {
|
|
||||||
if skip_duration || custom_files.is_some() || artist_img {
|
|
||||||
bad_arg = true;
|
|
||||||
eprintln!("--export-custom-files :: incompatible with other arguments except --dbdir!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bad_arg {
|
if bad_arg {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(path) = export_custom_files {
|
|
||||||
export_to_custom_files_dir(dbdir, path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
eprintln!("Library: {lib_dir}. press enter to start. result will be saved in 'dbfile'.");
|
eprintln!("Library: {lib_dir}. press enter to start. result will be saved in 'dbfile'.");
|
||||||
std::io::stdin().read_line(&mut String::new()).unwrap();
|
std::io::stdin().read_line(&mut String::new()).unwrap();
|
||||||
// start
|
// start
|
||||||
@ -118,7 +90,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
eprintln!("\nloaded metadata of {} files.", songs.len());
|
eprintln!("\nloaded metadata of {} files.", songs.len());
|
||||||
let mut database = Database::new_empty_in_dir(PathBuf::from(dbdir), PathBuf::from(&lib_dir));
|
let mut database = Database::new_empty_in_dir(PathBuf::from("."), PathBuf::from(&lib_dir));
|
||||||
let unknown_artist = database.add_artist_new(Artist {
|
let unknown_artist = database.add_artist_new(Artist {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: format!("<unknown>"),
|
name: format!("<unknown>"),
|
||||||
@ -161,21 +133,21 @@ fn main() {
|
|||||||
let mut general = GeneralData::default();
|
let mut general = GeneralData::default();
|
||||||
match (song_tags.track(), song_tags.total_tracks()) {
|
match (song_tags.track(), song_tags.total_tracks()) {
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
(Some(n), Some(t)) => general.tags.push(format!("SRCFILE:TrackNr={n}/{t}")),
|
(Some(n), Some(t)) => general.tags.push(format!("TrackNr={n}/{t}")),
|
||||||
(Some(n), None) => general.tags.push(format!("SRCFILE:TrackNr={n}")),
|
(Some(n), None) => general.tags.push(format!("TrackNr={n}")),
|
||||||
(None, Some(t)) => general.tags.push(format!("SRCFILE:TrackNr=?/{t}")),
|
(None, Some(t)) => general.tags.push(format!("TrackNr=?/{t}")),
|
||||||
}
|
}
|
||||||
match (song_tags.disc(), song_tags.total_discs()) {
|
match (song_tags.disc(), song_tags.total_discs()) {
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
(Some(n), Some(t)) => general.tags.push(format!("SRCFILE:DiscNr={n}/{t}")),
|
(Some(n), Some(t)) => general.tags.push(format!("DiscNr={n}/{t}")),
|
||||||
(Some(n), None) => general.tags.push(format!("SRCFILE:DiscNr={n}")),
|
(Some(n), None) => general.tags.push(format!("DiscNr={n}")),
|
||||||
(None, Some(t)) => general.tags.push(format!("SRCFILE:DiscNr=?/{t}")),
|
(None, Some(t)) => general.tags.push(format!("DiscNr=?/{t}")),
|
||||||
}
|
}
|
||||||
if let Some(year) = song_tags.year() {
|
if let Some(year) = song_tags.year() {
|
||||||
general.tags.push(format!("SRCFILE:Year={year}"));
|
general.tags.push(format!("Year={year}"));
|
||||||
}
|
}
|
||||||
if let Some(genre) = song_tags.genre_parsed() {
|
if let Some(genre) = song_tags.genre_parsed() {
|
||||||
general.tags.push(format!("SRCFILE:Genre={genre}"));
|
general.tags.push(format!("Genre={genre}"));
|
||||||
}
|
}
|
||||||
let (artist_id, album_id) = if let Some(artist) = song_tags
|
let (artist_id, album_id) = if let Some(artist) = song_tags
|
||||||
.album_artist()
|
.album_artist()
|
||||||
@ -277,6 +249,7 @@ fn main() {
|
|||||||
song_path
|
song_path
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title.clone(),
|
title.clone(),
|
||||||
@ -307,30 +280,6 @@ fn main() {
|
|||||||
general,
|
general,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
{
|
|
||||||
let (artists, albums, songs) = database.artists_albums_songs_mut();
|
|
||||||
fn unsrcfile(tags: &mut Vec<String>) {
|
|
||||||
let srcfile_tags = tags
|
|
||||||
.iter()
|
|
||||||
.filter_map(|tag| tag.strip_prefix("SRCFILE:"))
|
|
||||||
.map(|tag| tag.to_owned())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for tag in srcfile_tags {
|
|
||||||
if !tags.contains(&tag) {
|
|
||||||
tags.push(tag.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for v in artists.values_mut() {
|
|
||||||
unsrcfile(&mut v.general.tags);
|
|
||||||
}
|
|
||||||
for v in albums.values_mut() {
|
|
||||||
unsrcfile(&mut v.general.tags);
|
|
||||||
}
|
|
||||||
for v in songs.values_mut() {
|
|
||||||
unsrcfile(&mut v.general.tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eprintln!("searching for covers...");
|
eprintln!("searching for covers...");
|
||||||
let mut multiple_cover_options = vec![];
|
let mut multiple_cover_options = vec![];
|
||||||
let mut single_images = HashMap::new();
|
let mut single_images = HashMap::new();
|
||||||
@ -386,6 +335,23 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(custom_files) = custom_files {
|
if let Some(custom_files) = custom_files {
|
||||||
|
if artist_txt {
|
||||||
|
eprintln!("[info] Searching for <artist>.txt files in custom-files dir...");
|
||||||
|
let l = database.artists().len();
|
||||||
|
let mut c = 0;
|
||||||
|
for (i, artist) in database.artists_mut().values_mut().enumerate() {
|
||||||
|
if let Ok(info) =
|
||||||
|
fs::read_to_string(custom_files.join(format!("{}.txt", artist.name)))
|
||||||
|
{
|
||||||
|
c += 1;
|
||||||
|
for line in info.lines() {
|
||||||
|
artist.general.tags.push(line.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprint!(" {}/{l} ({c})\r", i + 1);
|
||||||
|
}
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
if artist_img {
|
if artist_img {
|
||||||
eprintln!("[info] Searching for <artist>.{{png,jpg,...}} files in custom-files dir...");
|
eprintln!("[info] Searching for <artist>.{{png,jpg,...}} files in custom-files dir...");
|
||||||
match fs::read_dir(&custom_files) {
|
match fs::read_dir(&custom_files) {
|
||||||
@ -416,102 +382,12 @@ fn main() {
|
|||||||
}
|
}
|
||||||
for artist in database.artists_mut().values_mut() {
|
for artist in database.artists_mut().values_mut() {
|
||||||
if let Some(ext) = files.get(&artist.name) {
|
if let Some(ext) = files.get(&artist.name) {
|
||||||
artist.general.tags.push(format!("SRCFILE:ImageExt={ext}"));
|
|
||||||
artist.general.tags.push(format!("ImageExt={ext}"));
|
artist.general.tags.push(format!("ImageExt={ext}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
eprintln!("[info] Searching for <artist>.tags, <artist>.d/<album>.tags, <artist>.d/singles.d/<song>.tags, <artist>.d/<album>.d/<song>.tags in custom-files dir...");
|
|
||||||
let l = database.artists().len() + database.albums().len() + database.songs().len();
|
|
||||||
let mut cc = 0;
|
|
||||||
let mut c = 0;
|
|
||||||
let (artists, albums, songs) = database.artists_albums_songs_mut();
|
|
||||||
fn push_tags(info: &str, tags: &mut Vec<String>) {
|
|
||||||
for line in info.lines() {
|
|
||||||
let tag = normalized_str_to_tag(line);
|
|
||||||
if !tags.contains(&tag) {
|
|
||||||
tags.push(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for artist in artists.values_mut() {
|
|
||||||
// <artist>.tags
|
|
||||||
cc += 1;
|
|
||||||
if let Ok(info) = fs::read_to_string(custom_files.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&artist.name)
|
|
||||||
))) {
|
|
||||||
c += 1;
|
|
||||||
push_tags(&info, &mut artist.general.tags);
|
|
||||||
}
|
|
||||||
// <artist>.d/
|
|
||||||
let dir = custom_files.join(format!(
|
|
||||||
"{}.d",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&artist.name)
|
|
||||||
));
|
|
||||||
if fs::metadata(&dir).is_ok_and(|meta| meta.is_dir()) {
|
|
||||||
// <artist>.d/singles/
|
|
||||||
{
|
|
||||||
let dir = dir.join("singles");
|
|
||||||
for song in artist.singles.iter() {
|
|
||||||
// <artist>.d/singles/<song>.tags
|
|
||||||
cc += 1;
|
|
||||||
if let Some(song) = songs.get_mut(song) {
|
|
||||||
if let Ok(info) = fs::read_to_string(dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&song.title)
|
|
||||||
))) {
|
|
||||||
c += 1;
|
|
||||||
push_tags(&info, &mut song.general.tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for album in artist.albums.iter() {
|
|
||||||
eprint!(" {cc}/{l} ({c})\r");
|
|
||||||
cc += 1;
|
|
||||||
if let Some(album) = albums.get_mut(album) {
|
|
||||||
// <artist>.d/<album>.tags
|
|
||||||
if let Ok(info) = fs::read_to_string(dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&album.name)
|
|
||||||
))) {
|
|
||||||
c += 1;
|
|
||||||
push_tags(&info, &mut album.general.tags);
|
|
||||||
}
|
|
||||||
// <artist>.d/<album>.d/
|
|
||||||
let dir = dir.join(format!(
|
|
||||||
"{}.d",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&album.name)
|
|
||||||
));
|
|
||||||
for song in album.songs.iter() {
|
|
||||||
cc += 1;
|
|
||||||
if let Some(song) = songs.get_mut(song) {
|
|
||||||
// <artist>.d/<album>.d/<song>.tags
|
|
||||||
if let Ok(info) = fs::read_to_string(dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&song.title)
|
|
||||||
))) {
|
|
||||||
c += 1;
|
|
||||||
push_tags(&info, &mut song.general.tags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cc += artist.albums.len();
|
|
||||||
for album in artist.albums.iter() {
|
|
||||||
if let Some(album) = albums.get(album) {
|
|
||||||
cc += album.songs.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eprint!(" {cc}/{l} ({c})\r");
|
|
||||||
}
|
|
||||||
eprintln!();
|
|
||||||
}
|
}
|
||||||
eprintln!("saving dbfile...");
|
eprintln!("saving dbfile...");
|
||||||
database.save_database(None).unwrap();
|
database.save_database(None).unwrap();
|
||||||
@ -558,7 +434,7 @@ fn get_cover(
|
|||||||
if let Ok(files) = fs::read_dir(&abs_dir) {
|
if let Ok(files) = fs::read_dir(&abs_dir) {
|
||||||
for file in files {
|
for file in files {
|
||||||
if let Ok(file) = file {
|
if let Ok(file) = file {
|
||||||
let path = file.path();
|
let path = file.path();
|
||||||
if let Ok(metadata) = path.metadata() {
|
if let Ok(metadata) = path.metadata() {
|
||||||
if metadata.is_file() {
|
if metadata.is_file() {
|
||||||
if path.extension().and_then(|v| v.to_str()).is_some_and(|v| {
|
if path.extension().and_then(|v| v.to_str()).is_some_and(|v| {
|
||||||
@ -595,199 +471,3 @@ fn get_cover(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_to_file_path_component_for_custom_files(str: &str) -> String {
|
|
||||||
str.replace('%', "%p")
|
|
||||||
.replace('\0', "%0")
|
|
||||||
.replace('/', "%s")
|
|
||||||
.replace('\\', "%S")
|
|
||||||
.replace('\t', "%t")
|
|
||||||
.replace('\r', "%r")
|
|
||||||
.replace('\n', "%n")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_tag_to_str(str: &str) -> String {
|
|
||||||
str.replace('\\', "\\S")
|
|
||||||
.replace('\n', "\\n")
|
|
||||||
.replace('\r', "\\r")
|
|
||||||
}
|
|
||||||
fn normalized_str_to_tag(str: &str) -> String {
|
|
||||||
str.replace(['\n', '\r'], "")
|
|
||||||
.replace("\\n", "\n")
|
|
||||||
.replace("\\r", "\r")
|
|
||||||
.replace("\\S", "\\")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn export_to_custom_files_dir(dbdir: String, path: PathBuf) {
|
|
||||||
let database = Database::load_database_from_dir(dbdir.into(), PathBuf::new()).unwrap();
|
|
||||||
for artist in database.artists().values() {
|
|
||||||
export_custom_files_tags(
|
|
||||||
&artist.general.tags,
|
|
||||||
&path.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&artist.name)
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
let dir = path.join(format!(
|
|
||||||
"{}.d",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&artist.name)
|
|
||||||
));
|
|
||||||
{
|
|
||||||
let dir = dir.join("singles");
|
|
||||||
for song in artist.singles.iter() {
|
|
||||||
if let Some(song) = database.songs().get(song) {
|
|
||||||
export_custom_files_tags(
|
|
||||||
&song.general.tags,
|
|
||||||
&dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&song.title,)
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for album in artist.albums.iter() {
|
|
||||||
if let Some(album) = database.albums().get(album) {
|
|
||||||
export_custom_files_tags(
|
|
||||||
&album.general.tags,
|
|
||||||
&dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&album.name,)
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
let dir = dir.join(format!(
|
|
||||||
"{}.d",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&album.name,)
|
|
||||||
));
|
|
||||||
for song in album.songs.iter() {
|
|
||||||
if let Some(song) = database.songs().get(song) {
|
|
||||||
export_custom_files_tags(
|
|
||||||
&song.general.tags,
|
|
||||||
&dir.join(format!(
|
|
||||||
"{}.tags",
|
|
||||||
normalize_to_file_path_component_for_custom_files(&song.title,)
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn export_custom_files_tags(tags: &Vec<String>, path: &Path) {
|
|
||||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
let mut normalized_tags = None;
|
|
||||||
fn mk_normalized_tags<'a>(
|
|
||||||
normalized_tags: &'a mut Option<Vec<String>>,
|
|
||||||
tags: &'_ Vec<String>,
|
|
||||||
) -> &'a Vec<String> {
|
|
||||||
&*normalized_tags.get_or_insert_with(|| {
|
|
||||||
let mut tags = tags.clone();
|
|
||||||
let mut rm = Vec::new();
|
|
||||||
for (i, srcfile_tag_stripped) in tags
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, tag)| Some((i, tag.strip_prefix("SRCFILE:")?)))
|
|
||||||
{
|
|
||||||
match rm.binary_search(&i) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(v) => rm.insert(v, i),
|
|
||||||
}
|
|
||||||
if let Some(i) = tags.iter().position(|tag| tag == srcfile_tag_stripped) {
|
|
||||||
// There is a tag which just repeats the information
|
|
||||||
// which is already present in the source (audio) file.
|
|
||||||
// We do not want to save this information, so that,
|
|
||||||
// if the audio file is replaced in the future, its new
|
|
||||||
// information is used by musicdb, and musicdb-internal
|
|
||||||
// information is only used if it was changed to be different
|
|
||||||
// from the source file by the user.
|
|
||||||
match rm.binary_search(&i) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(v) => rm.insert(v, i),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i in rm.into_iter().rev() {
|
|
||||||
tags.remove(i);
|
|
||||||
}
|
|
||||||
tags
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let allow_write = match fs::exists(path) {
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Cannot check for {}, skipping. Error: {e}", path.display());
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Ok(false) => true,
|
|
||||||
Ok(true) => {
|
|
||||||
if fs::read_to_string(path).is_ok_and(|file| {
|
|
||||||
file.lines()
|
|
||||||
.map(|str| normalized_str_to_tag(str))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
== *mk_normalized_tags(&mut normalized_tags, tags)
|
|
||||||
}) {
|
|
||||||
// file contains the same tags as database, don't write,
|
|
||||||
// but don't create backup either
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
let backup_path = path.with_file_name(format!("{file_name}.backup"));
|
|
||||||
match fs::exists(&backup_path) {
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!(
|
|
||||||
"Cannot check for {}, skipping {}. Error: {e}",
|
|
||||||
backup_path.display(),
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Ok(true) => {
|
|
||||||
eprintln!(
|
|
||||||
"Backup {} exists, skipping {}.",
|
|
||||||
backup_path.display(),
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
if let Err(e) = fs::rename(path, &backup_path) {
|
|
||||||
eprintln!(
|
|
||||||
"Failed to move previous file/dir {} to {}: {e}",
|
|
||||||
path.display(),
|
|
||||||
backup_path.display()
|
|
||||||
);
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if allow_write {
|
|
||||||
if !mk_normalized_tags(&mut normalized_tags, tags).is_empty() {
|
|
||||||
if let Some(p) = path.parent() {
|
|
||||||
if let Err(e) = fs::create_dir_all(p) {
|
|
||||||
eprintln!(
|
|
||||||
"Could not create directory to contain {}: {e}",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(e) = fs::write(
|
|
||||||
path,
|
|
||||||
mk_normalized_tags(&mut normalized_tags, tags)
|
|
||||||
.iter()
|
|
||||||
.map(|tag| normalize_tag_to_str(tag) + "\n")
|
|
||||||
.collect::<String>(),
|
|
||||||
) {
|
|
||||||
eprintln!("Could not save {}: {e}", path.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
eprintln!(
|
|
||||||
"[ERR] Somehow created a non-unicode path {path:?}! This should not have happened!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,10 +7,10 @@ edition = "2021"
|
|||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
colorize = "0.1.0"
|
colorize = "0.1.0"
|
||||||
playback-rs = { version = "0.4.4", optional = true }
|
playback-rs = { version = "0.4.4", optional = true }
|
||||||
rand = "0.9.2"
|
rand = "0.8.5"
|
||||||
rc-u8-reader = "2.0.16"
|
rc-u8-reader = "2.0.16"
|
||||||
rodio = { version = "0.21.1", optional = true }
|
rodio = { version = "0.20.1", optional = true }
|
||||||
sysinfo = "0.37.0"
|
sysinfo = "0.30.12"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
@ -39,8 +39,8 @@ impl CacheManager {
|
|||||||
let sleep_dur_long = Duration::from_secs(20);
|
let sleep_dur_long = Duration::from_secs(20);
|
||||||
let sleep_dur_short = Duration::from_secs(1);
|
let sleep_dur_short = Duration::from_secs(1);
|
||||||
let mut si = sysinfo::System::new_with_specifics(
|
let mut si = sysinfo::System::new_with_specifics(
|
||||||
sysinfo::RefreshKind::nothing()
|
sysinfo::RefreshKind::new()
|
||||||
.with_memory(sysinfo::MemoryRefreshKind::nothing().with_ram()),
|
.with_memory(sysinfo::MemoryRefreshKind::new().with_ram()),
|
||||||
);
|
);
|
||||||
eprintln!("[{}] Starting CacheManager", "INFO".cyan());
|
eprintln!("[{}] Starting CacheManager", "INFO".cyan());
|
||||||
let mut sleep_short = true;
|
let mut sleep_short = true;
|
||||||
@ -54,7 +54,7 @@ impl CacheManager {
|
|||||||
});
|
});
|
||||||
sleep_short = false;
|
sleep_short = false;
|
||||||
// memory stuff
|
// memory stuff
|
||||||
si.refresh_memory_specifics(sysinfo::MemoryRefreshKind::nothing().with_ram());
|
si.refresh_memory_specifics(sysinfo::MemoryRefreshKind::new().with_ram());
|
||||||
let available_memory = si.available_memory();
|
let available_memory = si.available_memory();
|
||||||
let min_avail_mem = min_avail_mem.load(std::sync::atomic::Ordering::Relaxed);
|
let min_avail_mem = min_avail_mem.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
let low_memory = available_memory < min_avail_mem;
|
let low_memory = available_memory < min_avail_mem;
|
||||||
|
@ -9,7 +9,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use colorize::AnsiColor;
|
use colorize::AnsiColor;
|
||||||
use rand::rng;
|
use rand::thread_rng;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
load::ToFromBytes,
|
load::ToFromBytes,
|
||||||
@ -690,7 +690,7 @@ impl Database {
|
|||||||
}) = elem.content_mut()
|
}) = elem.content_mut()
|
||||||
{
|
{
|
||||||
let mut ord: Vec<usize> = (0..content.len()).collect();
|
let mut ord: Vec<usize> = (0..content.len()).collect();
|
||||||
ord.shuffle(&mut rng());
|
ord.shuffle(&mut thread_rng());
|
||||||
self.apply_action_unchecked_seq(
|
self.apply_action_unchecked_seq(
|
||||||
Action::QueueSetShuffle(path, ord, set_index),
|
Action::QueueSetShuffle(path, ord, set_index),
|
||||||
client,
|
client,
|
||||||
@ -1145,15 +1145,6 @@ impl Database {
|
|||||||
self.modified_data();
|
self.modified_data();
|
||||||
&mut self.covers
|
&mut self.covers
|
||||||
}
|
}
|
||||||
pub fn artists_albums_songs_mut(
|
|
||||||
&mut self,
|
|
||||||
) -> (
|
|
||||||
&mut HashMap<ArtistId, Artist>,
|
|
||||||
&mut HashMap<AlbumId, Album>,
|
|
||||||
&mut HashMap<SongId, Song>,
|
|
||||||
) {
|
|
||||||
(&mut self.artists, &mut self.albums, &mut self.songs)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -418,7 +418,7 @@ impl Queue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl QueueFolder {
|
impl QueueFolder {
|
||||||
pub fn iter(&self) -> QueueFolderIter<'_> {
|
pub fn iter(&self) -> QueueFolderIter {
|
||||||
QueueFolderIter {
|
QueueFolderIter {
|
||||||
folder: self,
|
folder: self,
|
||||||
index: 0,
|
index: 0,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::{ffi::OsStr, sync::Arc};
|
use std::{ffi::OsStr, sync::Arc};
|
||||||
|
|
||||||
use rc_u8_reader::ArcU8Reader;
|
use rc_u8_reader::ArcU8Reader;
|
||||||
use rodio::{decoder::DecoderError, Decoder, OutputStream, Sink, Source};
|
use rodio::{decoder::DecoderError, Decoder, OutputStream, OutputStreamHandle, Sink, Source};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{song::Song, SongId},
|
data::{song::Song, SongId},
|
||||||
@ -13,6 +13,8 @@ use super::PlayerBackend;
|
|||||||
pub struct PlayerBackendRodio<T> {
|
pub struct PlayerBackendRodio<T> {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
output_stream: OutputStream,
|
output_stream: OutputStream,
|
||||||
|
#[allow(unused)]
|
||||||
|
output_stream_handle: OutputStreamHandle,
|
||||||
sink: Sink,
|
sink: Sink,
|
||||||
stopped: bool,
|
stopped: bool,
|
||||||
current: Option<(SongId, Arc<Vec<u8>>, Option<u128>, T)>,
|
current: Option<(SongId, Arc<Vec<u8>>, Option<u128>, T)>,
|
||||||
@ -32,11 +34,11 @@ impl<T> PlayerBackendRodio<T> {
|
|||||||
pub fn new_with_optional_command_sending(
|
pub fn new_with_optional_command_sending(
|
||||||
command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
|
command_sender: Option<std::sync::mpsc::Sender<(Command, Option<u64>)>>,
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let output_stream =
|
let (output_stream, output_stream_handle) = rodio::OutputStream::try_default()?;
|
||||||
rodio::OutputStreamBuilder::from_default_device()?.open_stream_or_fallback()?;
|
let sink = Sink::try_new(&output_stream_handle)?;
|
||||||
let sink = Sink::connect_new(&output_stream.mixer());
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
output_stream,
|
output_stream,
|
||||||
|
output_stream_handle,
|
||||||
sink,
|
sink,
|
||||||
stopped: true,
|
stopped: true,
|
||||||
current: None,
|
current: None,
|
||||||
|
@ -437,7 +437,7 @@ pub fn run_server_caching_thread_opt(
|
|||||||
pub fn handle_one_connection_as_main(
|
pub fn handle_one_connection_as_main(
|
||||||
db: Arc<Mutex<Database>>,
|
db: Arc<Mutex<Database>>,
|
||||||
connection: &mut impl Read,
|
connection: &mut impl Read,
|
||||||
mut send_to: impl Write + Sync + Send + 'static,
|
mut send_to: (impl Write + Sync + Send + 'static),
|
||||||
command_sender: &mpsc::Sender<(Command, Option<u64>)>,
|
command_sender: &mpsc::Sender<(Command, Option<u64>)>,
|
||||||
) -> Result<(), std::io::Error> {
|
) -> Result<(), std::io::Error> {
|
||||||
// sync database
|
// sync database
|
||||||
|
@ -7,12 +7,12 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
musicdb-lib = { path = "../musicdb-lib" }
|
musicdb-lib = { path = "../musicdb-lib" }
|
||||||
clap = { version = "4.5.45", features = ["derive"] }
|
clap = { version = "4.4.6", features = ["derive"] }
|
||||||
headers = "0.4.1"
|
headers = "0.3.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tokio = { version = "1.47.1", optional = true, features = ["rt"] }
|
tokio = { version = "1.37.0", optional = true, features = ["rt"] }
|
||||||
rocket = { version = "0.5.1", optional = true }
|
rocket = { version = "0.5.0", optional = true }
|
||||||
html-escape = { version = "0.2.13", optional = true }
|
html-escape = { version = "0.2.13", optional = true }
|
||||||
rocket_ws = "0.1.1"
|
rocket_ws = "0.1.1"
|
||||||
rocket_seek_stream = "0.2.6"
|
rocket_seek_stream = "0.2.6"
|
||||||
|
@ -302,14 +302,14 @@ fn now_playing_ids(data: &State<Data>) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/song/<id>")]
|
#[get("/song/<id>")]
|
||||||
fn song1(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
fn song1(data: &State<Data>, id: SongId) -> Option<SeekStream> {
|
||||||
song(data, id)
|
song(data, id)
|
||||||
}
|
}
|
||||||
#[get("/song/<id>/<_>")]
|
#[get("/song/<id>/<_>")]
|
||||||
fn song2(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
fn song2(data: &State<Data>, id: SongId) -> Option<SeekStream> {
|
||||||
song(data, id)
|
song(data, id)
|
||||||
}
|
}
|
||||||
fn song(data: &State<Data>, id: SongId) -> Option<SeekStream<'_>> {
|
fn song(data: &State<Data>, id: SongId) -> Option<SeekStream> {
|
||||||
let db = data.db.lock().unwrap();
|
let db = data.db.lock().unwrap();
|
||||||
if let Some(song) = db.get_song(&id) {
|
if let Some(song) = db.get_song(&id) {
|
||||||
song.cached_data().cache_data_start_thread(&*db, song);
|
song.cached_data().cache_data_start_thread(&*db, song);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user