Added song duration support

This commit is contained in:
Mark
2023-11-04 20:06:54 +01:00
parent 273eac4b43
commit 7cae108ffa
14 changed files with 701 additions and 216 deletions

2
musicdb-client/src/config_gui.toml Normal file → Executable file
View 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'''

View File

@@ -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);
}

View File

@@ -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
View File

View 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![]
}

View File

@@ -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
View 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, &current_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({