From 2fd4c84e1ab9827823e425b588cd3b1d2d681dd8 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Fri, 3 Apr 2026 20:45:35 +0200 Subject: [PATCH] :sparkles: Add queue refreshing indicator to UI - Introduce `QueueRefreshing` event type for dedicated queue refresh state - Add reactive tracking of refreshing queues via new `queueRefreshingIds` Set in useRenderers composable - Display visual indicator (spinning icon + text) when queue is refreshing in QueueViewer component - Update `picomatch` dependency to v4.0.4 (patch) - Minor cleanup in package-lock.json files --- package-lock.json | 6 ++- pmoapp/webapp/package-lock.json | 6 +-- .../src/components/pmocontrol/QueueViewer.vue | 37 ++++++++++++++++++- pmoapp/webapp/src/composables/useRenderers.ts | 16 ++++++++ .../webapp/src/services/pmocontrol/types.ts | 5 +++ pmocontrol/src/control_point.rs | 5 +-- pmocontrol/src/model.rs | 3 ++ pmocontrol/src/queue/openhome.rs | 11 ++++++ pmocontrol/src/sse.rs | 8 ++++ 9 files changed, 88 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index a13a2658..254f4e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2,5 +2,9 @@ "name": "pmomusic", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "name": "pmomusic" + } + } } diff --git a/pmoapp/webapp/package-lock.json b/pmoapp/webapp/package-lock.json index 220494aa..1c4d70b6 100644 --- a/pmoapp/webapp/package-lock.json +++ b/pmoapp/webapp/package-lock.json @@ -1458,9 +1458,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue b/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue index 6388ce12..49bd8e82 100644 --- a/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue +++ b/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue @@ -2,7 +2,7 @@ import { computed, ref, watch, nextTick, toRef } from "vue"; import { useRenderer } from "@/composables/useRenderers"; import QueueItem from "./QueueItem.vue"; -import { Link, Radio } from "lucide-vue-next"; +import { Link, Radio, RefreshCw } from "lucide-vue-next"; import type { QueueItem as QueueItemType } from "@/services/pmocontrol/types"; const props = defineProps<{ @@ -13,7 +13,7 @@ const emit = defineEmits<{ clickItem: [item: QueueItemType]; }>(); -const { queue, binding, isStream } = useRenderer(toRef(props, "rendererId")); +const { queue, binding, isStream, queueRefreshing } = useRenderer(toRef(props, "rendererId")); const isAttached = computed(() => !!binding.value); @@ -72,6 +72,12 @@ watch( Web Radio + + +
+ + Mise à jour... +
@@ -162,6 +168,33 @@ watch( font-size: var(--text-xs); } +.refresh-indicator { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); + background-color: var(--color-bg-secondary); + color: var(--color-text-secondary); + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 500; + border: 1px solid var(--color-border); + width: fit-content; +} + +.refresh-icon { + animation: spin 1s linear infinite; +} + +.refresh-text { + font-size: var(--text-xs); +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + .queue-list { flex: 1; overflow-y: auto; diff --git a/pmoapp/webapp/src/composables/useRenderers.ts b/pmoapp/webapp/src/composables/useRenderers.ts index a9947164..e607e8d9 100644 --- a/pmoapp/webapp/src/composables/useRenderers.ts +++ b/pmoapp/webapp/src/composables/useRenderers.ts @@ -20,6 +20,7 @@ interface RendererSnapshotState { lastSnapshotAt: Map; lastEventAt: Map; loadingIds: Set; + queueRefreshingIds: Set; selectedRendererId: string | null; } @@ -32,6 +33,7 @@ const snapshotState = reactive({ lastSnapshotAt: reactive(new Map()), lastEventAt: reactive(new Map()), loadingIds: reactive(new Set()), + queueRefreshingIds: reactive(new Set()), selectedRendererId: null, }); @@ -190,8 +192,13 @@ function ensureSSEConnected() { }); break; + case "queue_refreshing": + snapshotState.queueRefreshingIds.add(rendererId); + break; + case "queue_updated": snapshot.state.queue_len = event.queue_length; + snapshotState.queueRefreshingIds.delete(rendererId); // Pour la queue complète, on doit refetch void fetchRendererSnapshot(rendererId, { force: true }); break; @@ -268,6 +275,10 @@ function isSnapshotLoading(id: string) { return snapshotState.loadingIds.has(id); } +function isQueueRefreshing(id: string) { + return snapshotState.queueRefreshingIds.has(id); +} + function selectRenderer(id: string | null) { snapshotState.selectedRendererId = id; } @@ -454,6 +465,7 @@ export function useRenderers() { getQueueById, getBindingById, isSnapshotLoading, + isQueueRefreshing, selectRenderer, snapshotState, // Fetchers @@ -492,6 +504,9 @@ export function useRenderer(rendererId: Ref) { const queue = computed(() => snapshot.value?.queue ?? null); const binding = computed(() => snapshot.value?.binding ?? null); const isStream = computed(() => snapshot.value?.is_stream ?? false); + const queueRefreshing = computed(() => + snapshotState.queueRefreshingIds.has(rendererId.value), + ); async function refresh(force = true) { await Promise.all([ @@ -507,6 +522,7 @@ export function useRenderer(rendererId: Ref) { queue, binding, isStream, + queueRefreshing, refresh, }; } diff --git a/pmoapp/webapp/src/services/pmocontrol/types.ts b/pmoapp/webapp/src/services/pmocontrol/types.ts index 527b57ab..3c8b1daf 100644 --- a/pmoapp/webapp/src/services/pmocontrol/types.ts +++ b/pmoapp/webapp/src/services/pmocontrol/types.ts @@ -197,6 +197,11 @@ export type RendererEventPayload = queue_length: number; timestamp: string; } + | { + type: "queue_refreshing"; + renderer_id: string; + timestamp: string; + } | { type: "binding_changed"; renderer_id: string; diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index 9a5e0dc3..877117d2 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -1585,10 +1585,9 @@ fn refresh_attached_queue_for( return Ok(()); } - // Step 3: Notify UI that the renderer is loading (Transitioning state) - event_bus.broadcast(RendererEvent::StateChanged { + // Step 3: Notify UI that the queue is being refreshed (dedicated event, no state change) + event_bus.broadcast(RendererEvent::QueueRefreshing { id: renderer_id.clone(), - state: PlaybackState::Transitioning, }); // Step 4: Browse container (renamed from Step 3 for clarity) diff --git a/pmocontrol/src/model.rs b/pmocontrol/src/model.rs index 1bd6dd7c..9686d3a9 100644 --- a/pmocontrol/src/model.rs +++ b/pmocontrol/src/model.rs @@ -434,6 +434,9 @@ pub enum RendererEvent { id: DeviceId, queue_length: usize, }, + QueueRefreshing { + id: DeviceId, + }, BindingChanged { id: DeviceId, binding: Option, diff --git a/pmocontrol/src/queue/openhome.rs b/pmocontrol/src/queue/openhome.rs index dc34859a..d7047d5a 100644 --- a/pmocontrol/src/queue/openhome.rs +++ b/pmocontrol/src/queue/openhome.rs @@ -647,6 +647,17 @@ impl OpenHomeQueue { let snapshot = self.queue_snapshot()?; let current_track_ids = self.track_ids()?; + debug!( + renderer = self.renderer_id.0.as_str(), + current_count = snapshot.items.len(), + desired_count = items.len(), + current_uris = ?snapshot.items.iter().map(|i| i.uri.as_str()).collect::>(), + current_didl_ids = ?snapshot.items.iter().map(|i| i.didl_id.as_str()).collect::>(), + desired_uris = ?items.iter().map(|i| i.uri.as_str()).collect::>(), + desired_didl_ids = ?items.iter().map(|i| i.didl_id.as_str()).collect::>(), + "LCS input: current vs desired items" + ); + let (keep_current, keep_desired) = lcs_flags(&snapshot.items, &items); let items_to_keep = keep_current.iter().filter(|&&k| k).count(); diff --git a/pmocontrol/src/sse.rs b/pmocontrol/src/sse.rs index f2a1c79d..d5448592 100644 --- a/pmocontrol/src/sse.rs +++ b/pmocontrol/src/sse.rs @@ -79,6 +79,10 @@ pub enum RendererEventPayload { queue_length: usize, timestamp: chrono::DateTime, }, + QueueRefreshing { + renderer_id: String, + timestamp: chrono::DateTime, + }, BindingChanged { renderer_id: String, server_id: Option, @@ -214,6 +218,10 @@ fn renderer_event_to_payload( queue_length, timestamp, }, + RendererEvent::QueueRefreshing { id } => RendererEventPayload::QueueRefreshing { + renderer_id: id.0, + timestamp, + }, RendererEvent::BindingChanged { id, binding } => RendererEventPayload::BindingChanged { renderer_id: id.0, server_id: binding.as_ref().map(|b| b.server_id.0.clone()),