Implémentation du menu déroulant pour les actions d'éléments dans le ServerDrawer
Ajout d'un menu déroulant avec les actions 'Ajouter à la queue' et 'Ajouter après' pour les éléments dans le ServerDrawer. - Remplacement des fonctions handlePlayItem et handleQueueItem par des fonctions plus spécifiques (handlePlayItem, handleAddToQueue, handleAddAfterCurrent) - Ajout d'une gestion du menu déroulant avec ouverture/fermeture - Intégration du composant useRenderers pour les actions de lecture - Mise à jour des styles pour le menu déroulant avec animations - Correction de l'alignement du backdrop sur mobile - Amélioration des transitions et animations du drawer - Ajout de la fonction addAfterCurrent dans l'API et le control point - Support de l'ajout d'éléments après le morceau actuel dans les queues
This commit is contained in:
@@ -4,28 +4,28 @@
|
||||
* - Les snapshots complets proviennent de /renderers/{id}/full
|
||||
* - Les événements SSE ne servent qu'à déclencher un refetch.
|
||||
*/
|
||||
import { ref, reactive, computed, type Ref } from 'vue'
|
||||
import { api } from '../services/pmocontrol/api'
|
||||
import { sse } from '../services/pmocontrol/sse'
|
||||
import { ref, reactive, computed, type Ref } from "vue";
|
||||
import { api } from "../services/pmocontrol/api";
|
||||
import { sse } from "../services/pmocontrol/sse";
|
||||
import type {
|
||||
RendererSummary,
|
||||
RendererState,
|
||||
QueueSnapshot,
|
||||
AttachedPlaylistInfo,
|
||||
FullRendererSnapshot,
|
||||
} from '../services/pmocontrol/types'
|
||||
} from "../services/pmocontrol/types";
|
||||
|
||||
interface RendererSnapshotState {
|
||||
snapshots: Map<string, FullRendererSnapshot>
|
||||
lastSnapshotAt: Map<string, number>
|
||||
lastEventAt: Map<string, number>
|
||||
loadingIds: Set<string>
|
||||
selectedRendererId: string | null
|
||||
snapshots: Map<string, FullRendererSnapshot>;
|
||||
lastSnapshotAt: Map<string, number>;
|
||||
lastEventAt: Map<string, number>;
|
||||
loadingIds: Set<string>;
|
||||
selectedRendererId: string | null;
|
||||
}
|
||||
|
||||
const renderersCache = ref<Map<string, RendererSummary>>(new Map())
|
||||
const RENDERERS_CACHE_MS = 2000
|
||||
const lastRenderersFetch = ref(0)
|
||||
const renderersCache = ref<Map<string, RendererSummary>>(new Map());
|
||||
const RENDERERS_CACHE_MS = 2000;
|
||||
const lastRenderersFetch = ref(0);
|
||||
|
||||
const snapshotState = reactive<RendererSnapshotState>({
|
||||
snapshots: reactive(new Map<string, FullRendererSnapshot>()),
|
||||
@@ -33,23 +33,25 @@ const snapshotState = reactive<RendererSnapshotState>({
|
||||
lastEventAt: reactive(new Map<string, number>()),
|
||||
loadingIds: reactive(new Set<string>()),
|
||||
selectedRendererId: null,
|
||||
})
|
||||
});
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
let sseConnected = false
|
||||
let sseConnected = false;
|
||||
function ensureSSEConnected() {
|
||||
if (sseConnected) return
|
||||
if (sseConnected) return;
|
||||
|
||||
sse.onRendererEvent((event) => {
|
||||
const rendererId = event.renderer_id
|
||||
const timestamp = Date.parse(event.timestamp ?? '') || Date.now()
|
||||
const rendererId = event.renderer_id;
|
||||
const timestamp = Date.parse(event.timestamp ?? "") || Date.now();
|
||||
|
||||
// Gérer les événements Online/Offline différemment
|
||||
if (event.type === 'online') {
|
||||
if (event.type === "online") {
|
||||
// Nouveau renderer découvert
|
||||
console.log(`[useRenderers] Renderer ${rendererId} (${event.friendly_name}) est maintenant en ligne`)
|
||||
console.log(
|
||||
`[useRenderers] Renderer ${rendererId} (${event.friendly_name}) est maintenant en ligne`,
|
||||
);
|
||||
|
||||
// Ajouter au cache avec les infos disponibles
|
||||
// Note: on n'a pas toutes les infos (capabilities, protocol) donc on fetch ensuite
|
||||
@@ -57,7 +59,7 @@ function ensureSSEConnected() {
|
||||
id: rendererId,
|
||||
friendly_name: event.friendly_name,
|
||||
model_name: event.model_name,
|
||||
protocol: 'upnp', // Valeur par défaut, sera mise à jour par le fetch
|
||||
protocol: "upnp", // Valeur par défaut, sera mise à jour par le fetch
|
||||
capabilities: {
|
||||
has_avtransport: false,
|
||||
has_avtransport_set_next: false,
|
||||
@@ -72,187 +74,200 @@ function ensureSSEConnected() {
|
||||
has_oh_radio: false,
|
||||
},
|
||||
online: true,
|
||||
}
|
||||
renderersCache.value.set(rendererId, renderer)
|
||||
};
|
||||
renderersCache.value.set(rendererId, renderer);
|
||||
|
||||
// Fetch la liste complète pour avoir les bonnes infos
|
||||
void fetchRenderers(true)
|
||||
void fetchRenderers(true);
|
||||
|
||||
// Fetch le snapshot complet pour ce renderer
|
||||
void fetchRendererSnapshot(rendererId, { force: true })
|
||||
return
|
||||
void fetchRendererSnapshot(rendererId, { force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'offline') {
|
||||
if (event.type === "offline") {
|
||||
// Renderer déconnecté
|
||||
console.log(`[useRenderers] Renderer ${rendererId} est maintenant hors ligne`)
|
||||
console.log(
|
||||
`[useRenderers] Renderer ${rendererId} est maintenant hors ligne`,
|
||||
);
|
||||
|
||||
// Marquer comme offline dans le cache
|
||||
const renderer = renderersCache.value.get(rendererId)
|
||||
const renderer = renderersCache.value.get(rendererId);
|
||||
if (renderer) {
|
||||
renderer.online = false
|
||||
renderersCache.value.set(rendererId, renderer)
|
||||
renderer.online = false;
|
||||
renderersCache.value.set(rendererId, renderer);
|
||||
}
|
||||
|
||||
// Supprimer le snapshot (il n'est plus valide)
|
||||
snapshotState.snapshots.delete(rendererId)
|
||||
snapshotState.lastSnapshotAt.delete(rendererId)
|
||||
snapshotState.lastEventAt.delete(rendererId)
|
||||
return
|
||||
snapshotState.snapshots.delete(rendererId);
|
||||
snapshotState.lastSnapshotAt.delete(rendererId);
|
||||
snapshotState.lastEventAt.delete(rendererId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour les autres événements, comportement existant
|
||||
snapshotState.lastEventAt.set(rendererId, timestamp)
|
||||
const lastSnapshot = snapshotState.lastSnapshotAt.get(rendererId) ?? 0
|
||||
snapshotState.lastEventAt.set(rendererId, timestamp);
|
||||
const lastSnapshot = snapshotState.lastSnapshotAt.get(rendererId) ?? 0;
|
||||
if (!snapshotState.snapshots.has(rendererId) || timestamp > lastSnapshot) {
|
||||
void fetchRendererSnapshot(rendererId, { force: true })
|
||||
void fetchRendererSnapshot(rendererId, { force: true });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
sseConnected = true
|
||||
sseConnected = true;
|
||||
}
|
||||
|
||||
const allRenderers = computed(() => Array.from(renderersCache.value.values()))
|
||||
const onlineRenderers = computed(() => allRenderers.value.filter((r) => r.online))
|
||||
const allSnapshots = computed(() => Array.from(snapshotState.snapshots.values()))
|
||||
const allRenderers = computed(() => Array.from(renderersCache.value.values()));
|
||||
const onlineRenderers = computed(() =>
|
||||
allRenderers.value.filter((r) => r.online),
|
||||
);
|
||||
const allSnapshots = computed(() =>
|
||||
Array.from(snapshotState.snapshots.values()),
|
||||
);
|
||||
const playingRenderers = computed(() =>
|
||||
allSnapshots.value
|
||||
.filter((snapshot) => snapshot.state.transport_state === 'PLAYING')
|
||||
.filter((snapshot) => snapshot.state.transport_state === "PLAYING")
|
||||
.map((snapshot) => snapshot.state),
|
||||
)
|
||||
);
|
||||
|
||||
function getRendererById(id: string) {
|
||||
return renderersCache.value.get(id)
|
||||
return renderersCache.value.get(id);
|
||||
}
|
||||
|
||||
function getSnapshotById(id: string) {
|
||||
return snapshotState.snapshots.get(id) ?? null
|
||||
return snapshotState.snapshots.get(id) ?? null;
|
||||
}
|
||||
|
||||
function getStateById(id: string): RendererState | null {
|
||||
return snapshotState.snapshots.get(id)?.state ?? null
|
||||
return snapshotState.snapshots.get(id)?.state ?? null;
|
||||
}
|
||||
|
||||
function getQueueById(id: string): QueueSnapshot | null {
|
||||
return snapshotState.snapshots.get(id)?.queue ?? null
|
||||
return snapshotState.snapshots.get(id)?.queue ?? null;
|
||||
}
|
||||
|
||||
function getBindingById(id: string): AttachedPlaylistInfo | null {
|
||||
return snapshotState.snapshots.get(id)?.binding ?? null
|
||||
return snapshotState.snapshots.get(id)?.binding ?? null;
|
||||
}
|
||||
|
||||
function isSnapshotLoading(id: string) {
|
||||
return snapshotState.loadingIds.has(id)
|
||||
return snapshotState.loadingIds.has(id);
|
||||
}
|
||||
|
||||
function selectRenderer(id: string | null) {
|
||||
snapshotState.selectedRendererId = id
|
||||
snapshotState.selectedRendererId = id;
|
||||
}
|
||||
|
||||
async function fetchRenderers(force = false) {
|
||||
ensureSSEConnected()
|
||||
ensureSSEConnected();
|
||||
|
||||
const now = Date.now()
|
||||
const now = Date.now();
|
||||
if (!force && now - lastRenderersFetch.value < RENDERERS_CACHE_MS) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const data = await api.getRenderers()
|
||||
renderersCache.value = new Map(data.map((renderer) => [renderer.id, renderer]))
|
||||
lastRenderersFetch.value = now
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
const data = await api.getRenderers();
|
||||
renderersCache.value = new Map(
|
||||
data.map((renderer) => [renderer.id, renderer]),
|
||||
);
|
||||
lastRenderersFetch.value = now;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Erreur fetch renderers'
|
||||
console.error('[useRenderers] Erreur fetch:', err)
|
||||
error.value = err instanceof Error ? err.message : "Erreur fetch renderers";
|
||||
console.error("[useRenderers] Erreur fetch:", err);
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRendererSnapshot(rendererId: string, opts?: { force?: boolean }) {
|
||||
ensureSSEConnected()
|
||||
const force = opts?.force ?? false
|
||||
const hasSnapshot = snapshotState.snapshots.has(rendererId)
|
||||
async function fetchRendererSnapshot(
|
||||
rendererId: string,
|
||||
opts?: { force?: boolean },
|
||||
) {
|
||||
ensureSSEConnected();
|
||||
const force = opts?.force ?? false;
|
||||
const hasSnapshot = snapshotState.snapshots.has(rendererId);
|
||||
|
||||
if (!force && hasSnapshot) {
|
||||
const lastSnapshot = snapshotState.lastSnapshotAt.get(rendererId) ?? 0
|
||||
const lastEvent = snapshotState.lastEventAt.get(rendererId) ?? 0
|
||||
const lastSnapshot = snapshotState.lastSnapshotAt.get(rendererId) ?? 0;
|
||||
const lastEvent = snapshotState.lastEventAt.get(rendererId) ?? 0;
|
||||
if (lastEvent <= lastSnapshot) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshotState.loadingIds.has(rendererId)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
snapshotState.loadingIds.add(rendererId)
|
||||
snapshotState.loadingIds.add(rendererId);
|
||||
try {
|
||||
const snapshot = await api.getRendererFullSnapshot(rendererId)
|
||||
snapshotState.snapshots.set(rendererId, snapshot)
|
||||
snapshotState.lastSnapshotAt.set(rendererId, Date.now())
|
||||
const snapshot = await api.getRendererFullSnapshot(rendererId);
|
||||
snapshotState.snapshots.set(rendererId, snapshot);
|
||||
snapshotState.lastSnapshotAt.set(rendererId, Date.now());
|
||||
} catch (err) {
|
||||
console.error(`[useRenderers] Erreur snapshot ${rendererId}:`, err)
|
||||
console.error(`[useRenderers] Erreur snapshot ${rendererId}:`, err);
|
||||
} finally {
|
||||
snapshotState.loadingIds.delete(rendererId)
|
||||
snapshotState.loadingIds.delete(rendererId);
|
||||
}
|
||||
}
|
||||
|
||||
// Transport controls
|
||||
async function play(id: string) {
|
||||
await api.play(id)
|
||||
await api.play(id);
|
||||
}
|
||||
|
||||
async function resumeOrPlayFromQueue(id: string) {
|
||||
const snapshot = snapshotState.snapshots.get(id)
|
||||
const snapshot = snapshotState.snapshots.get(id);
|
||||
if (!snapshot) {
|
||||
throw new Error(`Renderer ${id} non trouvé`)
|
||||
throw new Error(`Renderer ${id} non trouvé`);
|
||||
}
|
||||
|
||||
const state = snapshot.state
|
||||
if (state.transport_state === 'PAUSED') {
|
||||
return play(id)
|
||||
const state = snapshot.state;
|
||||
if (state.transport_state === "PAUSED") {
|
||||
return play(id);
|
||||
}
|
||||
|
||||
if (
|
||||
['STOPPED', 'NO_MEDIA'].includes(state.transport_state) &&
|
||||
["STOPPED", "NO_MEDIA"].includes(state.transport_state) &&
|
||||
snapshot.queue.items.length > 0
|
||||
) {
|
||||
return api.resume(id)
|
||||
return api.resume(id);
|
||||
}
|
||||
|
||||
throw new Error('La file d\'attente est vide. Ajoutez des morceaux avant de démarrer la lecture.')
|
||||
throw new Error(
|
||||
"La file d'attente est vide. Ajoutez des morceaux avant de démarrer la lecture.",
|
||||
);
|
||||
}
|
||||
|
||||
async function pause(id: string) {
|
||||
await api.pause(id)
|
||||
await api.pause(id);
|
||||
}
|
||||
|
||||
async function stop(id: string) {
|
||||
await api.stop(id)
|
||||
await api.stop(id);
|
||||
}
|
||||
|
||||
async function next(id: string) {
|
||||
await api.next(id)
|
||||
await api.next(id);
|
||||
}
|
||||
|
||||
// Volume controls
|
||||
async function setVolume(id: string, volume: number) {
|
||||
await api.setVolume(id, volume)
|
||||
await api.setVolume(id, volume);
|
||||
}
|
||||
|
||||
async function volumeUp(id: string) {
|
||||
await api.volumeUp(id)
|
||||
await api.volumeUp(id);
|
||||
}
|
||||
|
||||
async function volumeDown(id: string) {
|
||||
await api.volumeDown(id)
|
||||
await api.volumeDown(id);
|
||||
}
|
||||
|
||||
async function toggleMute(id: string) {
|
||||
await api.toggleMute(id)
|
||||
await api.toggleMute(id);
|
||||
}
|
||||
|
||||
// Playlist binding
|
||||
@@ -262,11 +277,16 @@ async function attachPlaylist(
|
||||
containerId: string,
|
||||
options?: { autoPlay?: boolean },
|
||||
) {
|
||||
await api.attachPlaylist(rendererId, serverId, containerId, options?.autoPlay ?? false)
|
||||
await api.attachPlaylist(
|
||||
rendererId,
|
||||
serverId,
|
||||
containerId,
|
||||
options?.autoPlay ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
async function detachPlaylist(rendererId: string) {
|
||||
await api.detachPlaylist(rendererId)
|
||||
await api.detachPlaylist(rendererId);
|
||||
}
|
||||
|
||||
async function attachAndPlayPlaylist(
|
||||
@@ -274,20 +294,36 @@ async function attachAndPlayPlaylist(
|
||||
serverId: string,
|
||||
containerId: string,
|
||||
) {
|
||||
await attachPlaylist(rendererId, serverId, containerId, { autoPlay: true })
|
||||
await attachPlaylist(rendererId, serverId, containerId, { autoPlay: true });
|
||||
}
|
||||
|
||||
// Queue content
|
||||
async function playContent(rendererId: string, serverId: string, objectId: string) {
|
||||
await api.playContent(rendererId, serverId, objectId)
|
||||
async function playContent(
|
||||
rendererId: string,
|
||||
serverId: string,
|
||||
objectId: string,
|
||||
) {
|
||||
await api.playContent(rendererId, serverId, objectId);
|
||||
}
|
||||
|
||||
async function addToQueue(rendererId: string, serverId: string, objectId: string) {
|
||||
await api.addToQueue(rendererId, serverId, objectId)
|
||||
async function addToQueue(
|
||||
rendererId: string,
|
||||
serverId: string,
|
||||
objectId: string,
|
||||
) {
|
||||
await api.addToQueue(rendererId, serverId, objectId);
|
||||
}
|
||||
|
||||
async function addAfterCurrent(
|
||||
rendererId: string,
|
||||
serverId: string,
|
||||
objectId: string,
|
||||
) {
|
||||
await api.addAfterCurrent(rendererId, serverId, objectId);
|
||||
}
|
||||
|
||||
export function useRenderers() {
|
||||
ensureSSEConnected()
|
||||
ensureSSEConnected();
|
||||
|
||||
return {
|
||||
loading,
|
||||
@@ -326,23 +362,26 @@ export function useRenderers() {
|
||||
// Queue content
|
||||
playContent,
|
||||
addToQueue,
|
||||
}
|
||||
addAfterCurrent,
|
||||
};
|
||||
}
|
||||
|
||||
export function useRenderer(rendererId: Ref<string>) {
|
||||
ensureSSEConnected()
|
||||
ensureSSEConnected();
|
||||
|
||||
const renderer = computed(() => renderersCache.value.get(rendererId.value))
|
||||
const snapshot = computed(() => snapshotState.snapshots.get(rendererId.value) ?? null)
|
||||
const state = computed(() => snapshot.value?.state ?? null)
|
||||
const queue = computed(() => snapshot.value?.queue ?? null)
|
||||
const binding = computed(() => snapshot.value?.binding ?? null)
|
||||
const renderer = computed(() => renderersCache.value.get(rendererId.value));
|
||||
const snapshot = computed(
|
||||
() => snapshotState.snapshots.get(rendererId.value) ?? null,
|
||||
);
|
||||
const state = computed(() => snapshot.value?.state ?? null);
|
||||
const queue = computed(() => snapshot.value?.queue ?? null);
|
||||
const binding = computed(() => snapshot.value?.binding ?? null);
|
||||
|
||||
async function refresh(force = true) {
|
||||
await Promise.all([
|
||||
fetchRenderers(force),
|
||||
fetchRendererSnapshot(rendererId.value, { force: true }),
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -352,5 +391,5 @@ export function useRenderer(rendererId: Ref<string>) {
|
||||
queue,
|
||||
binding,
|
||||
refresh,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user