diff --git a/pmoapp/webapp/src/composables/useCoverImage.ts b/pmoapp/webapp/src/composables/useCoverImage.ts index 20e3c59c..e93de6d9 100644 --- a/pmoapp/webapp/src/composables/useCoverImage.ts +++ b/pmoapp/webapp/src/composables/useCoverImage.ts @@ -9,6 +9,7 @@ */ import { ref, watch, computed, type Ref } from "vue"; import { imageCache, useImageCache } from "./imageCache"; +import { simpleHash } from "../utils/string"; export interface CoverImageOptions { maxRetries?: number; @@ -44,17 +45,6 @@ export function useCoverImage( // Computed: synchroniser avec le cache centralisé // Note: on garde le controle local du loaded/error pour éviter les effets de bord - // Fonction de hash simple pour le cache-busting - function simpleHash(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - return Math.abs(hash).toString(36); - } - // Génère une URL avec cache-busting function getCacheBustedUrl(url: string, retry: number): string { if (!forceReload && retry === 0) { diff --git a/pmoapp/webapp/src/composables/useRenderers.ts b/pmoapp/webapp/src/composables/useRenderers.ts index 642b68d5..f9e62ae6 100644 --- a/pmoapp/webapp/src/composables/useRenderers.ts +++ b/pmoapp/webapp/src/composables/useRenderers.ts @@ -8,6 +8,7 @@ import { ref, reactive, computed, toRaw, type Ref } from "vue"; import { api } from "../services/pmocontrol/api"; import { useSSE } from "./useSSE"; import { apiCache } from "./apiCache"; +import { parseTimeToMs } from "../utils/time"; import type { RendererSummary, RendererState, @@ -125,35 +126,14 @@ function ensureSSEInitialized() { // Le backend envoie TOUJOURS les deux valeurs (même si null) // Convertir rel_time (HH:MM:SS) en millisecondes - if (event.rel_time) { - const parts = event.rel_time.split(":").map(Number); - if (parts.length === 3) { - snapshot.state.position_ms = - ((parts[0] ?? 0) * 3600 + - (parts[1] ?? 0) * 60 + - (parts[2] ?? 0)) * - 1000; - } - } else { - // Si rel_time est null/undefined, mettre position à 0 - snapshot.state.position_ms = 0; - } + const positionMs = parseTimeToMs(event.rel_time ?? null); + snapshot.state.position_ms = positionMs ?? 0; // Convertir track_duration (HH:MM:SS) en millisecondes - if (event.track_duration) { - const parts = event.track_duration.split(":").map(Number); - if (parts.length === 3) { - snapshot.state.duration_ms = - ((parts[0] ?? 0) * 3600 + - (parts[1] ?? 0) * 60 + - (parts[2] ?? 0)) * - 1000; - } - } else { - // Si track_duration est null/undefined (flux continu sans durée), - // mettre duration_ms à null pour afficher "--:--" - snapshot.state.duration_ms = null; - } + const durationMs = parseTimeToMs(event.track_duration ?? null); + // Si track_duration est null/undefined (flux continu sans durée), + // mettre duration_ms à null pour afficher "--:--" + snapshot.state.duration_ms = durationMs; // Important: Trigger reactivity en réassignant l'objet complet avec deep copy // Le shallow copy ne suffit pas car snapshot.state est partagé entre renderers diff --git a/pmoapp/webapp/src/utils/string.ts b/pmoapp/webapp/src/utils/string.ts new file mode 100644 index 00000000..1a825678 --- /dev/null +++ b/pmoapp/webapp/src/utils/string.ts @@ -0,0 +1,49 @@ +/** + * Utilitaires pour les chaînes de caractères + */ + +/** + * Génère un hash simple et rapide pour une chaîne + * @param str - Chaîne à hasher + * @returns Hash sous forme de chaîne hexadécimale positive + */ +export function simpleHash(str: string): string { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(36); +} + +/** + * Ajoute un paramètre cache-busting à une URL + * @param url - URL originale + * @param cacheKey - Clé de cache (hash ou timestamp) + * @returns URL avec le paramètre _cb ajouté + */ +export function addCacheBust(url: string, cacheKey: string): string { + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}_cb=${cacheKey}`; +} + +/** + * Nettoie une URL en supprimant les paramètres de cache-busting + * @param url - URL avec possibly _cb params + * @returns URL nettoyée + */ +export function normalizeUrl(url: string): string { + return url.replace(/[?&]_cb=[^&]*/, ''); +} + +/** + * Tronque une chaîne à une longueur maximale + * @param str - Chaîne à tronquer + * @param maxLength - Longueur maximale + * @returns Chaîne tronquée avec suffix si nécessaire + */ +export function truncate(str: string, maxLength: number, suffix = '...'): string { + if (str.length <= maxLength) return str; + return str.slice(0, maxLength - suffix.length) + suffix; +} \ No newline at end of file diff --git a/pmoapp/webapp/src/utils/time.ts b/pmoapp/webapp/src/utils/time.ts new file mode 100644 index 00000000..98edbe81 --- /dev/null +++ b/pmoapp/webapp/src/utils/time.ts @@ -0,0 +1,60 @@ +/** + * Utilitaires pour les dates et durées + */ + +/** + * Convertit une durée au format HH:MM:SS en millisecondes + * @param time - Durée au format "HH:MM:SS" ou "MM:SS" + * @returns Durée en millisecondes, ou null si invalide + */ +export function parseTimeToMs(time: string | null | undefined): number | null { + if (!time) return null; + + const parts = time.split(':').map(Number); + + if (parts.length === 3) { + // HH:MM:SS + const hours = parts[0] ?? 0; + const minutes = parts[1] ?? 0; + const seconds = parts[2] ?? 0; + if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return null; + return (hours * 3600 + minutes * 60 + seconds) * 1000; + } else if (parts.length === 2) { + // MM:SS + const minutes = parts[0] ?? 0; + const seconds = parts[1] ?? 0; + if (isNaN(minutes) || isNaN(seconds)) return null; + return (minutes * 60 + seconds) * 1000; + } + + return null; +} + +/** + * Convertit des millisecondes en format HH:MM:SS + * @param ms - Durée en millisecondes + * @returns Durée au format "HH:MM:SS" ou "MM:SS" + */ +export function formatMsToTime(ms: number | null): string { + if (ms === null || ms === undefined || ms < 0) return '--:--'; + + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const h = hours > 0 ? `${hours}:` : ''; + const m = `${minutes.toString().padStart(2, '0')}:`; + const s = seconds.toString().padStart(2, '0'); + + return `${h}${m}${s}`; +} + +/** + * Convertit des millisecondes en format court (pour l'affichage progress) + * @param ms - Durée en millisecondes + * @returns Durée au format "X:XX" ou "X:XX:XX" + */ +export function formatMsToShortTime(ms: number | null): string { + return formatMsToTime(ms); +} \ No newline at end of file