island: transport buttons answer at once
- play/pause flips its icon (card and pill) the moment it's clicked; the clicked state is shown until the player has answered - after play/pause, next or previous, Media is read again at 150 and 600 ms instead of waiting for its once-a-second poll, so title, time and play state follow within a fraction of a second - the cover loader checks 4 times a second instead of once - ipc: island play-pause, the same path as a click (keybinds, tests)
This commit is contained in:
@@ -50,6 +50,7 @@ The installed CLI on the current desktop is older than the Amane docs: it doesn'
|
||||
```sh
|
||||
amane ipc call island toggle # also: open, close
|
||||
amane ipc call island next # show the next player
|
||||
amane ipc call island play-pause # play/pause the shown player; the icon flips at once
|
||||
amane ipc call island menu output # open a dropdown: output | player | close
|
||||
amane ipc call island preset Rock # apply an equalizer preset to the shown player
|
||||
amane ipc call island output <sink> # move the shown player to a sink (pactl sink name)
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ impl Service for Art {
|
||||
*Self::write() = Self { url, path };
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
// 4 checks a second: after next/previous the new cover follows the title quickly
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+74
-9
@@ -152,6 +152,9 @@ pub struct Island {
|
||||
*/
|
||||
opened_on: Option<String>,
|
||||
pointer_on: Option<String>,
|
||||
|
||||
// a clicked play/pause, (player, playing), shown until the player has answered
|
||||
playing_now: Option<(String, bool)>,
|
||||
}
|
||||
|
||||
impl Service for Island {
|
||||
@@ -166,6 +169,7 @@ impl Service for Island {
|
||||
hover: HashMap::new(),
|
||||
opened_on: None,
|
||||
pointer_on: None,
|
||||
playing_now: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +305,7 @@ pub fn view(monitor: &Monitor) -> LayerWindow {
|
||||
|
||||
veiled(card(&scene), width, height, radius, reveal, &colors)
|
||||
} else if pill > 0.0 {
|
||||
veiled(compact(player, &art.path, levels), width, height, radius, pill, &colors)
|
||||
veiled(compact(player, playing(player, &island), &art.path, levels), width, height, radius, pill, &colors)
|
||||
} else {
|
||||
// mid-grow: the empty shape alone, the cheapest frames of the animation
|
||||
Rectangle::new().width(0.0).height(0.0)
|
||||
@@ -424,7 +428,7 @@ fn toggle_menu(menu: Menu) {
|
||||
|
||||
/*
|
||||
* `amane ipc call island ...` for keybinds (and tests):
|
||||
* open | close | toggle | next | menu output|player|close | preset <name> | output <sink name> | status
|
||||
* open | close | toggle | next | play-pause | menu output|player|close | preset <name> | output <sink name> | status
|
||||
*/
|
||||
pub fn command(arguments: &[String]) -> String {
|
||||
let words: Vec<&str> = arguments.iter().map(String::as_str).collect();
|
||||
@@ -442,6 +446,18 @@ pub fn command(arguments: &[String]) -> String {
|
||||
if expanded { collapse() } else { expand() }
|
||||
}
|
||||
["next"] => next_player(),
|
||||
["play-pause"] => {
|
||||
// copied out, so both locks are let go before play_pause writes the island
|
||||
let player = shown(&Media::read(), selected.as_deref()).cloned();
|
||||
|
||||
let Some(player) = player else {
|
||||
return String::from("no player");
|
||||
};
|
||||
|
||||
let before = playing(&player, &Island::read());
|
||||
|
||||
play_pause(&player, before);
|
||||
}
|
||||
["menu", which] => {
|
||||
let menu = match *which {
|
||||
"output" => Some(Menu::Output),
|
||||
@@ -474,7 +490,7 @@ pub fn command(arguments: &[String]) -> String {
|
||||
|
||||
return format!("player={identity} expanded={} menu={}", island.expanded, island.menu.is_some());
|
||||
}
|
||||
_ => return String::from("usage: island open|close|toggle|next|menu <output|player|close>|preset <name>|output <sink>|status"),
|
||||
_ => return String::from("usage: island open|close|toggle|next|play-pause|menu <output|player|close>|preset <name>|output <sink>|status"),
|
||||
}
|
||||
|
||||
String::from("ok")
|
||||
@@ -567,7 +583,7 @@ fn faded(color: Color, alpha: u8) -> Color {
|
||||
}
|
||||
|
||||
// the pill: cover, title, the player's live levels
|
||||
fn compact(player: &MediaPlayer, art: &str, levels: &[f32]) -> Rectangle {
|
||||
fn compact(player: &MediaPlayer, playing: bool, art: &str, levels: &[f32]) -> Rectangle {
|
||||
Rectangle::new()
|
||||
.width(COMPACT_W)
|
||||
.height(COMPACT_H)
|
||||
@@ -580,7 +596,7 @@ fn compact(player: &MediaPlayer, art: &str, levels: &[f32]) -> Rectangle {
|
||||
.height(COMPACT_H)
|
||||
.align_child(Start, Center)
|
||||
.child(Text::new(title(player)).size(12.0).color(TEXT).elide()),
|
||||
pill_levels(player.playing(), levels),
|
||||
pill_levels(playing, levels),
|
||||
])
|
||||
.gap(10.0)
|
||||
.align(Center),
|
||||
@@ -906,12 +922,14 @@ fn controls(player: &MediaPlayer, island: &Island) -> Row {
|
||||
let toggle = player.clone();
|
||||
let next = player.clone();
|
||||
|
||||
let shown = playing(player, island);
|
||||
|
||||
// hovered, the transport icons go bold: thicker strokes, nothing else
|
||||
let lit = island.lit(Spot::PlayPause);
|
||||
|
||||
let middle = Rectangle::new().width(CONTROLS_H).height(CONTROLS_H).align_child(Center, Center);
|
||||
|
||||
let middle = if player.playing() {
|
||||
let middle = if shown {
|
||||
let bar = || Rectangle::new().width(10.0 + BOLD * lit).height(30.0 + BOLD * lit).radius(3.0).fill(PINK);
|
||||
|
||||
middle.child(Row::new(children![bar(), bar()]).gap(7.0 - BOLD * lit))
|
||||
@@ -920,9 +938,15 @@ fn controls(player: &MediaPlayer, island: &Island) -> Row {
|
||||
};
|
||||
|
||||
Row::new(children![
|
||||
button(Glyph::Previous, island.lit(Spot::Previous)).on_hover(hovering(Spot::Previous)).on_click(move |_| previous.previous()),
|
||||
middle.cursor(Pointer).on_hover(hovering(Spot::PlayPause)).on_click(move |_| toggle.play_pause()),
|
||||
button(Glyph::Next, island.lit(Spot::Next)).on_hover(hovering(Spot::Next)).on_click(move |_| next.next()),
|
||||
button(Glyph::Previous, island.lit(Spot::Previous)).on_hover(hovering(Spot::Previous)).on_click(move |_| {
|
||||
previous.previous();
|
||||
refresh_soon(None);
|
||||
}),
|
||||
middle.cursor(Pointer).on_hover(hovering(Spot::PlayPause)).on_click(move |_| play_pause(&toggle, shown)),
|
||||
button(Glyph::Next, island.lit(Spot::Next)).on_hover(hovering(Spot::Next)).on_click(move |_| {
|
||||
next.next();
|
||||
refresh_soon(None);
|
||||
}),
|
||||
])
|
||||
.gap(26.0)
|
||||
.align(Center)
|
||||
@@ -931,6 +955,47 @@ fn controls(player: &MediaPlayer, island: &Island) -> Row {
|
||||
.height(CONTROLS_H)
|
||||
}
|
||||
|
||||
// whether to show `player` as playing: a just-clicked play/pause wins until the player has answered
|
||||
fn playing(player: &MediaPlayer, island: &Island) -> bool {
|
||||
match &island.playing_now {
|
||||
Some((name, playing)) if name == player.name() => *playing,
|
||||
_ => player.playing(),
|
||||
}
|
||||
}
|
||||
|
||||
// the icon flips now, the player confirms a moment later; `shown` is what the island shows before
|
||||
fn play_pause(player: &MediaPlayer, shown: bool) {
|
||||
let flipped = (player.name().to_string(), !shown);
|
||||
|
||||
Island::write().playing_now = Some(flipped.clone());
|
||||
|
||||
player.play_pause();
|
||||
refresh_soon(Some(flipped));
|
||||
}
|
||||
|
||||
/*
|
||||
* Media polls the players once a second, so a click would show up to a second late:
|
||||
* read them again right after the player has had time to answer, then let the real
|
||||
* state replace the clicked one (unless another click has replaced it since)
|
||||
*/
|
||||
fn refresh_soon(clicked: Option<(String, bool)>) {
|
||||
thread::spawn(move || {
|
||||
for wait in [150, 450] {
|
||||
thread::sleep(Duration::from_millis(wait));
|
||||
|
||||
let _ = Media::write().update();
|
||||
}
|
||||
|
||||
if clicked.is_some() {
|
||||
let mut island = Island::write();
|
||||
|
||||
if island.playing_now == clicked {
|
||||
island.playing_now = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// a transport icon button, going bold as `lit` rises
|
||||
fn button(glyph: Glyph, lit: f32) -> Rectangle {
|
||||
Rectangle::new()
|
||||
|
||||
Reference in New Issue
Block a user