feat(qconnect): gate volume on renderer volume_remote_control capability
Add volume_remote_control to QconnectRendererInfo plus the renderer_allows_remote_volume predicate (absent => allowed, only an explicit non-1 value disables). Parse it from device_info.capabilities in the ADD and UPDATE renderer paths, and short-circuit set_volume_if_remote when the active renderer disallows remote volume (returns handled no-op so the frontend does not fall back to local). Frontend surfaces volume_remote_control in the session snapshot, derives a unified volume lock + reason, guards handleVolumeChange, and shows a distinct player.volumeLockedRemote tooltip (added to all 5 locales). cc
This commit is contained in:
12 files changed
+111
-6
No files matched your search
@@ -242,6 +242,11 @@ impl TauriQconnectEventSink {
|
|||||||
.and_then(|d| d.get("device_type"))
|
.and_then(|d| d.get("device_type"))
|
||||||
.and_then(Value::as_i64)
|
.and_then(Value::as_i64)
|
||||||
.map(|v| v as i32),
|
.map(|v| v as i32),
|
||||||
|
volume_remote_control: device_info
|
||||||
|
.and_then(|d| d.get("capabilities"))
|
||||||
|
.and_then(|c| c.get("volume_remote_control"))
|
||||||
|
.and_then(Value::as_i64)
|
||||||
|
.map(|v| v as i32),
|
||||||
});
|
});
|
||||||
refresh_local_renderer_id(&mut state.session);
|
refresh_local_renderer_id(&mut state.session);
|
||||||
}
|
}
|
||||||
@@ -289,6 +294,13 @@ impl TauriQconnectEventSink {
|
|||||||
{
|
{
|
||||||
existing.device_type = Some(device_type as i32);
|
existing.device_type = Some(device_type as i32);
|
||||||
}
|
}
|
||||||
|
if let Some(vrc) = device_info
|
||||||
|
.and_then(|d| d.get("capabilities"))
|
||||||
|
.and_then(|c| c.get("volume_remote_control"))
|
||||||
|
.and_then(Value::as_i64)
|
||||||
|
{
|
||||||
|
existing.volume_remote_control = Some(vrc as i32);
|
||||||
|
}
|
||||||
refresh_local_renderer_id(&mut state.session);
|
refresh_local_renderer_id(&mut state.session);
|
||||||
}
|
}
|
||||||
let _ = ensure_session_renderer_state(&mut state, renderer_id);
|
let _ = ensure_session_renderer_state(&mut state, renderer_id);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use super::queue_resolution::{
|
|||||||
use super::session::{
|
use super::session::{
|
||||||
build_effective_renderer_snapshot, build_visible_queue_projection, compute_connection_state,
|
build_effective_renderer_snapshot, build_visible_queue_projection, compute_connection_state,
|
||||||
ensure_session_renderer_state, is_local_renderer_active, is_peer_renderer_active,
|
ensure_session_renderer_state, is_local_renderer_active, is_peer_renderer_active,
|
||||||
QconnectFileAudioQualitySnapshot,
|
renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
|
||||||
};
|
};
|
||||||
use super::transport::{
|
use super::transport::{
|
||||||
default_qconnect_device_info, default_qconnect_device_info_with_name, hex_preview,
|
default_qconnect_device_info, default_qconnect_device_info_with_name, hex_preview,
|
||||||
@@ -1659,6 +1659,24 @@ impl QconnectServiceState {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// P1-5: respect the active renderer's volume_remote_control capability.
|
||||||
|
// Absent => allowed. Only an explicit non-ALLOWED value disables.
|
||||||
|
if let Some(active_id) = session.active_renderer_id {
|
||||||
|
if let Some(info) = session
|
||||||
|
.renderers
|
||||||
|
.iter()
|
||||||
|
.find(|r| r.renderer_id == active_id)
|
||||||
|
{
|
||||||
|
if !renderer_allows_remote_volume(info) {
|
||||||
|
log::info!(
|
||||||
|
"[QConnect] set_volume_if_remote short-circuited: renderer {active_id} disallows remote volume"
|
||||||
|
);
|
||||||
|
// Handled (no-op): the frontend must NOT fall back to local.
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let payload = serde_json::to_value(QconnectSetVolumeRequest {
|
let payload = serde_json::to_value(QconnectSetVolumeRequest {
|
||||||
renderer_id: session.active_renderer_id,
|
renderer_id: session.active_renderer_id,
|
||||||
volume: Some(volume),
|
volume: Some(volume),
|
||||||
|
|||||||
@@ -152,6 +152,16 @@ pub struct QconnectRendererInfo {
|
|||||||
pub brand: Option<String>,
|
pub brand: Option<String>,
|
||||||
pub model: Option<String>,
|
pub model: Option<String>,
|
||||||
pub device_type: Option<i32>,
|
pub device_type: Option<i32>,
|
||||||
|
/// capabilities.volume_remote_control. 1 == ALLOWED. None == not advertised
|
||||||
|
/// (treated as allowed to avoid regressing renderers that omit it).
|
||||||
|
#[serde(default)]
|
||||||
|
pub volume_remote_control: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the active renderer permits remote volume control. Absent capability
|
||||||
|
/// (None) defaults to allowed; only an explicit non-ALLOWED value disables.
|
||||||
|
pub(super) fn renderer_allows_remote_volume(info: &QconnectRendererInfo) -> bool {
|
||||||
|
matches!(info.volume_remote_control, None | Some(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn refresh_local_renderer_id(session: &mut QconnectSessionState) {
|
pub(super) fn refresh_local_renderer_id(session: &mut QconnectSessionState) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use super::queue_resolution::{
|
|||||||
QconnectRemoteSkipDirection,
|
QconnectRemoteSkipDirection,
|
||||||
};
|
};
|
||||||
use super::session::{
|
use super::session::{
|
||||||
find_unique_renderer_id, refresh_local_renderer_id, QconnectFileAudioQualitySnapshot,
|
find_unique_renderer_id, refresh_local_renderer_id, renderer_allows_remote_volume,
|
||||||
|
QconnectFileAudioQualitySnapshot,
|
||||||
};
|
};
|
||||||
use super::transport::{
|
use super::transport::{
|
||||||
decode_hex_channel, default_qconnect_device_info, parse_subscribe_channels,
|
decode_hex_channel, default_qconnect_device_info, parse_subscribe_channels,
|
||||||
@@ -167,6 +168,7 @@ fn refreshes_local_renderer_id_from_exact_device_uuid_match() {
|
|||||||
brand: Some("Apple".to_string()),
|
brand: Some("Apple".to_string()),
|
||||||
model: Some("iPhone".to_string()),
|
model: Some("iPhone".to_string()),
|
||||||
device_type: Some(6),
|
device_type: Some(6),
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
QconnectRendererInfo {
|
QconnectRendererInfo {
|
||||||
renderer_id: 6,
|
renderer_id: 6,
|
||||||
@@ -175,6 +177,7 @@ fn refreshes_local_renderer_id_from_exact_device_uuid_match() {
|
|||||||
brand: Some("QBZ".to_string()),
|
brand: Some("QBZ".to_string()),
|
||||||
model: Some("QBZ".to_string()),
|
model: Some("QBZ".to_string()),
|
||||||
device_type: Some(5),
|
device_type: Some(5),
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -199,6 +202,7 @@ fn refreshes_local_renderer_id_from_unique_fingerprint_when_uuid_missing() {
|
|||||||
brand: Some("Apple".to_string()),
|
brand: Some("Apple".to_string()),
|
||||||
model: Some("iPhone".to_string()),
|
model: Some("iPhone".to_string()),
|
||||||
device_type: Some(6),
|
device_type: Some(6),
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
QconnectRendererInfo {
|
QconnectRendererInfo {
|
||||||
renderer_id: 6,
|
renderer_id: 6,
|
||||||
@@ -207,6 +211,7 @@ fn refreshes_local_renderer_id_from_unique_fingerprint_when_uuid_missing() {
|
|||||||
brand: local_device_info.brand.clone(),
|
brand: local_device_info.brand.clone(),
|
||||||
model: local_device_info.model.clone(),
|
model: local_device_info.model.clone(),
|
||||||
device_type: local_device_info.device_type,
|
device_type: local_device_info.device_type,
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -228,6 +233,7 @@ fn does_not_guess_local_renderer_id_when_fingerprint_is_ambiguous() {
|
|||||||
brand: Some("QBZ".to_string()),
|
brand: Some("QBZ".to_string()),
|
||||||
model: Some("QBZ".to_string()),
|
model: Some("QBZ".to_string()),
|
||||||
device_type: Some(5),
|
device_type: Some(5),
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
QconnectRendererInfo {
|
QconnectRendererInfo {
|
||||||
renderer_id: 9,
|
renderer_id: 9,
|
||||||
@@ -236,6 +242,7 @@ fn does_not_guess_local_renderer_id_when_fingerprint_is_ambiguous() {
|
|||||||
brand: Some("QBZ".to_string()),
|
brand: Some("QBZ".to_string()),
|
||||||
model: Some("QBZ".to_string()),
|
model: Some("QBZ".to_string()),
|
||||||
device_type: Some(5),
|
device_type: Some(5),
|
||||||
|
volume_remote_control: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -1212,3 +1219,23 @@ fn quality_from_max_audio_quality_maps_levels() {
|
|||||||
assert_eq!(quality_from_max_audio_quality(None), Quality::UltraHiRes);
|
assert_eq!(quality_from_max_audio_quality(None), Quality::UltraHiRes);
|
||||||
assert_eq!(quality_from_max_audio_quality(Some(99)), Quality::UltraHiRes);
|
assert_eq!(quality_from_max_audio_quality(Some(99)), Quality::UltraHiRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renderer_allows_remote_volume_defaults_allow_when_absent() {
|
||||||
|
let mut info = QconnectRendererInfo {
|
||||||
|
renderer_id: 7,
|
||||||
|
device_uuid: None,
|
||||||
|
friendly_name: None,
|
||||||
|
brand: None,
|
||||||
|
model: None,
|
||||||
|
device_type: None,
|
||||||
|
volume_remote_control: None,
|
||||||
|
};
|
||||||
|
assert!(renderer_allows_remote_volume(&info)); // absent => allowed
|
||||||
|
info.volume_remote_control = Some(1);
|
||||||
|
assert!(renderer_allows_remote_volume(&info)); // ALLOWED
|
||||||
|
info.volume_remote_control = Some(0);
|
||||||
|
assert!(!renderer_allows_remote_volume(&info)); // explicit non-allowed
|
||||||
|
info.volume_remote_control = Some(2);
|
||||||
|
assert!(!renderer_allows_remote_volume(&info)); // any other value
|
||||||
|
}
|
||||||
@@ -106,6 +106,9 @@
|
|||||||
qconnectBusy?: boolean;
|
qconnectBusy?: boolean;
|
||||||
showQconnectDevButton?: boolean;
|
showQconnectDevButton?: boolean;
|
||||||
volumeLocked?: boolean;
|
volumeLocked?: boolean;
|
||||||
|
// P1-5: distinguishes the hardware (ALSA Direct) lock from the remote
|
||||||
|
// renderer (cast device) lock so the tooltip can be specific.
|
||||||
|
volumeLockedReason?: 'hardware' | 'remote';
|
||||||
bufferProgress?: number | null;
|
bufferProgress?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +165,7 @@
|
|||||||
qconnectBusy = false,
|
qconnectBusy = false,
|
||||||
showQconnectDevButton = false,
|
showQconnectDevButton = false,
|
||||||
volumeLocked = false,
|
volumeLocked = false,
|
||||||
|
volumeLockedReason = 'hardware',
|
||||||
bufferProgress = null,
|
bufferProgress = null,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
@@ -823,7 +827,9 @@
|
|||||||
{#if volumeLocked}
|
{#if volumeLocked}
|
||||||
<button
|
<button
|
||||||
class="control-btn volume-btn"
|
class="control-btn volume-btn"
|
||||||
title={$translateStore('player.volumeLockedHw')}
|
title={volumeLockedReason === 'remote'
|
||||||
|
? $translateStore('player.volumeLockedRemote')
|
||||||
|
: $translateStore('player.volumeLockedHw')}
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<Volume2 size={16} />
|
<Volume2 size={16} />
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@
|
|||||||
"volumeUp": "Lauter",
|
"volumeUp": "Lauter",
|
||||||
"volumeDown": "Leiser",
|
"volumeDown": "Leiser",
|
||||||
"volumeLockedHw": "Lautstärke auf 100% fixiert — ALSA Direct hw: (am DAC/Hardware einstellen)",
|
"volumeLockedHw": "Lautstärke auf 100% fixiert — ALSA Direct hw: (am DAC/Hardware einstellen)",
|
||||||
|
"volumeLockedRemote": "Lautstärkeregelung durch das Cast-Gerät deaktiviert",
|
||||||
"clearQueue": "Warteschlange leeren",
|
"clearQueue": "Warteschlange leeren",
|
||||||
"queueEmpty": "Warteschlange ist leer",
|
"queueEmpty": "Warteschlange ist leer",
|
||||||
"queueEmptyDescription": "Suche nach Musik oder durchsuchen Sie Ihre Bibliothek, um Titel hinzuzufügen.",
|
"queueEmptyDescription": "Suche nach Musik oder durchsuchen Sie Ihre Bibliothek, um Titel hinzuzufügen.",
|
||||||
|
|||||||
@@ -1542,6 +1542,7 @@
|
|||||||
"volumeUp": "Increase volume",
|
"volumeUp": "Increase volume",
|
||||||
"volumeDown": "Decrease volume",
|
"volumeDown": "Decrease volume",
|
||||||
"volumeLockedHw": "Volume locked at 100% — ALSA Direct hw: (adjust at DAC/hardware level)",
|
"volumeLockedHw": "Volume locked at 100% — ALSA Direct hw: (adjust at DAC/hardware level)",
|
||||||
|
"volumeLockedRemote": "Volume control disabled by the cast device",
|
||||||
"clearQueue": "Clear Queue",
|
"clearQueue": "Clear Queue",
|
||||||
"queueEmpty": "Queue is empty",
|
"queueEmpty": "Queue is empty",
|
||||||
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
|
"queueEmptyDescription": "Search for music or browse your library to add tracks.",
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@
|
|||||||
"volumeUp": "Subir volumen",
|
"volumeUp": "Subir volumen",
|
||||||
"volumeDown": "Bajar volumen",
|
"volumeDown": "Bajar volumen",
|
||||||
"volumeLockedHw": "Volumen fijo al 100% — ALSA Direct hw: (ajusta desde el DAC/hardware)",
|
"volumeLockedHw": "Volumen fijo al 100% — ALSA Direct hw: (ajusta desde el DAC/hardware)",
|
||||||
|
"volumeLockedRemote": "El control de volumen está deshabilitado por el dispositivo",
|
||||||
"clearQueue": "Limpiar Cola",
|
"clearQueue": "Limpiar Cola",
|
||||||
"queueEmpty": "La cola está vacía",
|
"queueEmpty": "La cola está vacía",
|
||||||
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
|
"queueEmptyDescription": "Busca música o explora tu biblioteca para agregar pistas.",
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@
|
|||||||
"volumeUp": "Augmenter le volume",
|
"volumeUp": "Augmenter le volume",
|
||||||
"volumeDown": "Diminuer le volume",
|
"volumeDown": "Diminuer le volume",
|
||||||
"volumeLockedHw": "Volume verrouillé à 100% — ALSA Direct hw: (réglez au niveau DAC/matériel)",
|
"volumeLockedHw": "Volume verrouillé à 100% — ALSA Direct hw: (réglez au niveau DAC/matériel)",
|
||||||
|
"volumeLockedRemote": "Contrôle du volume désactivé par l'appareil de diffusion",
|
||||||
"clearQueue": "Vider la file d'attente",
|
"clearQueue": "Vider la file d'attente",
|
||||||
"queueEmpty": "La file d'attente est vide",
|
"queueEmpty": "La file d'attente est vide",
|
||||||
"queueEmptyDescription": "Recherchez de la musique ou parcourez votre bibliothèque pour ajouter des titres.",
|
"queueEmptyDescription": "Recherchez de la musique ou parcourez votre bibliothèque pour ajouter des titres.",
|
||||||
|
|||||||
@@ -1540,6 +1540,7 @@
|
|||||||
"volumeUp": "Aumentar volume",
|
"volumeUp": "Aumentar volume",
|
||||||
"volumeDown": "Diminuir volume",
|
"volumeDown": "Diminuir volume",
|
||||||
"volumeLockedHw": "Volume fixo em 100% — ALSA Direct hw: (ajuste no DAC/hardware)",
|
"volumeLockedHw": "Volume fixo em 100% — ALSA Direct hw: (ajuste no DAC/hardware)",
|
||||||
|
"volumeLockedRemote": "Controle de volume desativado pelo dispositivo de transmissão",
|
||||||
"clearQueue": "Limpar Fila",
|
"clearQueue": "Limpar Fila",
|
||||||
"queueEmpty": "A fila está vazia",
|
"queueEmpty": "A fila está vazia",
|
||||||
"queueEmptyDescription": "Pesquise músicas ou navegue pela sua biblioteca para adicionar faixas.",
|
"queueEmptyDescription": "Pesquise músicas ou navegue pela sua biblioteca para adicionar faixas.",
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export type QconnectRendererInfo = {
|
|||||||
brand?: string | null;
|
brand?: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
device_type?: number | null;
|
device_type?: number | null;
|
||||||
|
// P1-5: capabilities.volume_remote_control. 1 == ALLOWED. null/undefined ==
|
||||||
|
// not advertised (treated as allowed). Any other value disables remote volume.
|
||||||
|
volume_remote_control?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QconnectSessionSnapshot = {
|
export type QconnectSessionSnapshot = {
|
||||||
|
|||||||
+27
-3
@@ -1125,6 +1125,26 @@
|
|||||||
const qconnectPeerRendererActive = $derived(
|
const qconnectPeerRendererActive = $derived(
|
||||||
isQconnectPeerRendererActive(qobuzConnectSessionSnapshot)
|
isQconnectPeerRendererActive(qobuzConnectSessionSnapshot)
|
||||||
);
|
);
|
||||||
|
// P1-5: respect the active renderer's volume_remote_control capability.
|
||||||
|
// Absent (null/undefined) => allowed; only an explicit non-1 value disables.
|
||||||
|
const qconnectRemoteVolumeAllowed = $derived.by(() => {
|
||||||
|
if (!qconnectPeerRendererActive) return true;
|
||||||
|
const snap = qobuzConnectSessionSnapshot;
|
||||||
|
const activeId = snap?.active_renderer_id ?? null;
|
||||||
|
const info = snap?.renderers?.find((r) => r.renderer_id === activeId) ?? null;
|
||||||
|
const vrc = info?.volume_remote_control;
|
||||||
|
return vrc == null || vrc === 1;
|
||||||
|
});
|
||||||
|
// P1-5: unified volume-lock state shared by the now-playing bar + immersive
|
||||||
|
// player. Hardware lock applies only locally; remote lock applies when a peer
|
||||||
|
// renderer is active and disallows remote volume.
|
||||||
|
const qconnectVolumeLocked = $derived(
|
||||||
|
(isAlsaDirectHw && !qconnectPeerRendererActive) ||
|
||||||
|
(qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed)
|
||||||
|
);
|
||||||
|
const qconnectVolumeLockedReason: 'hardware' | 'remote' = $derived(
|
||||||
|
qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed ? 'remote' : 'hardware'
|
||||||
|
);
|
||||||
const qconnectSuppressLocalPlaybackAutomation = $derived(
|
const qconnectSuppressLocalPlaybackAutomation = $derived(
|
||||||
shouldQconnectSuppressLocalPlaybackAutomation(
|
shouldQconnectSuppressLocalPlaybackAutomation(
|
||||||
isQobuzConnectConnected,
|
isQobuzConnectConnected,
|
||||||
@@ -2887,6 +2907,8 @@
|
|||||||
async function handleVolumeChange(newVolume: number) {
|
async function handleVolumeChange(newVolume: number) {
|
||||||
// ALSA Direct hw: locks volume at 100% — unless controlling a remote renderer
|
// ALSA Direct hw: locks volume at 100% — unless controlling a remote renderer
|
||||||
if (isAlsaDirectHw && !qconnectPeerRendererActive) return;
|
if (isAlsaDirectHw && !qconnectPeerRendererActive) return;
|
||||||
|
// P1-5: the active remote renderer disallows remote volume control.
|
||||||
|
if (qconnectPeerRendererActive && !qconnectRemoteVolumeAllowed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const handledRemotely = await invoke<boolean>('v2_qconnect_set_volume_if_remote', { volume: newVolume });
|
const handledRemotely = await invoke<boolean>('v2_qconnect_set_volume_if_remote', { volume: newVolume });
|
||||||
@@ -7141,7 +7163,8 @@
|
|||||||
onToggleQconnectConnection={handleQobuzConnectButton}
|
onToggleQconnectConnection={handleQobuzConnectButton}
|
||||||
qconnectBusy={qobuzConnectBusy}
|
qconnectBusy={qobuzConnectBusy}
|
||||||
{showQconnectDevButton}
|
{showQconnectDevButton}
|
||||||
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
|
volumeLocked={qconnectVolumeLocked}
|
||||||
|
volumeLockedReason={qconnectVolumeLockedReason}
|
||||||
{bufferProgress}
|
{bufferProgress}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -7164,7 +7187,8 @@
|
|||||||
onToggleQconnectConnection={handleQobuzConnectButton}
|
onToggleQconnectConnection={handleQobuzConnectButton}
|
||||||
qconnectBusy={qobuzConnectBusy}
|
qconnectBusy={qobuzConnectBusy}
|
||||||
{showQconnectDevButton}
|
{showQconnectDevButton}
|
||||||
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
|
volumeLocked={qconnectVolumeLocked}
|
||||||
|
volumeLockedReason={qconnectVolumeLockedReason}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -7198,7 +7222,7 @@
|
|||||||
{volume}
|
{volume}
|
||||||
onVolumeChange={handleVolumeChange}
|
onVolumeChange={handleVolumeChange}
|
||||||
onToggleMute={handleToggleMute}
|
onToggleMute={handleToggleMute}
|
||||||
volumeLocked={isAlsaDirectHw && !qconnectPeerRendererActive}
|
volumeLocked={qconnectVolumeLocked}
|
||||||
{isShuffle}
|
{isShuffle}
|
||||||
onToggleShuffle={toggleShuffle}
|
onToggleShuffle={toggleShuffle}
|
||||||
{repeatMode}
|
{repeatMode}
|
||||||
|
|||||||
Reference in new issue
Block a user