- Fix renderer filtering logic to normalize `udn` and id by stripping 'uuid:' prefix
- Replace fragile model_name-based detection with reliable friendly-name check for WebRenderers ('Web Audio – ' prefix)
- Add `manufacturer` field to renderer types and registry
- Update badge count in UnifiedControlView.vue to use filtered renderers
904 lines
27 KiB
Vue
904 lines
27 KiB
Vue
<script setup lang="ts">
|
||
import { computed, watch, ref, onUnmounted } from "vue";
|
||
import {
|
||
X,
|
||
Music2,
|
||
Circle,
|
||
Settings,
|
||
Play,
|
||
Pause,
|
||
MoreVertical,
|
||
ArrowRightLeft,
|
||
} from "lucide-vue-next";
|
||
import { useRenderers } from "@/composables/useRenderers";
|
||
import { useWebRenderer } from "@/composables/useWebRenderer";
|
||
import { useRouter } from "vue-router";
|
||
import StatusBadge from "@/components/pmocontrol/StatusBadge.vue";
|
||
import type { RendererSummary } from "@/services/pmocontrol/types";
|
||
import { api } from "@/services/pmocontrol/api";
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean; // v-model pour contrôler l'ouverture
|
||
selectedRendererId?: string | null; // ID du renderer actuellement sélectionné
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
"update:modelValue": [value: boolean];
|
||
"select-renderer": [rendererId: string];
|
||
}>();
|
||
|
||
const { allRenderers, fetchRenderers, getStateById } = useRenderers();
|
||
const webRenderer = useWebRenderer();
|
||
const router = useRouter();
|
||
|
||
// Filtre pour n'afficher que notre WebRenderer (pas les autres onglets/navigateurs)
|
||
const myUdn = computed(() => webRenderer.rendererUdn.value);
|
||
|
||
const filteredRenderers = computed(() => {
|
||
const udn = myUdn.value;
|
||
const normalizedUdn = udn?.replace(/^uuid:/, '') ?? null;
|
||
return allRenderers.value.filter((r: RendererSummary) => {
|
||
// WebRenderers have "Web Audio – " as friendly_name prefix
|
||
const isWebRenderer = r.friendly_name?.startsWith("Web Audio – ") ?? false;
|
||
if (isWebRenderer) {
|
||
if (normalizedUdn === null) return false;
|
||
const normalizedId = r.id.replace(/^uuid:/, '');
|
||
return normalizedId === normalizedUdn;
|
||
}
|
||
return true;
|
||
});
|
||
});
|
||
|
||
// Gestion du menu déroulant
|
||
const openMenuId = ref<string | null>(null);
|
||
|
||
// Gestionnaire pour fermer le menu quand on clique en dehors
|
||
let clickOutsideHandler: ((event: MouseEvent) => void) | null = null;
|
||
|
||
watch(openMenuId, (newValue) => {
|
||
if (newValue) {
|
||
// Ajouter l'écouteur après un petit délai pour éviter la fermeture immédiate
|
||
setTimeout(() => {
|
||
clickOutsideHandler = (event: MouseEvent) => {
|
||
const target = event.target as HTMLElement;
|
||
if (!target.closest(".action-menu-container")) {
|
||
closeMenu();
|
||
}
|
||
};
|
||
document.addEventListener("click", clickOutsideHandler);
|
||
}, 100);
|
||
} else {
|
||
// Retirer l'écouteur
|
||
if (clickOutsideHandler) {
|
||
document.removeEventListener("click", clickOutsideHandler);
|
||
clickOutsideHandler = null;
|
||
}
|
||
}
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (clickOutsideHandler) {
|
||
document.removeEventListener("click", clickOutsideHandler);
|
||
}
|
||
});
|
||
|
||
// Fonction pour obtenir l'état d'un renderer
|
||
function getRendererState(rendererId: string) {
|
||
return getStateById(rendererId);
|
||
}
|
||
|
||
// Fonction pour obtenir le label du protocole
|
||
function getProtocolLabel(protocol: string): string {
|
||
switch (protocol) {
|
||
case "upnp":
|
||
return "UPnP";
|
||
case "openhome":
|
||
return "OpenHome";
|
||
case "hybrid":
|
||
return "Hybrid";
|
||
case "chromecast":
|
||
return "Chromecast";
|
||
default:
|
||
return protocol;
|
||
}
|
||
}
|
||
|
||
// Fonction pour obtenir la classe CSS du protocole
|
||
function getProtocolClass(protocol: string): string {
|
||
switch (protocol) {
|
||
case "upnp":
|
||
return "protocol-upnp";
|
||
case "openhome":
|
||
return "protocol-openhome";
|
||
case "hybrid":
|
||
return "protocol-hybrid";
|
||
case "chromecast":
|
||
return "protocol-chromecast";
|
||
default:
|
||
return "protocol-unknown";
|
||
}
|
||
}
|
||
|
||
// Rafraîchir la liste quand le drawer s'ouvre
|
||
watch(
|
||
() => props.modelValue,
|
||
(isOpen) => {
|
||
if (isOpen) {
|
||
fetchRenderers();
|
||
}
|
||
},
|
||
);
|
||
|
||
const onlineRenderers = computed(() =>
|
||
filteredRenderers.value.filter((r: RendererSummary) => r.online),
|
||
);
|
||
const offlineRenderers = computed(() =>
|
||
filteredRenderers.value.filter((r: RendererSummary) => !r.online),
|
||
);
|
||
|
||
// Noms de renderers qui apparaissent plus d'une fois (online + offline)
|
||
const duplicateNames = computed(() => {
|
||
const counts = new Map<string, number>();
|
||
for (const r of filteredRenderers.value) {
|
||
counts.set(r.friendly_name, (counts.get(r.friendly_name) ?? 0) + 1);
|
||
}
|
||
return new Set([...counts.entries()].filter(([, n]) => n > 1).map(([name]) => name));
|
||
});
|
||
|
||
function close() {
|
||
emit("update:modelValue", false);
|
||
}
|
||
|
||
function handleRendererClick(renderer: RendererSummary) {
|
||
if (!renderer.online) return;
|
||
|
||
// Sélectionner le renderer et fermer le drawer
|
||
emit("select-renderer", renderer.id);
|
||
close();
|
||
}
|
||
|
||
function handleSettingsClick() {
|
||
router.push("/debug");
|
||
close();
|
||
}
|
||
|
||
// Handlers pour les contrôles de transport
|
||
async function handlePlay(event: Event, rendererId: string) {
|
||
event.stopPropagation();
|
||
try {
|
||
await api.play(rendererId);
|
||
} catch (error) {
|
||
console.error("[RendererDrawer] Error playing:", error);
|
||
}
|
||
}
|
||
|
||
async function handlePause(event: Event, rendererId: string) {
|
||
event.stopPropagation();
|
||
try {
|
||
await api.pause(rendererId);
|
||
} catch (error) {
|
||
console.error("[RendererDrawer] Error pausing:", error);
|
||
}
|
||
}
|
||
|
||
// Gestion du menu déroulant
|
||
function toggleMenu(event: Event, rendererId: string) {
|
||
event.stopPropagation();
|
||
openMenuId.value = openMenuId.value === rendererId ? null : rendererId;
|
||
}
|
||
|
||
function closeMenu() {
|
||
openMenuId.value = null;
|
||
}
|
||
|
||
// Transfert de la queue
|
||
async function handleTransferQueue(event: Event, targetRendererId: string) {
|
||
event.stopPropagation();
|
||
closeMenu();
|
||
|
||
if (!props.selectedRendererId) {
|
||
console.warn("[RendererDrawer] No source renderer selected");
|
||
return;
|
||
}
|
||
|
||
if (props.selectedRendererId === targetRendererId) {
|
||
console.warn("[RendererDrawer] Cannot transfer to the same renderer");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log(
|
||
`[RendererDrawer] Transferring queue from ${props.selectedRendererId} to ${targetRendererId}`,
|
||
);
|
||
await api.transferQueue(props.selectedRendererId, targetRendererId);
|
||
|
||
// Sélectionner le nouveau renderer et fermer le drawer
|
||
emit("select-renderer", targetRendererId);
|
||
close();
|
||
} catch (error) {
|
||
console.error("[RendererDrawer] Error transferring queue:", error);
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div>
|
||
<!-- Backdrop -->
|
||
<Transition name="backdrop">
|
||
<div v-if="modelValue" class="drawer-backdrop" @click="close"></div>
|
||
</Transition>
|
||
|
||
<!-- Drawer -->
|
||
<Transition name="drawer">
|
||
<aside v-if="modelValue" class="app-drawer renderer-drawer">
|
||
<!-- Header -->
|
||
<header class="drawer-header">
|
||
<div class="drawer-title-section">
|
||
<Music2 :size="24" />
|
||
<h2 class="drawer-title">Renderers</h2>
|
||
</div>
|
||
<button
|
||
class="drawer-close-btn"
|
||
@click="close"
|
||
aria-label="Fermer"
|
||
>
|
||
<X :size="24" />
|
||
</button>
|
||
</header>
|
||
|
||
<!-- Contenu -->
|
||
<div class="drawer-content">
|
||
<!-- Renderers online -->
|
||
<section
|
||
v-if="onlineRenderers.length > 0"
|
||
class="renderer-section"
|
||
>
|
||
<h3 class="section-title">
|
||
Disponibles ({{ onlineRenderers.length }})
|
||
</h3>
|
||
<ul class="renderer-list">
|
||
<li
|
||
v-for="renderer in onlineRenderers"
|
||
:key="renderer.id"
|
||
class="renderer-item online"
|
||
:class="{
|
||
selected:
|
||
renderer.id === selectedRendererId,
|
||
}"
|
||
@click="handleRendererClick(renderer)"
|
||
>
|
||
<!-- Bouton transport à gauche -->
|
||
<button
|
||
v-if="getRendererState(renderer.id)"
|
||
class="transport-btn"
|
||
@click="
|
||
getRendererState(renderer.id)
|
||
?.transport_state === 'PLAYING'
|
||
? handlePause($event, renderer.id)
|
||
: getRendererState(renderer.id)
|
||
?.transport_state ===
|
||
'PAUSED'
|
||
? handlePlay($event, renderer.id)
|
||
: handlePlay($event, renderer.id)
|
||
"
|
||
:title="
|
||
getRendererState(renderer.id)
|
||
?.transport_state === 'PLAYING'
|
||
? 'Pause'
|
||
: 'Play'
|
||
"
|
||
>
|
||
<Pause
|
||
v-if="
|
||
getRendererState(renderer.id)
|
||
?.transport_state === 'PLAYING'
|
||
"
|
||
:size="18"
|
||
/>
|
||
<Play v-else :size="18" />
|
||
</button>
|
||
<button
|
||
v-else
|
||
class="transport-btn disabled"
|
||
disabled
|
||
>
|
||
<Play :size="18" />
|
||
</button>
|
||
|
||
<div class="renderer-icon">
|
||
<Music2 :size="20" />
|
||
</div>
|
||
<div class="renderer-info">
|
||
<div class="renderer-name-row">
|
||
<p class="renderer-name">
|
||
{{ renderer.friendly_name }}
|
||
</p>
|
||
<span
|
||
:class="[
|
||
'protocol-badge',
|
||
getProtocolClass(
|
||
renderer.protocol,
|
||
),
|
||
]"
|
||
>
|
||
{{
|
||
getProtocolLabel(
|
||
renderer.protocol,
|
||
)
|
||
}}{{ duplicateNames.has(renderer.friendly_name) && renderer.server_host ? ` [${renderer.server_host}]` : '' }}
|
||
</span>
|
||
</div>
|
||
<div class="renderer-details">
|
||
<p
|
||
v-if="renderer.model_name"
|
||
class="renderer-model"
|
||
>
|
||
{{ renderer.model_name }}
|
||
</p>
|
||
<StatusBadge
|
||
v-if="getRendererState(renderer.id)"
|
||
:status="
|
||
getRendererState(renderer.id)!
|
||
.transport_state
|
||
"
|
||
class="renderer-state-badge"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Menu actions (uniquement si ce n'est pas le renderer sélectionné) -->
|
||
<div
|
||
v-if="
|
||
selectedRendererId &&
|
||
renderer.id !== selectedRendererId
|
||
"
|
||
class="action-menu-container"
|
||
>
|
||
<button
|
||
class="menu-btn"
|
||
@click="toggleMenu($event, renderer.id)"
|
||
:aria-label="`Actions pour ${renderer.friendly_name}`"
|
||
>
|
||
<MoreVertical :size="18" />
|
||
</button>
|
||
|
||
<Transition name="menu-fade">
|
||
<div
|
||
v-if="openMenuId === renderer.id"
|
||
class="action-dropdown"
|
||
>
|
||
<button
|
||
class="dropdown-item"
|
||
@click="
|
||
handleTransferQueue(
|
||
$event,
|
||
renderer.id,
|
||
)
|
||
"
|
||
>
|
||
<ArrowRightLeft :size="16" />
|
||
<span
|
||
>Transférer la lecture
|
||
ici</span
|
||
>
|
||
</button>
|
||
</div>
|
||
</Transition>
|
||
</div>
|
||
|
||
<div class="renderer-status">
|
||
<Circle :size="8" fill="currentColor" />
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- Renderers offline -->
|
||
<section
|
||
v-if="offlineRenderers.length > 0"
|
||
class="renderer-section"
|
||
>
|
||
<h3 class="section-title">
|
||
Hors ligne ({{ offlineRenderers.length }})
|
||
</h3>
|
||
<ul class="renderer-list">
|
||
<li
|
||
v-for="renderer in offlineRenderers"
|
||
:key="renderer.id"
|
||
class="renderer-item offline"
|
||
>
|
||
<div class="renderer-icon">
|
||
<Music2 :size="20" />
|
||
</div>
|
||
<div class="renderer-info">
|
||
<p class="renderer-name">
|
||
{{ renderer.friendly_name }}{{ duplicateNames.has(renderer.friendly_name) && renderer.server_host ? ` [${renderer.server_host}]` : '' }}
|
||
</p>
|
||
<p
|
||
v-if="renderer.model_name"
|
||
class="renderer-model"
|
||
>
|
||
{{ renderer.model_name }}
|
||
</p>
|
||
</div>
|
||
<div class="renderer-status">
|
||
<Circle :size="8" fill="currentColor" />
|
||
</div>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- Aucun renderer -->
|
||
<div
|
||
v-if="allRenderers.length === 0"
|
||
class="empty-renderers"
|
||
>
|
||
<Music2 :size="48" />
|
||
<p>Aucun renderer détecté</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer avec bouton settings -->
|
||
<footer class="drawer-footer">
|
||
<button
|
||
class="settings-btn"
|
||
@click="handleSettingsClick"
|
||
title="Ouvrir le menu Debug"
|
||
>
|
||
<Settings :size="20" />
|
||
<span>Debug & Config</span>
|
||
</button>
|
||
</footer>
|
||
</aside>
|
||
</Transition>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.drawer-backdrop {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 50vw; /* Commence avant le drawer (desktop: jusqu'à 50vw) */
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.35); /* Moins sombre */
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
z-index: 200;
|
||
}
|
||
|
||
@media (max-width: 768px) and (orientation: portrait) {
|
||
.drawer-backdrop {
|
||
display: none; /* Mobile portrait: le drawer couvre 100vw, pas besoin de backdrop */
|
||
}
|
||
}
|
||
|
||
/* Positionnement spécifique — le reste est dans drawers.css (.app-drawer) */
|
||
.renderer-drawer {
|
||
right: 0;
|
||
border-left: 1px solid rgba(255, 255, 255, 0.12);
|
||
box-shadow: -4px 0 32px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
@media (prefers-color-scheme: light) {
|
||
.renderer-drawer {
|
||
border-left-color: rgba(0, 0, 0, 0.1);
|
||
box-shadow: -4px 0 32px rgba(0, 0, 0, 0.15);
|
||
}
|
||
}
|
||
|
||
/* .drawer-header, .drawer-title, .drawer-close-btn, .drawer-content définis dans drawers.css */
|
||
|
||
.renderer-section {
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.section-title {
|
||
font-size: var(--text-sm);
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--color-text-secondary);
|
||
margin: 0 0 var(--spacing-sm) 0;
|
||
padding: 0 var(--spacing-sm);
|
||
}
|
||
|
||
.renderer-list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.renderer-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
padding: var(--spacing-md);
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.renderer-item.online {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.renderer-item.online:hover {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-color: rgba(255, 255, 255, 0.2);
|
||
transform: translateX(-4px);
|
||
}
|
||
|
||
.renderer-item.online:active {
|
||
transform: translateX(-2px);
|
||
}
|
||
|
||
.renderer-item.selected {
|
||
background: rgba(102, 126, 234, 0.2);
|
||
border-color: var(--color-primary);
|
||
box-shadow: 0 0 16px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.renderer-item.selected:hover {
|
||
background: rgba(102, 126, 234, 0.25);
|
||
}
|
||
|
||
.renderer-item.offline {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.renderer-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
border-radius: 8px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.renderer-item.selected .renderer-icon {
|
||
background: rgba(102, 126, 234, 0.3);
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.renderer-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.renderer-name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.renderer-name {
|
||
font-size: var(--text-base);
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
margin: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.renderer-details {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.renderer-model {
|
||
font-size: var(--text-sm);
|
||
color: var(--color-text-secondary);
|
||
margin: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.renderer-state-badge {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Protocol badge classes (.protocol-badge, .protocol-upnp, etc.) are now global in pmocontrol.css */
|
||
|
||
/* Transport button */
|
||
.transport-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 36px;
|
||
height: 36px;
|
||
flex-shrink: 0;
|
||
padding: 0;
|
||
background: rgba(102, 126, 234, 0.2);
|
||
border: 1px solid var(--color-primary);
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
color: var(--color-primary);
|
||
}
|
||
|
||
.transport-btn:hover:not(.disabled) {
|
||
background: var(--color-primary);
|
||
color: white;
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.transport-btn:active:not(.disabled) {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.transport-btn.disabled {
|
||
opacity: 0.3;
|
||
cursor: not-allowed;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-color: rgba(255, 255, 255, 0.1);
|
||
color: var(--color-text-tertiary);
|
||
}
|
||
|
||
/* Menu actions */
|
||
.action-menu-container {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.menu-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
padding: 0;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
color: var(--color-text-secondary);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.menu-btn:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.action-dropdown {
|
||
position: absolute;
|
||
right: 0;
|
||
top: calc(100% + 4px);
|
||
min-width: 200px;
|
||
background: rgba(20, 20, 30, 0.98);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
overflow: hidden;
|
||
z-index: 1000;
|
||
}
|
||
|
||
@media (prefers-color-scheme: light) {
|
||
.action-dropdown {
|
||
background: white;
|
||
border-color: rgba(0, 0, 0, 0.1);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
}
|
||
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
width: 100%;
|
||
padding: var(--spacing-sm) var(--spacing-md);
|
||
background: none;
|
||
border: none;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s ease;
|
||
color: var(--color-text);
|
||
font-size: var(--text-sm);
|
||
}
|
||
|
||
.dropdown-item:hover {
|
||
background: rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.dropdown-item:active {
|
||
background: rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
.dropdown-item span {
|
||
flex: 1;
|
||
}
|
||
|
||
/* Transitions pour le menu */
|
||
.menu-fade-enter-active {
|
||
transition: all 0.15s ease-out;
|
||
}
|
||
|
||
.menu-fade-leave-active {
|
||
transition: all 0.1s ease-in;
|
||
}
|
||
|
||
.menu-fade-enter-from {
|
||
opacity: 0;
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.menu-fade-leave-to {
|
||
opacity: 0;
|
||
transform: translateY(-4px);
|
||
}
|
||
|
||
.renderer-status {
|
||
flex-shrink: 0;
|
||
color: var(--status-playing);
|
||
}
|
||
|
||
.renderer-item.offline .renderer-status {
|
||
color: var(--status-offline);
|
||
}
|
||
|
||
.empty-renderers {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--spacing-md);
|
||
padding: var(--spacing-2xl);
|
||
text-align: center;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.empty-renderers p {
|
||
margin: 0;
|
||
font-size: var(--text-base);
|
||
}
|
||
|
||
/* Footer */
|
||
.drawer-footer {
|
||
flex-shrink: 0;
|
||
padding: var(--spacing-md);
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.settings-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--spacing-sm);
|
||
width: 100%;
|
||
padding: var(--spacing-md);
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||
border-radius: 8px;
|
||
color: var(--color-text-secondary);
|
||
font-size: var(--text-sm);
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.settings-btn:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: var(--color-text);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.settings-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
/* Animations */
|
||
.backdrop-enter-active {
|
||
transition: opacity 0.3s ease-out;
|
||
transition-delay: 0.1s; /* Attend que le drawer soit un peu visible */
|
||
}
|
||
|
||
.backdrop-leave-active {
|
||
transition: opacity 0.25s ease-in;
|
||
}
|
||
|
||
.backdrop-enter-from,
|
||
.backdrop-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
.drawer-enter-active {
|
||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1); /* Courbe d'animation fluide (easeOutExpo) */
|
||
}
|
||
|
||
.drawer-leave-active {
|
||
transition: all 0.3s cubic-bezier(0.7, 0, 0.84, 0); /* Courbe d'animation de sortie (easeInExpo) */
|
||
}
|
||
|
||
.drawer-enter-from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
|
||
.drawer-leave-to {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
|
||
/* Scrollbar styling */
|
||
.drawer-content::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.drawer-content::-webkit-scrollbar-track {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.drawer-content::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.drawer-content::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
/* Mobile responsive - portrait */
|
||
@media (max-width: 768px) and (orientation: portrait) {
|
||
.renderer-drawer {
|
||
width: 100vw; /* Mobile portrait: 100% de l'écran */
|
||
background: rgba(
|
||
255,
|
||
255,
|
||
255,
|
||
0.06
|
||
); /* Encore plus transparent sur mobile */
|
||
box-shadow: none; /* Pas d'ombre sur les côtés */
|
||
}
|
||
|
||
.drawer-header {
|
||
padding: var(--spacing-md);
|
||
}
|
||
|
||
.drawer-title {
|
||
font-size: var(--text-lg);
|
||
}
|
||
|
||
.renderer-item {
|
||
padding: var(--spacing-md);
|
||
}
|
||
|
||
.renderer-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
}
|
||
}
|
||
|
||
/* Fallback pour navigateurs sans backdrop-filter */
|
||
@supports not (backdrop-filter: blur(30px)) {
|
||
.renderer-drawer {
|
||
background: rgba(255, 255, 255, 0.98);
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.renderer-drawer {
|
||
background: rgba(20, 20, 30, 0.98);
|
||
}
|
||
}
|
||
}
|
||
</style>
|