added edit panel and fixed bug where Modify* command would be decoded as Add* command by the server

This commit is contained in:
Mark 2023-08-27 21:53:00 +02:00
parent 55590a1549
commit 0e5e33367d
11 changed files with 661 additions and 15 deletions

1
.gitignore vendored
View File

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

View File

@ -367,6 +367,8 @@ pub enum GuiAction {
OpenMain, OpenMain,
SetIdle(bool), SetIdle(bool),
OpenSettings(bool), OpenSettings(bool),
OpenEditPanel(GuiElem),
CloseEditPanel,
/// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc) /// Build the GuiAction(s) later, when we have access to the Database (can turn an AlbumId into a QueueContent::Folder, etc)
Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>), Build(Box<dyn FnOnce(&mut Database) -> Vec<Self>>),
SendToServer(Command), SendToServer(Command),
@ -739,6 +741,38 @@ impl Gui {
} }
} }
} }
GuiAction::OpenEditPanel(p) => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 {
gui.inner.settings = (false, Some(Instant::now()));
}
gui.inner.open_edit(p);
}
}
GuiAction::CloseEditPanel => {
if let Some(gui) = self
.gui
.inner
.any_mut()
.downcast_mut::<WithFocusHotkey<GuiScreen>>()
{
if gui.inner.idle.0 {
gui.inner.idle = (false, Some(Instant::now()));
}
if gui.inner.settings.0 {
gui.inner.settings = (false, Some(Instant::now()));
}
gui.inner.close_edit();
}
}
} }
} }
} }

359
musicdb-client/src/gui_edit.rs Executable file
View File

