Files
pmomusic/pmoapp/webapp/src/components/unified/RendererTabContent.vue

383 lines
11 KiB
Vue
Raw Normal View History

2025-12-27 01:04:34 +01:00
<script setup lang="ts">
import { ref, computed, toRef, onMounted } from "vue";
import { useMediaQuery } from "@vueuse/core";
import { useRenderer } from "@/composables/useRenderers";
import CurrentTrack from "@/components/pmocontrol/CurrentTrack.vue";
import TransportControls from "@/components/pmocontrol/TransportControls.vue";
import VolumeControl from "@/components/pmocontrol/VolumeControl.vue";
import QueueViewer from "@/components/pmocontrol/QueueViewer.vue";
import { ChevronUp, ChevronDown } from "lucide-vue-next";
import { useUIStore } from "@/stores/ui";
import { api } from "@/services/pmocontrol/api";
import type { QueueItem } from "@/services/pmocontrol/types";
2025-12-27 01:04:34 +01:00
const props = defineProps<{
rendererId: string;
}>();
2025-12-27 01:04:34 +01:00
const { renderer, state, queue, refresh } = useRenderer(
toRef(props, "rendererId"),
);
const uiStore = useUIStore();
2025-12-27 01:04:34 +01:00
// Détection mobile portrait pour afficher le drawer au lieu de la colonne
const isMobilePortrait = useMediaQuery(
"(max-width: 768px) and (orientation: portrait)",
);
2025-12-27 01:04:34 +01:00
// État du drawer queue sur mobile
const queueDrawerOpen = ref(false);
2025-12-27 01:04:34 +01:00
function toggleQueueDrawer() {
queueDrawerOpen.value = !queueDrawerOpen.value;
2025-12-27 01:04:34 +01:00
}
// Charger les données au montage
onMounted(async () => {
await refresh();
});
2025-12-27 01:04:34 +01:00
// État du renderer pour affichage
const isOnline = computed(() => renderer.value?.online ?? false);
// Gérer le clic sur un item de la queue
async function handleQueueItemClick(item: QueueItem) {
try {
await api.seekQueueIndex(props.rendererId, item.index);
console.log(
"[RendererTabContent] Jumped to queue index:",
item.index,
item.title,
);
// Force un refetch immédiat pour synchroniser la cover affichée
// sans attendre l'événement SSE qui peut avoir un délai
await refresh(true);
} catch (error) {
console.error(
"[RendererTabContent] Error seeking to queue index:",
error,
);
uiStore.notifyError(
`Erreur: ${error instanceof Error ? error.message : "Impossible de sauter à cet item"}`,
);
}
}
2025-12-27 01:04:34 +01:00
</script>
<template>
<div class="renderer-tab-content">
<!-- Layout principal (header supprimé, infos déplacées dans BottomTabBar) -->
<div class="renderer-layout" :class="{ 'queue-open': queueDrawerOpen }">
<!-- Colonne gauche: Contrôles -->
<div class="controls-column">
<!-- Pochette + infos track -->
<CurrentTrack
v-if="state"
:renderer-id="rendererId"
class="current-track-section"
/>
<!-- Contrôles de transport -->
<div v-if="state" class="controls-section">
<TransportControls :renderer-id="rendererId" />
</div>
<!-- Contrôle de volume -->
<div v-if="state" class="volume-section">
<VolumeControl :renderer-id="rendererId" />
</div>
<!-- Message si offline -->
<div v-if="!isOnline" class="offline-message">
<p>Ce renderer est actuellement hors ligne</p>
</div>
</div>
<!-- Colonne droite: Queue (desktop et landscape uniquement) -->
<div v-if="!isMobilePortrait" class="queue-column">
<QueueViewer
:renderer-id="rendererId"
class="queue-viewer"
@click-item="handleQueueItemClick"
/>
</div>
<!-- Drawer queue (mobile portrait uniquement) -->
<div
v-if="isMobilePortrait"
class="queue-drawer"
:class="{ open: queueDrawerOpen }"
>
<!-- Toggle button -->
<button class="queue-drawer-toggle" @click="toggleQueueDrawer">
<ChevronUp v-if="queueDrawerOpen" :size="24" />
<ChevronDown v-else :size="24" />
<span>File d'attente ({{ queue?.items.length || 0 }})</span>
</button>
<!-- Contenu du drawer -->
<div class="queue-drawer-content">
<QueueViewer
:renderer-id="rendererId"
@click-item="handleQueueItemClick"
/>
</div>
</div>
<!-- Backdrop pour fermer le drawer -->
<div
v-if="queueDrawerOpen"
class="queue-drawer-backdrop"
@click="queueDrawerOpen = false"
></div>
2025-12-27 01:04:34 +01:00
</div>
</div>
</template>
<style scoped>
.renderer-tab-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
min-width: 0;
2025-12-27 01:04:34 +01:00
}
/* Layout principal - 800x600 landscape (2 colonnes) */
.renderer-layout {
display: grid;
grid-template-columns: 300px 1fr;
gap: var(--spacing-lg);
padding: var(--spacing-lg) 0 var(--spacing-lg) var(--spacing-lg);
/* padding-right: 0 pour coller la scrollbar au bord */
flex: 1;
overflow: hidden;
min-width: 0;
2025-12-27 01:04:34 +01:00
}
/* Colonne gauche - Contrôles */
.controls-column {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
overflow-y: auto;
padding-right: 16px; /* Dégager la scrollbar */
2025-12-27 01:04:34 +01:00
}
.current-track-section,
.controls-section,
.volume-section,
.playlist-section {
flex-shrink: 0;
2025-12-27 01:04:34 +01:00
}
.offline-message {
padding: var(--spacing-lg);
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: var(--radius-md);
text-align: center;
color: var(--status-offline);
2025-12-27 01:04:34 +01:00
}
/* Colonne droite - Queue */
.queue-column {
display: flex;
flex-direction: column;
overflow: hidden;
padding-bottom: 80px;
min-width: 0;
2025-12-27 01:04:34 +01:00
}
.queue-viewer {
flex: 1;
overflow-y: auto;
min-width: 0;
2025-12-27 01:04:34 +01:00
}
/* Queue drawer - masqué sur desktop, visible uniquement sur mobile portrait */
.queue-drawer {
display: none;
2025-12-27 01:04:34 +01:00
}
.queue-drawer-backdrop {
display: none;
2025-12-27 01:04:34 +01:00
}
/* Responsive - Mobile portrait */
@media (max-width: 768px) and (orientation: portrait) {
.renderer-header {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
padding: var(--spacing-md);
}
.header-badges {
width: 100%;
justify-content: flex-start;
}
.renderer-layout {
grid-template-columns: 1fr;
gap: var(--spacing-md);
padding: var(--spacing-md) 0 var(--spacing-xl) var(--spacing-md);
/* padding-right: 0 pour coller la scrollbar au bord */
}
/* Queue drawer visible sur mobile (géré par v-if maintenant) */
.queue-drawer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(22, 22, 32, 0.96);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-top: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.4);
/* Fermé: caché sauf le toggle (56px) qui dépasse au-dessus de la BottomTabBar (64px) */
transform: translateY(calc(100% - 56px - 64px));
transition: transform 0.3s ease;
z-index: 95; /* Au-dessus de la BottomTabBar (z-index: 100) */
max-height: 70vh;
display: flex;
flex-direction: column;
/* Fermé: ne bloque pas les clics en dehors du toggle */
pointer-events: none;
}
.queue-drawer.open {
/* Ouvert: remonte mais s'arrête à 64px du bas pour laisser la BottomTabBar accessible */
transform: translateY(64px);
pointer-events: auto; /* Ouvert: capture les clics */
}
@media (prefers-color-scheme: light) {
.queue-drawer {
background: rgba(245, 245, 250, 0.97);
border-top-color: rgba(0, 0, 0, 0.1);
}
}
.queue-drawer-toggle {
width: 100%;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
background: transparent;
border: none;
cursor: pointer;
color: var(--color-text);
font-size: var(--text-base);
font-weight: 600;
font-family: inherit;
pointer-events: auto; /* Le bouton est toujours cliquable */
}
.queue-drawer-content {
max-height: calc(70vh - 56px);
overflow-y: auto;
padding: var(--spacing-md);
}
.queue-drawer-backdrop {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 64px; /* S'arrête au-dessus de la BottomTabBar */
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 94; /* Entre la BottomTabBar et le drawer */
pointer-events: auto; /* Capture les clics pour fermer le drawer */
}
.controls-column {
padding-bottom: 100px; /* Espace supplémentaire pour éviter que la tab bar (64px) cache le volume */
}
2025-12-27 01:04:34 +01:00
}
/* Responsive - 800x600 landscape et petites hauteurs (mode kiosque) */
2025-12-27 01:04:34 +01:00
@media (min-width: 600px) and (max-width: 1024px) and (orientation: landscape) {
.renderer-layout {
grid-template-columns: 280px 1fr;
gap: var(--spacing-md);
padding: var(--spacing-md);
}
2025-12-27 01:04:34 +01:00
.renderer-header {
padding: var(--spacing-sm) var(--spacing-md);
}
2025-12-27 01:04:34 +01:00
.renderer-name {
font-size: var(--text-xl);
}
}
2025-12-27 01:04:34 +01:00
/* Mode kiosque - compactage pour hauteurs ≤ 700px (ex: 800x600) */
@media (max-height: 700px) and (orientation: landscape) {
.renderer-header {
padding: var(--spacing-xs) var(--spacing-md);
}
.renderer-name {
font-size: var(--text-lg);
}
.renderer-model {
font-size: 11px;
}
.renderer-layout {
gap: var(--spacing-sm);
padding: var(--spacing-sm);
}
.controls-column {
gap: var(--spacing-sm);
}
/* Masquer le scroll, tout doit tenir */
.controls-column {
overflow-y: visible;
}
2025-12-27 01:04:34 +01:00
}
/* Large desktop */
@media (min-width: 1200px) {
.renderer-layout {
grid-template-columns: 350px 1fr;
}
2025-12-27 01:04:34 +01:00
}
/* Scrollbar styling */
.controls-column::-webkit-scrollbar,
.queue-viewer::-webkit-scrollbar {
width: 6px;
2025-12-27 01:04:34 +01:00
}
.controls-column::-webkit-scrollbar-track,
.queue-viewer::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
2025-12-27 01:04:34 +01:00
}
.controls-column::-webkit-scrollbar-thumb,
.queue-viewer::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
2025-12-27 01:04:34 +01:00
}
.controls-column::-webkit-scrollbar-thumb:hover,
.queue-viewer::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
2025-12-27 01:04:34 +01:00
}
</style>