feat(hotkeys): Spotify-parity playback actions
Eight Playback actions the table never had, on Spotify's desktop defaults: volume up/down (Ctrl+Up/Down), mute (M), shuffle (Ctrl+S), repeat (Ctrl+R), like (Alt+Shift+B), and a surface-independent seek (Ctrl+Shift+Left/Right). Each reuses an existing transport seam, and the immersive bare-arrow seek rows are untouched. Volume honours the player bars' volLocked predicate, so the hotkey cannot move the slider on an ALSA-Direct hw path where the engine documents the write as a no-op. Like rides a QML seam, as nav.search does, because the track id is a QbzPlayer property rather than part of the Rust now-playing model. Two new tests; labels added to all eight catalogs.
This commit is contained in:
12 files changed
+378
-5
No files matched your search
@@ -6036,3 +6036,29 @@ msgstr "Dieses Album enthält keine Titel."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Reaktive Ringe"
|
msgstr "Reaktive Ringe"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Lauter"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Leiser"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Stumm / Ton an"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Zufallswiedergabe umschalten"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Wiederholmodus wechseln"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Titel liken"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Vorspulen"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Zurückspulen"
|
||||||
@@ -1641,3 +1641,29 @@ msgstr "This album has no tracks."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Reactive Rings"
|
msgstr "Reactive Rings"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Volume Up"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Volume Down"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Mute / Unmute"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Toggle Shuffle"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Cycle Repeat"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Like Track"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Seek Forward"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Seek Back"
|
||||||
@@ -6042,3 +6042,29 @@ msgstr "Este álbum no tiene pistas."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Anillos reactivos"
|
msgstr "Anillos reactivos"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Subir Volumen"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Bajar Volumen"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Silenciar / Activar Sonido"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Alternar Aleatorio"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Cambiar Repetición"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Me Gusta"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Avanzar"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Retroceder"
|
||||||
@@ -6034,3 +6034,29 @@ msgstr "Cet album ne contient aucun titre."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Anneaux réactifs"
|
msgstr "Anneaux réactifs"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Augmenter le volume"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Baisser le volume"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Couper / Rétablir le son"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Basculer la lecture aléatoire"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Changer le mode de répétition"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Aimer le titre"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Avancer"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Reculer"
|
||||||
@@ -6018,3 +6018,29 @@ msgstr "このアルバムには曲がありません。"
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "リアクティブリング"
|
msgstr "リアクティブリング"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "音量を上げる"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "音量を下げる"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "ミュート / 解除"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "シャッフル切り替え"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "リピート切り替え"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "お気に入りに追加"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "早送り"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "巻き戻し"
|
||||||
@@ -6024,3 +6024,29 @@ msgstr "Dit album bevat geen nummers."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Reactieve ringen"
|
msgstr "Reactieve ringen"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Volume omhoog"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Volume omlaag"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Dempen / Dempen opheffen"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Shuffle aan/uit"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Herhaling wisselen"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Nummer liken"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Vooruit spoelen"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Terugspoelen"
|
||||||
@@ -6034,3 +6034,29 @@ msgstr "Este álbum não tem faixas."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Anéis reativos"
|
msgstr "Anéis reativos"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Aumentar Volume"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Diminuir Volume"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Silenciar / Reativar Som"
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Alternar Aleatório"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Alternar Repetição"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Curtir Faixa"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Avançar"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Recuar"
|
||||||
@@ -6054,3 +6054,29 @@ msgstr "В этом альбоме нет треков."
|
|||||||
|
|
||||||
msgid "Reactive Rings"
|
msgid "Reactive Rings"
|
||||||
msgstr "Реактивные кольца"
|
msgstr "Реактивные кольца"
|
||||||
|
|
||||||
|
# Hotkeys: the Spotify-parity playback actions
|
||||||
|
|
||||||
|
msgid "Volume Up"
|
||||||
|
msgstr "Громче"
|
||||||
|
|
||||||
|
msgid "Volume Down"
|
||||||
|
msgstr "Тише"
|
||||||
|
|
||||||
|
msgid "Mute / Unmute"
|
||||||
|
msgstr "Звук выкл. / вкл."
|
||||||
|
|
||||||
|
msgid "Toggle Shuffle"
|
||||||
|
msgstr "Перемешивание"
|
||||||
|
|
||||||
|
msgid "Cycle Repeat"
|
||||||
|
msgstr "Режим повтора"
|
||||||
|
|
||||||
|
msgid "Like Track"
|
||||||
|
msgstr "Нравится"
|
||||||
|
|
||||||
|
msgid "Seek Forward"
|
||||||
|
msgstr "Перемотка вперёд"
|
||||||
|
|
||||||
|
msgid "Seek Back"
|
||||||
|
msgstr "Перемотка назад"
|
||||||
@@ -146,6 +146,14 @@ Rectangle {
|
|||||||
Connections {
|
Connections {
|
||||||
target: QbzHotkeys
|
target: QbzHotkeys
|
||||||
function onFocusSearchRequested() { header.focusSearch() }
|
function onFocusSearchRequested() { header.focusSearch() }
|
||||||
|
// playback.favorite — the player bar's own heart call, empty-id guard
|
||||||
|
// included. The now-playing track id is a QbzPlayer property, so the
|
||||||
|
// Rust dispatch signals and QML answers rather than resolving the id
|
||||||
|
// twice (PlayerBar.qml:769-773).
|
||||||
|
function onFavoriteRequested() {
|
||||||
|
var id = QbzPlayer.npTrackId
|
||||||
|
if (id !== "") QbzQueue.queueToggleFavorite("track", id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The §4.6 Ctrl+A + multi-select Escape-exit seam. Selection state is
|
// The §4.6 Ctrl+A + multi-select Escape-exit seam. Selection state is
|
||||||
|
|||||||
@@ -133,6 +133,14 @@ pub mod qbz_hotkeys {
|
|||||||
/// is stale.
|
/// is stale.
|
||||||
#[qsignal]
|
#[qsignal]
|
||||||
fn focus_search_requested(self: Pin<&mut QbzHotkeys>);
|
fn focus_search_requested(self: Pin<&mut QbzHotkeys>);
|
||||||
|
|
||||||
|
/// playback.favorite — the same QML seam as `focus_search_requested`.
|
||||||
|
/// The now-playing track id lives on QbzPlayer, not in the Rust
|
||||||
|
/// now-playing model, so AppShell answers this with the call the
|
||||||
|
/// player bar's own heart makes (`QbzQueue.queueToggleFavorite`),
|
||||||
|
/// empty-id guard included, instead of a second id resolution here.
|
||||||
|
#[qsignal]
|
||||||
|
fn favorite_requested(self: Pin<&mut QbzHotkeys>);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cxx_qt::Threading for QbzHotkeys {}
|
impl cxx_qt::Threading for QbzHotkeys {}
|
||||||
@@ -227,6 +235,28 @@ fn seek_relative(delta: i32) {
|
|||||||
crate::transport_seek(target as f32 / duration as f32);
|
crate::transport_seek(target as f32 / duration as f32);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keyboard volume step — the same 0.05 the player bars' +/- steppers use.
|
||||||
|
const VOLUME_STEP: f32 = 0.05;
|
||||||
|
|
||||||
|
/// The player bars' `volLocked` predicate (PlayerBar.qml:88-94) in Rust: the
|
||||||
|
/// settings-derived ALSA-Direct hardware lock is LIFTED while a peer renderer
|
||||||
|
/// owns playback, and the sink-pushed peer lock stands on its own. Without it
|
||||||
|
/// the hotkey would move the on-screen slider for a change the engine
|
||||||
|
/// documents as a no-op on that path.
|
||||||
|
fn volume_locked() -> bool {
|
||||||
|
let settings_locked =
|
||||||
|
crate::output_labels::volume_locked(&crate::settings_qt::audio_settings());
|
||||||
|
(settings_locked && !crate::now_playing::is_remote())
|
||||||
|
|| crate::now_playing::remote_volume_locked()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nudge_volume(delta: f32) {
|
||||||
|
if volume_locked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crate::transport_set_volume((crate::now_playing::volume() + delta).clamp(0.0, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
impl qbz_hotkeys::QbzHotkeys {
|
impl qbz_hotkeys::QbzHotkeys {
|
||||||
pub fn boot(self: Pin<&mut Self>) {
|
pub fn boot(self: Pin<&mut Self>) {
|
||||||
if QT_THREAD.set(self.qt_thread()).is_err() {
|
if QT_THREAD.set(self.qt_thread()).is_err() {
|
||||||
@@ -351,6 +381,14 @@ impl qbz_hotkeys::QbzHotkeys {
|
|||||||
// here: it lives in `QbzMini::enter`, so this hotkey and the view
|
// here: it lives in `QbzMini::enter`, so this hotkey and the view
|
||||||
// menu's row share ONE predicate (contract §4.10.1).
|
// menu's row share ONE predicate (contract §4.10.1).
|
||||||
"ui.miniPlayer" => crate::mini_bridge::toggle(),
|
"ui.miniPlayer" => crate::mini_bridge::toggle(),
|
||||||
|
"playback.volumeUp" => nudge_volume(VOLUME_STEP),
|
||||||
|
"playback.volumeDown" => nudge_volume(-VOLUME_STEP),
|
||||||
|
"playback.mute" => crate::transport_toggle_mute(),
|
||||||
|
"playback.shuffle" => crate::transport_toggle_shuffle(),
|
||||||
|
"playback.repeat" => crate::transport_cycle_repeat(),
|
||||||
|
"playback.favorite" => self.as_mut().favorite_requested(),
|
||||||
|
"playback.seekForward" => seek_relative(5),
|
||||||
|
"playback.seekBack" => seek_relative(-5),
|
||||||
"focus.seekForward" => seek_relative(5),
|
"focus.seekForward" => seek_relative(5),
|
||||||
"focus.seekBack" => seek_relative(-5),
|
"focus.seekBack" => seek_relative(-5),
|
||||||
"focus.seekForwardLong" => seek_relative(10),
|
"focus.seekForwardLong" => seek_relative(10),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
//! Keyboard / hotkeys layer — the PURE core (2026-08-03 hotkeys-port contract
|
//! Keyboard / hotkeys layer — the PURE core (2026-08-03 hotkeys-port contract
|
||||||
//! `qbz-nix-docs/qt-frontend/2026-08-03-hotkeys-port/00-CONTRACT.md` v2 §3,
|
//! `qbz-nix-docs/qt-frontend/2026-08-03-hotkeys-port/00-CONTRACT.md` v2 §3,
|
||||||
//! block B1). Byte-faithful port of the Slint `crates/qbz/src/keybindings.rs`
|
//! block B1). Port of the Slint `crates/qbz/src/keybindings.rs`
|
||||||
//! model: the 23-action DEFAULTS table (:88-117 — NB its :3 "26 actions"
|
//! model: the DEFAULTS table (:88-117 — 23 actions at the time of the port;
|
||||||
//! comment is STALE, do not "fix" the count), the shortcut-string grammar
|
//! NB its :3 "26 actions" comment is STALE, do not "fix" the count), the
|
||||||
|
//! shortcut-string grammar
|
||||||
//! (:153-249), the bindings store + conflict detection (:256-323), the groups
|
//! (:153-249), the bindings store + conflict detection (:256-323), the groups
|
||||||
//! builder + three-column round-robin (:329-380), the capture semantics
|
//! builder + three-column round-robin (:329-380), the capture semantics
|
||||||
//! (:434-467) and the §1.2 Escape priority stack (:585-615).
|
//! (:434-467) and the §1.2 Escape priority stack (:585-615).
|
||||||
@@ -12,6 +13,12 @@
|
|||||||
//! suggestions_bridge precedent). Do NOT list this file in build.rs
|
//! suggestions_bridge precedent). Do NOT list this file in build.rs
|
||||||
//! rust_files.
|
//! rust_files.
|
||||||
//!
|
//!
|
||||||
|
//! ONE deliberate addition POST-port, additive to the ported model: eight
|
||||||
|
//! Playback actions the Slint table never had (volume, mute, shuffle, repeat,
|
||||||
|
//! like, and a surface-independent seek — Spotify's desktop defaults).
|
||||||
|
//! Everything below still resolves through one action table and one canonical
|
||||||
|
//! shortcut string.
|
||||||
|
//!
|
||||||
//! The one intentional DIFFERENCE from Slint is the token source (contract
|
//! The one intentional DIFFERENCE from Slint is the token source (contract
|
||||||
//! §3.1 HYBRID rule, round-1 F1): Slint reads winit's LOGICAL key, which is
|
//! §3.1 HYBRID rule, round-1 F1): Slint reads winit's LOGICAL key, which is
|
||||||
//! already the shifted glyph AND Ctrl-independent; a QKeyEvent's `text()` is
|
//! already the shifted glyph AND Ctrl-independent; a QKeyEvent's `text()` is
|
||||||
@@ -156,6 +163,65 @@ pub const ACTIONS: &[ActionDef] = &[
|
|||||||
default: "Ctrl+ArrowLeft",
|
default: "Ctrl+ArrowLeft",
|
||||||
context: Context::None,
|
context: Context::None,
|
||||||
},
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.volumeUp",
|
||||||
|
label_en: "Volume Up",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+ArrowUp",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.volumeDown",
|
||||||
|
label_en: "Volume Down",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+ArrowDown",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.mute",
|
||||||
|
label_en: "Mute / Unmute",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "m",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.shuffle",
|
||||||
|
label_en: "Toggle Shuffle",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+s",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.repeat",
|
||||||
|
label_en: "Cycle Repeat",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+r",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.favorite",
|
||||||
|
label_en: "Like Track",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Alt+Shift+B",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
// Seek from ANY surface. The `focus.seek*` rows below stay bare arrows and
|
||||||
|
// stay immersive-only: there the arrows are free, everywhere else they
|
||||||
|
// belong to whatever the pointer is over.
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.seekForward",
|
||||||
|
label_en: "Seek Forward",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+Shift+ArrowRight",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
|
ActionDef {
|
||||||
|
id: "playback.seekBack",
|
||||||
|
label_en: "Seek Back",
|
||||||
|
category: Category::Playback,
|
||||||
|
default: "Ctrl+Shift+ArrowLeft",
|
||||||
|
context: Context::None,
|
||||||
|
},
|
||||||
// Navigation
|
// Navigation
|
||||||
ActionDef {
|
ActionDef {
|
||||||
id: "nav.back",
|
id: "nav.back",
|
||||||
@@ -996,8 +1062,8 @@ mod tests {
|
|||||||
overrides.insert("stale.removed.action".into(), "x".into()); // unknown id
|
overrides.insert("stale.removed.action".into(), "x".into()); // unknown id
|
||||||
let bindings = active_bindings_with(&overrides);
|
let bindings = active_bindings_with(&overrides);
|
||||||
assert_eq!(bindings.get("ui.queue").unwrap(), "w");
|
assert_eq!(bindings.get("ui.queue").unwrap(), "w");
|
||||||
// The known count is exactly the 23 actions; the stale id is absent.
|
// The known count is exactly the action table; the stale id is absent.
|
||||||
assert_eq!(bindings.len(), 23);
|
assert_eq!(bindings.len(), ACTIONS.len());
|
||||||
assert!(!bindings.contains_key("stale.removed.action"));
|
assert!(!bindings.contains_key("stale.removed.action"));
|
||||||
// …but the overrides map itself is untouched (kept in the file).
|
// …but the overrides map itself is untouched (kept in the file).
|
||||||
assert_eq!(overrides.get("stale.removed.action").unwrap(), "x");
|
assert_eq!(overrides.get("stale.removed.action").unwrap(), "x");
|
||||||
@@ -1238,4 +1304,45 @@ mod tests {
|
|||||||
assert!(!is_ctrl_a(key_a, 0, "a"));
|
assert!(!is_ctrl_a(key_a, 0, "a"));
|
||||||
assert!(!is_ctrl_a(QT_KEY_A + 1, QT_CONTROL, ""));
|
assert!(!is_ctrl_a(QT_KEY_A + 1, QT_CONTROL, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- The new Spotify-parity actions ------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_new_playback_actions_dispatch_on_their_defaults() {
|
||||||
|
let ov = no_overrides();
|
||||||
|
for (key, modifiers, text, id) in [
|
||||||
|
(QT_KEY_UP, QT_CONTROL, "", "playback.volumeUp"),
|
||||||
|
(QT_KEY_DOWN, QT_CONTROL, "", "playback.volumeDown"),
|
||||||
|
(QT_KEY_A + 12, 0, "m", "playback.mute"),
|
||||||
|
(QT_KEY_A + 18, QT_CONTROL, "", "playback.shuffle"),
|
||||||
|
(QT_KEY_A + 17, QT_CONTROL, "", "playback.repeat"),
|
||||||
|
(QT_KEY_A + 1, QT_ALT | QT_SHIFT, "", "playback.favorite"),
|
||||||
|
(QT_KEY_RIGHT, QT_CONTROL | QT_SHIFT, "", "playback.seekForward"),
|
||||||
|
(QT_KEY_LEFT, QT_CONTROL | QT_SHIFT, "", "playback.seekBack"),
|
||||||
|
] {
|
||||||
|
let a = action_for_key(&ov, key, modifiers, text, false)
|
||||||
|
.unwrap_or_else(|| panic!("{id} did not resolve"));
|
||||||
|
assert_eq!(a.id, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global seek rows fire anywhere; the immersive ones keep their bare
|
||||||
|
/// arrows and stay gated on the overlay.
|
||||||
|
#[test]
|
||||||
|
fn global_seek_is_context_free_and_immersive_seek_still_is_not() {
|
||||||
|
let ov = no_overrides();
|
||||||
|
let global = action_for_key(
|
||||||
|
&ov,
|
||||||
|
QT_KEY_RIGHT,
|
||||||
|
QT_CONTROL | QT_SHIFT,
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(global.id, "playback.seekForward");
|
||||||
|
assert_eq!(global.context, Context::None);
|
||||||
|
assert!(action_for_key(&ov, QT_KEY_RIGHT, 0, "", false).is_none());
|
||||||
|
let imm = action_for_key(&ov, QT_KEY_RIGHT, 0, "", true).unwrap();
|
||||||
|
assert_eq!(imm.id, "focus.seekForward");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -507,6 +507,18 @@ pub fn is_remote() -> bool {
|
|||||||
with_model(|m| m.is_remote).0
|
with_model(|m| m.is_remote).0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read-only: the current output volume 0..1. The keyboard volume steps read
|
||||||
|
/// it to compute the next value (the bars read the published property).
|
||||||
|
pub(crate) fn volume() -> f32 {
|
||||||
|
with_model(|m| m.volume).0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only mirror of `set_remote_volume_locked` — the active peer renderer
|
||||||
|
/// disallows remote volume control. Half of the hotkeys' `volume_locked`.
|
||||||
|
pub(crate) fn remote_volume_locked() -> bool {
|
||||||
|
with_model(|m| m.remote_volume_locked).0
|
||||||
|
}
|
||||||
|
|
||||||
/// The REMOTE volume lock (contract §11.3): the active peer Qobuz Connect
|
/// The REMOTE volume lock (contract §11.3): the active peer Qobuz Connect
|
||||||
/// renderer disallows remote volume control. Written from the qconnect sink's
|
/// renderer disallows remote volume control. Written from the qconnect sink's
|
||||||
/// badge refresh alongside `set_remote`, and cleared by the facade's
|
/// badge refresh alongside `set_remote`, and cleared by the facade's
|
||||||
|
|||||||
Reference in new issue
Block a user