@ -0,0 +1,359 @@
use std::sync::{atomic::AtomicBool, mpsc, Arc};
use musicdb_lib::{
data::{album::Album, artist::Artist, song::Song, AlbumId, ArtistId, SongId},
server::Command,
};
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle};
use crate::{
gui::{GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::{Button, Panel, ScrollBox},
gui_text::{Label, TextField},
};
pub struct GuiEdit {
config: GuiElemCfg,
children: Vec<GuiElem>,
editable: Editable,
editing: Editing,
reload: bool,
send: bool,
apply_change: mpsc::Sender<Box<dyn FnOnce(&mut Self)>>,
change_recv: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
}
#[derive(Clone)]
pub enum Editable {
Artist(ArtistId),
Album(AlbumId),
Song(SongId),
}
#[derive(Clone)]
pub enum Editing {
NotLoaded,
Artist(Artist),
Album(Album),
Song(Song),
}
impl GuiEdit {
pub fn new(config: GuiElemCfg, edit: Editable) -> Self {
let (apply_change, change_recv) = mpsc::channel();
let ac1 = apply_change.clone();
let ac2 = apply_change.clone();
Self {
config,
editable: edit,
editing: Editing::NotLoaded,
reload: true,
send: false,
apply_change,
change_recv,
children: vec![
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.95), (0.33, 1.0))),
|_| vec![GuiAction::CloseEditPanel],
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Back".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.33, 0.95), (0.67, 1.0))),
move |_| {
_ = ac1.send(Box::new(|s| s.reload = true));
vec![]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Reload".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.67, 0.95), (1.0, 1.0))),
move |_| {
_ = ac2.send(Box::new(|s| s.send = true));
vec![]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"Send".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
))],
)),
],
}
}
}
impl GuiElemTrait for GuiEdit {
fn config(&self) -> &GuiElemCfg {
&self.config
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
&mut self.config
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
Box::new(self.children.iter_mut())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
fn draw(&mut self, info: &mut crate::gui::DrawInfo, g: &mut speedy2d::Graphics2D) {
loop {
if let Ok(func) = self.change_recv.try_recv() {
func(self);
} else {
break;
}
}
if self.send {
self.send = false;
match &self.editing {
Editing::NotLoaded => {}
Editing::Artist(v) => info
.actions
.push(GuiAction::SendToServer(Command::ModifyArtist(v.clone()))),
Editing::Album(v) => info
.actions
.push(GuiAction::SendToServer(Command::ModifyAlbum(v.clone()))),
Editing::Song(v) => info
.actions
.push(GuiAction::SendToServer(Command::ModifySong(v.clone()))),
}
}
if self.reload {
self.reload = false;
self.editing = match &self.editable {
Editable::Artist(id) => {
if let Some(v) = info.database.artists().get(id).cloned() {
Editing::Artist(v)
} else {
Editing::NotLoaded
}
}
Editable::Album(id) => {
if let Some(v) = info.database.albums().get(id).cloned() {
Editing::Album(v)
} else {
Editing::NotLoaded
}
}
Editable::Song(id) => {
if let Some(v) = info.database.songs().get(id).cloned() {
Editing::Song(v)
} else {
Editing::NotLoaded
}
}
};
self.config.redraw = true;
}
if self.config.redraw {
self.config.redraw = false;
let scrollbox = if self.children.len() > 3 {
let o = self.children.pop();
while self.children.len() > 3 {
self.children.pop();
}
o
} else {
None
};
match &self.editing {
Editing::NotLoaded => {
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::default(),
"nothing here".to_string(),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)));
}
Editing::Artist(artist) => {
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.8, 0.08))),
artist.name.clone(),
Color::WHITE,
None,
Vec2::new(0.1, 0.5),
)));
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 0.04))),
"Artist".to_string(),
Color::WHITE,
None,
Vec2::new(0.8, 0.5),
)));
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.04), (1.0, 0.08))),
format!("#{}", artist.id),
Color::WHITE,
None,
Vec2::new(0.8, 0.5),
)));
let mut elems = vec![];
elems.push((
GuiElem::new(Panel::new(
GuiElemCfg::default(),
vec![
GuiElem::new(Label::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (0.6, 1.0))),
format!(
"{} album{}",
artist.albums.len(),
if artist.albums.len() != 1 { "s" } else { "" }
),
Color::LIGHT_GRAY,
None,
Vec2::new(0.0, 0.5),
)),
GuiElem::new(TextField::new(
GuiElemCfg::at(Rectangle::from_tuples((0.6, 0.0), (0.8, 1.0))),
"id".to_string(),
Color::DARK_GRAY,
Color::WHITE,
)),
GuiElem::new(Button::new(
GuiElemCfg::at(Rectangle::from_tuples((0.8, 0.0), (1.0, 1.0))),
{
let apply_change = self.apply_change.clone();
let my_index = elems.len();
move |_| {
_ = apply_change.send(Box::new(move |s| {
s.config.redraw = true;
if let Ok(id) = s
.children
.last_mut()
.unwrap()
.inner
.any_mut()
.downcast_mut::<ScrollBox>()
.unwrap()
.children[my_index]
.0
.inner
.children()
.nth(1)
.unwrap()
.inner
.children()
.next()
.unwrap()
.inner
.any()
.downcast_ref::<Label>()
.unwrap()
.content
.get_text()
.parse::<AlbumId>()
{
if let Editing::Artist(artist) = &mut s.editing
{
artist.albums.push(id);
}
}
}));
vec![]
}
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
"add".to_string(),
Color::LIGHT_GRAY,
None,
Vec2::new(0.0, 0.5),
))],
)),
],
)),
info.line_height,
));
for &album in &artist.albums {
elems.push((
GuiElem::new(Button::new(
GuiElemCfg::default(),
move |_| {
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
GuiElemCfg::default(),
Editable::Album(album),
)))]
},
vec![GuiElem::new(Label::new(
GuiElemCfg::default(),
if let Some(a) = info.database.albums().get(&album) {
format!("Album: {}", a.name)
} else {
format!("Album #{album}")
},
Color::WHITE,
None,
Vec2::new(0.0, 0.5),
))],
)),
info.line_height,
));
}
self.children.push(if let Some(mut sb) = scrollbox {
if let Some(s) = sb.inner.any_mut().downcast_mut::<ScrollBox>() {
s.children = elems;
s.config_mut().redraw = true;
sb
} else {
GuiElem::new(ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.08), (1.0, 1.0))),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
elems,
))
}
} else {
GuiElem::new(ScrollBox::new(
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.08), (1.0, 1.0))),
crate::gui_base::ScrollBoxSizeUnit::Pixels,
elems,
))
});
}
Editing::Album(album) => {
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::default(),
format!("Album: {}", album.name),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)));
}
Editing::Song(song) => {
self.children.push(GuiElem::new(Label::new(
GuiElemCfg::default(),
format!("Song: {}", song.title),
Color::WHITE,
None,
Vec2::new(0.5, 0.5),
)));
}
}
};
match self.editing {
_ => {}
}
}
}
impl Clone for GuiEdit {
fn clone(&self) -> Self {
Self::new(self.config.clone(), self.editable.clone())
}
}

