mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-12-14 11:56:16 +01:00
Added song duration support
This commit is contained in:
2
musicdb-client/src/config_gui.toml
Normal file → Executable file
2
musicdb-client/src/config_gui.toml
Normal file → Executable file
@@ -29,4 +29,4 @@ font = ''
|
||||
# If we know the title, write it. If not, write "(no title found)" instead.
|
||||
|
||||
status_bar = '''\t
|
||||
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##?%>Year=%#\c808080 (%>Year=%)##'''
|
||||
\s0.5;?\A#\c505050by \c593D6E\A##?\a#?\A# ##\c505050on \c264524\a##\c808080?%>Year=%# (%>Year=%)## | \d'''
|
||||
|
||||
@@ -257,7 +257,8 @@ impl Gui {
|
||||
| Command::ModifyArtist(_)
|
||||
| Command::RemoveSong(_)
|
||||
| Command::RemoveAlbum(_)
|
||||
| Command::RemoveArtist(_) => {
|
||||
| Command::RemoveArtist(_)
|
||||
| Command::SetSongDuration(..) => {
|
||||
if let Some(s) = &*event_sender_arc.lock().unwrap() {
|
||||
_ = s.send_event(GuiEvent::UpdatedLibrary);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ use speedy2d::{
|
||||
use crate::{
|
||||
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui_base::{Button, Panel, ScrollBox},
|
||||
gui_text::{Label, TextField},
|
||||
gui_text::{self, AdvancedLabel, Label, TextField},
|
||||
gui_wrappers::WithFocusHotkey,
|
||||
};
|
||||
|
||||
@@ -71,148 +71,13 @@ pub struct LibraryBrowser {
|
||||
filter_albums: Arc<Mutex<Filter>>,
|
||||
filter_artists: Arc<Mutex<Filter>>,
|
||||
do_something_receiver: mpsc::Receiver<Box<dyn FnOnce(&mut Self)>>,
|
||||
selected_popup_state: (f32, usize, usize, usize),
|
||||
}
|
||||
impl Clone for LibraryBrowser {
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(self.config.clone())
|
||||
}
|
||||
}
|
||||
mod selected {
|
||||
use super::*;
|
||||
#[derive(Clone)]
|
||||
pub struct Selected(
|
||||
// artist, album, songs
|
||||
Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>,
|
||||
Arc<AtomicBool>,
|
||||
);
|
||||
impl Selected {
|
||||
pub fn new(update: Arc<AtomicBool>) -> Self {
|
||||
Self(Default::default(), update)
|
||||
}
|
||||
pub fn clear(&self) {
|
||||
self.set_to(HashSet::new(), HashSet::new(), HashSet::new())
|
||||
}
|
||||
pub fn set_to(&self, artists: HashSet<u64>, albums: HashSet<u64>, songs: HashSet<u64>) {
|
||||
let mut s = self.0.lock().unwrap();
|
||||
s.0 = artists;
|
||||
s.1 = albums;
|
||||
s.2 = songs;
|
||||
self.changed();
|
||||
}
|
||||
pub fn contains_artist(&self, id: &ArtistId) -> bool {
|
||||
self.0.lock().unwrap().0.contains(id)
|
||||
}
|
||||
pub fn contains_album(&self, id: &AlbumId) -> bool {
|
||||
self.0.lock().unwrap().1.contains(id)
|
||||
}
|
||||
pub fn contains_song(&self, id: &SongId) -> bool {
|
||||
self.0.lock().unwrap().2.contains(id)
|
||||
}
|
||||
pub fn insert_artist(&self, id: ArtistId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().0.insert(id)
|
||||
}
|
||||
pub fn insert_album(&self, id: AlbumId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().1.insert(id)
|
||||
}
|
||||
pub fn insert_song(&self, id: SongId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().2.insert(id)
|
||||
}
|
||||
pub fn remove_artist(&self, id: &ArtistId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().0.remove(id)
|
||||
}
|
||||
pub fn remove_album(&self, id: &AlbumId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().1.remove(id)
|
||||
}
|
||||
pub fn remove_song(&self, id: &SongId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().2.remove(id)
|
||||
}
|
||||
pub fn view<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
|
||||
) -> T {
|
||||
f(&self.0.lock().unwrap())
|
||||
}
|
||||
pub fn view_mut<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut (HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
|
||||
) -> T {
|
||||
let v = f(&mut self.0.lock().unwrap());
|
||||
self.changed();
|
||||
v
|
||||
}
|
||||
fn changed(&self) {
|
||||
self.1.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let (sel_artists, sel_albums, sel_songs) = &*lock;
|
||||
let mut out = vec![];
|
||||
for (artist, singles, albums, _) in &lb.library_filtered {
|
||||
let artist_selected = sel_artists.contains(artist);
|
||||
let mut local_artist_owned = vec![];
|
||||
let mut local_artist = if artist_selected {
|
||||
&mut local_artist_owned
|
||||
} else {
|
||||
&mut out
|
||||
};
|
||||
for (song, _) in singles {
|
||||
let song_selected = sel_songs.contains(song);
|
||||
if song_selected {
|
||||
local_artist.push(QueueContent::Song(*song).into());
|
||||
}
|
||||
}
|
||||
for (album, songs, _) in albums {
|
||||
let album_selected = sel_albums.contains(album);
|
||||
let mut local_album_owned = vec![];
|
||||
let local_album = if album_selected {
|
||||
&mut local_album_owned
|
||||
} else {
|
||||
&mut local_artist
|
||||
};
|
||||
for (song, _) in songs {
|
||||
let song_selected = sel_songs.contains(song);
|
||||
if song_selected {
|
||||
local_album.push(QueueContent::Song(*song).into());
|
||||
}
|
||||
}
|
||||
if album_selected {
|
||||
local_artist.push(
|
||||
QueueContent::Folder(
|
||||
0,
|
||||
local_album_owned,
|
||||
match db.albums().get(album) {
|
||||
Some(v) => v.name.clone(),
|
||||
None => "< unknown album >".to_owned(),
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if artist_selected {
|
||||
out.push(
|
||||
QueueContent::Folder(
|
||||
0,
|
||||
local_artist_owned,
|
||||
match db.artists().get(artist) {
|
||||
Some(v) => v.name.to_owned(),
|
||||
None => "< unknown artist >".to_owned(),
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
fn search_regex_new(pat: &str, case_insensitive: bool) -> Result<Option<Regex>, regex::Error> {
|
||||
if pat.is_empty() {
|
||||
Ok(None)
|
||||
@@ -312,6 +177,17 @@ impl LibraryBrowser {
|
||||
selected.clone(),
|
||||
do_something_sender.clone(),
|
||||
)),
|
||||
GuiElem::new(Panel::with_background(
|
||||
GuiElemCfg::default().disabled(),
|
||||
vec![GuiElem::new(Label::new(
|
||||
GuiElemCfg::default(),
|
||||
String::new(),
|
||||
Color::LIGHT_GRAY,
|
||||
None,
|
||||
Vec2::new(0.5, 0.5),
|
||||
))],
|
||||
Color::from_rgba(0.0, 0.0, 0.0, 0.8),
|
||||
)),
|
||||
],
|
||||
// - - -
|
||||
library_sorted: vec![],
|
||||
@@ -336,6 +212,7 @@ impl LibraryBrowser {
|
||||
filter_albums,
|
||||
filter_artists,
|
||||
do_something_receiver,
|
||||
selected_popup_state: (0.0, 0, 0, 0),
|
||||
}
|
||||
}
|
||||
pub fn selected_add_all(&self) {
|
||||
@@ -400,6 +277,9 @@ impl GuiElemTrait for LibraryBrowser {
|
||||
fn clone_gui(&self) -> Box<dyn GuiElemTrait> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
fn draw_rev(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
||||
loop {
|
||||
if let Ok(action) = self.do_something_receiver.try_recv() {
|
||||
@@ -621,8 +501,121 @@ impl GuiElemTrait for LibraryBrowser {
|
||||
}
|
||||
},
|
||||
);
|
||||
// selected
|
||||
{
|
||||
let (artists, albums, songs) = self
|
||||
.selected
|
||||
.view(|sel| (sel.0.len(), sel.1.len(), sel.2.len()));
|
||||
if self.selected_popup_state.1 != artists
|
||||
|| self.selected_popup_state.2 != albums
|
||||
|| self.selected_popup_state.3 != songs
|
||||
{
|
||||
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) {
|
||||
(0, 0, 0) => None,
|
||||
|
||||
(0, 0, 1) => Some(format!("1 song selected")),
|
||||
(0, 0, s) => Some(format!("{s} songs selected")),
|
||||
|
||||
(0, 1, 0) => Some(format!("1 album selected")),
|
||||
(0, al, 0) => Some(format!("{al} albums selected")),
|
||||
|
||||
(1, 0, 0) => Some(format!("1 artist selected")),
|
||||
(ar, 0, 0) => Some(format!("{ar} artists selected")),
|
||||
|
||||
(0, 1, 1) => Some(format!("1 song and 1 album selected")),
|
||||
(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, 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, 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, s) => Some(format!("{s} songs, 1 album and 1 artist selected")),
|
||||
(1, al, 1) => {
|
||||
Some(format!("1 song, {al} albums and 1 artist selected"))
|
||||
}
|
||||
(ar, 1, 1) => {
|
||||
Some(format!("1 song, 1 album and {ar} artists selected"))
|
||||
}
|
||||
(1, al, s) => {
|
||||
Some(format!("{s} songs, {al} albums and 1 artist selected"))
|
||||
}
|
||||
(ar, 1, s) => {
|
||||
Some(format!("{s} songs, 1 album and {ar} artists selected"))
|
||||
}
|
||||
(ar, al, 1) => {
|
||||
Some(format!("1 song, {al} albums and {ar} artist selected"))
|
||||
}
|
||||
(ar, al, s) => {
|
||||
Some(format!("{s} songs, {al} albums and {ar} artists selected"))
|
||||
}
|
||||
} {
|
||||
*self.children[6]
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<Panel>()
|
||||
.unwrap()
|
||||
.children[0]
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<Label>()
|
||||
.unwrap()
|
||||
.content
|
||||
.text() = text;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.config.redraw = true;
|
||||
}
|
||||
// selected popup
|
||||
{
|
||||
let mut redraw = false;
|
||||
if self.selected_popup_state.1 > 0
|
||||
|| self.selected_popup_state.2 > 0
|
||||
|| self.selected_popup_state.3 > 0
|
||||
{
|
||||
if self.selected_popup_state.0 != 1.0 {
|
||||
redraw = true;
|
||||
self.children[6].inner.config_mut().enabled = true;
|
||||
self.selected_popup_state.0 = 0.3 + 0.7 * self.selected_popup_state.0;
|
||||
if self.selected_popup_state.0 > 0.99 {
|
||||
self.selected_popup_state.0 = 1.0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if self.selected_popup_state.0 != 0.0 {
|
||||
redraw = true;
|
||||
self.selected_popup_state.0 = 0.7 * self.selected_popup_state.0;
|
||||
if self.selected_popup_state.0 < 0.01 {
|
||||
self.selected_popup_state.0 = 0.0;
|
||||
self.children[6].inner.config_mut().enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if redraw {
|
||||
self.children[6].inner.config_mut().pos = Rectangle::from_tuples(
|
||||
(0.0, 1.0 - 0.05 * self.selected_popup_state.0),
|
||||
(1.0, 1.0),
|
||||
);
|
||||
if let Some(h) = &info.helper {
|
||||
h.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
|
||||
self.config.redraw = false;
|
||||
self.update_ui(&info.database, info.line_height);
|
||||
@@ -804,30 +797,57 @@ impl LibraryBrowser {
|
||||
)
|
||||
}
|
||||
fn build_ui_element_album(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) {
|
||||
let (name, duration) = if let Some(v) = db.albums().get(&id) {
|
||||
let duration = v
|
||||
.songs
|
||||
.iter()
|
||||
.filter_map(|id| db.get_song(id))
|
||||
.map(|s| s.duration_millis)
|
||||
.fold(0, u64::saturating_add)
|
||||
/ 1000;
|
||||
(
|
||||
v.name.to_owned(),
|
||||
if duration >= 60 * 60 {
|
||||
format!(
|
||||
" {}:{}:{:0>2}",
|
||||
duration / (60 * 60),
|
||||
(duration / 60) % 60,
|
||||
duration % 60
|
||||
)
|
||||
} else {
|
||||
format!(" {}:{:0>2}", duration / 60, duration % 60)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(format!("[ Album #{id} ]"), String::new())
|
||||
};
|
||||
(
|
||||
GuiElem::new(ListAlbum::new(
|
||||
GuiElemCfg::default(),
|
||||
id,
|
||||
if let Some(v) = db.albums().get(&id) {
|
||||
v.name.to_owned()
|
||||
} else {
|
||||
format!("[ Album #{id} ]")
|
||||
},
|
||||
name,
|
||||
duration,
|
||||
self.selected.clone(),
|
||||
)),
|
||||
h * 1.5,
|
||||
)
|
||||
}
|
||||
fn build_ui_element_song(&self, id: ArtistId, db: &Database, h: f32) -> (GuiElem, f32) {
|
||||
let (name, duration) = if let Some(v) = db.songs().get(&id) {
|
||||
let duration = v.duration_millis / 1000;
|
||||
(
|
||||
v.title.to_owned(),
|
||||
format!(" {}:{:0>2}", duration / 60, duration % 60),
|
||||
)
|
||||
} else {
|
||||
(format!("[ Song #{id} ]"), String::new())
|
||||
};
|
||||
(
|
||||
GuiElem::new(ListSong::new(
|
||||
GuiElemCfg::default(),
|
||||
id,
|
||||
if let Some(v) = db.songs().get(&id) {
|
||||
v.title.to_owned()
|
||||
} else {
|
||||
format!("[ Song #{id} ]")
|
||||
},
|
||||
name,
|
||||
duration,
|
||||
self.selected.clone(),
|
||||
)),
|
||||
h,
|
||||
@@ -989,13 +1009,28 @@ struct ListAlbum {
|
||||
sel: bool,
|
||||
}
|
||||
impl ListAlbum {
|
||||
pub fn new(mut config: GuiElemCfg, id: AlbumId, name: String, selected: Selected) -> Self {
|
||||
let label = Label::new(
|
||||
pub fn new(
|
||||
mut config: GuiElemCfg,
|
||||
id: AlbumId,
|
||||
name: String,
|
||||
half_sized_info: String,
|
||||
selected: Selected,
|
||||
) -> Self {
|
||||
let label = AdvancedLabel::new(
|
||||
GuiElemCfg::default(),
|
||||
name,
|
||||
Color::from_int_rgb(8, 61, 47),
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
vec![vec![
|
||||
(
|
||||
gui_text::Content::new(name, Color::from_int_rgb(8, 61, 47)),
|
||||
1.0,
|
||||
1.0,
|
||||
),
|
||||
(
|
||||
gui_text::Content::new(half_sized_info, Color::GRAY),
|
||||
0.5,
|
||||
1.0,
|
||||
),
|
||||
]],
|
||||
);
|
||||
config.redraw = true;
|
||||
Self {
|
||||
@@ -1132,13 +1167,24 @@ struct ListSong {
|
||||
sel: bool,
|
||||
}
|
||||
impl ListSong {
|
||||
pub fn new(mut config: GuiElemCfg, id: SongId, name: String, selected: Selected) -> Self {
|
||||
let label = Label::new(
|
||||
pub fn new(
|
||||
mut config: GuiElemCfg,
|
||||
id: SongId,
|
||||
name: String,
|
||||
duration: String,
|
||||
selected: Selected,
|
||||
) -> Self {
|
||||
let label = AdvancedLabel::new(
|
||||
GuiElemCfg::default(),
|
||||
name,
|
||||
Color::from_int_rgb(175, 175, 175),
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
vec![vec![
|
||||
(
|
||||
gui_text::Content::new(name, Color::from_int_rgb(175, 175, 175)),
|
||||
1.0,
|
||||
1.0,
|
||||
),
|
||||
(gui_text::Content::new(duration, Color::GRAY), 0.6, 1.0),
|
||||
]],
|
||||
);
|
||||
config.redraw = true;
|
||||
Self {
|
||||
@@ -2014,3 +2060,140 @@ impl FilterType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod selected {
|
||||
use super::*;
|
||||
#[derive(Clone)]
|
||||
pub struct Selected(
|
||||
// artist, album, songs
|
||||
Arc<Mutex<(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)>>,
|
||||
Arc<AtomicBool>,
|
||||
);
|
||||
impl Selected {
|
||||
pub fn new(update: Arc<AtomicBool>) -> Self {
|
||||
Self(Default::default(), update)
|
||||
}
|
||||
pub fn clear(&self) {
|
||||
self.set_to(HashSet::new(), HashSet::new(), HashSet::new())
|
||||
}
|
||||
pub fn set_to(&self, artists: HashSet<u64>, albums: HashSet<u64>, songs: HashSet<u64>) {
|
||||
let mut s = self.0.lock().unwrap();
|
||||
s.0 = artists;
|
||||
s.1 = albums;
|
||||
s.2 = songs;
|
||||
self.changed();
|
||||
}
|
||||
pub fn contains_artist(&self, id: &ArtistId) -> bool {
|
||||
self.0.lock().unwrap().0.contains(id)
|
||||
}
|
||||
pub fn contains_album(&self, id: &AlbumId) -> bool {
|
||||
self.0.lock().unwrap().1.contains(id)
|
||||
}
|
||||
pub fn contains_song(&self, id: &SongId) -> bool {
|
||||
self.0.lock().unwrap().2.contains(id)
|
||||
}
|
||||
pub fn insert_artist(&self, id: ArtistId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().0.insert(id)
|
||||
}
|
||||
pub fn insert_album(&self, id: AlbumId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().1.insert(id)
|
||||
}
|
||||
pub fn insert_song(&self, id: SongId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().2.insert(id)
|
||||
}
|
||||
pub fn remove_artist(&self, id: &ArtistId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().0.remove(id)
|
||||
}
|
||||
pub fn remove_album(&self, id: &AlbumId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().1.remove(id)
|
||||
}
|
||||
pub fn remove_song(&self, id: &SongId) -> bool {
|
||||
self.changed();
|
||||
self.0.lock().unwrap().2.remove(id)
|
||||
}
|
||||
pub fn view<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&(HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
|
||||
) -> T {
|
||||
f(&self.0.lock().unwrap())
|
||||
}
|
||||
pub fn view_mut<T>(
|
||||
&self,
|
||||
f: impl FnOnce(&mut (HashSet<ArtistId>, HashSet<AlbumId>, HashSet<SongId>)) -> T,
|
||||
) -> T {
|
||||
let v = f(&mut self.0.lock().unwrap());
|
||||
self.changed();
|
||||
v
|
||||
}
|
||||
fn changed(&self) {
|
||||
self.1.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
pub fn as_queue(&self, lb: &LibraryBrowser, db: &Database) -> Vec<Queue> {
|
||||
let lock = self.0.lock().unwrap();
|
||||
let (sel_artists, sel_albums, sel_songs) = &*lock;
|
||||
let mut out = vec![];
|
||||
for (artist, singles, albums, _) in &lb.library_filtered {
|
||||
let artist_selected = sel_artists.contains(artist);
|
||||
let mut local_artist_owned = vec![];
|
||||
let mut local_artist = if artist_selected {
|
||||
&mut local_artist_owned
|
||||
} else {
|
||||
&mut out
|
||||
};
|
||||
for (song, _) in singles {
|
||||
let song_selected = sel_songs.contains(song);
|
||||
if song_selected {
|
||||
local_artist.push(QueueContent::Song(*song).into());
|
||||
}
|
||||
}
|
||||
for (album, songs, _) in albums {
|
||||
let album_selected = sel_albums.contains(album);
|
||||
let mut local_album_owned = vec![];
|
||||
let local_album = if album_selected {
|
||||
&mut local_album_owned
|
||||
} else {
|
||||
&mut local_artist
|
||||
};
|
||||
for (song, _) in songs {
|
||||
let song_selected = sel_songs.contains(song);
|
||||
if song_selected {
|
||||
local_album.push(QueueContent::Song(*song).into());
|
||||
}
|
||||
}
|
||||
if album_selected {
|
||||
local_artist.push(
|
||||
QueueContent::Folder(
|
||||
0,
|
||||
local_album_owned,
|
||||
match db.albums().get(album) {
|
||||
Some(v) => v.name.clone(),
|
||||
None => "< unknown album >".to_owned(),
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if artist_selected {
|
||||
out.push(
|
||||
QueueContent::Folder(
|
||||
0,
|
||||
local_artist_owned,
|
||||
match db.artists().get(artist) {
|
||||
Some(v) => v.name.to_owned(),
|
||||
None => "< unknown artist >".to_owned(),
|
||||
},
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
0
musicdb-client/src/gui_notif.rs
Normal file → Executable file
0
musicdb-client/src/gui_notif.rs
Normal file → Executable file
@@ -3,7 +3,7 @@ use std::collections::VecDeque;
|
||||
use musicdb_lib::{
|
||||
data::{
|
||||
database::Database,
|
||||
queue::{Queue, QueueContent, ShuffleState},
|
||||
queue::{Queue, QueueContent, QueueDuration, ShuffleState},
|
||||
song::Song,
|
||||
AlbumId, ArtistId,
|
||||
},
|
||||
@@ -19,7 +19,7 @@ use speedy2d::{
|
||||
use crate::{
|
||||
gui::{Dragging, DrawInfo, GuiAction, GuiElem, GuiElemCfg, GuiElemTrait},
|
||||
gui_base::{Panel, ScrollBox},
|
||||
gui_text::Label,
|
||||
gui_text::{self, AdvancedLabel, Label},
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -35,6 +35,7 @@ because simple clicks have to be GoTo events.
|
||||
pub struct QueueViewer {
|
||||
config: GuiElemCfg,
|
||||
children: Vec<GuiElem>,
|
||||
queue_updated: bool,
|
||||
}
|
||||
const QP_QUEUE1: f32 = 0.0;
|
||||
const QP_QUEUE2: f32 = 0.95;
|
||||
@@ -63,7 +64,7 @@ impl QueueViewer {
|
||||
Rectangle::from_tuples((0.0, QP_QUEUE1), (1.0, QP_QUEUE2)),
|
||||
))),
|
||||
GuiElem::new(Panel::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (1.0, QP_INV2))),
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, QP_INV1), (0.5, QP_INV2))),
|
||||
vec![
|
||||
GuiElem::new(
|
||||
QueueLoop::new(
|
||||
@@ -129,7 +130,13 @@ impl QueueViewer {
|
||||
),
|
||||
],
|
||||
)),
|
||||
GuiElem::new(AdvancedLabel::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.5, QP_INV1), (1.0, QP_INV2))),
|
||||
Vec2::new(0.0, 0.5),
|
||||
vec![],
|
||||
)),
|
||||
],
|
||||
queue_updated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +160,58 @@ impl GuiElemTrait for QueueViewer {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
fn draw(&mut self, info: &mut DrawInfo, _g: &mut speedy2d::Graphics2D) {
|
||||
if self.queue_updated {
|
||||
self.queue_updated = false;
|
||||
let label = self.children[3]
|
||||
.inner
|
||||
.any_mut()
|
||||
.downcast_mut::<AdvancedLabel>()
|
||||
.unwrap();
|
||||
fn fmt_dur(dur: QueueDuration) -> String {
|
||||
if dur.infinite {
|
||||
"∞".to_owned()
|
||||
} else {
|
||||
let seconds = dur.millis / 1000;
|
||||
let minutes = seconds / 60;
|
||||
let h = minutes / 60;
|
||||
let m = minutes % 60;
|
||||
let s = seconds % 60;
|
||||
if dur.random_counter == 0 {
|
||||
if h > 0 {
|
||||
format!("{h}:{m:0>2}:{s:0>2}")
|
||||
} else {
|
||||
format!("{m:0>2}:{s:0>2}")
|
||||
}
|
||||
} else {
|
||||
let r = dur.random_counter;
|
||||
if dur.millis > 0 {
|
||||
if h > 0 {
|
||||
format!("{h}:{m:0>2}:{s:0>2} + {r} random songs")
|
||||
} else {
|
||||
format!("{m:0>2}:{s:0>2} + {r} random songs")
|
||||
}
|
||||
} else {
|
||||
format!("{r} random songs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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::Content::new(format!("Total: {dt}"), Color::GRAY),
|
||||
1.0,
|
||||
1.0,
|
||||
)],
|
||||
vec![(
|
||||
gui_text::Content::new(format!("Remaining: {dr}"), Color::GRAY),
|
||||
1.0,
|
||||
1.0,
|
||||
)],
|
||||
];
|
||||
label.config_mut().redraw = true;
|
||||
}
|
||||
if self.config.redraw || info.pos.size() != self.config.pixel_pos.size() {
|
||||
self.config.redraw = false;
|
||||
let mut c = vec![];
|
||||
@@ -173,6 +232,7 @@ impl GuiElemTrait for QueueViewer {
|
||||
}
|
||||
}
|
||||
fn updated_queue(&mut self) {
|
||||
self.queue_updated = true;
|
||||
self.config.redraw = true;
|
||||
}
|
||||
}
|
||||
@@ -412,16 +472,38 @@ impl QueueSong {
|
||||
Self {
|
||||
config: config.w_mouse().w_keyboard_watch().w_drag_target(),
|
||||
children: vec![
|
||||
GuiElem::new(Label::new(
|
||||
GuiElem::new(AdvancedLabel::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((0.0, 0.0), (1.0, 0.57))),
|
||||
song.title.clone(),
|
||||
if current {
|
||||
Color::from_int_rgb(194, 76, 178)
|
||||
} else {
|
||||
Color::from_int_rgb(120, 76, 194)
|
||||
},
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
vec![vec![
|
||||
(
|
||||
gui_text::Content::new(
|
||||
song.title.clone(),
|
||||
if current {
|
||||
Color::from_int_rgb(194, 76, 178)
|
||||
} else {
|
||||
Color::from_int_rgb(120, 76, 194)
|
||||
},
|
||||
),
|
||||
1.0,
|
||||
1.0,
|
||||
),
|
||||
(
|
||||
gui_text::Content::new(
|
||||
{
|
||||
let duration = song.duration_millis / 1000;
|
||||
format!(" {}:{:0>2}", duration / 60, duration % 60)
|
||||
},
|
||||
if current {
|
||||
Color::GRAY
|
||||
} else {
|
||||
Color::DARK_GRAY
|
||||
},
|
||||
),
|
||||
0.6,
|
||||
1.0,
|
||||
),
|
||||
]],
|
||||
)),
|
||||
GuiElem::new(Label::new(
|
||||
GuiElemCfg::at(Rectangle::from_tuples((sub_offset, 0.57), (1.0, 1.0))),
|
||||
@@ -443,9 +525,9 @@ impl QueueSong {
|
||||
}
|
||||
},
|
||||
if current {
|
||||
Color::from_int_rgb(146, 57, 133)
|
||||
Color::from_int_rgb(97, 38, 89)
|
||||
} else {
|
||||
Color::from_int_rgb(95, 57, 146)
|
||||
Color::from_int_rgb(60, 38, 97)
|
||||
},
|
||||
None,
|
||||
Vec2::new(0.0, 0.5),
|
||||
@@ -498,9 +580,13 @@ impl GuiElemTrait for QueueSong {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@@ -717,9 +803,13 @@ impl GuiElemTrait for QueueFolder {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@@ -944,9 +1034,13 @@ impl GuiElemTrait for QueueLoop {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@@ -1077,9 +1171,13 @@ impl GuiElemTrait for QueueRandom {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@@ -1209,9 +1307,13 @@ impl GuiElemTrait for QueueShuffle {
|
||||
fn mouse_up(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
||||
if self.mouse && button == MouseButton::Left {
|
||||
self.mouse = false;
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
if !self.always_copy {
|
||||
vec![GuiAction::SendToServer(Command::QueueGoto(
|
||||
self.path.clone(),
|
||||
))]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,11 @@ pub struct Content {
|
||||
impl Content {
|
||||
pub fn new(text: String, color: Color) -> Self {
|
||||
Self {
|
||||
text,
|
||||
text: if text.starts_with(' ') {
|
||||
text.replacen(' ', "\u{00A0}", 1)
|
||||
} else {
|
||||
text
|
||||
},
|
||||
color,
|
||||
background: None,
|
||||
formatted: None,
|
||||
|
||||
25
musicdb-client/src/textcfg.rs
Normal file → Executable file
25
musicdb-client/src/textcfg.rs
Normal file → Executable file
@@ -1,10 +1,9 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
iter::Peekable,
|
||||
str::{Chars, FromStr},
|
||||
};
|
||||
|
||||
use musicdb_lib::data::{database::Database, song::Song, GeneralData, SongId};
|
||||
use musicdb_lib::data::{database::Database, song::Song, GeneralData};
|
||||
use speedy2d::color::Color;
|
||||
|
||||
use crate::gui_text::Content;
|
||||
@@ -22,6 +21,7 @@ pub enum TextPart {
|
||||
SongTitle,
|
||||
AlbumName,
|
||||
ArtistName,
|
||||
SongDuration(bool),
|
||||
/// Searches for a tag with exactly the provided value.
|
||||
/// Returns nothing or one of the following characters:
|
||||
/// `s` for Song, `a` for Album, and `A` for Artist.
|
||||
@@ -114,6 +114,19 @@ impl TextBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
TextPart::SongDuration(show_millis) => {
|
||||
if let Some(s) = current_song {
|
||||
let seconds = s.duration_millis / 1000;
|
||||
let minutes = seconds / 60;
|
||||
let seconds = seconds % 60;
|
||||
push!(if *show_millis {
|
||||
let ms = s.duration_millis % 1000;
|
||||
format!("{minutes}:{seconds:0>2}.{ms:0>4}")
|
||||
} else {
|
||||
format!("{minutes}:{seconds:0>2}")
|
||||
});
|
||||
}
|
||||
}
|
||||
TextPart::TagEq(p) => {
|
||||
for (i, gen) in all_general(db, ¤t_song).into_iter().enumerate() {
|
||||
if let Some(_) = gen.and_then(|gen| gen.tags.iter().find(|t| *t == p)) {
|
||||
@@ -205,6 +218,14 @@ impl TextBuilder {
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user