Compare commits

...

3 Commits

Author SHA1 Message Date
Mark
e3293fffe6 feat: add right click to songs in queue 2026-08-01 13:09:43 +02:00
Mark
8ceecf6bb6 chore(client): update speedy2d and toml dependency 2026-08-01 11:32:10 +02:00
Mark
8b68995475 chore: update some dependencies 2026-08-01 11:27:57 +02:00
27 changed files with 9731 additions and 637 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,2 @@
*/Cargo.lock
*/target
TODO.txt

3175
musicdb-client/Cargo.lock generated Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,11 @@ edition = "2024"
[dependencies]
musicdb-lib = { path = "../musicdb-lib", default-features = false }
clap = { version = "4.5.45", features = ["derive"] }
clap = { version = "4.6.5", features = ["derive"] }
directories = "6.0.0"
regex = "1.11.1"
speedy2d = { version = "2.1.0", optional = true }
toml = "0.9.5"
regex = "1.13.1"
speedy2d = { version = "3.1.0", optional = true }
toml = "1.1.4"
# musicdb-mers = { version = "0.1.0", path = "../musicdb-mers", optional = true }
uianimator = "0.1.1"

View File

@@ -3,22 +3,23 @@ use std::{
collections::{BTreeMap, HashMap},
io::Cursor,
net::TcpStream,
sync::{mpsc::Sender, Arc, Mutex},
sync::{Arc, Mutex, mpsc::Sender},
thread::JoinHandle,
time::{Duration, Instant},
};
use musicdb_lib::{
data::{
AlbumId, ArtistId, CoverId, SongId,
database::{ClientIo, Database},
queue::Queue,
song::Song,
AlbumId, ArtistId, CoverId, SongId,
},
load::ToFromBytes,
server::{get, Action},
server::{Action, get},
};
use speedy2d::{
Graphics2D,
color::Color,
dimen::{UVec2, Vec2},
font::Font,
@@ -28,7 +29,6 @@ use speedy2d::{
KeyScancode, ModifiersState, MouseButton, MouseScrollDistance, UserEventSender,
VirtualKeyCode, WindowCreationOptions, WindowHandler, WindowHelper,
},
Graphics2D,
};
#[cfg(feature = "merscfg")]
@@ -658,11 +658,9 @@ pub(crate) trait GuiElemInternal: GuiElem {
// adjust info
let npos = adjust_area(&info.pos, &self.config_mut().pos);
let ppos = std::mem::replace(&mut info.pos, npos);
if info.child_has_keyboard_focus {
if self.config().keyboard_focus_index == usize::MAX {
info.has_keyboard_focus = true;
info.child_has_keyboard_focus = false;
}
if info.child_has_keyboard_focus && self.config().keyboard_focus_index == usize::MAX {
info.has_keyboard_focus = true;
info.child_has_keyboard_focus = false;
}
info.mouse_pos_in_bounds = info.pos.contains(info.mouse_pos);
if !info.mouse_pos_in_bounds {
@@ -716,12 +714,11 @@ pub(crate) trait GuiElemInternal: GuiElem {
) -> Option<Vec<GuiAction>> {
if self.config().enabled || allow_deactivated {
for c in &mut self.children() {
if c.config().enabled {
if c.config().pixel_pos.contains(pos) {
if let Some(v) = c._mouse_event(e, allow_deactivated, condition, pos) {
return Some(v);
}
}
if c.config().enabled
&& c.config().pixel_pos.contains(pos)
&& let Some(v) = c._mouse_event(e, allow_deactivated, condition, pos)
{
return Some(v);
}
}
condition(self.elem_mut(), e)
@@ -739,10 +736,10 @@ pub(crate) trait GuiElemInternal: GuiElem {
e,
false,
&mut |v, e| {
if v.config().drag_target {
if let Some(d) = dragged.take() {
return Some(v.dragged(e, d));
}
if v.config().drag_target
&& let Some(d) = dragged.take()
{
return Some(v.dragged(e, d));
}
None
},
@@ -775,7 +772,7 @@ pub(crate) trait GuiElemInternal: GuiElem {
v.config_mut().mouse_down.2 = true;
v.config_mut().mouse_pressed.2 = true;
}
MouseButton::Other(_) => {}
_ => {}
}
Some(v.mouse_down(e, button))
} else {
@@ -820,7 +817,7 @@ pub(crate) trait GuiElemInternal: GuiElem {
v.config_mut().mouse_down.2 = false;
v.config_mut().mouse_pressed.2 = false;
}
MouseButton::Other(_) => {}
_ => {}
}
vec.extend(v.mouse_up(e, button));
}
@@ -887,7 +884,7 @@ pub(crate) trait GuiElemInternal: GuiElem {
}
}
fn _keyboard_move_focus(&mut self, decrement: bool, refocus: bool) -> bool {
if self.config().enabled == false {
if !self.config().enabled {
return false;
}
let mut focus_index = if refocus {
@@ -1133,7 +1130,8 @@ pub struct GuiElemCfg {
pub enabled: bool,
/// if true, indicates that something (text size, screen size, ...) has changed
/// and you should probably relayout and redraw from scratch.
pub redraw: bool,
redraw: bool,
redraw2: bool,
/// will be set to false after `draw`.
/// can be used to, for example, add the keybinds for your element.
pub init: bool,
@@ -1193,9 +1191,27 @@ impl GuiElemCfg {
self.drag_target = true;
self
}
pub fn force_redraw(mut self) -> Self {
pub fn redraw(&self) -> bool {
self.redraw
}
/// sets `redraw`, causing this element to redraw itself on the next frame.
pub fn redraw_once(&mut self) {
self.redraw = true;
self
}
/// same as calling `redraw()` and then
/// calling `redraw()` again on the next frame.
/// in other words, the next 2 frames will see a redraw of this item.
pub fn redraw_twice(&mut self) {
self.redraw = true;
self.redraw2 = true;
}
/// sets `redraw` back to `false` (usually)
pub fn redrawn(&mut self) {
if self.redraw2 {
self.redraw2 = false;
} else {
self.redraw = false;
}
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
@@ -1207,6 +1223,7 @@ impl Default for GuiElemCfg {
Self {
enabled: true,
redraw: false,
redraw2: false,
init: true,
pos: Rectangle::new(Vec2::ZERO, Vec2::new(1.0, 1.0)),
pixel_pos: Rectangle::ZERO,
@@ -1270,6 +1287,7 @@ pub enum Dragging {
Queue(Result<Queue, Vec<usize>>),
Queues(Vec<Queue>),
}
#[allow(clippy::enum_variant_names)]
pub enum SpecificGuiElem {
SearchArtist,
SearchAlbum,
@@ -1322,7 +1340,7 @@ impl Gui {
pub fn exec_gui_action(&mut self, action: GuiAction) {
match action {
GuiAction::Build(f) => {
let actions = f(&mut *self.database.lock().unwrap());
let actions = f(&mut self.database.lock().unwrap());
for action in actions {
self.exec_gui_action(action);
}
@@ -1478,7 +1496,7 @@ impl WindowHandler<GuiEvent> for Gui {
time: draw_start_time,
actions: Vec::with_capacity(0),
pos: Rectangle::new(Vec2::ZERO, self.size.into_f32()),
database: &mut *dblock,
database: &mut dblock,
font: &self.font,
mouse_pos: self.mouse_pos,
mouse_pos_in_bounds: false,
@@ -1572,9 +1590,9 @@ impl WindowHandler<GuiEvent> for Gui {
}
}
fn on_mouse_button_down(&mut self, helper: &mut WindowHelper<GuiEvent>, button: MouseButton) {
if let Some(a) =
self.gui
._mouse_button(&mut EventInfo::new(), button, true, self.mouse_pos.clone())
if let Some(a) = self
.gui
._mouse_button(&mut EventInfo::new(), button, true, self.mouse_pos)
{
for a in a {
self.exec_gui_action(a)
@@ -1586,9 +1604,9 @@ impl WindowHandler<GuiEvent> for Gui {
if self.dragging.is_some() {
let (dr, _) = self.dragging.take().unwrap();
let mut opt = Some(dr);
if let Some(a) =
self.gui
._release_drag(&mut EventInfo::new(), &mut opt, self.mouse_pos.clone())
if let Some(a) = self
.gui
._release_drag(&mut EventInfo::new(), &mut opt, self.mouse_pos)
{
for a in a {
self.exec_gui_action(a)
@@ -1609,7 +1627,7 @@ impl WindowHandler<GuiEvent> for Gui {
}
if let Some(a) =
self.gui
._mouse_button(&mut EventInfo::new(), button, false, self.mouse_pos.clone())
._mouse_button(&mut EventInfo::new(), button, false, self.mouse_pos)
{
for a in a {
self.exec_gui_action(a)
@@ -1639,7 +1657,7 @@ impl WindowHandler<GuiEvent> for Gui {
};
if let Some(a) = self
.gui
._mouse_wheel(&mut EventInfo::new(), dist, self.mouse_pos.clone())
._mouse_wheel(&mut EventInfo::new(), dist, self.mouse_pos)
{
for a in a {
self.exec_gui_action(a)
@@ -1673,10 +1691,10 @@ impl WindowHandler<GuiEvent> for Gui {
scancode: KeyScancode,
) {
helper.request_redraw();
if let Some(VirtualKeyCode::Tab) = virtual_key_code {
if !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo()) {
self.gui._keyboard_move_focus(self.modifiers.shift(), false);
}
if let Some(VirtualKeyCode::Tab) = virtual_key_code
&& !(self.modifiers.ctrl() || self.modifiers.alt() || self.modifiers.logo())
{
self.gui._keyboard_move_focus(self.modifiers.shift(), false);
}
for a in self.gui._keyboard_event(
&mut EventInfo::new(),
@@ -1717,18 +1735,19 @@ impl WindowHandler<GuiEvent> for Gui {
// handle keybinds unless settings are open, opening or closing
let mut e = EventInfo::new();
let mut post_action = None;
if self.gui.settings.0 == false && self.gui.settings.1.is_none() {
if let Some(key) = virtual_key_code {
let keybind = KeyBinding::new(&self.modifiers, key);
if let Some(action) = self.keybinds.get(&keybind) {
if action.has_priority() {
e.take();
for a in self.key_actions.get(&action.id()).execute() {
self.exec_gui_action(a);
}
} else {
post_action = Some(action.id());
if !self.gui.settings.0
&& self.gui.settings.1.is_none()
&& let Some(key) = virtual_key_code
{
let keybind = KeyBinding::new(&self.modifiers, key);
if let Some(action) = self.keybinds.get(&keybind) {
if action.has_priority() {
e.take();
for a in self.key_actions.get(&action.id()).execute() {
self.exec_gui_action(a);
}
} else {
post_action = Some(action.id());
}
}
}
@@ -1760,11 +1779,11 @@ impl WindowHandler<GuiEvent> for Gui {
) {
self.exec_gui_action(a);
}
if let Some(post_action) = post_action.take() {
if e.take() {
for a in self.key_actions.get(&post_action).execute() {
self.exec_gui_action(a);
}
if let Some(post_action) = post_action.take()
&& e.take()
{
for a in self.key_actions.get(&post_action).execute() {
self.exec_gui_action(a);
}
}
}
@@ -1791,7 +1810,9 @@ impl WindowHandler<GuiEvent> for Gui {
MersCfg::run(&mut gc, self, |m| &m.func_library_updated);
self.gui_config = Some(gc);
} else {
eprintln!("WARN: Skipping call to merscfg's library_updated because gui_config is not available");
eprintln!(
"WARN: Skipping call to merscfg's library_updated because gui_config is not available"
);
}
self.gui._recursive_all(true, &mut |e| e.updated_library());
helper.request_redraw();
@@ -1802,7 +1823,9 @@ impl WindowHandler<GuiEvent> for Gui {
MersCfg::run(&mut gc, self, |m| &m.func_queue_updated);
self.gui_config = Some(gc);
} else {
eprintln!("WARN: Skipping call to merscfg's queue_updated because gui_config is not available");
eprintln!(
"WARN: Skipping call to merscfg's queue_updated because gui_config is not available"
);
}
self.gui._recursive_all(true, &mut |e| e.updated_queue());
helper.request_redraw();

View File

@@ -73,7 +73,7 @@ pub struct Square<T: GuiElem> {
#[allow(unused)]
impl<T: GuiElem> Square<T> {
pub fn new(mut config: GuiElemCfg, inner: T) -> Self {
config.redraw = true;
config.redraw_once();
Self { config, inner }
}
}
@@ -101,10 +101,10 @@ impl<T: GuiElem + 'static> GuiElem for Square<T> {
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = true;
self.config.redraw_once();
}
if self.config.redraw {
self.config.redraw = false;
if self.config.redraw() {
self.config.redrawn();
if info.pos.width() > info.pos.height() {
let w = 0.5 * info.pos.height() / info.pos.width();
self.inner.config_mut().pos =
@@ -193,7 +193,7 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.pixel_pos.size() != info.pos.size() {
self.config.redraw = true;
self.config.redraw_once();
}
// smooth scrolling animation
if self.scroll_target > self.max_scroll {
@@ -202,7 +202,7 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
self.scroll_target = 0.0;
}
if self.scroll_target != self.scroll_display {
self.config.redraw = true;
self.config.redraw_once();
if info.high_performance {
self.scroll_display = self.scroll_target;
} else {
@@ -215,7 +215,7 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
}
}
// recalculate positions
if self.config.redraw {
if self.config.redraw() {
// adjust height vector length if necessary
if self.children_heights.len() != self.children.len() {
let target = self.children.len();
@@ -229,7 +229,7 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
//
self.mouse_scroll_margin_right = info.line_height * 0.2;
let max_x = 1.0 - self.mouse_scroll_margin_right / info.pos.width();
self.config.redraw = false;
self.config.redrawn();
let mut y_pos = -self.scroll_display;
for (e, h) in self.children.iter().zip(self.children_heights.iter()) {
let h_rel = self.size_unit.to_rel(*h, info.pos.height());
@@ -292,8 +292,8 @@ impl<C: GuiElemChildren + 'static> GuiElem for ScrollBox<C> {
}
}
fn mouse_wheel(&mut self, e: &mut EventInfo, diff: f32) -> Vec<crate::gui::GuiAction> {
let nst = (self.scroll_target - self.size_unit.from_abs(diff as f32, self.last_height_px))
.max(0.0);
let nst =
(self.scroll_target - self.size_unit.from_abs(diff, self.last_height_px)).max(0.0);
// only take the event if this would actually scroll, and only scroll if we can actually take the event
if nst != self.scroll_target && e.take() {
self.scroll_target = nst;
@@ -323,12 +323,14 @@ impl ScrollBoxSizeUnit {
Self::Pixels => val / draw_height,
}
}
#[allow(clippy::wrong_self_convention)]
fn from_rel(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val,
Self::Pixels => val * draw_height,
}
}
#[allow(clippy::wrong_self_convention)]
fn from_abs(&self, val: f32, draw_height: f32) -> f32 {
match self {
Self::Relative => val / draw_height,
@@ -545,21 +547,13 @@ impl Slider {
{
if since >= 1.0 {
s.display_since = None;
if s.display {
1.0
} else {
0.0
}
if s.display { 1.0 } else { 0.0 }
} else {
if let Some(h) = &i.helper {
h.request_redraw();
}
s.config.redraw = true;
if s.display {
since
} else {
1.0 - since
}
s.config.redraw_once();
if s.display { since } else { 1.0 - since }
}
} else {
1.0
@@ -613,7 +607,7 @@ impl GuiElem for Slider {
if self.display != (self.config.mouse_down.0 || info.pos.contains(info.mouse_pos)) {
self.display = !self.display;
self.display_since = Some(info.time);
self.config.redraw = true;
self.config.redraw_once();
}
let dot_size = (info.pos.height() * 0.9).min(info.pos.width() * 0.25);
let y_mid_line = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
@@ -634,10 +628,8 @@ impl GuiElem for Slider {
(info.mouse_pos.x - line_pos.top_left().x) as f64 / line_pos.width() as f64,
));
self.val_changed = true;
for v in &mut self.val_changed_subs {
*v = true;
}
self.config.redraw = true;
self.val_changed_subs.fill(true);
self.config.redraw_once();
}
let line_color = Color::from_int_rgb(50, 50, 100);
g.draw_circle(
@@ -660,8 +652,8 @@ impl GuiElem for Slider {
0.5 * dot_size,
Color::CYAN,
);
if self.config.redraw {
self.config.redraw = false;
if self.config.redraw() {
self.config.redrawn();
(Arc::clone(&self.on_update))(self, info);
}
}

View File

@@ -1,16 +1,16 @@
use std::{collections::BTreeSet, time::Instant};
use speedy2d::{
Graphics2D,
color::Color,
dimen::{Vec2, Vector2},
shape::Rectangle,
Graphics2D,
};
use crate::{
gui::{DrawInfo, GuiElem, GuiElemCfg},
gui_anim::AnimationController,
gui_base::{Button, ScrollBox},
gui_base::{Button, Panel, ScrollBox},
gui_text::{Label, TextField},
};
@@ -24,36 +24,62 @@ pub enum Event {
pub struct EditorForAnyTagInList {
config: GuiElemCfg,
pub tag: String,
label: Label,
rm_button: Button<[IconDelete; 1]>,
panel: Panel<(Label, Button<[IconDelete; 1]>)>,
}
impl EditorForAnyTagInList {
pub fn new<T: From<Event> + 'static>(
tag: String,
index: usize,
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(),
let label = Label::new(
GuiElemCfg::default(),
tag.clone(),
Color::WHITE,
None,
Vector2::new(0.0, 0.5),
);
let rm_button = Button::new(
GuiElemCfg::default(),
{
let tag = tag.clone();
move |btn| {
btn.disable();
sender.send(Event::RemoveTag(tag.clone()).into()).unwrap();
vec![]
},
[IconDelete::new(GuiElemCfg::default())],
),
}
}
},
[IconDelete::new(GuiElemCfg::default())],
);
let panel = Panel::with_background(
GuiElemCfg::default(),
(label, rm_button),
match index % 2 {
1 => Color::from_rgba(0.0, 1.0, 0.0, 0.1),
_ => Color::from_rgba(0.0, 0.0, 1.0, 0.14),
},
);
Self { config, tag, panel }
}
fn row(&self) -> &(Label, Button<[IconDelete; 1]>) {
&self.panel.children
}
fn row_mut(&mut self) -> &mut (Label, Button<[IconDelete; 1]>) {
&mut self.panel.children
}
fn label(&self) -> &Label {
&self.panel.children.0
}
fn rm_button(&self) -> &Button<[IconDelete; 1]> {
&self.panel.children.1
}
fn label_mut(&mut self) -> &mut Label {
&mut self.panel.children.0
}
fn rm_button_mut(&mut self) -> &mut Button<[IconDelete; 1]> {
&mut self.panel.children.1
}
}
@@ -63,11 +89,11 @@ impl GuiElem for EditorForAnyTagInList {
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(
self.rm_button_mut().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(
self.label_mut().config_mut().pos = Rectangle::from_tuples(
(0.0, label_padding / info.pos.height()),
(x_split, 1.0 - label_padding / info.pos.height()),
);
@@ -79,7 +105,7 @@ impl GuiElem for EditorForAnyTagInList {
&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())
Box::new([self.panel.elem_mut()].into_iter())
}
fn any(&self) -> &dyn std::any::Any {
self
@@ -162,7 +188,7 @@ impl<T: From<Event> + 'static> EditorForAnyTagAdder<T> {
expand_to,
c_value: TextField::new(
GuiElemCfg::default(),
"artist".to_owned(),
"tag".to_owned(),
Color::DARK_GRAY,
Color::WHITE,
),
@@ -180,7 +206,7 @@ impl<T: From<Event> + 'static> EditorForAnyTagAdder<T> {
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;
self.config_mut().redraw_once();
}
}
impl<T: From<Event> + 'static> GuiElem for EditorForAnyTagAdder<T> {
@@ -196,8 +222,8 @@ impl<T: From<Event> + 'static> GuiElem for EditorForAnyTagAdder<T> {
}
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 {
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() {
@@ -224,7 +250,7 @@ impl<T: From<Event> + 'static> GuiElem for EditorForAnyTagAdder<T> {
.flat_map(|s| s.general.tags.iter()),
)
.filter(|tag| tag.to_lowercase().contains(&search))
.map(|tag| tag.clone())
.cloned()
.collect::<BTreeSet<_>>();
if !tags.contains(self.c_value.c_input.content.get_text()) {
tags.insert(self.c_value.c_input.content.get_text().clone());
@@ -252,8 +278,9 @@ impl<T: From<Event> + 'static> GuiElem for EditorForAnyTagAdder<T> {
)
})
.collect();
self.c_picker.config_mut().redraw = true;
self.c_picker.config_mut().redraw_once();
self.last_search = search;
self.config.redrawn();
}
}
fn config(&self) -> &GuiElemCfg {

View File

@@ -1,7 +1,7 @@
use std::time::Instant;
use musicdb_lib::{
data::{song::Song, ArtistId},
data::{ArtistId, song::Song},
server::{Action, Req},
};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
@@ -11,7 +11,7 @@ use crate::{
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren},
gui_anim::AnimationController,
gui_base::{Button, Panel, ScrollBox},
gui_edit_any::{EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox, ELEM_HEIGHT},
gui_edit_any::{ELEM_HEIGHT, EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox},
gui_text::{Label, TextField},
};
@@ -28,6 +28,7 @@ pub struct EditorForSongs {
event_sender: std::sync::mpsc::Sender<Event>,
event_recv: std::sync::mpsc::Receiver<Event>,
}
#[allow(clippy::enum_variant_names)]
pub enum Event {
Close,
Apply,
@@ -102,7 +103,7 @@ impl EditorForSongs {
c_artist: EditorForSongArtistChooser::new(sender.clone()),
c_album: Label::new(
GuiElemCfg::default(),
format!("(todo...)"),
"(todo...)".to_owned(),
Color::GRAY,
None,
Vec2::new(0.0, 0.5),
@@ -117,9 +118,11 @@ impl EditorForSongs {
}
}
tags.into_iter()
.map(|tag| {
.enumerate()
.map(|(i, tag)| {
EditorForAnyTagInList::new(
tag.to_owned(),
i,
sender.clone(),
GuiElemCfg::default(),
)
@@ -198,110 +201,112 @@ impl GuiElem for EditorForSongs {
)
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
loop {
match self.event_recv.try_recv() {
Ok(e) => match e {
Event::Close => info.actions.push(GuiAction::Do(Box::new(|gui| {
gui.gui.c_editing_songs = None;
gui.gui.set_normal_ui_enabled(true);
}))),
Event::Apply => {
let mut actions = Vec::new();
for song in self.songs.iter() {
let mut song = song.clone();
while let Ok(e) = self.event_recv.try_recv() {
match e {
Event::Close => info.actions.push(GuiAction::Do(Box::new(|gui| {
gui.gui.c_editing_songs = None;
gui.gui.set_normal_ui_enabled(true);
}))),
Event::Apply => {
let mut actions = Vec::new();
for song in self.songs.iter() {
let mut song = song.clone();
let new_title = self
.c_scrollbox
.children
.c_title
.c_input
.content
.get_text()
.trim();
if !new_title.is_empty() {
song.title = new_title.to_owned();
}
if let Some(artist_id) = self.c_scrollbox.children.c_artist.chosen_id {
song.artist = artist_id;
song.album = None;
}
actions.push(Action::ModifySong(song, Req::none()));
}
if actions.len() == 1 {
info.actions
.push(GuiAction::SendToServer(actions.pop().unwrap()));
} else if actions.len() > 1 {
info.actions
.push(GuiAction::SendToServer(Action::Multiple(actions)));
}
}
Event::SetArtist(name, 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
.open_prog
.set_target(info.time, 1.0);
*self
let new_title = self
.c_scrollbox
.children
.c_artist
.c_name
.c_title
.c_input
.content
.text() = name;
self.c_scrollbox.children.c_artist.config_mut().redraw = true;
.get_text()
.trim();
if !new_title.is_empty() {
song.title = new_title.to_owned();
}
if let Some(artist_id) = self.c_scrollbox.children.c_artist.chosen_id {
song.artist = artist_id;
song.album = None;
}
actions.push(Action::ModifySong(song, Req::none()));
}
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;
if actions.len() == 1 {
info.actions
.push(GuiAction::SendToServer(actions.pop().unwrap()));
} else if actions.len() > 1 {
info.actions
.push(GuiAction::SendToServer(Action::Multiple(actions)));
}
}
Event::SetArtist(name, 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
.open_prog
.set_target(info.time, 1.0);
*self
.c_scrollbox
.children
.c_artist
.c_name
.c_input
.content
.text() = name;
self.c_scrollbox
.children
.c_artist
.config_mut()
.redraw_once();
}
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);
}
}
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;
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_once();
}
}
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,
);
let i = self.c_scrollbox.children.c_tags.len();
self.c_scrollbox
.children
.c_tags
.push(EditorForAnyTagInList::new(
tag,
i,
self.event_sender.clone(),
GuiElemCfg::default(),
));
self.c_scrollbox.config_mut().redraw_once();
}
}
}
},
Err(_) => break,
}
}
}
// animation
@@ -332,7 +337,7 @@ impl GuiElem for EditorForSongs {
{
if let Some(v) = self.c_scrollbox.children_heights.get_mut(1) {
*v = ELEM_HEIGHT * val as f32;
self.c_scrollbox.config_mut().redraw = true;
self.c_scrollbox.config_mut().redraw_once();
}
if let Some(h) = &info.helper {
h.request_redraw();
@@ -351,7 +356,7 @@ impl GuiElem for EditorForSongs {
.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_once();
}
if let Some(h) = &info.helper {
h.request_redraw();
@@ -428,8 +433,8 @@ impl GuiElem for EditorForSongArtistChooser {
}
let search = self.c_name.c_input.content.get_text().to_lowercase();
let search_changed = &self.last_search != &search;
if self.config.redraw || search_changed {
let search_changed = self.last_search != search;
if self.config.redraw() || search_changed {
*self.c_name.c_input.content.color() = if self.chosen_id.is_some() {
Color::GREEN
} else {
@@ -480,8 +485,9 @@ impl GuiElem for EditorForSongArtistChooser {
)
})
.collect();
self.c_picker.config_mut().redraw = true;
self.c_picker.config_mut().redraw_once();
self.last_search = search;
self.config.redrawn();
}
}
fn config(&self) -> &GuiElemCfg {

View File

@@ -126,27 +126,27 @@ impl GuiElem for IdleDisplay {
self.c_top_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config
.idle_top_text
.gen_new(&info.database, info.database.get_song(&song))
.gen_new(info.database, info.database.get_song(&song))
} else {
vec![]
};
self.c_top_label.config_mut().redraw = true;
self.c_top_label.config_mut().redraw_once();
self.c_side1_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config
.idle_side1_text
.gen_new(&info.database, info.database.get_song(&song))
.gen_new(info.database, info.database.get_song(&song))
} else {
vec![]
};
self.c_side1_label.config_mut().redraw = true;
self.c_side1_label.config_mut().redraw_once();
self.c_side2_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config
.idle_side2_text
.gen_new(&info.database, info.database.get_song(&song))
.gen_new(info.database, info.database.get_song(&song))
} else {
vec![]
};
self.c_side2_label.config_mut().redraw = true;
self.c_side2_label.config_mut().redraw_once();
// check artist
if let Some(artist_id) = self
.current_info
@@ -165,8 +165,8 @@ impl GuiElem for IdleDisplay {
self.artist_image_aspect_ratio.set_target(info.time, 0.0);
if let Some(artist) = info.database.artists().get(&artist_id) {
for tag in &artist.general.tags {
if tag.starts_with("ImageExt=") {
let filename = format!("{}.{}", artist.name, &tag[9..]);
if let Some(tag) = tag.strip_prefix("ImageExt=") {
let filename = format!("{}.{}", artist.name, tag);
self.current_artist_image =
Some((artist_id, Some((filename.clone(), None))));
if !info.custom_images.contains_key(&filename) {
@@ -199,15 +199,14 @@ impl GuiElem for IdleDisplay {
Some((_, None)) | Some((_, Some(Some(_)))) => {}
}
}
if let Some((_, Some((img, h)))) = &mut self.current_artist_image {
if h.is_none() {
if let Some(img) = info.custom_images.get_mut(img) {
if let Some(img) = img.get_init(g) {
*h = Some(Some(img));
} else if img.is_err() {
*h = Some(None);
}
}
if let Some((_, Some((img, h)))) = &mut self.current_artist_image
&& h.is_none()
&& let Some(img) = info.custom_images.get_mut(img)
{
if let Some(img) = img.get_init(g) {
*h = Some(Some(img));
} else if img.is_err() {
*h = Some(None);
}
}
// draw cover

View File

@@ -3,18 +3,19 @@ use std::{
collections::HashSet,
sync::Arc,
sync::{
Mutex,
atomic::{AtomicBool, AtomicUsize},
mpsc, Mutex,
mpsc,
},
};
use musicdb_lib::data::{
AlbumId, ArtistId, GeneralData, SongId,
album::Album,
artist::Artist,
database::Database,
queue::{Queue, QueueContent},
song::Song,
AlbumId, ArtistId, GeneralData, SongId,
};
use regex::{Regex, RegexBuilder};
use speedy2d::{
@@ -296,12 +297,8 @@ impl GuiElem for LibraryBrowser {
false
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
loop {
if let Ok(action) = self.do_something_receiver.try_recv() {
action(self);
} else {
break;
}
while let Ok(action) = self.do_something_receiver.try_recv() {
action(self);
}
// search
let mut search_changed = false;
@@ -396,7 +393,7 @@ impl GuiElem for LibraryBrowser {
// -
if self.library_updated {
self.library_updated = false;
self.update_local_library(&info.database, |(_, a), (_, b)| a.name.cmp(&b.name));
self.update_local_library(info.database, |(_, a), (_, b)| a.name.cmp(&b.name));
search_changed = true;
}
if search_changed {
@@ -404,7 +401,7 @@ impl GuiElem for LibraryBrowser {
s: &LibraryBrowser,
pat: &str,
regex: &Option<Regex>,
search_text: &String,
search_text: &str,
filter: &Filter,
search_gd: &GeneralData,
) -> f32 {
@@ -414,7 +411,7 @@ impl GuiElem for LibraryBrowser {
if let Some(r) = regex {
if s.search_prefers_start_matches {
r.find_iter(pat)
.map(|m| match pat[0..m.start()].chars().rev().next() {
.map(|m| match pat[0..m.start()].chars().next_back() {
// found at the start of h, reaches to the end (whole pattern is part of the match)
None if m.end() == pat.len() => 6.0,
// found at start of h
@@ -433,11 +430,7 @@ impl GuiElem for LibraryBrowser {
})
.fold(0.0, f32::max)
} else {
if r.is_match(pat) {
2.0
} else {
0.0
}
if r.is_match(pat) { 2.0 } else { 0.0 }
}
} else if search_text.is_empty() {
1.0
@@ -448,7 +441,7 @@ impl GuiElem for LibraryBrowser {
let allow_singles = self.search_album.is_empty()
&& self.filter_albums.lock().unwrap().filters.is_empty();
self.filter_local_library(
&info.database,
info.database,
|s, artist| {
filter(
s,
@@ -496,35 +489,35 @@ impl GuiElem for LibraryBrowser {
self.selected_popup_state.1 = artists;
self.selected_popup_state.2 = albums;
self.selected_popup_state.3 = songs;
if artists > 0 || albums > 0 || songs > 0 {
if let Some(text) = match (artists, albums, songs) {
if (artists > 0 || albums > 0 || songs > 0)
&& let Some(text) = match (artists, albums, songs) {
(0, 0, 0) => None,
(0, 0, 1) => Some(format!("1 song selected")),
(0, 0, 1) => Some("1 song selected".to_owned()),
(0, 0, s) => Some(format!("{s} songs selected")),
(0, 1, 0) => Some(format!("1 album selected")),
(0, 1, 0) => Some("1 album selected".to_owned()),
(0, al, 0) => Some(format!("{al} albums selected")),
(1, 0, 0) => Some(format!("1 artist selected")),
(1, 0, 0) => Some("1 artist selected".to_owned()),
(ar, 0, 0) => Some(format!("{ar} artists selected")),
(0, 1, 1) => Some(format!("1 song and 1 album selected")),
(0, 1, 1) => Some("1 song and 1 album selected".to_owned()),
(0, 1, s) => Some(format!("{s} songs and 1 album selected")),
(0, al, 1) => Some(format!("1 song and {al} albums selected")),
(0, al, s) => Some(format!("{s} songs and {al} albums selected")),
(1, 0, 1) => Some(format!("1 song and 1 artist selected")),
(1, 0, 1) => Some("1 song and 1 artist selected".to_owned()),
(1, 0, s) => Some(format!("{s} songs and 1 artist selected")),
(ar, 0, 1) => Some(format!("1 song and {ar} artists selected")),
(ar, 0, s) => Some(format!("{s} songs and {ar} artists selected")),
(1, 1, 0) => Some(format!("1 album and 1 artist selected")),
(1, 1, 0) => Some("1 album and 1 artist selected".to_owned()),
(1, al, 0) => Some(format!("{al} albums and 1 artist selected")),
(ar, 1, 0) => Some(format!("1 album and {ar} artists selected")),
(ar, al, 0) => Some(format!("{al} albums and {ar} artists selected")),
(1, 1, 1) => Some(format!("1 song, 1 album and 1 artist selected")),
(1, 1, 1) => Some("1 song, 1 album and 1 artist selected".to_owned()),
(1, 1, s) => Some(format!("{s} songs, 1 album and 1 artist selected")),
(1, al, 1) => {
Some(format!("1 song, {al} albums and 1 artist selected"))
@@ -544,14 +537,13 @@ impl GuiElem for LibraryBrowser {
(ar, al, s) => {
Some(format!("{s} songs, {al} albums and {ar} artists selected"))
}
} {
*self.c_selected_counter_panel.children[0].content.text() = text;
}
} else {
{
*self.c_selected_counter_panel.children[0].content.text() = text;
}
}
}
self.config.redraw = true;
self.config.redraw_once();
}
// selected popup
{
@@ -571,7 +563,7 @@ impl GuiElem for LibraryBrowser {
} else {
if self.selected_popup_state.0 != 0.0 {
redraw = true;
self.selected_popup_state.0 = 0.7 * self.selected_popup_state.0;
self.selected_popup_state.0 *= 0.7;
if self.selected_popup_state.0 < 0.01 {
self.selected_popup_state.0 = 0.0;
self.c_selected_counter_panel.config_mut().enabled = false;
@@ -588,9 +580,9 @@ impl GuiElem for LibraryBrowser {
}
}
}
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
self.update_ui(&info.database, info.line_height);
if self.config.redraw() || info.pos.size() != self.config.pixel_pos.size() {
self.config.redrawn();
self.update_ui(info.database, info.line_height);
}
}
fn updated_library(&mut self) {
@@ -631,13 +623,13 @@ impl LibraryBrowser {
self.library_sorted = artists
.into_iter()
.map(|(ar_id, artist)| {
let singles = artist.singles.iter().map(|id| *id).collect();
let singles = artist.singles.clone();
let albums = artist
.albums
.iter()
.map(|id| {
let songs = if let Some(album) = db.albums().get(id) {
album.songs.iter().map(|id| *id).collect()
album.songs.clone()
} else {
eprintln!("[warn] No album with id {id} found in db!");
vec![]
@@ -762,7 +754,7 @@ impl LibraryBrowser {
let library_scroll_box = &mut self.c_scroll_box;
library_scroll_box.children = elems;
library_scroll_box.children_heights = elemh;
library_scroll_box.config_mut().redraw = true;
library_scroll_box.config_mut().redraw_once();
}
fn build_ui_element_artist(&self, id: ArtistId, db: &Database, h: f32) -> (ListElement, f32) {
(
@@ -878,7 +870,7 @@ impl ListArtist {
None,
Vec2::new(0.0, 0.5),
);
config.redraw = true;
config.redraw_once();
Self {
config: config.w_mouse(),
id,
@@ -913,8 +905,8 @@ impl GuiElem for ListArtist {
self
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.config.redraw {
self.config.redraw = false;
if self.config.redraw() {
self.config.redrawn();
let sel = self.selected.contains_artist(&self.id);
if sel != self.sel {
self.sel = sel;
@@ -972,7 +964,7 @@ impl GuiElem for ListArtist {
fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
self.config.redraw = true;
self.config.redraw_once();
if e.take() {
if !self.sel {
self.selected.insert_artist(self.id);
@@ -1024,7 +1016,7 @@ impl ListAlbum {
),
]],
);
config.redraw = true;
config.redraw_once();
Self {
config: config.w_mouse(),
id,
@@ -1059,8 +1051,8 @@ impl GuiElem for ListAlbum {
self
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.config.redraw {
self.config.redraw = false;
if self.config.redraw() {
self.config.redrawn();
let sel = self.selected.contains_album(&self.id);
if sel != self.sel {
self.sel = sel;
@@ -1118,7 +1110,7 @@ impl GuiElem for ListAlbum {
fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
self.config.redraw = true;
self.config.redraw_once();
if e.take() {
if !self.sel {
self.selected.insert_album(self.id);
@@ -1167,7 +1159,7 @@ impl ListSong {
),
]],
);
config.redraw = true;
config.redraw_once();
Self {
config: config.w_mouse(),
id,
@@ -1202,8 +1194,8 @@ impl GuiElem for ListSong {
self
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.config.redraw {
self.config.redraw = false;
if self.config.redraw() {
self.config.redrawn();
let sel = self.selected.contains_song(&self.id);
if sel != self.sel {
self.sel = sel;
@@ -1261,7 +1253,7 @@ impl GuiElem for ListSong {
fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
self.config.redraw = true;
self.config.redraw_once();
if e.take() {
if !self.sel {
self.selected.insert_song(self.id);
@@ -1288,7 +1280,7 @@ impl GuiElem for ListSong {
},
[Label::new(
GuiElemCfg::default(),
format!("Edit this song"),
"Edit this song".to_owned(),
Color::WHITE,
None,
Vec2::new_y(0.5),
@@ -1315,7 +1307,7 @@ impl GuiElem for ListSong {
},
[Label::new(
GuiElemCfg::default(),
format!("Edit selected songs"),
"Edit selected songs".to_owned(),
Color::WHITE,
None,
Vec2::new_y(0.5),
@@ -1354,6 +1346,7 @@ struct FilterTab {
buttons: Vec<Button<[Label; 1]>>,
filters: Vec<FilterLine>,
}
#[allow(clippy::large_enum_variant)]
enum FilterLine {
Joiner(Button<[Label; 1]>),
Not(Label),
@@ -1394,10 +1387,10 @@ impl GuiElemChildren for FilterTab {
self.buttons.len() + self.filters.len()
}
}
const FP_CASESENS_N: &'static str = "search is case-insensitive";
const FP_CASESENS_Y: &'static str = "search is case-sensitive!";
const FP_PREFSTART_N: &'static str = "simple search";
const FP_PREFSTART_Y: &'static str = "will prefer matches at the start of a word";
const FP_CASESENS_N: &str = "search is case-insensitive";
const FP_CASESENS_Y: &str = "search is case-sensitive!";
const FP_PREFSTART_N: &str = "simple search";
const FP_PREFSTART_Y: &str = "will prefer matches at the start of a word";
impl FilterPanel {
pub fn new(
search_settings_changed: Arc<AtomicBool>,
@@ -1697,8 +1690,8 @@ impl FilterPanel {
.iter()
.cloned()
.map(|(text, preset)| {
let f = Arc::clone(&filter);
let oc = Arc::clone(&on_change);
let f = Arc::clone(filter);
let oc = Arc::clone(on_change);
Button::new(
GuiElemCfg::default(),
move |_| {
@@ -1876,26 +1869,24 @@ impl FilterPanel {
let oc = Arc::clone(on_change);
let p = path.clone();
tf1.on_changed = Some(Box::new(move |text| {
if let Ok(n) = text.parse() {
if let Some(Ok(FilterType::TagWithValueInt(_, v, _))) =
if let Ok(n) = text.parse()
&& let Some(Ok(FilterType::TagWithValueInt(_, v, _))) =
mx.lock().unwrap().get_mut(&p)
{
*v = n;
oc(false);
}
{
*v = n;
oc(false);
}
}));
let mx = Arc::clone(mutex);
let oc = Arc::clone(on_change);
let p = path.clone();
tf2.on_changed = Some(Box::new(move |text| {
if let Ok(n) = text.parse() {
if let Some(Ok(FilterType::TagWithValueInt(_, _, v))) =
if let Ok(n) = text.parse()
&& let Some(Ok(FilterType::TagWithValueInt(_, _, v))) =
mx.lock().unwrap().get_mut(&p)
{
*v = n;
oc(false);
}
{
*v = n;
oc(false);
}
}));
children.push(FilterLine::TagWithValueInt(Panel::new(
@@ -1922,22 +1913,20 @@ impl GuiElem for FilterPanel {
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
// set line height
if info.line_height != self.line_height {
for h in &mut self.c_tab_main.children_heights {
*h = info.line_height;
}
for h in &mut self.c_tab_filters_songs.children_heights {
*h = info.line_height;
}
for h in &mut self.c_tab_filters_albums.children_heights {
*h = info.line_height;
}
for h in &mut self.c_tab_filters_artists.children_heights {
*h = info.line_height;
}
self.c_tab_main.config_mut().redraw = true;
self.c_tab_filters_songs.config_mut().redraw = true;
self.c_tab_filters_albums.config_mut().redraw = true;
self.c_tab_filters_artists.config_mut().redraw = true;
self.c_tab_main.children_heights.fill(info.line_height);
self.c_tab_filters_songs
.children_heights
.fill(info.line_height);
self.c_tab_filters_albums
.children_heights
.fill(info.line_height);
self.c_tab_filters_artists
.children_heights
.fill(info.line_height);
self.c_tab_main.config_mut().redraw_once();
self.c_tab_filters_songs.config_mut().redraw_once();
self.c_tab_filters_albums.config_mut().redraw_once();
self.c_tab_filters_artists.config_mut().redraw_once();
self.line_height = info.line_height;
}
// maybe switch tabs
@@ -2001,7 +1990,7 @@ impl GuiElem for FilterPanel {
);
sb.children = ft;
sb.children_heights = heights;
sb.config_mut().redraw = true;
sb.config_mut().redraw_once();
}
_ => {}
}

View File

@@ -177,10 +177,10 @@ impl GuiElem for NotifOverlay {
}
}
// redraw
if !self.notifs.is_empty() {
if let Some(h) = &info.helper {
h.request_redraw();
}
if !self.notifs.is_empty()
&& let Some(h) = &info.helper
{
h.request_redraw();
}
}
fn draw_rev(&self) -> bool {

View File

@@ -86,7 +86,7 @@ impl CurrentInfo {
GuiElemCfg::default(),
[Label::new(
GuiElemCfg::default(),
format!("Couldn't load cover"),
"Couldn't load cover".to_owned(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),

View File

@@ -1,9 +1,9 @@
use musicdb_lib::{
data::{
AlbumId, ArtistId,
database::Database,
queue::{Queue, QueueContent, QueueDuration},
song::Song,
AlbumId, ArtistId,
},
server::{Action, Req},
};
@@ -16,7 +16,7 @@ use speedy2d::{
use crate::{
gui::{Dragging, DrawInfo, EventInfo, GuiAction, GuiElem, GuiElemCfg},
gui_base::{Panel, ScrollBox},
gui_base::{Button, Panel, ScrollBox},
gui_text::{self, AdvancedLabel, Label, TextField},
};
@@ -95,7 +95,7 @@ impl QueueViewer {
musicdb_lib::data::queue::QueueFolder {
index: 0,
content: vec![],
name: format!("folder name"),
name: "folder name".to_owned(),
order: None,
},
false,
@@ -104,7 +104,7 @@ impl QueueViewer {
{
let mut tf = TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.5, 0.5), (1.0, 1.0))),
format!("folder name"),
"folder name".to_owned(),
Color::from_rgb(0.0, 0.33, 0.0),
Color::from_rgb(0.0, 0.67, 0.0),
);
@@ -219,8 +219,8 @@ impl GuiElem for QueueViewer {
}
}
}
let dt = fmt_dur(info.database.queue.duration_total(&info.database));
let dr = fmt_dur(info.database.queue.duration_remaining(&info.database));
let dt = fmt_dur(info.database.queue.duration_total(info.database));
let dr = fmt_dur(info.database.queue.duration_remaining(info.database));
label.content = vec![
vec![(
gui_text::AdvancedContent::Text(gui_text::Content::new(
@@ -239,15 +239,15 @@ impl GuiElem for QueueViewer {
1.0,
)],
];
label.config_mut().redraw = true;
label.config_mut().redraw_once();
}
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
self.config.redraw = false;
if self.config.redraw() || info.pos.size() != self.config.pixel_pos.size() {
self.config.redrawn();
let mut c = vec![];
let mut h = vec![];
queue_gui(
&info.database.queue,
&info.database,
info.database,
0.0,
0.02,
info.line_height,
@@ -260,12 +260,12 @@ impl GuiElem for QueueViewer {
let scroll_box = &mut self.c_scroll_box;
scroll_box.children = c;
scroll_box.children_heights = h;
scroll_box.config_mut().redraw = true;
scroll_box.config_mut().redraw_once();
}
}
fn updated_queue(&mut self) {
self.queue_updated = true;
self.config.redraw = true;
self.config.redraw_once();
}
}
@@ -353,7 +353,7 @@ fn queue_gui(
Box::new(QueueLoop::new(cfg.clone(), path, queue.clone(), current)),
);
if let Some(mut inner) = queue_gui(
&inner,
inner,
db,
depth,
depth_inc_by,
@@ -423,7 +423,7 @@ impl GuiElem for QueueEmptySpaceDragHandler {
fn generic_queue_draw(
info: &mut DrawInfo,
path: &Vec<usize>,
path: &[usize],
queue: impl FnOnce() -> Queue,
mouse: &mut bool,
copy_on_mouse_down: bool,
@@ -435,7 +435,7 @@ fn generic_queue_draw(
Dragging::Queue(if copy_on_mouse_down {
Ok(queue())
} else {
Err(path.clone())
Err(path.to_vec())
}),
None,
))));
@@ -567,8 +567,24 @@ impl GuiElem for QueueSong {
if button == MouseButton::Left && e.take() {
self.mouse = true;
self.copy_on_mouse_down = self.copy;
vec![]
} else if button == MouseButton::Right && e.take() {
let me = self.song.clone();
let menu_actions: Vec<Box<dyn GuiElem + 'static>> = vec![Box::new(Button::new(
GuiElemCfg::default(),
move |_| vec![GuiAction::EditSongs(vec![me.clone()])],
[Label::new(
GuiElemCfg::default(),
"Edit this song".to_owned(),
Color::WHITE,
None,
Vec2::new_y(0.5),
)],
))];
vec![GuiAction::ContextMenu(Some(menu_actions))]
} else {
vec![]
}
vec![]
}
fn mouse_up(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
@@ -648,10 +664,8 @@ impl GuiElem for QueueSong {
}
},
move |mut p, q| {
if insert_below {
if let Some(l) = p.last_mut() {
*l += 1;
}
if insert_below && let Some(l) = p.last_mut() {
*l += 1;
}
Action::QueueMove(q, p)
},
@@ -971,9 +985,9 @@ impl QueueLoop {
match queue.content() {
QueueContent::Loop(total, _current, _) => {
if *total == 0 {
format!("repeat forever")
"repeat forever".to_owned()
} else if *total == 1 {
format!("repeat 1 time")
"repeat 1 time".to_owned()
} else {
format!("repeat {total} times")
}
@@ -1141,46 +1155,38 @@ fn dragged_add_to_queue<T: 'static>(
}
fn add_to_queue_album_by_id(id: AlbumId, db: &Database) -> Option<Queue> {
if let Some(album) = db.albums().get(&id) {
Some(
QueueContent::Folder(musicdb_lib::data::queue::QueueFolder {
index: 0,
content: album
.songs
.iter()
.map(|id| QueueContent::Song(*id).into())
.collect(),
name: album.name.clone(),
order: None,
})
.into(),
)
} else {
None
}
db.albums().get(&id).map(|album| {
QueueContent::Folder(musicdb_lib::data::queue::QueueFolder {
index: 0,
content: album
.songs
.iter()
.map(|id| QueueContent::Song(*id).into())
.collect(),
name: album.name.clone(),
order: None,
})
.into()
})
}
fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
if let Some(artist) = db.artists().get(&id) {
Some(
QueueContent::Folder(musicdb_lib::data::queue::QueueFolder {
index: 0,
content: artist
.singles
.iter()
.map(|id| QueueContent::Song(*id).into())
.chain(
artist
.albums
.iter()
.filter_map(|id| add_to_queue_album_by_id(*id, db)),
)
.collect(),
name: artist.name.clone(),
order: None,
})
.into(),
)
} else {
None
}
db.artists().get(&id).map(|artist| {
QueueContent::Folder(musicdb_lib::data::queue::QueueFolder {
index: 0,
content: artist
.singles
.iter()
.map(|id| QueueContent::Song(*id).into())
.chain(
artist
.albums
.iter()
.filter_map(|id| add_to_queue_album_by_id(*id, db)),
)
.collect(),
name: artist.name.clone(),
order: None,
})
.into()
})
}

View File

@@ -4,8 +4,8 @@ use musicdb_lib::{
data::queue::{QueueContent, QueueFolder},
server::{Action, Req},
};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode, Graphics2D};
use uianimator::{default_animator_f64_quadratic::DefaultAnimatorF64Quadratic, Animator};
use speedy2d::{Graphics2D, color::Color, dimen::Vec2, shape::Rectangle, window::VirtualKeyCode};
use uianimator::{Animator, default_animator_f64_quadratic::DefaultAnimatorF64Quadratic};
use crate::{
gui::{
@@ -54,7 +54,6 @@ pub struct GuiScreen {
pub c_main_view: Panel<MainView>,
pub c_context_menu: Option<Box<dyn GuiElem>>,
pub idle: DefaultAnimatorF64Quadratic,
pub idle_prev_val: f32,
// pub settings: (bool, Option<Instant>),
pub settings: (bool, Option<Instant>),
pub last_interaction: Instant,
@@ -172,7 +171,6 @@ impl GuiScreen {
c_context_menu: None,
hotkey: Hotkey::new_noshift(VirtualKeyCode::Escape),
idle: DefaultAnimatorF64Quadratic::new(0.0, 0.67),
idle_prev_val: 0.0,
settings: (false, None),
last_interaction: Instant::now(),
idle_timeout: Some(60.0),
@@ -184,17 +182,9 @@ impl GuiScreen {
let prog = since.elapsed().as_secs_f32() / seconds;
if prog >= 1.0 {
v.1 = None;
if v.0 {
1.0
} else {
0.0
}
if v.0 { 1.0 } else { 0.0 }
} else {
if v.0 {
prog
} else {
1.0 - prog
}
if v.0 { prog } else { 1.0 - prog }
}
} else if v.0 {
1.0
@@ -222,12 +212,11 @@ impl GuiScreen {
self.idle.set_target(0.0, Instant::now());
}
fn idle_check(&mut self) {
if self.idle.target() == 0.0 {
if let Some(dur) = &self.idle_timeout {
if self.last_interaction.elapsed().as_secs_f64() > *dur {
self.idle.set_target(1.0, Instant::now());
}
}
if self.idle.target() == 0.0
&& let Some(dur) = &self.idle_timeout
&& self.last_interaction.elapsed().as_secs_f64() > *dur
{
self.idle.set_target(1.0, Instant::now());
}
}
@@ -253,7 +242,7 @@ impl GuiElem for GuiScreen {
]
.into_iter()
.chain(self.c_editing_songs.as_mut().map(|v| v.elem_mut()))
.chain(self.c_song_adder.as_mut().map(|v| v.elem_mut()).into_iter())
.chain(self.c_song_adder.as_mut().map(|v| v.elem_mut()))
.chain([
self.c_status_bar.elem_mut(),
self.c_settings.elem_mut(),
@@ -404,15 +393,12 @@ impl GuiElem for GuiScreen {
};
// request_redraw for animations
let idle_value = self.idle.get_value(info.time) as f32;
let idle_changed = self.idle_prev_val != idle_value;
let idle_changed = self.idle.target() as f32 != idle_value;
if idle_changed || idle_exit_anim || self.settings.1.is_some() {
self.idle_prev_val = idle_value;
if let Some(h) = &info.helper {
h.request_redraw()
}
}
// animations: idle
if idle_changed {
// animations: idle
let enable_normal_ui = idle_value < 1.0;
self.set_normal_ui_enabled(enable_normal_ui);
if let Some(h) = &info.helper {

View File

@@ -1,12 +1,12 @@
use std::sync::{atomic::AtomicBool, Arc, Mutex};
use std::sync::{Arc, Mutex, atomic::AtomicBool};
use musicdb_lib::server::Action;
use speedy2d::{
Graphics2D,
color::Color,
dimen::Vec2,
shape::Rectangle,
window::{KeyScancode, ModifiersState, MouseButton, VirtualKeyCode},
Graphics2D,
};
use crate::{
@@ -32,7 +32,7 @@ impl Settings {
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
config.redraw = true;
config.redraw_once();
Self {
config,
c_scroll_box: ScrollBox::new(
@@ -53,11 +53,7 @@ impl Settings {
}
pub fn get_timeout_val(&self) -> Option<f64> {
let v = self.c_scroll_box.children.idle_time.children.1.val;
if v > 0.0 {
Some(v * v)
} else {
None
}
if v > 0.0 { Some(v * v) } else { None }
}
}
pub struct SettingsContent {
@@ -125,7 +121,7 @@ impl KeybindInput {
b.key,
)
} else {
format!("")
String::new()
},
Color::WHITE,
None,
@@ -429,7 +425,7 @@ impl SettingsContent {
}
if hours == 0 && minutes < 10 && (seconds > 0 || minutes == 0) {
s.push_str(&seconds.to_string());
s.push_str("s");
s.push('s');
} else if s.ends_with(" ") {
s.pop();
}
@@ -532,7 +528,7 @@ impl GuiElem for Settings {
}
fn draw(&mut self, info: &mut DrawInfo, _g: &mut Graphics2D) {
if self.c_scroll_box.children.draw(info) {
self.c_scroll_box.config_mut().redraw = true;
self.c_scroll_box.config_mut().redraw_once();
}
let scrollbox = &mut self.c_scroll_box;
let background = &mut self.c_background;
@@ -547,9 +543,9 @@ impl GuiElem for Settings {
settings_opacity_slider.val as _,
);
}
if self.config.redraw {
self.config.redraw = false;
scrollbox.config_mut().redraw = true;
if self.config.redraw() {
self.config.redrawn();
scrollbox.config_mut().redraw_once();
if scrollbox.children_heights.len() == scrollbox.children.len() {
for (i, h) in scrollbox.children_heights.iter_mut().enumerate() {
*h = if i == 0 || i >= 8 {
@@ -560,7 +556,7 @@ impl GuiElem for Settings {
}
} else {
// try again next frame (scrollbox will autofill the children_heights vec)
self.config.redraw = true;
self.config.redraw_once();
}
}
}
@@ -591,7 +587,7 @@ pub fn build_keybind_elems(
vec![
vec![(
AdvancedContent::Text(Content::new(
format!("{}", v.title),
v.title.to_string(),
if v.enabled {
Color::WHITE
} else {
@@ -603,7 +599,7 @@ pub fn build_keybind_elems(
)],
vec![(
AdvancedContent::Text(Content::new(
format!("{}", v.description),
v.description.to_string(),
if v.enabled {
Color::LIGHT_GRAY
} else {

View File

@@ -1,5 +1,5 @@
use musicdb_lib::data::{AlbumId, ArtistId};
use speedy2d::{color::Color, dimen::Vec2, Graphics2D};
use speedy2d::{Graphics2D, color::Color, dimen::Vec2};
use crate::{
gui::{DrawInfo, GuiElem, GuiElemCfg},
@@ -30,13 +30,13 @@ impl SongAdder {
scroll_sensitivity_lines: f64,
scroll_sensitivity_pages: f64,
) -> Self {
config.redraw = true;
config.redraw_once();
Self {
config,
state: 0,
c_loading: Some(Label::new(
GuiElemCfg::default(),
format!("Loading..."),
"Loading...".to_owned(),
Color::GRAY,
None,
Vec2::new(0.5, 0.5),
@@ -99,7 +99,7 @@ impl GuiElem for SongAdder {
.iter()
.map(|(path, is_bad)| AddableSong::new(path.to_owned(), *is_bad))
.collect();
self.c_scroll_box.config_mut().redraw = true;
self.c_scroll_box.config_mut().redraw_once();
self.data = Some(
data.into_iter()
.map(|(p, b)| AddSong {
@@ -125,9 +125,9 @@ impl GuiElem for SongAdder {
}
}
if self.config.redraw {
self.config.redraw = false;
self.c_scroll_box.config_mut().redraw = true;
if self.config.redraw() {
self.config.redrawn();
self.c_scroll_box.config_mut().redraw_once();
}
}
}
@@ -147,7 +147,7 @@ impl AddableSong {
|_| vec![],
[Label::new(
GuiElemCfg::default(),
format!("{path}"),
path.to_string(),
if is_bad {
Color::LIGHT_GRAY
} else {

View File

@@ -61,11 +61,11 @@ impl GuiElem for StatusBar {
self.c_song_label.content = if let Some(song) = self.current_info.current_song {
info.gui_config
.status_bar_text
.gen_new(&info.database, info.database.get_song(&song))
.gen_new(info.database, info.database.get_song(&song))
} else {
vec![]
};
self.c_song_label.config_mut().redraw = true;
self.c_song_label.config_mut().redraw_once();
}
if self.current_info.new_cover {
self.current_info.new_cover = false;

View File

@@ -387,14 +387,14 @@ impl GuiElem for AdvancedLabel {
self
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
if self.config.redraw
if self.config.redraw()
|| self.config.pixel_pos.size() != info.pos.size()
|| self
.content
.iter()
.any(|v| v.iter().any(|(c, _, _)| c.will_redraw()))
{
self.config.redraw = false;
self.config.redrawn();
let mut max_len = 0.0;
let mut total_height = 0.0;
for line in &self.content {
@@ -461,7 +461,7 @@ impl GuiElem for AdvancedLabel {
if handle.is_none() {
match source {
ImageSource::Cover(id) => {
if let Some(img) = info.covers.get_mut(&id) {
if let Some(img) = info.covers.get_mut(id) {
if let Some(img) = img.get_init(g) {
*handle = Some(Some(img));
} else {

View File

@@ -18,14 +18,13 @@ impl Hotkey {
if self.modifiers == u8::MAX {
return false;
}
down == false
!down
&& key.is_some_and(|v| v == self.key)
&& (self.modifiers & 0b10 == 1 || (self.modifiers & 0b01 == 1) == modifiers.ctrl())
&& (self.modifiers & 0b1000 == 1 || (self.modifiers & 0b0100 == 1) == modifiers.shift())
&& (self.modifiers & 0b100000 == 1
|| (self.modifiers & 0b010000 == 1) == modifiers.alt())
&& (self.modifiers & 0b10000000 == 1
|| (self.modifiers & 0b01000000 == 1) == modifiers.logo())
&& (self.modifiers & 0b10 > 0 || (self.modifiers & 0b01 > 0) == modifiers.ctrl())
&& (self.modifiers & 0b1000 > 0 || (self.modifiers & 0b0100 > 0) == modifiers.shift())
&& (self.modifiers & 0b100000 > 0 || (self.modifiers & 0b010000 > 0) == modifiers.alt())
&& (self.modifiers & 0b10000000 > 0
|| (self.modifiers & 0b01000000 > 0) == modifiers.logo())
}
/// unlike noshift, this ignores the shift modifier
pub fn new_key(key: VirtualKeyCode) -> Self {

View File

@@ -1,5 +1,7 @@
#![allow(dead_code)]
#![allow(unused_variables)]
#![allow(clippy::type_complexity)]
#![allow(clippy::too_many_arguments)]
use std::{
io::{BufReader, Write},
@@ -18,8 +20,8 @@ use musicdb_lib::data::cache_manager::CacheManager;
use musicdb_lib::player::{Player, PlayerBackendFeat};
use musicdb_lib::{
data::{
database::{ClientIo, Database},
CoverId, SongId,
database::{ClientIo, Database},
},
load::ToFromBytes,
server::Command,
@@ -107,7 +109,9 @@ fn main() {
#[cfg(not(feature = "speedy2d"))]
#[cfg(not(feature = "mers"))]
#[cfg(not(feature = "playback"))]
compile_error!("None of the optional features are enabled. Without at least one of these, the application is useless! See Cargo.toml for info.");
compile_error!(
"None of the optional features are enabled. Without at least one of these, the application is useless! See Cargo.toml for info."
);
// parse args
let args = Args::parse();
// start
@@ -215,7 +219,7 @@ fn main() {
}
#[cfg(feature = "playback")]
if let Some(player) = &mut player {
player.update_dont_uncache(&mut *db);
player.update_dont_uncache(&mut db);
}
drop(db);
#[cfg(feature = "speedy2d")]
@@ -250,10 +254,12 @@ fn main() {
Some(Arc::clone(&get_con));
}
let occasional_refresh_sender = Arc::clone(&sender);
thread::spawn(move || loop {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(v) = &*occasional_refresh_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
thread::spawn(move || {
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
if let Some(v) = &*occasional_refresh_sender.lock().unwrap() {
v.send_event(GuiEvent::Refresh).unwrap();
}
}
});
gui::main(
@@ -328,12 +334,8 @@ fn main() {
pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
let mut o = vec![];
loop {
if let Some(v) = f() {
o.push(v);
} else {
break;
}
while let Some(v) = f() {
o.push(v);
}
o
}

View File

@@ -113,7 +113,7 @@ impl TextBuilder {
}
for part in &self.0 {
match part {
TextPart::LineBreak => out.push(std::mem::replace(line, vec![])),
TextPart::LineBreak => out.push(std::mem::take(line)),
TextPart::SetColor(c) => *color = *c,
TextPart::SetScale(v) => *scale = *v,
TextPart::SetHeightAlign(v) => *align = *v,
@@ -124,17 +124,17 @@ impl TextBuilder {
}
}
TextPart::AlbumName => {
if let Some(s) = current_song {
if let Some(album) = s.album.and_then(|id| db.albums().get(&id)) {
push!(album.name.to_owned());
}
if let Some(s) = current_song
&& let Some(album) = s.album.and_then(|id| db.albums().get(&id))
{
push!(album.name.to_owned());
}
}
TextPart::ArtistName => {
if let Some(s) = current_song {
if let Some(artist) = db.artists().get(&s.artist) {
push!(artist.name.to_owned());
}
if let Some(s) = current_song
&& let Some(artist) = db.artists().get(&s.artist)
{
push!(artist.name.to_owned());
}
}
TextPart::SongDuration(show_millis) => {
@@ -152,7 +152,7 @@ impl TextBuilder {
}
TextPart::TagEq(p) => {
for (i, g) in all_general(db, &current_song).into_iter().enumerate() {
if let Some(_) = g.and_then(|g| g.tags.iter().find(|t| *t == p)) {
if g.and_then(|g| g.tags.iter().find(|t| *t == p)).is_some() {
push!(
match i {
0 => 's',
@@ -223,174 +223,165 @@ impl TextBuilder {
if current.starts_with(' ') {
current = current.replacen(' ', "\u{00A0}", 1);
}
vec.push(TextPart::Literal(std::mem::replace(
&mut current,
String::new(),
)));
vec.push(TextPart::Literal(std::mem::take(&mut current)));
}
};
}
loop {
if let Some(ch) = chars.next() {
match ch {
'\n' => {
while let Some(ch) = chars.next() {
match ch {
'\n' => {
done!();
vec.push(TextPart::LineBreak);
}
'\\' => match chars.next() {
None => current.push('\\'),
Some('t') => {
done!();
vec.push(TextPart::LineBreak);
vec.push(TextPart::SongTitle);
}
'\\' => match chars.next() {
None => current.push('\\'),
Some('t') => {
done!();
vec.push(TextPart::SongTitle);
}
Some('a') => {
done!();
vec.push(TextPart::AlbumName);
}
Some('A') => {
done!();
vec.push(TextPart::ArtistName);
}
Some('d') => {
done!();
vec.push(TextPart::SongDuration(false));
}
Some('D') => {
done!();
vec.push(TextPart::SongDuration(true));
}
Some('s') => {
done!();
vec.push(TextPart::SetScale({
let mut str = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => str.push(c),
}
}
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('h') => {
done!();
vec.push(TextPart::SetHeightAlign({
let mut str = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => str.push(c),
}
}
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('c') => {
done!();
vec.push(TextPart::SetColor({
let mut str = String::new();
for _ in 0..6 {
if let Some(ch) = chars.next() {
str.push(ch);
} else {
return Err(TextBuilderParseError::TooFewCharsForColor);
}
}
if let Ok(i) = u32::from_str_radix(&str, 16) {
Color::from_hex_rgb(i)
} else {
return Err(TextBuilderParseError::ColorNotHex);
}
}));
}
Some('i') => {
done!();
let mut src = String::new();
Some('a') => {
done!();
vec.push(TextPart::AlbumName);
}
Some('A') => {
done!();
vec.push(TextPart::ArtistName);
}
Some('d') => {
done!();
vec.push(TextPart::SongDuration(false));
}
Some('D') => {
done!();
vec.push(TextPart::SongDuration(true));
}
Some('s') => {
done!();
vec.push(TextPart::SetScale({
let mut str = String::new();
loop {
match chars.next() {
None => {
return Err(TextBuilderParseError::InvalidImageSourceName(
src,
));
}
Some(':') => break,
Some(c) => src.push(c),
None | Some(';') => break,
Some(c) => str.push(c),
}
}
vec.push(match src.as_str() {
"Cover" => {
let mut id = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => id.push(c),
}
}
if let Ok(id) = id.parse() {
TextPart::ImgCover(id)
} else {
return Err(TextBuilderParseError::InvalidImageCoverId(id));
}
}
"CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?),
_ => {
return Err(TextBuilderParseError::InvalidImageSourceName(src));
}
});
}
Some(ch) => current.push(ch),
},
'%' => {
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('h') => {
done!();
let mode = if let Some(ch) = chars.next() {
ch
} else {
return Err(TextBuilderParseError::UnclosedPercent);
};
vec.push(TextPart::SetHeightAlign({
let mut str = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => str.push(c),
}
}
if let Ok(v) = str.parse() {
v
} else {
return Err(TextBuilderParseError::CouldntParse(
str,
"number (float)".to_string(),
));
}
}))
}
Some('c') => {
done!();
vec.push(TextPart::SetColor({
let mut str = String::new();
for _ in 0..6 {
if let Some(ch) = chars.next() {
str.push(ch);
} else {
return Err(TextBuilderParseError::TooFewCharsForColor);
}
}
if let Ok(i) = u32::from_str_radix(&str, 16) {
Color::from_hex_rgb(i)
} else {
return Err(TextBuilderParseError::ColorNotHex);
}
}));
}
Some('i') => {
done!();
let mut src = String::new();
loop {
match chars.next() {
Some('%') => {
let s = std::mem::replace(&mut current, String::new());
vec.push(match mode {
'=' => TextPart::TagEq(s),
'>' => TextPart::TagEnd(s),
'_' => TextPart::TagContains(s),
c => return Err(TextBuilderParseError::TagModeUnknown(c)),
});
break;
None => {
return Err(TextBuilderParseError::InvalidImageSourceName(src));
}
Some(ch) => current.push(ch),
None => return Err(TextBuilderParseError::UnclosedPercent),
Some(':') => break,
Some(c) => src.push(c),
}
}
vec.push(match src.as_str() {
"Cover" => {
let mut id = String::new();
loop {
match chars.next() {
None | Some(';') => break,
Some(c) => id.push(c),
}
}
if let Ok(id) = id.parse() {
TextPart::ImgCover(id)
} else {
return Err(TextBuilderParseError::InvalidImageCoverId(id));
}
}
"CustomFile" => TextPart::ImgCustom(Self::from_chars(chars)?),
_ => {
return Err(TextBuilderParseError::InvalidImageSourceName(src));
}
});
}
'?' => {
done!();
vec.push(TextPart::If(
Self::from_chars(chars)?,
Self::from_chars(chars)?,
Self::from_chars(chars)?,
));
Some(ch) => current.push(ch),
},
'%' => {
done!();
let mode = if let Some(ch) = chars.next() {
ch
} else {
return Err(TextBuilderParseError::UnclosedPercent);
};
loop {
match chars.next() {
Some('%') => {
let s = std::mem::take(&mut current);
vec.push(match mode {
'=' => TextPart::TagEq(s),
'>' => TextPart::TagEnd(s),
'_' => TextPart::TagContains(s),
c => return Err(TextBuilderParseError::TagModeUnknown(c)),
});
break;
}
Some(ch) => current.push(ch),
None => return Err(TextBuilderParseError::UnclosedPercent),
}
}
'#' => break,
ch => current.push(ch),
}
} else {
break;
'?' => {
done!();
vec.push(TextPart::If(
Self::from_chars(chars)?,
Self::from_chars(chars)?,
Self::from_chars(chars)?,
));
}
'#' => break,
ch => current.push(ch),
}
}
done!();

467
musicdb-filldb/Cargo.lock generated Executable file
View File

@@ -0,0 +1,467 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "base64"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9"
[[package]]
name = "bitflags"
version = "2.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha20"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
dependencies = [
"cfg-if",
"cpufeatures",
"rand_core",
]
[[package]]
name = "colorize"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc17e449bc7854c50b943d113a98bc0e01dc6585d2c66eaa09ca645ebd8a7e62"
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "debug-helper"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "dispatch2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core",
]
[[package]]
name = "id3"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24993fcabcbc07c8ac076a8e62db8593d1a5c4dbe81d57e531d2b7cb7f737380"
dependencies = [
"bitflags",
"byteorder",
"flate2",
]
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "miniz_oxide"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
]
[[package]]
name = "mp3-duration"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "348bdc7300502f0801e5b57c448815713cd843b744ef9bda252a2698fdf90a0f"
dependencies = [
"thiserror",
]
[[package]]
name = "musicdb-filldb"
version = "0.1.0"
dependencies = [
"id3",
"mp3-duration",
"musicdb-lib",
]
[[package]]
name = "musicdb-lib"
version = "0.1.0"
dependencies = [
"base64",
"colorize",
"rand",
"rc-u8-reader",
"sysinfo",
]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]]
name = "objc2"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
"dispatch2",
"objc2",
]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags",
"objc2",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "objc2-open-directory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d"
dependencies = [
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rc-u8-reader"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef271f94154149f9480043517e7a1845ef1ae9713e7c8b32abe4acb014bdbab9"
dependencies = [
"debug-helper",
]
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.39.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"objc2-open-directory",
"windows",
]
[[package]]
name = "thiserror"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]

View File

@@ -6,6 +6,6 @@ edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
id3 = "1.16.3"
id3 = "1.17.1"
mp3-duration = "0.1.10"
musicdb-lib = { version = "0.1.0", path = "../musicdb-lib" }

View File

@@ -1,2 +1 @@
/target
/Cargo.lock

1792
musicdb-lib/Cargo.lock generated Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
base64 = "0.22.1"
base64 = "0.23.0"
colorize = "0.1.0"
playback-rs = { version = "0.4.4", optional = true }
rand = "0.9.2"
rc-u8-reader = "2.0.16"
rodio = { version = "0.21.1", optional = true }
sysinfo = "0.37.0"
playback-rs = { version = "0.4.8", optional = true }
rand = "0.10.2"
rc-u8-reader = "2.0.17"
rodio = { version = "0.22.2", optional = true }
sysinfo = "0.39.6"
[features]
default = []

3646
musicdb-server/Cargo.lock generated Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -7,13 +7,13 @@ edition = "2024"
[dependencies]
musicdb-lib = { path = "../musicdb-lib" }
clap = { version = "4.5.45", features = ["derive"] }
clap = { version = "4.6.5", features = ["derive"] }
headers = "0.4.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.47.1", optional = true, features = ["rt"] }
tokio = { version = "1.53.1", optional = true, features = ["rt"] }
rocket = { version = "0.5.1", optional = true }
html-escape = { version = "0.2.13", optional = true }
html-escape = { version = "0.2.14", optional = true }
rocket_ws = "0.1.1"
rocket_seek_stream = "0.2.6"