Implémentation du shuffle de la queue de lecture

Ajout de la fonctionnalité de shuffle pour mélanger l'ordre des morceaux dans la queue de lecture.

- Implémentation de la méthode shuffle_queue() dans MusicRenderer
- Ajout de l'endpoint REST /api/control/renderers/{renderer_id}/queue/shuffle
- Intégration du bouton shuffle dans l'interface Vue.js
- Centralisation des émissions d'événements SSE dans le MusicRenderer
- Mise à jour de la documentation OpenAPI

Cette fonctionnalité permet de randomiser l'ordre des morceaux dans la queue et de redémarrer la lecture depuis le premier morceau, tout en améliorant l'architecture en centralisant les émissions d'événements.
This commit is contained in:
2026-01-16 08:56:14 +01:00
parent 50bc0570ba
commit 8e9dd7601f
15 changed files with 763 additions and 44 deletions

View File

@@ -0,0 +1,108 @@
<template>
<div class="shuffle-control">
<button
class="shuffle-button"
:class="{ loading: isLoading }"
@click.stop="handleShuffle"
:disabled="isLoading"
title="Mélanger la queue"
>
<Shuffle :size="24" />
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Shuffle } from "lucide-vue-next";
import { api } from "@/services/pmocontrol/api";
const props = defineProps<{
rendererId: string;
}>();
const isLoading = ref(false);
async function handleShuffle() {
if (isLoading.value) return;
isLoading.value = true;
try {
await api.shuffleQueue(props.rendererId);
} catch (error) {
console.error("Erreur lors du shuffle de la queue:", error);
} finally {
isLoading.value = false;
}
}
</script>
<style scoped>
.shuffle-control {
position: relative;
}
.shuffle-button {
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
color: var(--color-text);
}
.shuffle-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.shuffle-button:active:not(:disabled) {
transform: scale(0.95);
}
.shuffle-button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.shuffle-button.loading {
animation: pulse 1s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@media (prefers-color-scheme: dark) {
.shuffle-button {
background: rgba(255, 255, 255, 0.15);
}
.shuffle-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.25);
}
}
@media (max-width: 768px) {
.shuffle-button {
width: 36px;
height: 36px;
}
}
</style>

View File

@@ -3,6 +3,7 @@ import { computed } from "vue";
import { Server, Music2 } from "lucide-vue-next";
import StatusBadge from "@/components/pmocontrol/StatusBadge.vue";
import TimerControl from "@/components/pmocontrol/TimerControl.vue";
import ShuffleControl from "@/components/pmocontrol/ShuffleControl.vue";
import type {
RendererSummary,
RendererState,
@@ -113,8 +114,9 @@ function handleRendererDrawerClick() {
</div>
</div>
<!-- Sleep Timer (si un renderer est actif) -->
<div v-if="activeRenderer" class="timer-section">
<!-- Shuffle et Sleep Timer (si un renderer est actif) -->
<div v-if="activeRenderer" class="controls-section">
<ShuffleControl :renderer-id="activeRenderer.id" />
<TimerControl :renderer-id="activeRenderer.id" />
</div>
@@ -342,6 +344,14 @@ function handleRendererDrawerClick() {
border: 1px solid rgba(239, 68, 68, 0.3);
}
/* Controls section (shuffle + timer) */
.controls-section {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex-shrink: 0;
}
/* Mobile responsive */
@media (max-width: 768px) {
.bottom-bar {

View File

@@ -197,6 +197,19 @@ class PMOControlAPI {
);
}
/**
* Mélange la queue de lecture et démarre au premier morceau
* POST /api/control/renderers/{id}/queue/shuffle
*/
async shuffleQueue(id: string): Promise<SuccessResponse> {
return this.request<SuccessResponse>(
`/renderers/${encodeURIComponent(id)}/queue/shuffle`,
{
method: "POST",
},
);
}
// ============================================================================
// CONTRÔLE VOLUME
// ============================================================================