refactor(qconnect-app): hoist renderer-projection helpers from the Tauri adapter
Move build_session_renderer_snapshot, queue_item_snapshot_for_cursor and cache_renderer_snapshot into qconnect-app (session.rs / sync_state.rs) and re-export them; repoint the Tauri qconnect adapter to consume them. This lets the Slint adapter share one definition (ADR-006) instead of duplicating the projection logic. Behavior is byte-identical. cc
This commit is contained in:
4 files changed
+115
-83
No files matched your search
@@ -22,12 +22,13 @@ pub use startup::{compute_effective_startup, QconnectStartupMode};
|
|||||||
pub use error::QconnectAppError;
|
pub use error::QconnectAppError;
|
||||||
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
|
||||||
pub use session::{
|
pub use session::{
|
||||||
compute_connection_state, deferred_join_reason, find_unique_renderer_id,
|
build_session_renderer_snapshot, compute_connection_state, deferred_join_reason,
|
||||||
is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id,
|
find_unique_renderer_id, is_local_renderer_active, is_peer_renderer_active,
|
||||||
quality_from_max_audio_quality, refresh_local_renderer_id, renderer_allows_remote_volume,
|
normalize_active_renderer_id, quality_from_max_audio_quality, queue_item_snapshot_for_cursor,
|
||||||
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision, LocalIdentity,
|
refresh_local_renderer_id, renderer_allows_remote_volume, should_arm_renderer_watchdog,
|
||||||
QconnectFileAudioQualitySnapshot, QconnectLifecycleState, QconnectRendererInfo,
|
should_reask_queue_state, ConnectionDecision, LocalIdentity, QconnectFileAudioQualitySnapshot,
|
||||||
QconnectSessionRendererState, QconnectSessionState, RendererStatus, ServerActiveState,
|
QconnectLifecycleState, QconnectRendererInfo, QconnectSessionRendererState,
|
||||||
|
QconnectSessionState, RendererStatus, ServerActiveState,
|
||||||
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
|
||||||
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
|
||||||
};
|
};
|
||||||
@@ -44,5 +45,6 @@ pub use qconnect_protocol::{QueueCommandType, RendererReport, RendererReportType
|
|||||||
pub use renderer_engine::QconnectRendererEngine;
|
pub use renderer_engine::QconnectRendererEngine;
|
||||||
pub use state::QconnectRuntimeState;
|
pub use state::QconnectRuntimeState;
|
||||||
pub use sync_state::{
|
pub use sync_state::{
|
||||||
ensure_session_renderer_state, sync_session_renderer_active_flags, QconnectRemoteSyncState,
|
cache_renderer_snapshot, ensure_session_renderer_state, sync_session_renderer_active_flags,
|
||||||
|
QconnectRemoteSyncState,
|
||||||
};
|
};
|
||||||
@@ -6,8 +6,14 @@
|
|||||||
//! byte-for-byte identical to the prior Tauri-side definitions.
|
//! byte-for-byte identical to the prior Tauri-side definitions.
|
||||||
|
|
||||||
use qbz_models::Quality;
|
use qbz_models::Quality;
|
||||||
|
use qconnect_core::{QConnectQueueState, QConnectRendererState, QueueItem};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::queue_resolution::{
|
||||||
|
find_cursor_index_by_queue_item_id, normalize_current_queue_item_id_from_queue_state,
|
||||||
|
ordered_queue_cursors, QconnectOrderedQueueCursor,
|
||||||
|
};
|
||||||
|
|
||||||
/// Lifecycle state surfaced to the UI so the toggle can reflect what the user
|
/// Lifecycle state surfaced to the UI so the toggle can reflect what the user
|
||||||
/// asked for (`running`) separately from what the transport currently has
|
/// asked for (`running`) separately from what the transport currently has
|
||||||
/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI
|
/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI
|
||||||
@@ -360,6 +366,66 @@ pub fn find_unique_renderer_id(
|
|||||||
Some(first)
|
Some(first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Session-projection helpers (relocated from the Tauri adapter — slice 6, Slint
|
||||||
|
// port). Pure functions over the cloud queue + cached per-renderer state. They
|
||||||
|
// move here (frontend-agnostic) so the Tauri event sink AND the Slint event sink
|
||||||
|
// build the same renderer projection from one definition. Byte-for-byte
|
||||||
|
// identical to the prior Tauri-side bodies.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Snapshot the queue item a cursor points at, normalizing the queue_item_id for
|
||||||
|
/// in-queue cursors (autoplay items carry their own id verbatim).
|
||||||
|
pub fn queue_item_snapshot_for_cursor(
|
||||||
|
queue: &QConnectQueueState,
|
||||||
|
cursor: QconnectOrderedQueueCursor,
|
||||||
|
) -> Option<QueueItem> {
|
||||||
|
match cursor {
|
||||||
|
QconnectOrderedQueueCursor::Queue(index) => {
|
||||||
|
queue.queue_items.get(index).cloned().map(|mut item| {
|
||||||
|
item.queue_item_id = normalize_current_queue_item_id_from_queue_state(queue, index);
|
||||||
|
item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
QconnectOrderedQueueCursor::Autoplay(index) => queue.autoplay_items.get(index).cloned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a renderer-state projection from the cloud queue + a cached per-renderer
|
||||||
|
/// state, resolving the current/next track by the renderer's current_queue_item_id
|
||||||
|
/// against the ordered cursors. `session_loop_mode` is the fallback loop mode when
|
||||||
|
/// the renderer state omits one.
|
||||||
|
pub fn build_session_renderer_snapshot(
|
||||||
|
queue: &QConnectQueueState,
|
||||||
|
renderer_state: Option<&QconnectSessionRendererState>,
|
||||||
|
session_loop_mode: Option<i32>,
|
||||||
|
) -> QConnectRendererState {
|
||||||
|
let renderer_state = renderer_state.cloned().unwrap_or_default();
|
||||||
|
let cursors = ordered_queue_cursors(queue);
|
||||||
|
let current_index =
|
||||||
|
find_cursor_index_by_queue_item_id(&cursors, queue, renderer_state.current_queue_item_id);
|
||||||
|
let current_track =
|
||||||
|
current_index.and_then(|index| queue_item_snapshot_for_cursor(queue, cursors[index]));
|
||||||
|
let next_track = current_index
|
||||||
|
.and_then(|index| cursors.get(index + 1).copied())
|
||||||
|
.and_then(|cursor| queue_item_snapshot_for_cursor(queue, cursor));
|
||||||
|
|
||||||
|
QConnectRendererState {
|
||||||
|
active: renderer_state.active,
|
||||||
|
playing_state: renderer_state.playing_state,
|
||||||
|
current_position_ms: renderer_state.current_position_ms,
|
||||||
|
current_track,
|
||||||
|
next_track,
|
||||||
|
volume: renderer_state.volume,
|
||||||
|
volume_delta: None,
|
||||||
|
muted: renderer_state.muted,
|
||||||
|
max_audio_quality: renderer_state.max_audio_quality,
|
||||||
|
loop_mode: renderer_state.loop_mode.or(session_loop_mode),
|
||||||
|
shuffle_mode: renderer_state.shuffle_mode,
|
||||||
|
updated_at_ms: renderer_state.updated_at_ms,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use qconnect_core::QConnectQueueState;
|
use qconnect_core::{QConnectQueueState, QConnectRendererState};
|
||||||
|
|
||||||
use crate::session::{
|
use crate::session::{
|
||||||
QconnectFileAudioQualitySnapshot, QconnectSessionRendererState, QconnectSessionState,
|
QconnectFileAudioQualitySnapshot, QconnectSessionRendererState, QconnectSessionState,
|
||||||
@@ -94,3 +94,31 @@ pub fn ensure_session_renderer_state(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cache the queue_item/track ids + playing_state derived from a renderer
|
||||||
|
/// snapshot into the accumulator, so subsequent outbound reports and visible
|
||||||
|
/// projections reuse them. Pure mutation; relocated from the Tauri adapter
|
||||||
|
/// (slice 6, Slint port) so both the Tauri and Slint event sinks share one
|
||||||
|
/// definition. Byte-identical behavior.
|
||||||
|
pub fn cache_renderer_snapshot(
|
||||||
|
state: &mut QconnectRemoteSyncState,
|
||||||
|
renderer_snapshot: &QConnectRendererState,
|
||||||
|
) {
|
||||||
|
state.last_renderer_queue_item_id = renderer_snapshot
|
||||||
|
.current_track
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| item.queue_item_id);
|
||||||
|
state.last_renderer_next_queue_item_id = renderer_snapshot
|
||||||
|
.next_track
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| item.queue_item_id);
|
||||||
|
state.last_renderer_track_id = renderer_snapshot
|
||||||
|
.current_track
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| item.track_id);
|
||||||
|
state.last_renderer_next_track_id = renderer_snapshot
|
||||||
|
.next_track
|
||||||
|
.as_ref()
|
||||||
|
.map(|item| item.track_id);
|
||||||
|
state.last_renderer_playing_state = renderer_snapshot.playing_state;
|
||||||
|
}
|
||||||
@@ -9,14 +9,9 @@ use qconnect_app::{QConnectQueueState, QConnectRendererState};
|
|||||||
use qconnect_core::QueueItem;
|
use qconnect_core::QueueItem;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::queue_resolution::{
|
use super::queue_resolution::{find_cursor_index_by_queue_item_id, ordered_queue_cursors};
|
||||||
find_cursor_index_by_queue_item_id, normalize_current_queue_item_id_from_queue_state,
|
|
||||||
ordered_queue_cursors, QconnectOrderedQueueCursor,
|
|
||||||
};
|
|
||||||
use super::transport::default_qconnect_device_info;
|
use super::transport::default_qconnect_device_info;
|
||||||
use super::{
|
use super::{QconnectQueueVersionPayload, QconnectVisibleQueueProjection};
|
||||||
QconnectQueueVersionPayload, QconnectRemoteSyncState, QconnectVisibleQueueProjection,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Session topology types now live in the frontend-agnostic `qconnect_app::session`
|
/// Session topology types now live in the frontend-agnostic `qconnect_app::session`
|
||||||
/// module (slice 2+4). Re-exported here so existing `super::session::…` /
|
/// module (slice 2+4). Re-exported here so existing `super::session::…` /
|
||||||
@@ -31,27 +26,21 @@ pub(super) use qconnect_app::{
|
|||||||
ensure_session_renderer_state, is_local_renderer_active, is_peer_renderer_active,
|
ensure_session_renderer_state, is_local_renderer_active, is_peer_renderer_active,
|
||||||
renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
|
renderer_allows_remote_volume, QconnectFileAudioQualitySnapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The session-projection helpers (`queue_item_snapshot_for_cursor`,
|
||||||
|
/// `build_session_renderer_snapshot`) and the renderer-snapshot cache mutator
|
||||||
|
/// (`cache_renderer_snapshot`) also moved to qconnect-app (slice 6, Slint port)
|
||||||
|
/// so the Tauri and Slint event sinks share one definition. Re-exported so the
|
||||||
|
/// existing `super::session::…` / `super::…` callers compile unchanged.
|
||||||
|
pub(super) use qconnect_app::{
|
||||||
|
build_session_renderer_snapshot, cache_renderer_snapshot, queue_item_snapshot_for_cursor,
|
||||||
|
};
|
||||||
// find_unique_renderer_id's sole non-test caller (refresh_local_renderer_id)
|
// find_unique_renderer_id's sole non-test caller (refresh_local_renderer_id)
|
||||||
// moved to qconnect-app, so it is referenced only by the test module now; gate
|
// moved to qconnect-app, so it is referenced only by the test module now; gate
|
||||||
// the re-export to test builds.
|
// the re-export to test builds.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(super) use qconnect_app::find_unique_renderer_id;
|
pub(super) use qconnect_app::find_unique_renderer_id;
|
||||||
|
|
||||||
pub(super) fn queue_item_snapshot_for_cursor(
|
|
||||||
queue: &QConnectQueueState,
|
|
||||||
cursor: QconnectOrderedQueueCursor,
|
|
||||||
) -> Option<QueueItem> {
|
|
||||||
match cursor {
|
|
||||||
QconnectOrderedQueueCursor::Queue(index) => {
|
|
||||||
queue.queue_items.get(index).cloned().map(|mut item| {
|
|
||||||
item.queue_item_id = normalize_current_queue_item_id_from_queue_state(queue, index);
|
|
||||||
item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
QconnectOrderedQueueCursor::Autoplay(index) => queue.autoplay_items.get(index).cloned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub(crate) struct QconnectRendererReportDebugEvent {
|
pub(crate) struct QconnectRendererReportDebugEvent {
|
||||||
pub(super) requested_current_queue_item_id: Option<i32>,
|
pub(super) requested_current_queue_item_id: Option<i32>,
|
||||||
@@ -85,37 +74,6 @@ pub(super) fn resolve_local_identity() -> qconnect_app::LocalIdentity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn build_session_renderer_snapshot(
|
|
||||||
queue: &QConnectQueueState,
|
|
||||||
renderer_state: Option<&QconnectSessionRendererState>,
|
|
||||||
session_loop_mode: Option<i32>,
|
|
||||||
) -> QConnectRendererState {
|
|
||||||
let renderer_state = renderer_state.cloned().unwrap_or_default();
|
|
||||||
let cursors = ordered_queue_cursors(queue);
|
|
||||||
let current_index =
|
|
||||||
find_cursor_index_by_queue_item_id(&cursors, queue, renderer_state.current_queue_item_id);
|
|
||||||
let current_track =
|
|
||||||
current_index.and_then(|index| queue_item_snapshot_for_cursor(queue, cursors[index]));
|
|
||||||
let next_track = current_index
|
|
||||||
.and_then(|index| cursors.get(index + 1).copied())
|
|
||||||
.and_then(|cursor| queue_item_snapshot_for_cursor(queue, cursor));
|
|
||||||
|
|
||||||
QConnectRendererState {
|
|
||||||
active: renderer_state.active,
|
|
||||||
playing_state: renderer_state.playing_state,
|
|
||||||
current_position_ms: renderer_state.current_position_ms,
|
|
||||||
current_track,
|
|
||||||
next_track,
|
|
||||||
volume: renderer_state.volume,
|
|
||||||
volume_delta: None,
|
|
||||||
muted: renderer_state.muted,
|
|
||||||
max_audio_quality: renderer_state.max_audio_quality,
|
|
||||||
loop_mode: renderer_state.loop_mode.or(session_loop_mode),
|
|
||||||
shuffle_mode: renderer_state.shuffle_mode,
|
|
||||||
updated_at_ms: renderer_state.updated_at_ms,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn build_effective_renderer_snapshot(
|
pub(super) fn build_effective_renderer_snapshot(
|
||||||
queue: &QConnectQueueState,
|
queue: &QConnectQueueState,
|
||||||
base_renderer_state: &QConnectRendererState,
|
base_renderer_state: &QConnectRendererState,
|
||||||
@@ -235,25 +193,3 @@ pub(super) fn build_visible_queue_projection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn cache_renderer_snapshot(
|
|
||||||
sync_state: &mut QconnectRemoteSyncState,
|
|
||||||
renderer_snapshot: &QConnectRendererState,
|
|
||||||
) {
|
|
||||||
sync_state.last_renderer_queue_item_id = renderer_snapshot
|
|
||||||
.current_track
|
|
||||||
.as_ref()
|
|
||||||
.map(|item| item.queue_item_id);
|
|
||||||
sync_state.last_renderer_next_queue_item_id = renderer_snapshot
|
|
||||||
.next_track
|
|
||||||
.as_ref()
|
|
||||||
.map(|item| item.queue_item_id);
|
|
||||||
sync_state.last_renderer_track_id = renderer_snapshot
|
|
||||||
.current_track
|
|
||||||
.as_ref()
|
|
||||||
.map(|item| item.track_id);
|
|
||||||
sync_state.last_renderer_next_track_id = renderer_snapshot
|
|
||||||
.next_track
|
|
||||||
.as_ref()
|
|
||||||
.map(|item| item.track_id);
|
|
||||||
sync_state.last_renderer_playing_state = renderer_snapshot.playing_state;
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user