diff --git a/musicdb-client/src/gui.rs b/musicdb-client/src/gui.rs index ba0246c..0ad2bf9 100755 --- a/musicdb-client/src/gui.rs +++ b/musicdb-client/src/gui.rs @@ -1258,8 +1258,8 @@ pub enum GuiAction { Do(Box), Exit, EditSongs(Vec), - // EditAlbums(Vec), - // EditArtists(Vec), + // EditAlbums(Vec, bool), + // EditArtists(Vec, bool), OpenAddSongsMenu, CloseAddSongsMenu, } diff --git a/musicdb-client/src/gui_base.rs b/musicdb-client/src/gui_base.rs index 40a7179..187eaf9 100755 --- a/musicdb-client/src/gui_base.rs +++ b/musicdb-client/src/gui_base.rs @@ -355,6 +355,14 @@ impl Button { 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 GuiElem for Button { fn config(&self) -> &GuiElemCfg { diff --git a/musicdb-client/src/gui_edit_any.rs b/musicdb-client/src/gui_edit_any.rs new file mode 100644 index 0000000..30c7f20 --- /dev/null +++ b/musicdb-client/src/gui_edit_any.rs @@ -0,0 +1,315 @@ +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 + 'static>( + tag: String, + sender: std::sync::mpsc::Sender, + 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 + '_> { + 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 + '_> { + 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> { + config: GuiElemCfg, + event_sender: std::sync::mpsc::Sender, + /// `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>>, + last_search: String, +} +impl + 'static> EditorForAnyTagAdder { + pub fn new(event_sender: std::sync::mpsc::Sender) -> 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 + 'static> GuiElem for EditorForAnyTagAdder { + 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::>(); + 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 + '_> { + 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 + '_> { + 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 + } +} diff --git a/musicdb-client/src/gui_edit_song.rs b/musicdb-client/src/gui_edit_song.rs index 741fe33..a6b4289 100644 --- a/musicdb-client/src/gui_edit_song.rs +++ b/musicdb-client/src/gui_edit_song.rs @@ -11,13 +11,12 @@ use crate::{ gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemChildren}, gui_anim::AnimationController, gui_base::{Button, Panel, ScrollBox}, + gui_edit_any::{EditorForAnyTagAdder, EditorForAnyTagInList, SpacerForScrollBox, ELEM_HEIGHT}, gui_text::{Label, TextField}, }; // 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 { config: GuiElemCfg, songs: Vec, @@ -33,11 +32,21 @@ pub enum Event { Close, Apply, SetArtist(String, Option), + GeneralEvent(super::gui_edit_any::Event), } +impl From for Event { + fn from(value: super::gui_edit_any::Event) -> Self { + Self::GeneralEvent(value) + } +} + pub struct EditorForSongElems { c_title: TextField, c_artist: EditorForSongArtistChooser, c_album: Label, + c_tags: Vec, + c_new_tag: EditorForAnyTagAdder, + c_spacers: [SpacerForScrollBox; 4], } impl GuiElemChildren for EditorForSongElems { fn iter(&mut self) -> Box + '_> { @@ -47,11 +56,13 @@ impl GuiElemChildren for EditorForSongElems { self.c_artist.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 { - 3 + 3 + self.c_tags.len() + 1 + self.c_spacers.len() } } @@ -96,6 +107,32 @@ impl EditorForSongs { None, 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![], ELEM_HEIGHT, @@ -169,7 +206,8 @@ impl GuiElem for EditorForSongs { gui.gui.set_normal_ui_enabled(true); }))), Event::Apply => { - for song in &self.songs { + let mut actions = Vec::new(); + for song in self.songs.iter() { let mut song = song.clone(); let new_title = self @@ -188,11 +226,14 @@ impl GuiElem for EditorForSongs { song.artist = artist_id; song.album = None; } + actions.push(Action::ModifySong(song, Req::none())); + } + if actions.len() == 1 { info.actions - .push(GuiAction::SendToServer(Action::ModifySong( - song, - Req::none(), - ))); + .push(GuiAction::SendToServer(actions.pop().unwrap())); + } else if actions.len() > 1 { + info.actions + .push(GuiAction::SendToServer(Action::Multiple(actions))); } } Event::SetArtist(name, id) => { @@ -213,6 +254,52 @@ impl GuiElem for EditorForSongs { .text() = name; 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, } @@ -251,6 +338,25 @@ impl GuiElem for EditorForSongs { 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; + } + if let Some(h) = &info.helper { + h.request_redraw(); + } + } } fn config(&self) -> &GuiElemCfg { &self.config diff --git a/musicdb-client/src/gui_library.rs b/musicdb-client/src/gui_library.rs index 856ec49..e9453a3 100755 --- a/musicdb-client/src/gui_library.rs +++ b/musicdb-client/src/gui_library.rs @@ -1275,24 +1275,54 @@ impl GuiElem for ListSong { fn mouse_pressed(&mut self, e: &mut EventInfo, button: MouseButton) -> Vec { if button == MouseButton::Right && e.take() { let id = self.id; - vec![GuiAction::Build(Box::new(move |db| { - if let Some(me) = db.songs().get(&id) { - let me = me.clone(); - vec![GuiAction::ContextMenu(Some(vec![Box::new(Button::new( + let mut menu_actions: Vec> = vec![Box::new(Button::new( + GuiElemCfg::default(), + move |_| { + vec![GuiAction::Build(Box::new(move |db| { + 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(), - move |_| vec![GuiAction::EditSongs(vec![me.clone()])], - [Label::new( - GuiElemCfg::default(), - format!("Edit"), - Color::WHITE, - None, - Vec2::new_y(0.5), - )], - ))]))] - } else { - vec![] - } - }))] + format!("Edit selected songs"), + Color::WHITE, + None, + Vec2::new_y(0.5), + )], + ))); + } + vec![GuiAction::ContextMenu(Some(menu_actions))] } else { vec![] } diff --git a/musicdb-client/src/main.rs b/musicdb-client/src/main.rs index 129e729..bd461c5 100755 --- a/musicdb-client/src/main.rs +++ b/musicdb-client/src/main.rs @@ -32,6 +32,7 @@ mod gui; mod gui_anim; #[cfg(feature = "speedy2d")] mod gui_base; +pub mod gui_edit_any; #[cfg(feature = "speedy2d")] mod gui_edit_song; #[cfg(feature = "speedy2d")]