Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ pub struct MediaPlayerModuleConfig {
pub max_text_length: u32,
pub indicator_visualizer: Option<MediaPlayerVisualizer>,
pub menu_visualizer: bool,
pub indicator_controls: bool,
}

impl Default for MediaPlayerModuleConfig {
Expand All @@ -760,6 +761,7 @@ impl Default for MediaPlayerModuleConfig {
max_text_length: 100,
indicator_visualizer: None,
menu_visualizer: false,
indicator_controls: false,
}
}
}
Expand Down
56 changes: 54 additions & 2 deletions src/modules/media_player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ pub enum Message {
PlayPause(String),
Next(String),
SetVolume(String, f64),
ActivePrev,
ActivePlayPause,
ActiveNext,
ActiveVolumeUp,
ActiveVolumeDown,
Event(ServiceEvent<MprisPlayerService>),
ConfigReloaded(MediaPlayerModuleConfig),
Bars(Vec<f32>),
Expand All @@ -161,9 +166,10 @@ pub enum Action {
}

pub struct MediaPlayer {
config: MediaPlayerModuleConfig,
pub config: MediaPlayerModuleConfig,
service: Option<MprisPlayerService>,
bars: Vec<f32>,
last_active: Option<String>,
}

impl MediaPlayer {
Expand All @@ -172,23 +178,40 @@ impl MediaPlayer {
config,
service: None,
bars: Vec::new(),
last_active: None,
}
}

/// The player to represent in the bar: the one currently playing, or the
/// first known player when nothing is playing.
/// last one that was playing, or the first known player when nothing is
/// playing.
fn active_player(&self) -> Option<&MprisPlayerData> {
let players = self.service.as_ref()?.players();
players
.iter()
.find(|p| p.state == PlaybackStatus::Playing)
.or_else(|| {
players
.iter()
.find(|p| Some(p.service.as_str()) == self.last_active.as_deref())
})
.or_else(|| players.first())
}

fn is_playing(&self) -> bool {
self.active_player().map(|p| p.state) == Some(PlaybackStatus::Playing)
}

fn refresh_last_active(&mut self) {
if let Some(playing) = self.service.as_ref().and_then(|s| {
s.players()
.iter()
.find(|p| p.state == PlaybackStatus::Playing)
}) {
self.last_active = Some(playing.service.clone());
}
}

fn visualizer_enabled(&self) -> bool {
self.config.indicator_visualizer.is_some() || self.config.menu_visualizer
}
Expand All @@ -203,15 +226,22 @@ impl MediaPlayer {
Message::SetVolume(s, v) => {
Action::Command(self.handle_command(s, PlayerCommand::Volume(v)))
}
Message::ActivePrev => self.active_command(PlayerCommand::Prev),
Message::ActivePlayPause => self.active_command(PlayerCommand::PlayPause),
Message::ActiveNext => self.active_command(PlayerCommand::Next),
Message::ActiveVolumeUp => self.active_volume_command(5.0),
Message::ActiveVolumeDown => self.active_volume_command(-5.0),
Message::Event(event) => match event {
ServiceEvent::Init(s) => {
self.service = Some(s);
self.refresh_last_active();
Action::None
}
ServiceEvent::Update(d) => {
if let Some(service) = self.service.as_mut() {
service.update(d);
}
self.refresh_last_active();
if !self.is_playing() {
self.bars.clear();
}
Expand Down Expand Up @@ -422,6 +452,28 @@ impl MediaPlayer {
}
}

fn active_command(&mut self, command: PlayerCommand) -> Action {
match self.active_player().map(|p| p.service.clone()) {
Some(service) => Action::Command(self.handle_command(service, command)),
None => Action::None,
}
}

fn active_volume_command(&mut self, delta: f64) -> Action {
match self.active_player() {
Some(p) => match p.volume {
Some(v) => {
let new_volume = (v + delta).clamp(0.0, 100.0);
Action::Command(
self.handle_command(p.service.clone(), PlayerCommand::Volume(new_volume)),
)
}
None => Action::None,
},
None => Action::None,
}
}

fn field_value(metadata: &MprisPlayerMetadata, field: MediaPlayerTextField) -> Option<String> {
match field {
MediaPlayerTextField::Artist => {
Expand Down
30 changes: 24 additions & 6 deletions src/modules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,12 +308,30 @@ impl App {
.privacy
.view()
.map(|view| (view.map(Message::Privacy), None)),
ModuleName::MediaPlayer => self.media_player.view().map(|view| {
(
view.map(Message::MediaPlayer),
Some(OnModulePress::ToggleMenu(MenuType::MediaPlayer)),
)
}),
ModuleName::MediaPlayer => {
let on_press = if self.media_player.config.indicator_controls {
OnModulePress::CustomAction {
on_press: Box::new(Message::MediaPlayer(media_player::Message::ActivePrev)),
on_right_press: Some(Box::new(Message::MediaPlayer(
media_player::Message::ActiveNext,
))),
on_middle_press: Some(Box::new(Message::MediaPlayer(
media_player::Message::ActivePlayPause,
))),
on_scroll_up: Some(Box::new(Message::MediaPlayer(
media_player::Message::ActiveVolumeUp,
))),
on_scroll_down: Some(Box::new(Message::MediaPlayer(
media_player::Message::ActiveVolumeDown,
))),
}
} else {
OnModulePress::ToggleMenu(MenuType::MediaPlayer)
};
self.media_player
.view()
.map(|view| (view.map(Message::MediaPlayer), Some(on_press)))
}
ModuleName::Settings => Some((
self.settings.view(id).map(Message::Settings),
Some(OnModulePress::ToggleMenu(MenuType::Settings)),
Expand Down
1 change: 1 addition & 0 deletions website/docs/configuration/full_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ alert_threshold = 85
# max_text_length = 100 # (default)
# indicator_visualizer = "Background" # (default: None = disabled), "Before", or "After"
# menu_visualizer = false # (default) bars behind the menu cards
# indicator_controls = false # (default) direct playback controls on the bar indicator

[tray]
# blocklist = ["regex"] # (default: []) hide tray items matching regex patterns
Expand Down
22 changes: 22 additions & 0 deletions website/docs/configuration/modules/media_player.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ and `cava` is started on demand and stopped again while playback is paused.
> **Requires** `cava` to be installed and available on your `$PATH`. If `cava`
> is missing the visualizer stays hidden.

### Indicator Controls

By default, clicking the indicator opens the media menu. Set
`indicator_controls = true` to control playback directly from the bar without
opening the menu:

| Input | Action |
| ----------- | ------------------------------- |
| Left click | Previous track |
| Middle click | Play/Pause |
| Right click | Next track |
| Scroll up/down | Volume up/down (5% steps) |

Controls target the currently playing player, or the last player that was
playing when nothing is playing. While this option is enabled the media menu
cannot be opened from the bar.

```toml
[media_player]
indicator_controls = true
```

## Menu

The menu shows all active media players with playback controls:
Expand Down