View File

@ -10,6 +10,7 @@ use speedy2d::{
use crate::{ use crate::{
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait}, gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
gui_base::ScrollBox, gui_base::ScrollBox,
gui_edit::GuiEdit,
gui_text::{Label, TextField}, gui_text::{Label, TextField},
gui_wrappers::WithFocusHotkey, gui_wrappers::WithFocusHotkey,
}; };
@ -268,6 +269,7 @@ struct ListArtist {
config: GuiElemCfg, config: GuiElemCfg,
id: ArtistId, id: ArtistId,
children: Vec<GuiElem>, children: Vec<GuiElem>,
mouse: bool,
mouse_pos: Vec2, mouse_pos: Vec2,
} }
impl ListArtist { impl ListArtist {
@ -283,6 +285,7 @@ impl ListArtist {
config: config.w_mouse(), config: config.w_mouse(),
id, id,
children: vec![GuiElem::new(label)], children: vec![GuiElem::new(label)],
mouse: false,
mouse_pos: Vec2::ZERO, mouse_pos: Vec2::ZERO,
} }
} }
@ -307,6 +310,13 @@ impl GuiElemTrait for ListArtist {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.mouse {
if info.pos.contains(info.mouse_pos) {
return;
} else {
self.mouse = false;
}
}
self.mouse_pos = Vec2::new( self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y, info.mouse_pos.y - self.config.pixel_pos.top_left().y,
@ -314,6 +324,7 @@ impl GuiElemTrait for ListArtist {
} }
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left { if button == MouseButton::Left {
self.mouse = true;
let mouse_pos = self.mouse_pos; let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width(); let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height(); let h = self.config.pixel_pos.height();
@ -334,6 +345,17 @@ impl GuiElemTrait for ListArtist {
vec![] vec![]
} }
} }
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
GuiElemCfg::default(),
crate::gui_edit::Editable::Artist(self.id),
)))]
} else {
vec![]
}
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -341,6 +363,7 @@ struct ListAlbum {
config: GuiElemCfg, config: GuiElemCfg,
id: AlbumId, id: AlbumId,
children: Vec<GuiElem>, children: Vec<GuiElem>,
mouse: bool,
mouse_pos: Vec2, mouse_pos: Vec2,
} }
impl ListAlbum { impl ListAlbum {
@ -356,6 +379,7 @@ impl ListAlbum {
config: config.w_mouse(), config: config.w_mouse(),
id, id,
children: vec![GuiElem::new(label)], children: vec![GuiElem::new(label)],
mouse: false,
mouse_pos: Vec2::ZERO, mouse_pos: Vec2::ZERO,
} }
} }
@ -380,6 +404,13 @@ impl GuiElemTrait for ListAlbum {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.mouse {
if info.pos.contains(info.mouse_pos) {
return;
} else {
self.mouse = false;
}
}
self.mouse_pos = Vec2::new( self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y, info.mouse_pos.y - self.config.pixel_pos.top_left().y,
@ -387,6 +418,7 @@ impl GuiElemTrait for ListAlbum {
} }
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left { if button == MouseButton::Left {
self.mouse = true;
let mouse_pos = self.mouse_pos; let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width(); let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height(); let h = self.config.pixel_pos.height();
@ -407,6 +439,17 @@ impl GuiElemTrait for ListAlbum {
vec![] vec![]
} }
} }
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
GuiElemCfg::default(),
crate::gui_edit::Editable::Album(self.id),
)))]
} else {
vec![]
}
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -414,6 +457,7 @@ struct ListSong {
config: GuiElemCfg, config: GuiElemCfg,
id: SongId, id: SongId,
children: Vec<GuiElem>, children: Vec<GuiElem>,
mouse: bool,
mouse_pos: Vec2, mouse_pos: Vec2,
} }
impl ListSong { impl ListSong {
@ -429,6 +473,7 @@ impl ListSong {
config: config.w_mouse(), config: config.w_mouse(),
id, id,
children: vec![GuiElem::new(label)], children: vec![GuiElem::new(label)],
mouse: false,
mouse_pos: Vec2::ZERO, mouse_pos: Vec2::ZERO,
} }
} }
@ -453,6 +498,13 @@ impl GuiElemTrait for ListSong {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
if self.mouse {
if info.pos.contains(info.mouse_pos) {
return;
} else {
self.mouse = false;
}
}
self.mouse_pos = Vec2::new( self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.x - self.config.pixel_pos.top_left().x,
info.mouse_pos.y - self.config.pixel_pos.top_left().y, info.mouse_pos.y - self.config.pixel_pos.top_left().y,
@ -460,6 +512,7 @@ impl GuiElemTrait for ListSong {
} }
fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> { fn mouse_down(&mut self, button: MouseButton) -> Vec<GuiAction> {
if button == MouseButton::Left { if button == MouseButton::Left {
self.mouse = true;
let mouse_pos = self.mouse_pos; let mouse_pos = self.mouse_pos;
let w = self.config.pixel_pos.width(); let w = self.config.pixel_pos.width();
let h = self.config.pixel_pos.height(); let h = self.config.pixel_pos.height();
@ -480,4 +533,15 @@ impl GuiElemTrait for ListSong {
vec![] vec![]
} }
} }
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
if self.mouse && button == MouseButton::Left {
self.mouse = false;
vec![GuiAction::OpenEditPanel(GuiElem::new(GuiEdit::new(
GuiElemCfg::default(),
crate::gui_edit::Editable::Song(self.id),
)))]
} else {
vec![]
}
}
} }

