mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 18:51:04 +02:00
parent
bd90167f4e
commit
12053f111a
5 changed files with 284 additions and 103 deletions
|
@ -128,8 +128,6 @@ and will be replaced with values from the currently playing track:
|
||||||
| `{track}` | Track number |
|
| `{track}` | Track number |
|
||||||
| `{disc}` | Disc number |
|
| `{disc}` | Disc number |
|
||||||
| `{genre}` | Genre |
|
| `{genre}` | Genre |
|
||||||
| `{duration}` | Duration in `mm:ss` |
|
|
||||||
| `{elapsed}` | Time elapsed in `mm:ss` |
|
|
||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
|
@ -166,7 +164,10 @@ and will be replaced with values from the currently playing track:
|
||||||
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
| `.popup-music .controls .btn-pause` | Pause button inside popup box |
|
||||||
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
| `.popup-music .controls .btn-next` | Next button inside popup box |
|
||||||
| `.popup-music .volume` | Volume container inside popup box |
|
| `.popup-music .volume` | Volume container inside popup box |
|
||||||
| `.popup-music .volume .slider` | Volume slider popup box |
|
| `.popup-music .volume .slider` | Slider inside volume container |
|
||||||
| `.popup-music .volume .icon` | Volume icon label inside popup box |
|
| `.popup-music .volume .icon` | Icon inside volume container |
|
||||||
|
| `.popup-music .progress` | Progress (seek) bar container |
|
||||||
|
| `.popup-music .progress .slider` | Slider inside progress container |
|
||||||
|
| `.popup-music .progress .label` | Duration label inside progress container |
|
||||||
|
|
||||||
For more information on styling, please see the [styling guide](styling-guide).
|
For more information on styling, please see the [styling guide](styling-guide).
|
|
@ -9,9 +9,17 @@ pub mod mpd;
|
||||||
#[cfg(feature = "music+mpris")]
|
#[cfg(feature = "music+mpris")]
|
||||||
pub mod mpris;
|
pub mod mpris;
|
||||||
|
|
||||||
|
pub const TICK_INTERVAL_MS: u64 = 200;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PlayerUpdate {
|
pub enum PlayerUpdate {
|
||||||
|
/// Triggered when the track or player state notably changes,
|
||||||
|
/// such as a new track playing, the player being paused, or a volume change.
|
||||||
Update(Box<Option<Track>>, Status),
|
Update(Box<Option<Track>>, Status),
|
||||||
|
/// Triggered at regular intervals while a track is playing.
|
||||||
|
/// Used to keep track of the progress through the current track.
|
||||||
|
ProgressTick(ProgressTick),
|
||||||
|
/// Triggered when the client disconnects from the player.
|
||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,23 +35,27 @@ pub struct Track {
|
||||||
pub cover_path: Option<String>,
|
pub cover_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub enum PlayerState {
|
pub enum PlayerState {
|
||||||
Playing,
|
Playing,
|
||||||
Paused,
|
Paused,
|
||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Status {
|
pub struct Status {
|
||||||
pub state: PlayerState,
|
pub state: PlayerState,
|
||||||
pub volume_percent: u8,
|
pub volume_percent: Option<u8>,
|
||||||
pub duration: Option<Duration>,
|
|
||||||
pub elapsed: Option<Duration>,
|
|
||||||
pub playlist_position: u32,
|
pub playlist_position: u32,
|
||||||
pub playlist_length: u32,
|
pub playlist_length: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct ProgressTick {
|
||||||
|
pub duration: Option<Duration>,
|
||||||
|
pub elapsed: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait MusicClient {
|
pub trait MusicClient {
|
||||||
fn play(&self) -> Result<()>;
|
fn play(&self) -> Result<()>;
|
||||||
fn pause(&self) -> Result<()>;
|
fn pause(&self) -> Result<()>;
|
||||||
|
@ -51,6 +63,7 @@ pub trait MusicClient {
|
||||||
fn prev(&self) -> Result<()>;
|
fn prev(&self) -> Result<()>;
|
||||||
|
|
||||||
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
fn set_volume_percent(&self, vol: u8) -> Result<()>;
|
||||||
|
fn seek(&self, duration: Duration) -> Result<()>;
|
||||||
|
|
||||||
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
use super::{MusicClient, Status, Track};
|
use super::{
|
||||||
use crate::await_sync;
|
MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
|
||||||
use crate::clients::music::{PlayerState, PlayerUpdate};
|
};
|
||||||
|
use crate::{await_sync, send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
use mpd_client::client::{Connection, ConnectionEvent, Subsystem};
|
||||||
|
use mpd_client::commands::SeekMode;
|
||||||
use mpd_client::protocol::MpdProtocolError;
|
use mpd_client::protocol::MpdProtocolError;
|
||||||
use mpd_client::responses::{PlayState, Song};
|
use mpd_client::responses::{PlayState, Song};
|
||||||
use mpd_client::tag::Tag;
|
use mpd_client::tag::Tag;
|
||||||
|
@ -16,7 +18,7 @@ use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::net::{TcpStream, UnixStream};
|
use tokio::net::{TcpStream, UnixStream};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
use tokio::sync::broadcast::{channel, error::SendError, Receiver, Sender};
|
use tokio::sync::broadcast;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
@ -29,8 +31,8 @@ lazy_static! {
|
||||||
pub struct MpdClient {
|
pub struct MpdClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
music_dir: PathBuf,
|
music_dir: PathBuf,
|
||||||
tx: Sender<PlayerUpdate>,
|
tx: broadcast::Sender<PlayerUpdate>,
|
||||||
_rx: Receiver<PlayerUpdate>,
|
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -57,7 +59,7 @@ impl MpdClient {
|
||||||
let (client, mut state_changes) =
|
let (client, mut state_changes) =
|
||||||
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
wait_for_connection(host, Duration::from_secs(5), None).await?;
|
||||||
|
|
||||||
let (tx, rx) = channel(16);
|
let (tx, rx) = broadcast::channel(16);
|
||||||
|
|
||||||
{
|
{
|
||||||
let music_dir = music_dir.clone();
|
let music_dir = music_dir.clone();
|
||||||
|
@ -78,7 +80,19 @@ impl MpdClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), SendError<(Option<Track>, Status)>>(())
|
Ok::<(), broadcast::error::SendError<(Option<Track>, Status)>>(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let client = client.clone();
|
||||||
|
let tx = tx.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
loop {
|
||||||
|
Self::send_tick_update(&client, &tx).await;
|
||||||
|
sleep(Duration::from_millis(TICK_INTERVAL_MS)).await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,9 +106,9 @@ impl MpdClient {
|
||||||
|
|
||||||
async fn send_update(
|
async fn send_update(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
tx: &Sender<PlayerUpdate>,
|
tx: &broadcast::Sender<PlayerUpdate>,
|
||||||
music_dir: &Path,
|
music_dir: &Path,
|
||||||
) -> Result<(), SendError<PlayerUpdate>> {
|
) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||||
let current_song = client.command(commands::CurrentSong).await;
|
let current_song = client.command(commands::CurrentSong).await;
|
||||||
let status = client.command(commands::Status).await;
|
let status = client.command(commands::Status).await;
|
||||||
|
|
||||||
|
@ -102,17 +116,33 @@ impl MpdClient {
|
||||||
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
let track = current_song.map(|s| Self::convert_song(&s.song, music_dir));
|
||||||
let status = Status::from(status);
|
let status = Status::from(status);
|
||||||
|
|
||||||
tx.send(PlayerUpdate::Update(Box::new(track), status))?;
|
let update = PlayerUpdate::Update(Box::new(track), status);
|
||||||
|
send!(tx, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_tick_update(client: &Client, tx: &broadcast::Sender<PlayerUpdate>) {
|
||||||
|
let status = client.command(commands::Status).await;
|
||||||
|
|
||||||
|
if let Ok(status) = status {
|
||||||
|
if status.state == PlayState::Playing {
|
||||||
|
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||||
|
duration: status.duration,
|
||||||
|
elapsed: status.elapsed,
|
||||||
|
});
|
||||||
|
|
||||||
|
send!(tx, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_connected(&self) -> bool {
|
fn is_connected(&self) -> bool {
|
||||||
!self.client.is_connection_closed()
|
!self.client.is_connection_closed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_disconnect_update(&self) -> Result<(), SendError<PlayerUpdate>> {
|
fn send_disconnect_update(&self) -> Result<(), broadcast::error::SendError<PlayerUpdate>> {
|
||||||
info!("Connection to MPD server lost");
|
info!("Connection to MPD server lost");
|
||||||
self.tx.send(PlayerUpdate::Disconnect)?;
|
self.tx.send(PlayerUpdate::Disconnect)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -182,7 +212,12 @@ impl MusicClient for MpdClient {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
fn seek(&self, duration: Duration) -> Result<()> {
|
||||||
|
async_command!(self.client, commands::Seek(SeekMode::Absolute(duration)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||||
let rx = self.tx.subscribe();
|
let rx = self.tx.subscribe();
|
||||||
await_sync(async {
|
await_sync(async {
|
||||||
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
Self::send_update(&self.client, &self.tx, &self.music_dir)
|
||||||
|
@ -291,9 +326,7 @@ impl From<mpd_client::responses::Status> for Status {
|
||||||
fn from(status: mpd_client::responses::Status) -> Self {
|
fn from(status: mpd_client::responses::Status) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: PlayerState::from(status.state),
|
state: PlayerState::from(status.state),
|
||||||
volume_percent: status.volume,
|
volume_percent: Some(status.volume),
|
||||||
duration: status.duration,
|
|
||||||
elapsed: status.elapsed,
|
|
||||||
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
playlist_position: status.current_song.map_or(0, |(pos, _)| pos.0 as u32),
|
||||||
playlist_length: status.playlist_length as u32,
|
playlist_length: status.playlist_length as u32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
use super::{MusicClient, PlayerUpdate, Status, Track};
|
use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
|
||||||
use crate::clients::music::PlayerState;
|
use crate::clients::music::ProgressTick;
|
||||||
use crate::{arc_mut, lock, send};
|
use crate::{arc_mut, lock, send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::string;
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
use std::{cmp, string};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
|
@ -19,13 +19,13 @@ lazy_static! {
|
||||||
|
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
current_player: Arc<Mutex<Option<String>>>,
|
current_player: Arc<Mutex<Option<String>>>,
|
||||||
tx: Sender<PlayerUpdate>,
|
tx: broadcast::Sender<PlayerUpdate>,
|
||||||
_rx: Receiver<PlayerUpdate>,
|
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let (tx, rx) = channel(32);
|
let (tx, rx) = broadcast::channel(32);
|
||||||
|
|
||||||
let current_player = arc_mut!(None);
|
let current_player = arc_mut!(None);
|
||||||
|
|
||||||
|
@ -84,6 +84,20 @@ impl Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let current_player = current_player.clone();
|
||||||
|
let tx = tx.clone();
|
||||||
|
|
||||||
|
spawn_blocking(move || {
|
||||||
|
let player_finder = PlayerFinder::new().expect("to get new player finder");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
Self::send_tick_update(&player_finder, ¤t_player, &tx);
|
||||||
|
sleep(Duration::from_millis(TICK_INTERVAL_MS));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
current_player,
|
current_player,
|
||||||
tx,
|
tx,
|
||||||
|
@ -95,7 +109,7 @@ impl Client {
|
||||||
player_id: String,
|
player_id: String,
|
||||||
players: Arc<Mutex<HashSet<String>>>,
|
players: Arc<Mutex<HashSet<String>>>,
|
||||||
current_player: Arc<Mutex<Option<String>>>,
|
current_player: Arc<Mutex<Option<String>>>,
|
||||||
tx: Sender<PlayerUpdate>,
|
tx: broadcast::Sender<PlayerUpdate>,
|
||||||
) {
|
) {
|
||||||
spawn_blocking(move || {
|
spawn_blocking(move || {
|
||||||
let player_finder = PlayerFinder::new()?;
|
let player_finder = PlayerFinder::new()?;
|
||||||
|
@ -138,7 +152,7 @@ impl Client {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_update(player: &Player, tx: &Sender<PlayerUpdate>) -> Result<()> {
|
fn send_update(player: &Player, tx: &broadcast::Sender<PlayerUpdate>) -> Result<()> {
|
||||||
debug!("Sending update using '{}'", player.identity());
|
debug!("Sending update using '{}'", player.identity());
|
||||||
|
|
||||||
let metadata = player.get_metadata()?;
|
let metadata = player.get_metadata()?;
|
||||||
|
@ -159,8 +173,6 @@ impl Client {
|
||||||
playlist_position: 1,
|
playlist_position: 1,
|
||||||
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
playlist_length: track_list.map(|list| list.len() as u32).unwrap_or(u32::MAX),
|
||||||
state: PlayerState::from(playback_status),
|
state: PlayerState::from(playback_status),
|
||||||
elapsed: player.get_position().ok(),
|
|
||||||
duration: metadata.length(),
|
|
||||||
volume_percent,
|
volume_percent,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,6 +193,26 @@ impl Client {
|
||||||
player_finder.find_by_name(player_name).ok()
|
player_finder.find_by_name(player_name).ok()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn send_tick_update(
|
||||||
|
player_finder: &PlayerFinder,
|
||||||
|
current_player: &Mutex<Option<String>>,
|
||||||
|
tx: &broadcast::Sender<PlayerUpdate>,
|
||||||
|
) {
|
||||||
|
if let Some(player) = lock!(current_player)
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|name| player_finder.find_by_name(name).ok())
|
||||||
|
{
|
||||||
|
if let Ok(metadata) = player.get_metadata() {
|
||||||
|
let update = PlayerUpdate::ProgressTick(ProgressTick {
|
||||||
|
elapsed: player.get_position().ok(),
|
||||||
|
duration: metadata.length(),
|
||||||
|
});
|
||||||
|
|
||||||
|
send!(tx, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! command {
|
macro_rules! command {
|
||||||
|
@ -223,7 +255,23 @@ impl MusicClient for Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_change(&self) -> Receiver<PlayerUpdate> {
|
fn seek(&self, duration: Duration) -> Result<()> {
|
||||||
|
if let Some(player) = Self::get_player(self) {
|
||||||
|
let pos = player.get_position().unwrap_or_default();
|
||||||
|
|
||||||
|
let duration = duration.as_micros() as i64;
|
||||||
|
let position = pos.as_micros() as i64;
|
||||||
|
|
||||||
|
let seek = cmp::max(duration, 0) - position;
|
||||||
|
|
||||||
|
player.seek(seek)?;
|
||||||
|
} else {
|
||||||
|
error!("Could not find player");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscribe_change(&self) -> broadcast::Receiver<PlayerUpdate> {
|
||||||
debug!("Creating new subscription");
|
debug!("Creating new subscription");
|
||||||
let rx = self.tx.subscribe();
|
let rx = self.tx.subscribe();
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
use crate::clients::music::{self, MusicClient, PlayerState, PlayerUpdate, Status, Track};
|
use crate::clients::music::{
|
||||||
|
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
|
||||||
|
};
|
||||||
use crate::gtk_helpers::add_class;
|
use crate::gtk_helpers::add_class;
|
||||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
use crate::popup::Popup;
|
use crate::popup::Popup;
|
||||||
use crate::{send_async, try_send};
|
use crate::{send_async, try_send};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::Continue;
|
use glib::{Continue, PropertySet};
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
@ -28,6 +31,7 @@ pub enum PlayerCommand {
|
||||||
Pause,
|
Pause,
|
||||||
Next,
|
Next,
|
||||||
Volume(u8),
|
Volume(u8),
|
||||||
|
Seek(Duration),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a duration given in seconds
|
/// Formats a duration given in seconds
|
||||||
|
@ -47,6 +51,12 @@ fn get_tokens(re: &Regex, format_string: &str) -> Vec<String> {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ControllerEvent {
|
||||||
|
Update(Option<SongUpdate>),
|
||||||
|
UpdateProgress(ProgressTick),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SongUpdate {
|
pub struct SongUpdate {
|
||||||
song: Track,
|
song: Track,
|
||||||
|
@ -67,7 +77,7 @@ async fn get_client(
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Module<Button> for MusicModule {
|
impl Module<Button> for MusicModule {
|
||||||
type SendMessage = Option<SongUpdate>;
|
type SendMessage = ControllerEvent;
|
||||||
type ReceiveMessage = PlayerCommand;
|
type ReceiveMessage = PlayerCommand;
|
||||||
|
|
||||||
fn name() -> &'static str {
|
fn name() -> &'static str {
|
||||||
|
@ -103,7 +113,7 @@ impl Module<Button> for MusicModule {
|
||||||
PlayerUpdate::Update(track, status) => match *track {
|
PlayerUpdate::Update(track, status) => match *track {
|
||||||
Some(track) => {
|
Some(track) => {
|
||||||
let display_string =
|
let display_string =
|
||||||
replace_tokens(format.as_str(), &tokens, &track, &status);
|
replace_tokens(format.as_str(), &tokens, &track);
|
||||||
|
|
||||||
let update = SongUpdate {
|
let update = SongUpdate {
|
||||||
song: track,
|
song: track,
|
||||||
|
@ -111,10 +121,24 @@ impl Module<Button> for MusicModule {
|
||||||
display_string,
|
display_string,
|
||||||
};
|
};
|
||||||
|
|
||||||
send_async!(tx, ModuleUpdateEvent::Update(Some(update)));
|
send_async!(
|
||||||
|
tx,
|
||||||
|
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
|
||||||
|
update
|
||||||
|
)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
None => send_async!(tx, ModuleUpdateEvent::Update(None)),
|
None => send_async!(
|
||||||
|
tx,
|
||||||
|
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
|
||||||
|
tx,
|
||||||
|
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
|
||||||
|
progress_tick
|
||||||
|
))
|
||||||
|
),
|
||||||
PlayerUpdate::Disconnect => break,
|
PlayerUpdate::Disconnect => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,6 +161,7 @@ impl Module<Button> for MusicModule {
|
||||||
PlayerCommand::Pause => client.pause(),
|
PlayerCommand::Pause => client.pause(),
|
||||||
PlayerCommand::Next => client.next(),
|
PlayerCommand::Next => client.next(),
|
||||||
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
|
PlayerCommand::Volume(vol) => client.set_volume_percent(vol),
|
||||||
|
PlayerCommand::Seek(duration) => client.seek(duration),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
|
@ -191,7 +216,9 @@ impl Module<Button> for MusicModule {
|
||||||
let button = button.clone();
|
let button = button.clone();
|
||||||
let tx = context.tx.clone();
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
context.widget_rx.attach(None, move |mut event| {
|
context.widget_rx.attach(None, move |event| {
|
||||||
|
let ControllerEvent::Update(mut event) = event else { return Continue(true) };
|
||||||
|
|
||||||
if let Some(event) = event.take() {
|
if let Some(event) = event.take() {
|
||||||
label.set_label(&event.display_string);
|
label.set_label(&event.display_string);
|
||||||
|
|
||||||
|
@ -241,7 +268,8 @@ impl Module<Button> for MusicModule {
|
||||||
) -> Option<gtk::Box> {
|
) -> Option<gtk::Box> {
|
||||||
let icon_theme = info.icon_theme;
|
let icon_theme = info.icon_theme;
|
||||||
|
|
||||||
let container = gtk::Box::new(Orientation::Horizontal, 10);
|
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||||
|
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||||
|
|
||||||
let album_image = gtk::Image::builder()
|
let album_image = gtk::Image::builder()
|
||||||
.width_request(128)
|
.width_request(128)
|
||||||
|
@ -299,9 +327,10 @@ impl Module<Button> for MusicModule {
|
||||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||||
|
|
||||||
container.add(&album_image);
|
main_container.add(&album_image);
|
||||||
container.add(&info_box);
|
main_container.add(&info_box);
|
||||||
container.add(&volume_box);
|
main_container.add(&volume_box);
|
||||||
|
container.add(&main_container);
|
||||||
|
|
||||||
let tx_prev = tx.clone();
|
let tx_prev = tx.clone();
|
||||||
btn_prev.connect_clicked(move |_| {
|
btn_prev.connect_clicked(move |_| {
|
||||||
|
@ -323,12 +352,49 @@ impl Module<Button> for MusicModule {
|
||||||
try_send!(tx_next, PlayerCommand::Next);
|
try_send!(tx_next, PlayerCommand::Next);
|
||||||
});
|
});
|
||||||
|
|
||||||
let tx_vol = tx;
|
let tx_vol = tx.clone();
|
||||||
volume_slider.connect_change_value(move |_, _, val| {
|
volume_slider.connect_change_value(move |_, _, val| {
|
||||||
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
||||||
Inhibit(false)
|
Inhibit(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let progress_box = gtk::Box::new(Orientation::Horizontal, 5);
|
||||||
|
add_class(&progress_box, "progress");
|
||||||
|
|
||||||
|
let progress_label = Label::new(None);
|
||||||
|
add_class(&progress_label, "label");
|
||||||
|
|
||||||
|
let progress = Scale::builder()
|
||||||
|
.orientation(Orientation::Horizontal)
|
||||||
|
.draw_value(false)
|
||||||
|
.hexpand(true)
|
||||||
|
.build();
|
||||||
|
add_class(&progress, "slider");
|
||||||
|
|
||||||
|
progress_box.add(&progress);
|
||||||
|
progress_box.add(&progress_label);
|
||||||
|
container.add(&progress_box);
|
||||||
|
|
||||||
|
let drag_lock = Arc::new(AtomicBool::new(false));
|
||||||
|
{
|
||||||
|
let drag_lock = drag_lock.clone();
|
||||||
|
progress.connect_button_press_event(move |_, _| {
|
||||||
|
drag_lock.set(true);
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let drag_lock = drag_lock.clone();
|
||||||
|
progress.connect_button_release_event(move |scale, _| {
|
||||||
|
let value = scale.value();
|
||||||
|
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
|
||||||
|
|
||||||
|
drag_lock.set(false);
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
container.show_all();
|
container.show_all();
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -336,25 +402,26 @@ impl Module<Button> for MusicModule {
|
||||||
let image_size = self.cover_image_size;
|
let image_size = self.cover_image_size;
|
||||||
|
|
||||||
let mut prev_cover = None;
|
let mut prev_cover = None;
|
||||||
rx.attach(None, move |update| {
|
rx.attach(None, move |event| {
|
||||||
if let Some(update) = update {
|
match event {
|
||||||
// only update art when album changes
|
ControllerEvent::Update(Some(update)) => {
|
||||||
let new_cover = update.song.cover_path;
|
// only update art when album changes
|
||||||
if prev_cover != new_cover {
|
let new_cover = update.song.cover_path;
|
||||||
prev_cover = new_cover.clone();
|
if prev_cover != new_cover {
|
||||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
prev_cover = new_cover.clone();
|
||||||
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||||
}) {
|
ImageProvider::parse(&cover_path, &icon_theme, image_size)
|
||||||
image.load_into_image(album_image.clone())
|
}) {
|
||||||
} else {
|
image.load_into_image(album_image.clone())
|
||||||
album_image.set_from_pixbuf(None);
|
} else {
|
||||||
Ok(())
|
album_image.set_from_pixbuf(None);
|
||||||
};
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
error!("{err:?}");
|
error!("{err:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
title_label
|
title_label
|
||||||
.label
|
.label
|
||||||
|
@ -366,38 +433,64 @@ impl Module<Button> for MusicModule {
|
||||||
.label
|
.label
|
||||||
.set_text(&update.song.artist.unwrap_or_default());
|
.set_text(&update.song.artist.unwrap_or_default());
|
||||||
|
|
||||||
match update.status.state {
|
match update.status.state {
|
||||||
PlayerState::Stopped => {
|
PlayerState::Stopped => {
|
||||||
btn_pause.hide();
|
btn_pause.hide();
|
||||||
btn_play.show();
|
btn_play.show();
|
||||||
btn_play.set_sensitive(false);
|
btn_play.set_sensitive(false);
|
||||||
}
|
}
|
||||||
PlayerState::Playing => {
|
PlayerState::Playing => {
|
||||||
btn_play.set_sensitive(false);
|
btn_play.set_sensitive(false);
|
||||||
btn_play.hide();
|
btn_play.hide();
|
||||||
|
|
||||||
btn_pause.set_sensitive(true);
|
btn_pause.set_sensitive(true);
|
||||||
btn_pause.show();
|
btn_pause.show();
|
||||||
}
|
}
|
||||||
PlayerState::Paused => {
|
PlayerState::Paused => {
|
||||||
btn_pause.set_sensitive(false);
|
btn_pause.set_sensitive(false);
|
||||||
btn_pause.hide();
|
btn_pause.hide();
|
||||||
|
|
||||||
btn_play.set_sensitive(true);
|
btn_play.set_sensitive(true);
|
||||||
btn_play.show();
|
btn_play.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let enable_prev = update.status.playlist_position > 0;
|
||||||
|
|
||||||
|
let enable_next =
|
||||||
|
update.status.playlist_position < update.status.playlist_length;
|
||||||
|
|
||||||
|
btn_prev.set_sensitive(enable_prev);
|
||||||
|
btn_next.set_sensitive(enable_next);
|
||||||
|
|
||||||
|
if let Some(volume) = update.status.volume_percent {
|
||||||
|
volume_slider.set_value(volume as f64);
|
||||||
|
volume_box.show();
|
||||||
|
} else {
|
||||||
|
volume_box.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ControllerEvent::UpdateProgress(progress_tick)
|
||||||
|
if !drag_lock.load(Ordering::Relaxed) =>
|
||||||
|
{
|
||||||
|
if let (Some(elapsed), Some(duration)) =
|
||||||
|
(progress_tick.elapsed, progress_tick.duration)
|
||||||
|
{
|
||||||
|
progress_label.set_label(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
format_time(elapsed),
|
||||||
|
format_time(duration)
|
||||||
|
));
|
||||||
|
|
||||||
let enable_prev = update.status.playlist_position > 0;
|
progress.set_value(elapsed.as_secs_f64());
|
||||||
|
progress.set_range(0.0, duration.as_secs_f64());
|
||||||
let enable_next =
|
progress_box.show_all();
|
||||||
update.status.playlist_position < update.status.playlist_length;
|
} else {
|
||||||
|
progress_box.hide();
|
||||||
btn_prev.set_sensitive(enable_prev);
|
}
|
||||||
btn_next.set_sensitive(enable_next);
|
}
|
||||||
|
_ => {}
|
||||||
volume_slider.set_value(update.status.volume_percent as f64);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
});
|
});
|
||||||
|
@ -409,15 +502,10 @@ impl Module<Button> for MusicModule {
|
||||||
|
|
||||||
/// Replaces each of the formatting tokens in the formatting string
|
/// Replaces each of the formatting tokens in the formatting string
|
||||||
/// with actual data pulled from the music player
|
/// with actual data pulled from the music player
|
||||||
fn replace_tokens(
|
fn replace_tokens(format_string: &str, tokens: &Vec<String>, song: &Track) -> String {
|
||||||
format_string: &str,
|
|
||||||
tokens: &Vec<String>,
|
|
||||||
song: &Track,
|
|
||||||
status: &Status,
|
|
||||||
) -> String {
|
|
||||||
let mut compiled_string = format_string.to_string();
|
let mut compiled_string = format_string.to_string();
|
||||||
for token in tokens {
|
for token in tokens {
|
||||||
let value = get_token_value(song, status, token);
|
let value = get_token_value(song, token);
|
||||||
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
|
compiled_string = compiled_string.replace(format!("{{{token}}}").as_str(), value.as_str());
|
||||||
}
|
}
|
||||||
compiled_string
|
compiled_string
|
||||||
|
@ -425,7 +513,7 @@ fn replace_tokens(
|
||||||
|
|
||||||
/// Converts a string format token value
|
/// Converts a string format token value
|
||||||
/// into its respective value.
|
/// into its respective value.
|
||||||
fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
|
fn get_token_value(song: &Track, token: &str) -> String {
|
||||||
match token {
|
match token {
|
||||||
"title" => song.title.clone(),
|
"title" => song.title.clone(),
|
||||||
"album" => song.album.clone(),
|
"album" => song.album.clone(),
|
||||||
|
@ -434,8 +522,6 @@ fn get_token_value(song: &Track, status: &Status, token: &str) -> String {
|
||||||
"disc" => song.disc.map(|x| x.to_string()),
|
"disc" => song.disc.map(|x| x.to_string()),
|
||||||
"genre" => song.genre.clone(),
|
"genre" => song.genre.clone(),
|
||||||
"track" => song.track.map(|x| x.to_string()),
|
"track" => song.track.map(|x| x.to_string()),
|
||||||
"duration" => status.duration.map(format_time),
|
|
||||||
"elapsed" => status.elapsed.map(format_time),
|
|
||||||
_ => Some(token.to_string()),
|
_ => Some(token.to_string()),
|
||||||
}
|
}
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue