✨ 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
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -2,5 +2,9 @@
|
||||
"name": "pmomusic",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pmomusic"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
pmoapp/webapp/package-lock.json
generated
6
pmoapp/webapp/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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(
|
||||
<Radio :size="16" />
|
||||
<span class="stream-text"> Web Radio </span>
|
||||
</div>
|
||||
|
||||
<!-- Indicateur de mise à jour de queue -->
|
||||
<div v-if="queueRefreshing" class="refresh-indicator">
|
||||
<RefreshCw :size="14" class="refresh-icon" />
|
||||
<span class="refresh-text"> Mise à jour... </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -20,6 +20,7 @@ interface RendererSnapshotState {
|
||||
lastSnapshotAt: Map<string, number>;
|
||||
lastEventAt: Map<string, number>;
|
||||
loadingIds: Set<string>;
|
||||
queueRefreshingIds: Set<string>;
|
||||
selectedRendererId: string | null;
|
||||
}
|
||||
|
||||
@@ -32,6 +33,7 @@ const snapshotState = reactive<RendererSnapshotState>({
|
||||
lastSnapshotAt: reactive(new Map<string, number>()),
|
||||
lastEventAt: reactive(new Map<string, number>()),
|
||||
loadingIds: reactive(new Set<string>()),
|
||||
queueRefreshingIds: reactive(new Set<string>()),
|
||||
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<string>) {
|
||||
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<string>) {
|
||||
queue,
|
||||
binding,
|
||||
isStream,
|
||||
queueRefreshing,
|
||||
refresh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -434,6 +434,9 @@ pub enum RendererEvent {
|
||||
id: DeviceId,
|
||||
queue_length: usize,
|
||||
},
|
||||
QueueRefreshing {
|
||||
id: DeviceId,
|
||||
},
|
||||
BindingChanged {
|
||||
id: DeviceId,
|
||||
binding: Option<PlaylistBinding>,
|
||||
|
||||
@@ -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::<Vec<_>>(),
|
||||
current_didl_ids = ?snapshot.items.iter().map(|i| i.didl_id.as_str()).collect::<Vec<_>>(),
|
||||
desired_uris = ?items.iter().map(|i| i.uri.as_str()).collect::<Vec<_>>(),
|
||||
desired_didl_ids = ?items.iter().map(|i| i.didl_id.as_str()).collect::<Vec<_>>(),
|
||||
"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();
|
||||
|
||||
@@ -79,6 +79,10 @@ pub enum RendererEventPayload {
|
||||
queue_length: usize,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
},
|
||||
QueueRefreshing {
|
||||
renderer_id: String,
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
},
|
||||
BindingChanged {
|
||||
renderer_id: String,
|
||||
server_id: Option<String>,
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user