View File

@ -198,7 +198,12 @@ fn queue_gui(
QueueContent::Folder(ia, q, _) => { QueueContent::Folder(ia, q, _) => {
if !skip_folder { if !skip_folder {
target.push(( target.push((
GuiElem::new(QueueFolder::new(cfg, path.clone(), queue.clone(), current)), GuiElem::new(QueueFolder::new(
cfg.clone(),
path.clone(),
queue.clone(),
current,
)),
line_height * 0.8, line_height * 0.8,
)); ));
} }
@ -217,12 +222,22 @@ fn queue_gui(
false, false,
); );
} }
if !skip_folder {
let mut p1 = path;
let p2 = p1.pop().unwrap_or(0) + 1;
target.push((
GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))),
line_height * 0.4,
));
}
} }
QueueContent::Loop(_, _, inner) => { QueueContent::Loop(_, _, inner) => {
let mut p = path.clone(); let mut p = path.clone();
let mut p1 = path.clone();
let p2 = p1.pop().unwrap_or(0) + 1;
p.push(0); p.push(0);
target.push(( target.push((
GuiElem::new(QueueLoop::new(cfg, path, queue.clone(), current)), GuiElem::new(QueueLoop::new(cfg.clone(), path, queue.clone(), current)),
line_height * 0.8, line_height * 0.8,
)); ));
queue_gui( queue_gui(
@ -236,6 +251,10 @@ fn queue_gui(
current, current,
true, true,
); );
target.push((
GuiElem::new(QueueIndentEnd::new(cfg, (p1, p2))),
line_height * 0.4,
));
} }
QueueContent::Random(q) => { QueueContent::Random(q) => {
target.push(( target.push((
@ -543,6 +562,7 @@ struct QueueFolder {
path: Vec<usize>, path: Vec<usize>,
queue: Queue, queue: Queue,
current: bool, current: bool,
insert_into: bool,
mouse: bool, mouse: bool,
mouse_pos: Vec2, mouse_pos: Vec2,
copy: bool, copy: bool,
@ -579,6 +599,7 @@ impl QueueFolder {
path, path,
queue, queue,
current, current,
insert_into: false,
mouse: false, mouse: false,
mouse_pos: Vec2::ZERO, mouse_pos: Vec2::ZERO,
copy: false, copy: false,
@ -612,6 +633,29 @@ impl GuiElemTrait for QueueFolder {
Box::new(self.clone()) Box::new(self.clone())
} }
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) { fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
self.insert_into = info.mouse_pos.y > info.pos.top_left().y + info.pos.height() * 0.5;
if !self.always_copy && info.dragging.is_some() && info.pos.contains(info.mouse_pos) {
g.draw_rectangle(
if self.insert_into {
Rectangle::new(
Vec2::new(
info.pos.top_left().x,
info.pos.top_left().y + info.pos.height() * 0.5,
),
*info.pos.bottom_right(),
)
} else {
Rectangle::new(
*info.pos.top_left(),
Vec2::new(
info.pos.bottom_right().x,
info.pos.top_left().y + info.pos.height() * 0.25,
),
)
},
Color::from_rgba(1.0, 1.0, 1.0, 0.25),
);
}
if !self.mouse { if !self.mouse {
self.mouse_pos = Vec2::new( self.mouse_pos = Vec2::new(
info.mouse_pos.x - self.config.pixel_pos.top_left().x, info.mouse_pos.x - self.config.pixel_pos.top_left().x,
@ -666,13 +710,75 @@ impl GuiElemTrait for QueueFolder {
} }
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> { fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
if !self.always_copy { if !self.always_copy {
if self.insert_into {
let p = self.path.clone(); let p = self.path.clone();
dragged_add_to_queue(dragged, move |q| Command::QueueAdd(p, q)) dragged_add_to_queue(dragged, move |q| Command::QueueAdd(p, q))
} else {
let mut p = self.path.clone();
let i = p.pop();
dragged_add_to_queue(dragged, move |q| Command::QueueInsert(p, i.unwrap_or(0), q))
}
} else { } else {
vec![] vec![]
} }
} }
} }
#[derive(Clone)]
pub struct QueueIndentEnd {
config: GuiElemCfg,
children: Vec<GuiElem>,
path_insert: (Vec<usize>, usize),
}
impl QueueIndentEnd {
pub fn new(config: GuiElemCfg, path_insert: (Vec<usize>, usize)) -> Self {
Self {
config: config.w_drag_target(),
children: vec![],
path_insert,
}
}
}
impl GuiElemTrait for QueueIndentEnd {
fn config(&self) -> &GuiElemCfg {
&self.config
}
fn config_mut(&mut self) -> &mut GuiElemCfg {
&mut self.config
}
fn children(&mut self) -> Box<dyn Iterator<Item = &mut GuiElem> + '_> {
Box::new(self.children.iter_mut())
}
fn any(&self) -> &dyn std::any::Any {
self
}
fn any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
Box::new(self.clone())
}
fn draw(&mut self, info: &mut DrawInfo, g: &mut speedy2d::Graphics2D) {
if info.dragging.is_some() {
g.draw_rectangle(
info.pos.clone(),
Color::from_rgba(
1.0,
1.0,
1.0,
if info.pos.contains(info.mouse_pos) {
0.3
} else {
0.2
},
),
);
}
}
fn dragged(&mut self, dragged: Dragging) -> Vec<GuiAction> {
let (p, i) = self.path_insert.clone();
dragged_add_to_queue(dragged, move |q| Command::QueueInsert(p, i, q))
}
}
#[derive(Clone)] #[derive(Clone)]
struct QueueLoop { struct QueueLoop {
@ -1156,10 +1262,16 @@ fn add_to_queue_artist_by_id(id: ArtistId, db: &Database) -> Option<Queue> {
Some( Some(
QueueContent::Folder( QueueContent::Folder(
0, 0,
artist
.singles
.iter()
.map(|id| QueueContent::Song(*id).into())
.chain(
artist artist
.albums .albums
.iter() .iter()
.filter_map(|id| add_to_queue_album_by_id(*id, db)) .filter_map(|id| add_to_queue_album_by_id(*id, db)),
)
.collect(), .collect(),
artist.name.clone(), artist.name.clone(),
) )

View File

@ -40,14 +40,36 @@ pub struct GuiScreen {
/// 0: StatusBar / Idle display /// 0: StatusBar / Idle display
/// 1: Settings /// 1: Settings
/// 2: Panel for Main view /// 2: Panel for Main view
/// 3: Edit Panel
children: Vec<GuiElem>, children: Vec<GuiElem>,
pub idle: (bool, Option<Instant>), pub idle: (bool, Option<Instant>),
pub settings: (bool, Option<Instant>), pub settings: (bool, Option<Instant>),
pub edit_panel: (bool, Option<Instant>),
pub last_interaction: Instant, pub last_interaction: Instant,
idle_timeout: Option<f64>, idle_timeout: Option<f64>,
pub prev_mouse_pos: Vec2, pub prev_mouse_pos: Vec2,
} }
impl GuiScreen { impl GuiScreen {
pub fn open_edit(&mut self, mut edit: GuiElem) {
if !self.edit_panel.0 {
self.edit_panel = (true, Some(Instant::now()));
edit.inner.config_mut().pos = Rectangle::from_tuples((-0.5, 0.0), (0.0, 0.9));
} else {
edit.inner.config_mut().pos = Rectangle::from_tuples((0.0, 0.0), (0.5, 0.9));
}
if let Some(prev) = self.children.get_mut(3) {
prev.inner.config_mut().enabled = false;
}
self.children.insert(3, edit);
}
pub fn close_edit(&mut self) {
if self.children.len() > 4 {
self.children.remove(3);
self.children[3].inner.config_mut().enabled = true;
} else if self.edit_panel.0 {
self.edit_panel = (false, Some(Instant::now()));
}
}
pub fn new<T: Read + Write + 'static + Sync + Send>( pub fn new<T: Read + Write + 'static + Sync + Send>(
config: GuiElemCfg, config: GuiElemCfg,
get_con: get::Client<T>, get_con: get::Client<T>,
@ -109,6 +131,7 @@ impl GuiScreen {
], ],
idle: (false, None), idle: (false, None),
settings: (false, None), settings: (false, None),
edit_panel: (false, None),
last_interaction: Instant::now(), last_interaction: Instant::now(),
idle_timeout: Some(60.0), idle_timeout: Some(60.0),
prev_mouse_pos: Vec2::ZERO, prev_mouse_pos: Vec2::ZERO,
@ -193,7 +216,7 @@ impl GuiElemTrait for GuiScreen {
} }
self.idle_check(); self.idle_check();
// request_redraw for animations // request_redraw for animations
if self.idle.1.is_some() | self.settings.1.is_some() { if self.idle.1.is_some() || self.settings.1.is_some() || self.edit_panel.1.is_some() {
if let Some(h) = &info.helper { if let Some(h) = &info.helper {
h.request_redraw() h.request_redraw()
} }
@ -208,6 +231,11 @@ impl GuiElemTrait for GuiScreen {
if self.settings.0 { if self.settings.0 {
self.children[1].inner.config_mut().enabled = !self.idle.0; self.children[1].inner.config_mut().enabled = !self.idle.0;
} }
if self.edit_panel.0 {
if let Some(c) = self.children.get_mut(3) {
c.inner.config_mut().enabled = !self.idle.0;
}
}
self.children[2].inner.config_mut().enabled = !self.idle.0; self.children[2].inner.config_mut().enabled = !self.idle.0;
} }
} }
@ -229,6 +257,23 @@ impl GuiElemTrait for GuiScreen {
cfg.enabled = p > 0.0; cfg.enabled = p > 0.0;
cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9)); cfg.pos = Rectangle::from_tuples((0.0, 0.9 - 0.9 * p), (1.0, 0.9));
} }
// animations: edit_panel
if self.edit_panel.1.is_some() {
let p1 = Self::get_prog(&mut self.edit_panel, 0.3);
let p = transition(p1);
if let Some(c) = self.children.get_mut(3) {
c.inner.config_mut().enabled = p > 0.0;
c.inner.config_mut().pos =
Rectangle::from_tuples((-0.5 + 0.5 * p, 0.0), (0.5 * p, 0.9));
}
if !self.edit_panel.0 && p == 0.0 {
while self.children.len() > 3 {
self.children.pop();
}
}
self.children[2].inner.config_mut().pos =
Rectangle::from_tuples((0.5 * p, 0.0), (1.0 + 0.5 * p, 0.9));
}
// set idle timeout (only when settings are open) // set idle timeout (only when settings are open)
if self.settings.0 || self.settings.1.is_some() { if self.settings.0 || self.settings.1.is_some() {
self.idle_timeout = self.children[1] self.idle_timeout = self.children[1]

View File

@ -148,7 +148,7 @@ impl TextField {
hint, hint,
color_hint, color_hint,
None, None,
Vec2::new(0.0, 0.5), Vec2::new(0.5, 0.5),
)), )),
], ],
} }

View File

@ -27,6 +27,8 @@ mod gui;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_base; mod gui_base;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_edit;
#[cfg(feature = "speedy2d")]
mod gui_library; mod gui_library;
#[cfg(feature = "speedy2d")] #[cfg(feature = "speedy2d")]
mod gui_playback; mod gui_playback;

View File

@ -48,8 +48,16 @@ impl Song {
cached_data: Arc::new(Mutex::new(None)), cached_data: Arc::new(Mutex::new(None)),
} }
} }
pub fn uncache_data(&self) { pub fn uncache_data(&self) -> Result<(), ()> {
*self.cached_data.lock().unwrap() = None; let mut cached = self.cached_data.lock().unwrap();
match cached.as_ref() {
Some(Ok(_data)) => {
*cached = None;
Ok(())
}
Some(Err(_thread)) => Err(()),
None => Ok(()),
}
} }
/// If no data is cached yet and no caching thread is running, starts a thread to cache the data. /// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
pub fn cache_data_start_thread(&self, db: &Database) -> bool { pub fn cache_data_start_thread(&self, db: &Database) -> bool {

View File

@ -1,4 +1,4 @@
use std::sync::Arc; use std::{collections::HashSet, sync::Arc};
use awedio::{ use awedio::{
backends::CpalBackend, backends::CpalBackend,
@ -23,6 +23,7 @@ pub struct Player {
)>, )>,
manager: Manager, manager: Manager,
current_song_id: SongOpt, current_song_id: SongOpt,
cached: HashSet<SongId>,
} }
pub enum SongOpt { pub enum SongOpt {
@ -40,6 +41,7 @@ impl Player {
backend, backend,
source: None, source: None,
current_song_id: SongOpt::None, current_song_id: SongOpt::None,
cached: HashSet::new(),
}) })
} }
pub fn handle_command(&mut self, command: &Command) { pub fn handle_command(&mut self, command: &Command) {
@ -121,6 +123,7 @@ impl Player {
}; };
if db.playing { if db.playing {
if let Some(bytes) = song.cached_data_now(db) { if let Some(bytes) = song.cached_data_now(db) {
self.cached.insert(song.id);
match Self::sound_from_bytes(ext, bytes) { match Self::sound_from_bytes(ext, bytes) {
Ok(v) => { Ok(v) => {
let (sound, notif) = let (sound, notif) =
@ -140,6 +143,7 @@ impl Player {
} else { } else {
self.source = None; self.source = None;
song.cache_data_start_thread(&db); song.cache_data_start_thread(&db);
self.cached.insert(song.id);
} }
} else { } else {
panic!("invalid song ID: current_song_id not found in DB!"); panic!("invalid song ID: current_song_id not found in DB!");
@ -148,8 +152,25 @@ impl Player {
} else { } else {
self.current_song_id = SongOpt::None; self.current_song_id = SongOpt::None;
} }
if let Some(Some(song)) = db.queue.get_next_song().map(|v| db.get_song(v)) { let next_song = db.queue.get_next_song().and_then(|v| db.get_song(v));
for &id in &self.cached {
if Some(id) != next_song.map(|v| v.id)
&& !matches!(self.current_song_id, SongOpt::Some(v) if v == id)
{
if let Some(song) = db.songs().get(&id) {
if let Ok(()) = song.uncache_data() {
self.cached.remove(&id);
break;
}
} else {
self.cached.remove(&id);
break;
}
}
}
if let Some(song) = next_song {
song.cache_data_start_thread(&db); song.cache_data_start_thread(&db);
self.cached.insert(song.id);
} }
} }
} }

View File

@ -305,9 +305,9 @@ impl ToFromBytes for Command {
0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), 0b01010000 => Self::AddSong(ToFromBytes::from_bytes(s)?),
0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), 0b01010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?),
0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?), 0b01011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?),
0b10010000 => Self::AddSong(ToFromBytes::from_bytes(s)?), 0b10010000 => Self::ModifySong(ToFromBytes::from_bytes(s)?),
0b10010011 => Self::AddAlbum(ToFromBytes::from_bytes(s)?), 0b10010011 => Self::ModifyAlbum(ToFromBytes::from_bytes(s)?),
0b10011100 => Self::AddArtist(ToFromBytes::from_bytes(s)?), 0b10011100 => Self::ModifyArtist(ToFromBytes::from_bytes(s)?),
0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?), 0b01011101 => Self::AddCover(ToFromBytes::from_bytes(s)?),
0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?), 0b00110001 => Self::SetLibraryDirectory(ToFromBytes::from_bytes(s)?),
_ => { _ => {