Nouvelle UI du controle point

This commit is contained in:
2025-12-27 01:04:34 +01:00
parent 18d12fa088
commit 86711c1e48
13 changed files with 2618 additions and 7 deletions

View File

@@ -0,0 +1,337 @@
/**
* Composable pour gérer les onglets dynamiques de l'interface unifiée.
* Supporte les onglets home, renderer et server avec persistance localStorage.
*/
import { reactive, computed, watch, onMounted, type Component } from 'vue'
import { Home, Radio, Server } from 'lucide-vue-next'
import type { RendererSummary, MediaServerSummary } from '../services/pmocontrol/types'
export interface Tab {
id: string // "home", "renderer-{id}", "server-{id}"
type: 'home' | 'renderer' | 'server'
title: string // Nom affiché (tronqué sur mobile)
icon: Component
metadata?: {
rendererId?: string
serverId?: string
}
closeable: boolean // home tab non fermable
}
interface TabsState {
tabs: Tab[]
activeTabId: string
tabHistory: string[] // Pour back/forward navigation
}
const MAX_TABS = 8
const STORAGE_KEY = 'pmo-tabs-state'
// État global partagé entre toutes les instances du composable
const state = reactive<TabsState>({
tabs: [
{
id: 'home',
type: 'home',
title: 'Home',
icon: Home,
closeable: false,
},
],
activeTabId: 'home',
tabHistory: ['home'],
})
// Flag pour éviter les boucles de sauvegarde
let isRestoringFromStorage = false
/**
* Tronque un titre pour mobile si nécessaire
*/
function truncateTitle(title: string, maxLength = 15): string {
return title.length > maxLength ? title.slice(0, maxLength) + '...' : title
}
/**
* Sauvegarde l'état dans localStorage
*/
function saveToLocalStorage() {
if (isRestoringFromStorage) return
try {
const stateToSave = {
tabs: state.tabs.map((tab) => ({
...tab,
// On ne peut pas sauvegarder les composants Vue, on sauve juste le type
icon: undefined,
})),
activeTabId: state.activeTabId,
tabHistory: state.tabHistory,
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave))
} catch (error) {
console.error('[useTabs] Erreur sauvegarde localStorage:', error)
}
}
/**
* Restaure l'état depuis localStorage
*/
function restoreFromLocalStorage() {
try {
const saved = localStorage.getItem(STORAGE_KEY)
if (!saved) return
isRestoringFromStorage = true
const savedState = JSON.parse(saved)
// Reconstituer les tabs avec les bonnes icônes
state.tabs = savedState.tabs.map((tab: Tab) => ({
...tab,
icon: tab.type === 'home' ? Home : tab.type === 'renderer' ? Radio : Server,
}))
state.activeTabId = savedState.activeTabId || 'home'
state.tabHistory = savedState.tabHistory || ['home']
// Vérifier que l'onglet actif existe toujours
if (!state.tabs.find((t) => t.id === state.activeTabId)) {
state.activeTabId = 'home'
}
isRestoringFromStorage = false
} catch (error) {
console.error('[useTabs] Erreur restauration localStorage:', error)
isRestoringFromStorage = false
}
}
/**
* Trouve un onglet par son ID
*/
function findTab(tabId: string): Tab | undefined {
return state.tabs.find((t) => t.id === tabId)
}
/**
* Ouvre un nouvel onglet ou active un onglet existant
*/
function openTab(newTab: Omit<Tab, 'id'> & { id?: string }): string {
// Générer un ID si non fourni
const tabId =
newTab.id ||
(newTab.type === 'home'
? 'home'
: newTab.type === 'renderer'
? `renderer-${newTab.metadata?.rendererId}`
: `server-${newTab.metadata?.serverId}`)
// Si l'onglet existe déjà, on le sélectionne
const existingTab = findTab(tabId)
if (existingTab) {
switchTab(tabId)
return tabId
}
// Vérifier la limite max
if (state.tabs.length >= MAX_TABS) {
console.warn(`[useTabs] Limite max de ${MAX_TABS} onglets atteinte`)
return state.activeTabId
}
// Créer le nouvel onglet
const tab: Tab = {
id: tabId,
type: newTab.type,
title: truncateTitle(newTab.title),
icon: newTab.icon,
metadata: newTab.metadata,
closeable: newTab.closeable !== false, // true par défaut sauf si explicitement false
}
state.tabs.push(tab)
switchTab(tabId)
return tabId
}
/**
* Ferme un onglet
*/
function closeTab(tabId: string) {
const tab = findTab(tabId)
if (!tab) return
// Ne pas fermer l'onglet home
if (!tab.closeable) {
console.warn('[useTabs] Impossible de fermer un onglet non fermable')
return
}
const tabIndex = state.tabs.findIndex((t) => t.id === tabId)
if (tabIndex === -1) return
// Supprimer l'onglet
state.tabs.splice(tabIndex, 1)
// Supprimer de l'historique
state.tabHistory = state.tabHistory.filter((id) => id !== tabId)
// Si c'était l'onglet actif, basculer vers le précédent dans l'historique
if (state.activeTabId === tabId) {
// Chercher le précédent dans l'historique qui existe encore
const previousTab = state.tabHistory
.slice()
.reverse()
.find((id) => state.tabs.some((t) => t.id === id))
if (previousTab) {
switchTab(previousTab)
} else {
// Fallback sur home
switchTab('home')
}
}
}
/**
* Change l'onglet actif
*/
function switchTab(tabId: string) {
const tab = findTab(tabId)
if (!tab) {
console.warn(`[useTabs] Onglet ${tabId} introuvable`)
return
}
state.activeTabId = tabId
// Ajouter à l'historique (en évitant les doublons consécutifs)
if (state.tabHistory[state.tabHistory.length - 1] !== tabId) {
state.tabHistory.push(tabId)
// Limiter la taille de l'historique
if (state.tabHistory.length > 20) {
state.tabHistory.shift()
}
}
}
/**
* Onglet suivant (pour swipe gesture)
*/
function nextTab() {
const currentIndex = state.tabs.findIndex((t) => t.id === state.activeTabId)
const nextIndex = (currentIndex + 1) % state.tabs.length
const nextTabObj = state.tabs[nextIndex]
if (nextTabObj) switchTab(nextTabObj.id)
}
/**
* Onglet précédent (pour swipe gesture)
*/
function previousTab() {
const currentIndex = state.tabs.findIndex((t) => t.id === state.activeTabId)
const previousIndex = currentIndex === 0 ? state.tabs.length - 1 : currentIndex - 1
const prevTabObj = state.tabs[previousIndex]
if (prevTabObj) switchTab(prevTabObj.id)
}
/**
* Ouvre un onglet renderer
*/
function openRenderer(renderer: RendererSummary | undefined) {
if (!renderer) return 'home'
return openTab({
type: 'renderer',
title: renderer.friendly_name,
icon: Radio,
metadata: { rendererId: renderer.id },
closeable: true,
})
}
/**
* Ouvre un onglet server
*/
function openServer(server: MediaServerSummary | undefined) {
if (!server) return 'home'
return openTab({
type: 'server',
title: server.friendly_name,
icon: Server,
metadata: { serverId: server.id },
closeable: true,
})
}
/**
* Ferme tous les onglets sauf home
*/
function closeAllTabs() {
state.tabs = state.tabs.filter((t) => !t.closeable)
state.activeTabId = 'home'
state.tabHistory = ['home']
}
/**
* Ferme tous les onglets sauf l'actif
*/
function closeOtherTabs() {
const activeTab = findTab(state.activeTabId)
if (!activeTab) return
state.tabs = state.tabs.filter((t) => !t.closeable || t.id === state.activeTabId)
state.tabHistory = state.tabHistory.filter((id) => state.tabs.some((t) => t.id === id))
}
/**
* Composable principal
*/
export function useTabs() {
// Watch pour sauvegarde automatique
watch(
() => [state.tabs, state.activeTabId, state.tabHistory],
() => {
saveToLocalStorage()
},
{ deep: true },
)
// Restaurer au montage (une seule fois)
onMounted(() => {
const firstTab = state.tabs[0]
if (state.tabs.length === 1 && firstTab && firstTab.id === 'home') {
restoreFromLocalStorage()
}
})
// Computed properties
const activeTab = computed(() => findTab(state.activeTabId) || state.tabs[0] || null)
const hasMultipleTabs = computed(() => state.tabs.length > 1)
const canAddTab = computed(() => state.tabs.length < MAX_TABS)
return {
// State
tabs: computed(() => state.tabs),
activeTabId: computed(() => state.activeTabId),
activeTab,
tabHistory: computed(() => state.tabHistory),
hasMultipleTabs,
canAddTab,
// Actions
openTab,
closeTab,
switchTab,
nextTab,
previousTab,
openRenderer,
openServer,
closeAllTabs,
closeOtherTabs,
findTab,
}
}