Optimize UPnP discovery and server response handling

Refactor UPnP discovery to use a thread pool for fetching device descriptions, improving performance and responsiveness. Also, use spawn_blocking in server endpoints to prevent blocking the async runtime when performing sync operations. Additionally, add read timeout to SSDP socket and handle WouldBlock errors gracefully.

Refactorisation du contrôle des renderers et amélioration de l'interface utilisateur

Refactorisation complète du composant VolumeControl avec gestion d'erreur améliorée et debounce.

Migration de l'interface de contrôle des renderers :
- Suppression de l'ancienne barre d'onglets en bas
- Intégration d'une nouvelle barre d'infos en bas avec les détails du renderer actif
- Création d'un nouveau drawer pour la sélection des renderers
- Déplacement des informations du renderer de l'onglet vers la barre d'infos

Améliorations UI/UX :
- Nouvelle interface de contrôle des renderers dans le drawer avec boutons de lecture/pause
- Mise à jour des styles et animations pour une meilleure expérience utilisateur
- Adaptation responsive pour les appareils mobiles
This commit is contained in:
2026-01-09 19:59:04 +01:00
parent 256495b483
commit 58ac28489c
8 changed files with 1452 additions and 800 deletions

View File

@@ -1,286 +1,292 @@
<script setup lang="ts">
import { ref, watch, toRef } from 'vue'
import { useRenderer, useRenderers } from '@/composables/useRenderers'
import { useUIStore } from '@/stores/ui'
import { Volume2, VolumeX } from 'lucide-vue-next'
import { ref, watch, toRef } from "vue";
import { useRenderer, useRenderers } from "@/composables/useRenderers";
import { useUIStore } from "@/stores/ui";
import { Volume2, VolumeX } from "lucide-vue-next";
const props = defineProps<{
rendererId: string
}>()
rendererId: string;
}>();
const { state } = useRenderer(toRef(props, 'rendererId'))
const { setVolume, toggleMute } = useRenderers()
const uiStore = useUIStore()
const { state } = useRenderer(toRef(props, "rendererId"));
const { setVolume, toggleMute } = useRenderers();
const uiStore = useUIStore();
const localVolume = ref(state.value?.volume ?? 50)
const localVolume = ref(state.value?.volume ?? 50);
// Synchroniser localVolume avec le state
watch(() => state.value?.volume, (newVolume) => {
if (newVolume !== undefined && newVolume !== null) {
localVolume.value = newVolume
}
}, { immediate: true })
watch(
() => state.value?.volume,
(newVolume) => {
if (newVolume !== undefined && newVolume !== null) {
localVolume.value = newVolume;
}
},
{ immediate: true },
);
// Debounce pour le slider
let debounceTimer: number | null = null
let debounceTimer: number | null = null;
function handleVolumeChange(event: Event) {
const target = event.target as HTMLInputElement
localVolume.value = parseInt(target.value, 10)
const target = event.target as HTMLInputElement;
localVolume.value = parseInt(target.value, 10);
// Debounce: attendre 300ms avant d'envoyer à l'API
if (debounceTimer !== null) {
clearTimeout(debounceTimer)
}
debounceTimer = window.setTimeout(async () => {
try {
await setVolume(props.rendererId, localVolume.value)
} catch (error) {
uiStore.notifyError(`Impossible de régler le volume: ${error instanceof Error ? error.message : 'Erreur inconnue'}`)
// Debounce: attendre 300ms avant d'envoyer à l'API
if (debounceTimer !== null) {
clearTimeout(debounceTimer);
}
debounceTimer = null
}, 300)
debounceTimer = window.setTimeout(async () => {
try {
await setVolume(props.rendererId, localVolume.value);
} catch (error) {
uiStore.notifyError(
`Impossible de régler le volume: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
);
}
debounceTimer = null;
}, 300);
}
async function handleToggleMute() {
try {
await toggleMute(props.rendererId)
} catch (error) {
uiStore.notifyError(`Impossible de basculer le mode muet: ${error instanceof Error ? error.message : 'Erreur inconnue'}`)
}
try {
await toggleMute(props.rendererId);
} catch (error) {
uiStore.notifyError(
`Impossible de basculer le mode muet: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
);
}
}
</script>
<template>
<div class="volume-control">
<button
class="btn btn-icon"
@click="handleToggleMute"
:title="state?.mute ? 'Réactiver le son' : 'Couper le son'"
>
<VolumeX v-if="state?.mute" :size="20" />
<Volume2 v-else :size="20" />
</button>
<div class="volume-control">
<button
class="btn btn-icon"
@click="handleToggleMute"
:title="state?.mute ? 'Réactiver le son' : 'Couper le son'"
>
<VolumeX v-if="state?.mute" :size="20" />
<Volume2 v-else :size="20" />
</button>
<input
type="range"
min="0"
max="100"
:value="localVolume"
@input="handleVolumeChange"
class="volume-slider"
:disabled="state?.mute ?? false"
/>
<input
type="range"
min="0"
max="100"
:value="localVolume"
@input="handleVolumeChange"
class="volume-slider"
:disabled="state?.mute ?? false"
/>
<span class="volume-value">{{ localVolume }}</span>
</div>
<span class="volume-value">{{ localVolume }}</span>
</div>
</template>
<style scoped>
.volume-control {
display: flex;
align-items: center;
gap: var(--spacing-md);
width: 100%;
max-width: 300px;
padding: var(--spacing-md) var(--spacing-lg);
border-radius: 20px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
border-radius: 20px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
@media (prefers-color-scheme: dark) {
.volume-control {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.12);
}
.volume-control {
background: rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.12);
}
}
/* Bouton mute avec effet glass */
.volume-control .btn-icon {
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
transition: all 0.3s ease;
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
transition: all 0.3s ease;
}
.volume-control .btn-icon:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.volume-control .btn-icon:active {
transform: scale(1.0);
transform: scale(1);
}
/* Slider avec effet glass */
.volume-slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 8px;
height: 8px;
outline: none;
transition: all 0.3s ease;
flex: 1;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 8px;
height: 8px;
outline: none;
transition: all 0.3s ease;
}
.volume-slider:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.25);
}
/* Thumb Webkit (Chrome, Safari) */
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
border: 2px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
border: 2px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
transform: scale(1.2);
box-shadow:
0 6px 16px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.volume-slider::-webkit-slider-thumb:active {
transform: scale(1.1);
transform: scale(1.1);
}
/* Thumb Mozilla */
.volume-slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
border: 2px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.5);
border: 2px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.2);
transform: scale(1.2);
}
.volume-value {
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text);
min-width: 2.5rem;
text-align: right;
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text);
min-width: 2.5rem;
text-align: right;
}
.volume-slider:disabled {
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.1);
opacity: 0.5;
cursor: not-allowed;
background: rgba(255, 255, 255, 0.1);
}
.volume-slider:disabled::-webkit-slider-thumb {
cursor: not-allowed;
background: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
background: rgba(255, 255, 255, 0.5);
}
.volume-slider:disabled::-moz-range-thumb {
cursor: not-allowed;
background: rgba(255, 255, 255, 0.5);
cursor: not-allowed;
background: rgba(255, 255, 255, 0.5);
}
@media (prefers-color-scheme: dark) {
.volume-control .btn-icon {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.15);
}
.volume-control .btn-icon {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.15);
}
.volume-control .btn-icon:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.25);
}
.volume-control .btn-icon:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.25);
}
.volume-slider {
background: rgba(255, 255, 255, 0.15);
}
.volume-slider {
background: rgba(255, 255, 255, 0.15);
}
}
/* Fallback pour navigateurs sans backdrop-filter */
@supports not (backdrop-filter: blur(20px)) {
.volume-control {
background: rgba(255, 255, 255, 0.95);
}
.volume-slider {
background: rgba(200, 200, 200, 0.8);
}
@media (prefers-color-scheme: dark) {
.volume-control {
background: rgba(0, 0, 0, 0.95);
background: rgba(255, 255, 255, 0.95);
}
.volume-slider {
background: rgba(100, 100, 100, 0.8);
background: rgba(200, 200, 200, 0.8);
}
@media (prefers-color-scheme: dark) {
.volume-control {
background: rgba(0, 0, 0, 0.95);
}
.volume-slider {
background: rgba(100, 100, 100, 0.8);
}
}
}
}
/* Mode kiosque - compactage pour petites hauteurs (800x600) */
@media (max-height: 700px) and (orientation: landscape) {
.volume-control {
padding: var(--spacing-sm) var(--spacing-md);
gap: var(--spacing-sm);
}
.volume-control {
padding: var(--spacing-sm) var(--spacing-md);
gap: var(--spacing-sm);
}
.volume-control .btn-icon {
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
}
.volume-control .btn-icon {
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
}
.volume-slider {
height: 6px;
}
.volume-slider {
height: 6px;
}
.volume-slider::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
.volume-slider::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
.volume-slider::-moz-range-thumb {
width: 18px;
height: 18px;
}
.volume-slider::-moz-range-thumb {
width: 18px;
height: 18px;
}
.volume-value {
font-size: 11px;
min-width: 2rem;
}
.volume-value {
font-size: 11px;
min-width: 2rem;
}
}
</style>