Replace malformed max-height values with precise calc() expressions and update open/closed transform states. Apply flexbox improvements to ensure reliable mobile drawer layout and proper safe-area padding above the BottomTabBar.
386 lines
11 KiB
Vue
386 lines
11 KiB
Vue
<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";
|
|
|
|
const props = defineProps<{
|
|
rendererId: string;
|
|
}>();
|
|
|
|
const { renderer, state, queue, refresh } = useRenderer(
|
|
toRef(props, "rendererId"),
|
|
);
|
|
const uiStore = useUIStore();
|
|
const rendererName = computed(() => state.value?.friendly_name ?? props.rendererId);
|
|
|
|
// Détection mobile portrait pour afficher le drawer au lieu de la colonne
|
|
const isMobilePortrait = useMediaQuery(
|
|
"(max-width: 768px) and (orientation: portrait)",
|
|
);
|
|
|
|
// État du drawer queue sur mobile
|
|
const queueDrawerOpen = ref(false);
|
|
|
|
function toggleQueueDrawer() {
|
|
queueDrawerOpen.value = !queueDrawerOpen.value;
|
|
}
|
|
|
|
// Charger les données au montage
|
|
onMounted(async () => {
|
|
await refresh();
|
|
});
|
|
|
|
// É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(
|
|
`« ${rendererName.value} » — impossible de lire ce morceau: ${error instanceof Error ? error.message : "Erreur inconnue"}`,
|
|
);
|
|
}
|
|
}
|
|
</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>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.renderer-tab-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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 */
|
|
}
|
|
|
|
.current-track-section,
|
|
.controls-section,
|
|
.volume-section,
|
|
.playlist-section {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
/* Colonne droite - Queue */
|
|
.queue-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
padding-bottom: 80px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.queue-viewer {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Queue drawer - masqué sur desktop, visible uniquement sur mobile portrait */
|
|
.queue-drawer {
|
|
display: none;
|
|
}
|
|
|
|
.queue-drawer-backdrop {
|
|
display: none;
|
|
}
|
|
|
|
/* 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;
|
|
/* Ancré juste au-dessus de la BottomTabBar (64px) et de la safe-area système */
|
|
bottom: calc(64px + env(safe-area-inset-bottom, 0px));
|
|
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é: seul le toggle (56px) visible */
|
|
transform: translateY(calc(100% - 56px));
|
|
transition: transform 0.3s ease;
|
|
z-index: 95; /* En dessous de la BottomTabBar (z-index: 100) */
|
|
max-height: calc(100vh - 64px - env(safe-area-inset-bottom, 0px));
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Fermé: ne bloque pas les clics en dehors du toggle */
|
|
pointer-events: none;
|
|
}
|
|
|
|
.queue-drawer.open {
|
|
/* Ouvert: le drawer occupe toute la hauteur disponible au-dessus de la tab bar */
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
@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;
|
|
flex-shrink: 0;
|
|
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 {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--spacing-md);
|
|
}
|
|
|
|
.queue-drawer-backdrop {
|
|
display: block;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: calc(64px + env(safe-area-inset-bottom, 0px));
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
z-index: 94;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.controls-column {
|
|
padding-bottom: 100px; /* Espace supplémentaire pour éviter que la tab bar (64px) cache le volume */
|
|
}
|
|
}
|
|
|
|
/* Responsive - 800x600 landscape et petites hauteurs (mode kiosque) */
|
|
@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);
|
|
}
|
|
|
|
.renderer-header {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
}
|
|
|
|
.renderer-name {
|
|
font-size: var(--text-xl);
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
}
|
|
|
|
/* Large desktop */
|
|
@media (min-width: 1200px) {
|
|
.renderer-layout {
|
|
grid-template-columns: 350px 1fr;
|
|
}
|
|
}
|
|
|
|
/* Scrollbar styling */
|
|
.controls-column::-webkit-scrollbar,
|
|
.queue-viewer::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.controls-column::-webkit-scrollbar-track,
|
|
.queue-viewer::-webkit-scrollbar-track {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.controls-column::-webkit-scrollbar-thumb,
|
|
.queue-viewer::-webkit-scrollbar-thumb {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.controls-column::-webkit-scrollbar-thumb:hover,
|
|
.queue-viewer::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(255, 255, 255, 0.3);
|
|
}
|
|
</style>
|