mirror of
https://github.com/Dummi26/musicdb.git
synced 2025-04-28 10:06:05 +02:00

filldb: Year and Genre will be read from MP3/ID3 tags and added as Tags. client: Tags can (and will) be displayed in the status bar. client: StatusBar text can be configured via ~/.config/musicdb-client/config_gui.toml client: Added a proper default config file at src/config_gui.toml (note: this is in src/ so that include_bytes! can use a path without /, so it will compile on windows) client: users will need to add the new `[text]` section to their gui_config.toml!
372 lines
13 KiB
Rust
Executable File
372 lines
13 KiB
Rust
Executable File
use std::{collections::VecDeque, sync::Arc, time::Instant};
|
|
|
|
use musicdb_lib::{
|
|
data::{CoverId, SongId},
|
|
server::Command,
|
|
};
|
|
use speedy2d::{color::Color, dimen::Vec2, shape::Rectangle, window::MouseButton};
|
|
|
|
use crate::{
|
|
gui::{adjust_area, adjust_pos, GuiAction, GuiCover, GuiElem, GuiElemCfg, GuiElemTrait},
|
|
gui_text::{AdvancedLabel, Content, Label},
|
|
};
|
|
|
|
/*
|
|
|
|
Components for the StatusBar.
|
|
This file could probably have a better name.
|
|
|
|
*/
|
|
|
|
#[derive(Clone)]
|
|
pub struct CurrentSong {
|
|
config: GuiElemCfg,
|
|
children: Vec<GuiElem>,
|
|
prev_song: Option<SongId>,
|
|
cover_pos: Rectangle,
|
|
covers: VecDeque<(CoverId, Option<(bool, Instant)>)>,
|
|
text_updated: Option<Instant>,
|
|
}
|
|
impl CurrentSong {
|
|
pub fn new(config: GuiElemCfg) -> Self {
|
|
Self {
|
|
config,
|
|
children: vec![GuiElem::new(AdvancedLabel::new(
|
|
GuiElemCfg::at(Rectangle::from_tuples((0.4, 0.0), (1.0, 1.0))),
|
|
Vec2::new(0.0, 0.5),
|
|
vec![],
|
|
))],
|
|
cover_pos: Rectangle::new(Vec2::ZERO, Vec2::ZERO),
|
|
covers: VecDeque::new(),
|
|
prev_song: None,
|
|
text_updated: None,
|
|
}
|
|
}
|
|
pub fn set_idle_mode(&mut self, idle_mode: f32) {
|
|
// ...
|
|
}
|
|
fn color_title(a: f32) -> Color {
|
|
Self::color_with_alpha(&Color::WHITE, a)
|
|
}
|
|
fn color_artist(a: f32) -> Color {
|
|
Color::from_rgba(0.32, 0.20, 0.49, a)
|
|
}
|
|
fn color_album(a: f32) -> Color {
|
|
Color::from_rgba(0.03, 0.24, 0.18, a)
|
|
}
|
|
fn color_by(a: f32) -> Color {
|
|
Self::color_with_alpha(&Color::DARK_GRAY, a)
|
|
}
|
|
fn color_on(a: f32) -> Color {
|
|
Self::color_with_alpha(&Color::DARK_GRAY, a)
|
|
}
|
|
fn color_with_alpha(c: &Color, a: f32) -> Color {
|
|
Color::from_rgba(c.r(), c.g(), c.b(), a)
|
|
}
|
|
}
|
|
impl GuiElemTrait for CurrentSong {
|
|
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) {
|
|
// check if there is a new song
|
|
let new_song = if let Some(song) = info.database.queue.get_current_song() {
|
|
if Some(*song) == self.prev_song {
|
|
// same song as before
|
|
None
|
|
} else {
|
|
Some(Some(*song))
|
|
}
|
|
} else if self.prev_song.is_none() {
|
|
// no song, nothing in queue
|
|
None
|
|
} else {
|
|
// end of last song
|
|
Some(None)
|
|
};
|
|
if let Some(new_song) = new_song {
|
|
// if there is a new song:
|
|
if self.prev_song != new_song {
|
|
self.config.redraw = true;
|
|
self.prev_song = new_song;
|
|
}
|
|
// get cover
|
|
let get_cover = |song: Option<u64>| crate::get_cover(song?, info.database);
|
|
let cover = get_cover(new_song);
|
|
// fade out all covers
|
|
for (_, t) in &mut self.covers {
|
|
if !t.is_some_and(|t| t.0) {
|
|
// not fading out yet, so make it start
|
|
*t = Some((true, Instant::now()));
|
|
}
|
|
}
|
|
// cover fades in now.
|
|
if let Some(cover) = cover {
|
|
self.covers
|
|
.push_back((cover, Some((false, Instant::now()))));
|
|
}
|
|
if let Some(next_cover) = get_cover(info.database.queue.get_next_song().cloned()) {
|
|
if !info.covers.contains_key(&next_cover) {
|
|
info.covers.insert(
|
|
next_cover,
|
|
GuiCover::new(next_cover, Arc::clone(&info.get_con)),
|
|
);
|
|
}
|
|
}
|
|
// redraw
|
|
if self.config.redraw {
|
|
self.config.redraw = false;
|
|
if let Some(song) = new_song {
|
|
let status_bar_text = info
|
|
.gui_config
|
|
.status_bar_text
|
|
.gen(&info.database, info.database.get_song(&song));
|
|
self.children[0]
|
|
.try_as_mut::<AdvancedLabel>()
|
|
.unwrap()
|
|
.content = status_bar_text;
|
|
self.text_updated = Some(Instant::now());
|
|
}
|
|
}
|
|
}
|
|
if let Some(updated) = &self.text_updated {
|
|
if let Some(h) = &info.helper {
|
|
h.request_redraw();
|
|
}
|
|
let mut prog = updated.elapsed().as_secs_f32();
|
|
if prog >= 1.0 {
|
|
prog = 1.0;
|
|
self.text_updated = None;
|
|
}
|
|
self.children[0]
|
|
.try_as_mut::<AdvancedLabel>()
|
|
.unwrap()
|
|
.content
|
|
.iter_mut()
|
|
.count();
|
|
}
|
|
// drawing stuff
|
|
if self.config.pixel_pos.size() != info.pos.size() {
|
|
let leftright = 0.05;
|
|
let topbottom = 0.05;
|
|
let mut width = 0.3;
|
|
let mut height = 1.0 - topbottom * 2.0;
|
|
if width * info.pos.width() < height * info.pos.height() {
|
|
height = width * info.pos.width() / info.pos.height();
|
|
} else {
|
|
width = height * info.pos.height() / info.pos.width();
|
|
}
|
|
let right = leftright + width + leftright;
|
|
self.cover_pos = Rectangle::from_tuples(
|
|
(leftright, 0.5 - 0.5 * height),
|
|
(leftright + width, 0.5 + 0.5 * height),
|
|
);
|
|
for el in self.children.iter_mut().take(2) {
|
|
let pos = &mut el.inner.config_mut().pos;
|
|
*pos = Rectangle::new(Vec2::new(right, pos.top_left().y), *pos.bottom_right());
|
|
}
|
|
}
|
|
let mut cover_to_remove = None;
|
|
for (cover_index, (cover_id, time)) in self.covers.iter_mut().enumerate() {
|
|
let pos = match time {
|
|
None => 1.0,
|
|
Some((false, t)) => {
|
|
let el = t.elapsed().as_secs_f32();
|
|
if el >= 1.0 {
|
|
*time = None;
|
|
1.0
|
|
} else {
|
|
if let Some(h) = &info.helper {
|
|
h.request_redraw();
|
|
}
|
|
el
|
|
}
|
|
}
|
|
Some((true, t)) => {
|
|
let el = t.elapsed().as_secs_f32();
|
|
if el >= 1.0 {
|
|
cover_to_remove = Some(cover_index);
|
|
2.0
|
|
} else {
|
|
if let Some(h) = &info.helper {
|
|
h.request_redraw();
|
|
}
|
|
1.0 + el
|
|
}
|
|
}
|
|
};
|
|
if let Some(cover) = info.covers.get_mut(cover_id) {
|
|
if let Some(cover) = cover.get_init(g) {
|
|
let rect = Rectangle::new(
|
|
Vec2::new(
|
|
info.pos.top_left().x + info.pos.width() * self.cover_pos.top_left().x,
|
|
info.pos.top_left().y + info.pos.height() * self.cover_pos.top_left().y,
|
|
),
|
|
Vec2::new(
|
|
info.pos.top_left().x
|
|
+ info.pos.width() * self.cover_pos.bottom_right().x,
|
|
info.pos.top_left().y
|
|
+ info.pos.height() * self.cover_pos.bottom_right().y,
|
|
),
|
|
);
|
|
if pos == 1.0 {
|
|
g.draw_rectangle_image(rect, &cover);
|
|
} else {
|
|
let prog = (pos - 1.0).abs();
|
|
// shrink to half (0.5x0.5) size while moving left and fading out
|
|
let lx = rect.top_left().x + rect.width() * prog * 0.25;
|
|
let rx = rect.bottom_right().x - rect.width() * prog * 0.25;
|
|
let ty = rect.top_left().y + rect.height() * prog * 0.25;
|
|
let by = rect.bottom_right().y - rect.height() * prog * 0.25;
|
|
let mut moved = rect.width() * prog * prog;
|
|
if pos > 1.0 {
|
|
moved = -moved;
|
|
}
|
|
g.draw_rectangle_image_tinted(
|
|
Rectangle::from_tuples((lx + moved, ty), (rx + moved, by)),
|
|
Color::from_rgba(
|
|
1.0,
|
|
1.0,
|
|
1.0,
|
|
if pos > 1.0 { 2.0 - pos } else { pos },
|
|
),
|
|
&cover,
|
|
);
|
|
}
|
|
} else {
|
|
// cover still loading, just wait
|
|
}
|
|
} else {
|
|
// cover not loading or loaded, start loading!
|
|
info.covers
|
|
.insert(*cover_id, GuiCover::new(*cover_id, info.get_con.clone()));
|
|
}
|
|
}
|
|
// removing one cover per frame is good enough
|
|
if let Some(index) = cover_to_remove {
|
|
self.covers.remove(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct PlayPauseToggle {
|
|
config: GuiElemCfg,
|
|
children: Vec<GuiElem>,
|
|
playing_target: bool,
|
|
playing_waiting_for_change: bool,
|
|
}
|
|
impl PlayPauseToggle {
|
|
/// automatically adds w_mouse to config
|
|
pub fn new(config: GuiElemCfg, playing: bool) -> Self {
|
|
Self {
|
|
config: config.w_mouse(),
|
|
children: vec![],
|
|
playing_target: playing,
|
|
playing_waiting_for_change: false,
|
|
}
|
|
}
|
|
}
|
|
impl GuiElemTrait for PlayPauseToggle {
|
|
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) {
|
|
if self.playing_waiting_for_change {
|
|
if info.database.playing == self.playing_target {
|
|
self.playing_waiting_for_change = false;
|
|
}
|
|
} else {
|
|
// not waiting for change, update if the value changes
|
|
self.playing_target = info.database.playing;
|
|
}
|
|
let pos = if info.pos.width() > info.pos.height() {
|
|
let a = 0.5 * info.pos.height();
|
|
let m = 0.5 * (info.pos.top_left().x + info.pos.bottom_right().x);
|
|
Rectangle::new(
|
|
Vec2::new(m - a, info.pos.top_left().y),
|
|
Vec2::new(m + a, info.pos.bottom_right().y),
|
|
)
|
|
} else {
|
|
let a = 0.5 * info.pos.width();
|
|
let m = 0.5 * (info.pos.top_left().y + info.pos.bottom_right().y);
|
|
Rectangle::new(
|
|
Vec2::new(info.pos.top_left().x, m - a),
|
|
Vec2::new(info.pos.bottom_right().x, m + a),
|
|
)
|
|
};
|
|
if self.playing_target {
|
|
g.draw_triangle(
|
|
[
|
|
adjust_pos(&pos, &Vec2::new(0.25, 0.25)),
|
|
adjust_pos(&pos, &Vec2::new(0.75, 0.5)),
|
|
adjust_pos(&pos, &Vec2::new(0.25, 0.75)),
|
|
],
|
|
if self.playing_waiting_for_change {
|
|
Color::GRAY
|
|
} else {
|
|
Color::GREEN
|
|
},
|
|
)
|
|
} else {
|
|
g.draw_rectangle(
|
|
adjust_area(&pos, &Rectangle::from_tuples((0.25, 0.25), (0.75, 0.75))),
|
|
if self.playing_waiting_for_change {
|
|
Color::RED
|
|
} else {
|
|
Color::GRAY
|
|
},
|
|
);
|
|
}
|
|
}
|
|
fn mouse_pressed(&mut self, button: MouseButton) -> Vec<GuiAction> {
|
|
match button {
|
|
MouseButton::Left => {
|
|
if !self.playing_waiting_for_change {
|
|
self.playing_target = !self.playing_target;
|
|
self.playing_waiting_for_change = true;
|
|
vec![GuiAction::SendToServer(if self.playing_target {
|
|
Command::Resume
|
|
} else {
|
|
Command::Pause
|
|
})]
|
|
} else {
|
|
vec![]
|
|
}
|
|
}
|
|
MouseButton::Right => vec![GuiAction::SendToServer(Command::NextSong)],
|
|
_ => vec![],
|
|
}
|
|
}
|
|
}
|