Merge feature/internal/tray-rich-tooltip into pre-release
This commit is contained in:
5 files changed
+268
-23
No files matched your search
@@ -418,6 +418,7 @@ pub async fn v2_set_media_metadata(
|
|||||||
duration_secs: Option<u64>,
|
duration_secs: Option<u64>,
|
||||||
cover_url: Option<String>,
|
cover_url: Option<String>,
|
||||||
app_state: State<'_, AppState>,
|
app_state: State<'_, AppState>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
) -> Result<(), RuntimeError> {
|
) -> Result<(), RuntimeError> {
|
||||||
log::info!("[V2] Command: set_media_metadata - {} by {}", title, artist);
|
log::info!("[V2] Command: set_media_metadata - {} by {}", title, artist);
|
||||||
crate::update_media_controls_metadata(
|
crate::update_media_controls_metadata(
|
||||||
@@ -428,6 +429,17 @@ pub async fn v2_set_media_metadata(
|
|||||||
duration_secs,
|
duration_secs,
|
||||||
cover_url,
|
cover_url,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Push the same metadata into the Linux SNI tooltip so panel hover shows
|
||||||
|
// the live track info instead of the static "QBZ — Music Player" string.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use tauri::Manager;
|
||||||
|
if let Some(tray) = app_handle.try_state::<crate::tray_linux_ksni::LinuxTrayHandle>() {
|
||||||
|
tray.set_track(title.clone(), artist.clone(), album.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = app_handle;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1211,6 +1211,27 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirror playback state into the Linux SNI tooltip so the
|
||||||
|
// tray hover hint flips between "Middle-click to pause"
|
||||||
|
// and "Middle-click to play". Track metadata is pushed
|
||||||
|
// separately via v2_set_media_metadata; here we only
|
||||||
|
// touch the play/pause flag and clear on track-stop.
|
||||||
|
// `should_update_mpris` already debounces to actual
|
||||||
|
// state transitions (plus position ticks while playing),
|
||||||
|
// so the cost is bounded.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if should_update_mpris {
|
||||||
|
if let Some(tray) = app_handle
|
||||||
|
.try_state::<crate::tray_linux_ksni::LinuxTrayHandle>()
|
||||||
|
{
|
||||||
|
if track_id == 0 {
|
||||||
|
tray.clear_track();
|
||||||
|
} else {
|
||||||
|
tray.set_playing(is_playing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Idle inhibit: prevent screen/suspend while playing (Linux only, via XDG Portal)
|
// Idle inhibit: prevent screen/suspend while playing (Linux only, via XDG Portal)
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
if is_playing != last_is_playing || track_cleared {
|
if is_playing != last_is_playing || track_cleared {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ use tauri::{
|
|||||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||||
Emitter, Manager,
|
Emitter, Manager,
|
||||||
};
|
};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use tauri::Manager;
|
||||||
use tauri::AppHandle;
|
use tauri::AppHandle;
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -94,10 +96,16 @@ fn load_tray_icon() -> Image<'static> {
|
|||||||
/// Initialize the system tray icon. Dispatches to the platform-specific
|
/// Initialize the system tray icon. Dispatches to the platform-specific
|
||||||
/// backend: ksni on Linux (see `tray_linux_ksni`), Tauri's built-in tray on
|
/// backend: ksni on Linux (see `tray_linux_ksni`), Tauri's built-in tray on
|
||||||
/// macOS. Falls back to a clean error on unknown targets.
|
/// macOS. Falls back to a clean error on unknown targets.
|
||||||
|
///
|
||||||
|
/// On Linux this also installs the live `LinuxTrayHandle` into Tauri state
|
||||||
|
/// so the rest of the backend can push live tooltip updates as the player
|
||||||
|
/// state changes.
|
||||||
pub fn init_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn init_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
return tray_linux_ksni::init(app);
|
let handle = tray_linux_ksni::init(app)?;
|
||||||
|
app.manage(handle);
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
|||||||
@@ -5,20 +5,30 @@
|
|||||||
//! always shows the menu. See issue #310.
|
//! always shows the menu. See issue #310.
|
||||||
//!
|
//!
|
||||||
//! `ksni` is a pure-Rust SNI implementation that exposes the full protocol
|
//! `ksni` is a pure-Rust SNI implementation that exposes the full protocol
|
||||||
//! including `Activate` (left-click), `SecondaryActivate` (middle-click) and
|
//! including `Activate` (left-click), `SecondaryActivate` (middle-click),
|
||||||
//! `ContextMenu` (right-click → menu). Same SNI compatibility as the old
|
//! `Scroll` (wheel) and `ContextMenu` (right-click → menu). Same SNI
|
||||||
//! tray (both need an SNI-aware panel: KDE native, GNOME w/ extension,
|
//! compatibility as the old tray (both need an SNI-aware panel: KDE native,
|
||||||
//! XFCE/Cinnamon/MATE/Budgie, wlroots tray widgets).
|
//! GNOME w/ extension, XFCE/Cinnamon/MATE/Budgie, wlroots tray widgets).
|
||||||
//!
|
//!
|
||||||
//! This module runs a ksni `TrayService` in a dedicated background thread
|
//! This module runs a ksni `TrayService` in a dedicated background thread
|
||||||
//! (via the blocking API). All callbacks receive `&mut QbzTray` and close
|
//! (via the blocking API). All callbacks receive `&mut QbzTray` and close
|
||||||
//! over the Tauri `AppHandle` to emit events and manipulate the main window.
|
//! over the Tauri `AppHandle` to emit events and manipulate the main window.
|
||||||
|
//!
|
||||||
|
//! The returned `LinuxTrayHandle` is stored in Tauri state so the rest of
|
||||||
|
//! the backend (metadata setter, playback poll loop) can push live updates
|
||||||
|
//! into the SNI tooltip — replicating the rich tooltip the Plasma media
|
||||||
|
//! plasmoid shows on hover.
|
||||||
|
|
||||||
|
use std::sync::{
|
||||||
|
mpsc::{self, Sender},
|
||||||
|
Arc, Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
use image::GenericImageView;
|
use image::GenericImageView;
|
||||||
use ksni::{
|
use ksni::{
|
||||||
blocking::TrayMethods,
|
blocking::TrayMethods,
|
||||||
menu::StandardItem,
|
menu::StandardItem,
|
||||||
Icon, MenuItem, ToolTip, Tray,
|
Icon, MenuItem, Orientation, ToolTip, Tray,
|
||||||
};
|
};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
|
||||||
@@ -106,9 +116,19 @@ fn decode_tray_icons() -> Result<Vec<Icon>, String> {
|
|||||||
sources.iter().map(|b| decode_pixmap(b)).collect()
|
sources.iter().map(|b| decode_pixmap(b)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Now-playing info shown in the tooltip. Cleared when no track is loaded.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct NowPlaying {
|
||||||
|
title: String,
|
||||||
|
artist: String,
|
||||||
|
album: String,
|
||||||
|
}
|
||||||
|
|
||||||
struct QbzTray {
|
struct QbzTray {
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
icons: Vec<Icon>,
|
icons: Vec<Icon>,
|
||||||
|
now_playing: Option<NowPlaying>,
|
||||||
|
is_playing: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QbzTray {
|
impl QbzTray {
|
||||||
@@ -135,6 +155,12 @@ impl QbzTray {
|
|||||||
let _ = window.emit(event, ());
|
let _ = window.emit(event, ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_payload<S: serde::Serialize + Clone>(&self, event: &str, payload: S) {
|
||||||
|
if let Some(window) = self.app.get_webview_window("main") {
|
||||||
|
let _ = window.emit(event, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tray for QbzTray {
|
impl Tray for QbzTray {
|
||||||
@@ -160,26 +186,76 @@ impl Tray for QbzTray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tool_tip(&self) -> ToolTip {
|
fn tool_tip(&self) -> ToolTip {
|
||||||
|
// Plasma's media plasmoid renders a multi-line tooltip with the track
|
||||||
|
// title bolded on top and secondary lines below. SNI's ToolTip has a
|
||||||
|
// (title, description) split that maps onto the same visual: panels
|
||||||
|
// typically render `title` bold and `description` as wrapping body
|
||||||
|
// text that respects '\n'.
|
||||||
|
let (title, description) = match &self.now_playing {
|
||||||
|
Some(np) => {
|
||||||
|
let header = if np.title.is_empty() {
|
||||||
|
"QBZ".to_string()
|
||||||
|
} else {
|
||||||
|
np.title.clone()
|
||||||
|
};
|
||||||
|
let mut lines: Vec<String> = Vec::with_capacity(3);
|
||||||
|
if !np.artist.is_empty() {
|
||||||
|
lines.push(format!("by {}", np.artist));
|
||||||
|
}
|
||||||
|
if !np.album.is_empty() {
|
||||||
|
lines.push(np.album.clone());
|
||||||
|
}
|
||||||
|
lines.push(if self.is_playing {
|
||||||
|
"Middle-click to pause".to_string()
|
||||||
|
} else {
|
||||||
|
"Middle-click to play".to_string()
|
||||||
|
});
|
||||||
|
lines.push("Scroll to adjust volume".to_string());
|
||||||
|
(header, lines.join("\n"))
|
||||||
|
}
|
||||||
|
None => (
|
||||||
|
"QBZ".to_string(),
|
||||||
|
"Music Player\nNothing playing".to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
ToolTip {
|
ToolTip {
|
||||||
title: "QBZ".into(),
|
title,
|
||||||
description: "Music Player".into(),
|
description,
|
||||||
icon_name: String::new(),
|
icon_name: String::new(),
|
||||||
icon_pixmap: vec![],
|
icon_pixmap: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Primary click (left) — the headline feature of switching to ksni.
|
/// Primary click (left) — toggle main window visibility.
|
||||||
fn activate(&mut self, _x: i32, _y: i32) {
|
fn activate(&mut self, _x: i32, _y: i32) {
|
||||||
log::info!("[tray] primary activate (left click)");
|
log::info!("[tray] primary activate (left click)");
|
||||||
self.toggle_window();
|
self.toggle_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Secondary click (middle) — same as left for symmetry; users who were
|
/// Secondary click (middle) — play/pause, mirroring the Plasma media
|
||||||
/// middle-clicking on the old build to open the menu still get a useful
|
/// plasmoid behaviour. When nothing is loaded the frontend simply ignores
|
||||||
/// behavior.
|
/// the toggle, so emitting unconditionally is safe.
|
||||||
fn secondary_activate(&mut self, _x: i32, _y: i32) {
|
fn secondary_activate(&mut self, _x: i32, _y: i32) {
|
||||||
log::info!("[tray] secondary activate (middle click)");
|
log::info!("[tray] secondary activate (middle click) -> play/pause");
|
||||||
self.toggle_window();
|
self.emit_to_main("tray:play_pause");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mouse wheel — adjust volume in 5%-per-notch steps. Most panels (KDE
|
||||||
|
/// Plasma, GNOME Shell appindicator) report ±120 per wheel notch
|
||||||
|
/// following the X11/wayland convention; touch-pad scrolls produce
|
||||||
|
/// smaller fractional deltas. We normalise by dividing by 120 and fall
|
||||||
|
/// back to `signum()` so very small deltas still register one tick.
|
||||||
|
fn scroll(&mut self, delta: i32, orientation: Orientation) {
|
||||||
|
if !matches!(orientation, Orientation::Vertical) || delta == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut ticks = delta / 120;
|
||||||
|
if ticks == 0 {
|
||||||
|
ticks = delta.signum();
|
||||||
|
}
|
||||||
|
log::debug!("[tray] scroll delta={} ticks={}", delta, ticks);
|
||||||
|
// Positive ticks = wheel-up = volume up, matching Plasma plasmoid.
|
||||||
|
self.emit_payload("tray:volume_delta", ticks);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn menu(&self) -> Vec<MenuItem<Self>> {
|
fn menu(&self) -> Vec<MenuItem<Self>> {
|
||||||
@@ -223,17 +299,132 @@ impl Tray for QbzTray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates dispatched from the rest of the backend into the ksni worker.
|
||||||
|
///
|
||||||
|
/// We cannot call `ksni::blocking::Handle::update` directly from a Tauri
|
||||||
|
/// command or from the playback poll loop — `ksni` 0.3 with `feature =
|
||||||
|
/// "blocking"` wraps every update in `Runtime::block_on`, which panics when
|
||||||
|
/// invoked from within an existing tokio runtime (which is exactly what
|
||||||
|
/// Tauri commands and `tauri::async_runtime::spawn` tasks run on). The
|
||||||
|
/// panic is silently swallowed by the runtime, so updates appear to
|
||||||
|
/// succeed but the tooltip never changes.
|
||||||
|
///
|
||||||
|
/// To avoid the nested-runtime issue we serialise updates over a
|
||||||
|
/// `std::sync::mpsc` channel and apply them from a dedicated `std::thread`
|
||||||
|
/// that lives entirely outside any tokio context.
|
||||||
|
enum TrayUpdate {
|
||||||
|
SetTrack {
|
||||||
|
title: String,
|
||||||
|
artist: String,
|
||||||
|
album: String,
|
||||||
|
},
|
||||||
|
ClearTrack,
|
||||||
|
SetPlaying(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cross-thread handle to the live ksni tray. Cloneable; mutators just
|
||||||
|
/// forward to the worker thread, so they are safe to call from any
|
||||||
|
/// async context. When the tray is disabled or failed to start, the
|
||||||
|
/// inner sender stays `None` and every mutator becomes a no-op.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LinuxTrayHandle {
|
||||||
|
sender: Arc<Mutex<Option<Sender<TrayUpdate>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxTrayHandle {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
sender: Arc::new(Mutex::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install(&self, handle: ksni::blocking::Handle<QbzTray>) {
|
||||||
|
let (tx, rx) = mpsc::channel::<TrayUpdate>();
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("qbz-tray-updater".into())
|
||||||
|
.spawn(move || {
|
||||||
|
while let Ok(msg) = rx.recv() {
|
||||||
|
match msg {
|
||||||
|
TrayUpdate::SetTrack {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
} => {
|
||||||
|
log::debug!(
|
||||||
|
"[tray] tooltip update -> {} / {} / {}",
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album
|
||||||
|
);
|
||||||
|
handle.update(move |tray| {
|
||||||
|
tray.now_playing = Some(NowPlaying {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TrayUpdate::ClearTrack => {
|
||||||
|
log::debug!("[tray] tooltip cleared");
|
||||||
|
handle.update(|tray| {
|
||||||
|
tray.now_playing = None;
|
||||||
|
tray.is_playing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TrayUpdate::SetPlaying(is_playing) => {
|
||||||
|
handle.update(move |tray| {
|
||||||
|
tray.is_playing = is_playing;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::debug!("[tray] updater thread exiting");
|
||||||
|
})
|
||||||
|
.expect("spawn tray updater thread");
|
||||||
|
if let Ok(mut guard) = self.sender.lock() {
|
||||||
|
*guard = Some(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&self, msg: TrayUpdate) {
|
||||||
|
let guard = match self.sender.lock() {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if let Some(tx) = guard.as_ref() {
|
||||||
|
let _ = tx.send(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_track(&self, title: String, artist: String, album: String) {
|
||||||
|
self.send(TrayUpdate::SetTrack {
|
||||||
|
title,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_track(&self) {
|
||||||
|
self.send(TrayUpdate::ClearTrack);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_playing(&self, is_playing: bool) {
|
||||||
|
self.send(TrayUpdate::SetPlaying(is_playing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize the Linux ksni tray service. Spawns a background thread that
|
/// Initialize the Linux ksni tray service. Spawns a background thread that
|
||||||
/// owns the SNI service; the returned Handle is intentionally leaked because
|
/// owns the SNI service and returns a cloneable handle so live tooltip
|
||||||
/// the tray is a singleton that lives for the app's lifetime (dropping the
|
/// updates can be pushed in from the rest of the backend.
|
||||||
/// handle would tear down the tray).
|
pub fn init(app: &AppHandle) -> Result<LinuxTrayHandle, Box<dyn std::error::Error>> {
|
||||||
pub fn init(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
log::info!("Initializing ksni tray (Linux, SNI primary-activate enabled)");
|
log::info!("Initializing ksni tray (Linux, SNI primary-activate enabled)");
|
||||||
|
|
||||||
let icons = decode_tray_icons()?;
|
let icons = decode_tray_icons()?;
|
||||||
let tray = QbzTray {
|
let tray = QbzTray {
|
||||||
app: app.clone(),
|
app: app.clone(),
|
||||||
icons,
|
icons,
|
||||||
|
now_playing: None,
|
||||||
|
is_playing: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Flatpak requires disabling the well-known DBus name because the sandbox
|
// Flatpak requires disabling the well-known DBus name because the sandbox
|
||||||
@@ -245,11 +436,9 @@ pub fn init(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
tray.spawn()?
|
tray.spawn()?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep the service alive for the app lifetime. We don't need to update the
|
let live = LinuxTrayHandle::empty();
|
||||||
// tray dynamically yet; if we do in the future (e.g., change icon when
|
live.install(handle);
|
||||||
// playing), we'd store this handle in AppState instead.
|
|
||||||
std::mem::forget(handle);
|
|
||||||
|
|
||||||
log::info!("ksni tray initialized");
|
log::info!("ksni tray initialized");
|
||||||
Ok(())
|
Ok(live)
|
||||||
}
|
}
|
||||||
@@ -5210,6 +5210,7 @@
|
|||||||
let unlistenTrayPlayPause: UnlistenFn | null = null;
|
let unlistenTrayPlayPause: UnlistenFn | null = null;
|
||||||
let unlistenTrayNext: UnlistenFn | null = null;
|
let unlistenTrayNext: UnlistenFn | null = null;
|
||||||
let unlistenTrayPrevious: UnlistenFn | null = null;
|
let unlistenTrayPrevious: UnlistenFn | null = null;
|
||||||
|
let unlistenTrayVolumeDelta: UnlistenFn | null = null;
|
||||||
let unlistenMediaControls: UnlistenFn | null = null;
|
let unlistenMediaControls: UnlistenFn | null = null;
|
||||||
let unlistenLinkResolved: UnlistenFn | null = null;
|
let unlistenLinkResolved: UnlistenFn | null = null;
|
||||||
let unlistenQconnectEvent: UnlistenFn | null = null;
|
let unlistenQconnectEvent: UnlistenFn | null = null;
|
||||||
@@ -5242,6 +5243,19 @@
|
|||||||
if (disposed) { unlisten3(); return; }
|
if (disposed) { unlisten3(); return; }
|
||||||
unlistenTrayPrevious = unlisten3;
|
unlistenTrayPrevious = unlisten3;
|
||||||
|
|
||||||
|
// Tray scroll wheel: backend emits a normalised tick count (positive
|
||||||
|
// = wheel-up = volume up, negative = wheel-down). Each tick = 5%.
|
||||||
|
const unlistenVol = await listen<number>('tray:volume_delta', async (event) => {
|
||||||
|
const ticks = typeof event.payload === 'number' ? event.payload : 0;
|
||||||
|
if (!ticks) return;
|
||||||
|
const delta = ticks * 5;
|
||||||
|
const next = Math.max(0, Math.min(100, Math.round(volume + delta)));
|
||||||
|
if (next === volume) return;
|
||||||
|
await handleVolumeChange(next);
|
||||||
|
});
|
||||||
|
if (disposed) { unlistenVol(); return; }
|
||||||
|
unlistenTrayVolumeDelta = unlistenVol;
|
||||||
|
|
||||||
const unlisten4 = await listen('media:control', async (event) => {
|
const unlisten4 = await listen('media:control', async (event) => {
|
||||||
const payload = event.payload as MediaControlPayload;
|
const payload = event.payload as MediaControlPayload;
|
||||||
if (!payload?.action) return;
|
if (!payload?.action) return;
|
||||||
@@ -5446,6 +5460,7 @@
|
|||||||
unlistenTrayPlayPause?.();
|
unlistenTrayPlayPause?.();
|
||||||
unlistenTrayNext?.();
|
unlistenTrayNext?.();
|
||||||
unlistenTrayPrevious?.();
|
unlistenTrayPrevious?.();
|
||||||
|
unlistenTrayVolumeDelta?.();
|
||||||
unlistenMediaControls?.();
|
unlistenMediaControls?.();
|
||||||
unlistenLinkResolved?.();
|
unlistenLinkResolved?.();
|
||||||
unlistenQconnectEvent?.();
|
unlistenQconnectEvent?.();
|
||||||
|
|||||||
Reference in new issue
Block a user