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:
vicrodh committed 2026-06-03 00:11:21 -06:00
1 parent 8e61568dc0
commit 3861efa79f
4 files changed
+115 -83

No files matched your search

+9 -7
View File
@@ -22,12 +22,13 @@ pub use startup::{compute_effective_startup, QconnectStartupMode};
pub use error::QconnectAppError;
pub use events::{NoOpEventSink, QconnectAppEvent, QconnectEventSink};
pub use session::{
compute_connection_state, deferred_join_reason, find_unique_renderer_id,
is_local_renderer_active, is_peer_renderer_active, normalize_active_renderer_id,
quality_from_max_audio_quality, refresh_local_renderer_id, renderer_allows_remote_volume,
should_arm_renderer_watchdog, should_reask_queue_state, ConnectionDecision, LocalIdentity,
QconnectFileAudioQualitySnapshot, QconnectLifecycleState, QconnectRendererInfo,
QconnectSessionRendererState, QconnectSessionState, RendererStatus, ServerActiveState,
build_session_renderer_snapshot, compute_connection_state, deferred_join_reason,
find_unique_renderer_id, is_local_renderer_active, is_peer_renderer_active,
normalize_active_renderer_id, quality_from_max_audio_quality, queue_item_snapshot_for_cursor,
refresh_local_renderer_id, renderer_allows_remote_volume, should_arm_renderer_watchdog,
should_reask_queue_state, ConnectionDecision, LocalIdentity, QconnectFileAudioQualitySnapshot,
QconnectLifecycleState, QconnectRendererInfo, QconnectSessionRendererState,
QconnectSessionState, RendererStatus, ServerActiveState,
JOIN_SESSION_REASON_CONTROLLER_REQUEST, JOIN_SESSION_REASON_RECONNECTION,
QCONNECT_RENDERER_LOST_TIMEOUT_MS,
};
@@ -44,5 +45,6 @@ pub use qconnect_protocol::{QueueCommandType, RendererReport, RendererReportType
pub use renderer_engine::QconnectRendererEngine;
pub use state::QconnectRuntimeState;
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,
};
+66
View File
@@ -6,8 +6,14 @@
//! byte-for-byte identical to the prior Tauri-side definitions.
use qbz_models::Quality;
use qconnect_core::{QConnectQueueState, QConnectRendererState, QueueItem};
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
/// asked for (`running`) separately from what the transport currently has
/// (`Connecting`/`Connected`/`Reconnecting`). Without this distinction the UI
@@ -360,6 +366,66 @@ pub fn find_unique_renderer_id(
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)]
mod tests {
use super::*;
+29 -1
View File
@@ -20,7 +20,7 @@
use std::collections::HashMap;
use std::time::Instant;
use qconnect_core::QConnectQueueState;
use qconnect_core::{QConnectQueueState, QConnectRendererState};
use crate::session::{
QconnectFileAudioQualitySnapshot, QconnectSessionRendererState, QconnectSessionState,
@@ -94,3 +94,31 @@ pub fn ensure_session_renderer_state(
..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;
}