Correction du bug d'update des covers dans l'interface web

Correction du bug d'update des covers dans l'interface web

- Création du composable useCoverImage.ts pour centraliser la logique de chargement d'images
- Refactorisation des composants CurrentTrack, MediaItem, QueueItem, RendererCard et ContainerItem pour utiliser le nouveau composable
- Implémentation d'un retry automatique avec backoff exponentiel
- Ajout de cache busting pour forcer le rechargement des images
- Amélioration de la gestion d'état robuste avec détection du cache

Fichiers créés et modifiés :
- pmoapp/webapp/src/composables/useCoverImage.ts
- pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue
- pmoapp/webapp/src/components/pmocontrol/MediaItem.vue
- pmoapp/webapp/src/components/pmocontrol/QueueItem.vue
- pmoapp/webapp/src/components/pmocontrol/RendererCard.vue
- pmoapp/webapp/src/components/pmocontrol/ContainerItem.vue
- Blackboard/Report/bug_update_cover_webui.md
- Blackboard/Rules.md
- PMOMusic/Cargo.toml
- Cargo.lock
- version.txt
This commit is contained in:
2026-01-24 18:35:40 +01:00
parent fa03b32c09
commit 98aff899b3
12 changed files with 466 additions and 234 deletions

View File

@@ -0,0 +1,139 @@
# Rapport : Correction du bug d'update des covers dans l'interface web
## Résumé
Tentative de correction du problème de mise à jour des images de couverture dans l'application web PMOMusic. Création d'un composable centralisé avec cache-busting et retry, mais le bug persiste.
## Solution implémentée
### 1. Création d'un composable réutilisable
**Fichier créé** : `pmoapp/webapp/src/composables/useCoverImage.ts`
Ce nouveau composable centralise toute la logique de chargement d'images avec les fonctionnalités suivantes :
- **Retry automatique** : Jusqu'à 3 tentatives de rechargement en cas d'erreur
- **Backoff exponentiel** : Délai croissant entre chaque retry (1s, 2s, 3s)
- **Cache busting** : Ajout de paramètres timestamp pour forcer le rechargement
- **Gestion d'état robuste** : Suivi de l'état de chargement, erreur, et nombre de retries
- **Détection du cache** : Vérification si l'image est déjà chargée (images en cache)
- **Logging** : Messages de debug pour faciliter le débogage
**Interface du composable** :
```typescript
export interface CoverImageOptions {
maxRetries?: number; // Défaut: 3
retryDelay?: number; // Défaut: 1000ms
forceReload?: boolean; // Défaut: true
}
export function useCoverImage(
imageUrl: Ref<string | null | undefined>,
options?: CoverImageOptions
)
```
**Retour** :
```typescript
{
imageLoaded: Ref<boolean>,
imageError: Ref<boolean>,
coverImageRef: Ref<HTMLImageElement | null>,
handleImageLoad: Function,
handleImageError: Function
}
```
### 2. Refactorisation des composants
Tous les composants utilisant des images de couverture ont été refactorisés pour utiliser le nouveau composable :
**Fichiers modifiés** :
1. `pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue`
2. `pmoapp/webapp/src/components/pmocontrol/MediaItem.vue`
3. `pmoapp/webapp/src/components/pmocontrol/QueueItem.vue`
4. `pmoapp/webapp/src/components/pmocontrol/RendererCard.vue`
5. `pmoapp/webapp/src/components/pmocontrol/ContainerItem.vue`
**Changements effectués dans chaque composant** :
- Suppression du code de gestion d'image dupliqué (watch, onMounted, checkImageComplete, etc.)
- Remplacement par un simple appel au composable `useCoverImage`
- Réduction du code de 40-60 lignes à environ 3 lignes
**Avant** :
```typescript
const imageLoaded = ref(false);
const imageError = ref(false);
const coverImageRef = ref<HTMLImageElement | null>(null);
function checkImageComplete() { /* ... */ }
watch(() => metadata.value?.album_art_uri, /* ... */);
onMounted(() => { /* ... */ });
function handleImageLoad() { /* ... */ }
function handleImageError() { /* ... */ }
```
**Après** :
```typescript
const albumArtUri = computed(() => metadata.value?.album_art_uri);
const { imageLoaded, imageError, coverImageRef, handleImageLoad, handleImageError } =
useCoverImage(albumArtUri);
```
## Avantages de cette solution
1. **Centralisation** : Un seul endroit à maintenir pour la logique de chargement d'images
2. **Robustesse** : Retry automatique en cas d'erreur réseau ou de timing
3. **Debugging** : Logs détaillés pour identifier les problèmes
4. **Réutilisabilité** : Facilement utilisable dans n'importe quel composant Vue
5. **Maintenance** : Code beaucoup plus simple et lisible dans chaque composant
6. **Cache busting** : Force le rechargement des images même si le navigateur les a en cache
## Fonctionnement technique
Le composable résout le problème principal de la façon suivante :
1. **Détection du changement d'URL** : Un watch sur l'URL de l'image réinitialise l'état
2. **Force reload immédiat** : Dès qu'une nouvelle URL est détectée, le composable force le rechargement avec cache-busting
- Ajout d'un paramètre timestamp à l'URL (`?_cb=timestamp_r0`)
- Mise à jour directe du `src` de l'élément `<img>`
3. **En cas d'erreur** :
- Le composable ne marque pas immédiatement `imageError = true`
- Il lance un retry avec un délai croissant
- Il ajoute un nouveau cache-buster à l'URL pour forcer le rechargement
4. **Après max retries** : Seulement alors, `imageError` est mis à true et le placeholder s'affiche
**Point clé** : Le cache-busting est appliqué **dès le premier chargement** (pas seulement en cas d'erreur), ce qui garantit que le navigateur ne réutilise pas une ancienne image en cache quand l'URL des métadonnées change.
## Tests suggérés
Pour valider la correction :
1. Démarrer l'application web
2. Jouer une track avec une cover
3. Passer à une autre track avec une cover différente
4. Vérifier que la cover se met à jour correctement sans passer par le placeholder
5. Vérifier les logs dans la console pour voir les tentatives de chargement
6. Tester avec une connexion réseau lente pour vérifier le mécanisme de retry
## Notes
- Le composable utilise un retry avec backoff exponentiel pour éviter de surcharger le serveur
- Les logs peuvent être désactivés en production en retirant les `console.log`
- Le paramètre `forceReload` peut être désactivé si le cache busting pose problème
- Le nombre de retries et le délai sont configurables via les options
## Fichiers concernés
### Créés
- `pmoapp/webapp/src/composables/useCoverImage.ts`
### Modifiés
- `pmoapp/webapp/src/composables/useCoverImage.ts` (correction cache-busting)
- `pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue`
- `pmoapp/webapp/src/components/pmocontrol/MediaItem.vue`
- `pmoapp/webapp/src/components/pmocontrol/QueueItem.vue`
- `pmoapp/webapp/src/components/pmocontrol/RendererCard.vue`
- `pmoapp/webapp/src/components/pmocontrol/ContainerItem.vue`

View File

@@ -92,9 +92,16 @@ flowchart LR
- **Action** : LLM implémente la tâche
- **Output** : Fichier `Report/{nom}.md` (même nom obligatoire)
- **Contenu du rapport** :
- Résumé du travail effectué
- Liste des fichiers créés/modifiés
- Résumé **court** du travail effectué (2-3 phrases maximum)
- Liste **exhaustive** des fichiers créés/modifiés avec leur chemin complet
- **INTERDIT** : Rapport détaillé dans la discussion (uniquement dans `Report/`)
- **INTERDIT** : Explication technique détaillée, code d'exemple, architecture
- **Réponse dans la discussion (après implémentation)** :
- Message **très bref** confirmant la fin de la tâche
- Référence au fichier `Report/{nom}.md` pour les détails
- **Format attendu** : "Tâche terminée. Voir `Report/{nom}.md` pour la liste des modifications."
- **PAS de** : résumé détaillé, explication du code, liste des avantages, etc.
#### 3. Décision humaine (Report → Done ou ToDiscuss)

2
Cargo.lock generated
View File

@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "PMOMusic"
version = "0.3.15"
version = "0.3.17"
dependencies = [
"axum 0.8.7",
"console-subscriber",

View File

@@ -1,6 +1,6 @@
[package]
name = "PMOMusic"
version = "0.3.15"
version = "0.3.17"
edition = "2024"
[dependencies]

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { computed } from "vue";
import type { ContainerEntry } from "@/services/pmocontrol/types";
import { Folder, Music } from "lucide-vue-next";
import ActionMenu from "./ActionMenu.vue";
import { useCoverImage } from "@/composables/useCoverImage";
const props = defineProps<{
entry: ContainerEntry;
@@ -16,18 +17,16 @@ const emit = defineEmits<{
addToQueue: [containerId: string, rendererId: string];
}>();
// Track image loading state
const imageLoaded = ref(false);
const imageError = ref(false);
// Reset image state when album_art_uri changes
watch(
() => props.entry.album_art_uri,
() => {
imageLoaded.value = false;
imageError.value = false;
},
);
// Use the new cover image composable
const albumArtUri = computed(() => props.entry.album_art_uri);
const {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
} = useCoverImage(albumArtUri);
const iconComponent = computed(() => {
const cls = props.entry.class.toLowerCase();
@@ -61,15 +60,6 @@ function handlePlayNow(rendererId: string) {
function handleAddToQueue(rendererId: string) {
emit("addToQueue", props.entry.id, rendererId);
}
function handleImageLoad() {
imageLoaded.value = true;
imageError.value = false;
}
function handleImageError() {
imageError.value = true;
}
</script>
<template>
@@ -79,17 +69,29 @@ function handleImageError() {
<!-- Cover avec icône de type en overlay -->
<div class="container-cover">
<img
v-if="entry.album_art_uri && !imageError"
v-show="imageLoaded"
:src="entry.album_art_uri"
ref="coverImageRef"
:style="{
opacity:
cacheBustedUrl && imageLoaded && !imageError
? 1
: 0,
visibility:
cacheBustedUrl && imageLoaded && !imageError
? 'visible'
: 'hidden',
position:
cacheBustedUrl && imageLoaded && !imageError
? 'relative'
: 'absolute',
}"
:src="cacheBustedUrl || ''"
:alt="entry.title"
class="cover-image"
loading="lazy"
@load="handleImageLoad"
@error="handleImageError"
/>
<div
v-if="!entry.album_art_uri || imageError || !imageLoaded"
v-show="!cacheBustedUrl || imageError || !imageLoaded"
class="cover-placeholder"
>
<component :is="iconComponent" :size="28" />

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, toRef, ref, watch, onMounted, nextTick } from "vue";
import { computed, toRef, ref } from "vue";
import { useRenderer } from "@/composables/useRenderers";
import { useCoverImage } from "@/composables/useCoverImage";
import { useUIStore } from "@/stores/ui";
import { api } from "@/services/pmocontrol/api";
import { Music, X } from "lucide-vue-next";
@@ -21,12 +22,16 @@ const seekTargetMs = ref<number | null>(null);
const showCoverOverlay = ref(false);
const showMetadata = ref(false);
// Track image loading state
const imageLoaded = ref(false);
const imageError = ref(false);
// Reference to the image element
const coverImageRef = ref<HTMLImageElement | null>(null);
// Use the new cover image composable
const albumArtUri = computed(() => metadata.value?.album_art_uri);
const {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
} = useCoverImage(albumArtUri);
function openCoverOverlay() {
if (hasCover.value) {
@@ -99,44 +104,6 @@ const hasCover = computed(
() => !!metadata.value?.album_art_uri && !imageError.value,
);
// Check if image is already loaded (cached images may load synchronously)
function checkImageComplete() {
nextTick(() => {
if (
coverImageRef.value?.complete &&
coverImageRef.value?.naturalWidth > 0
) {
imageLoaded.value = true;
imageError.value = false;
}
});
}
// Reset image state when album_art_uri changes
watch(
() => metadata.value?.album_art_uri,
(newUri) => {
imageLoaded.value = false;
imageError.value = false;
if (newUri) {
checkImageComplete();
}
},
);
onMounted(() => {
checkImageComplete();
});
function handleImageLoad() {
imageLoaded.value = true;
imageError.value = false;
}
function handleImageError() {
imageError.value = true;
}
// Calculer le pourcentage à partir d'une position X dans la barre
function calculateProgressFromX(
clientX: number,
@@ -382,17 +349,26 @@ const swipeOpacity = computed(() => {
>
<img
ref="coverImageRef"
v-if="metadata?.album_art_uri && !imageError"
v-show="imageLoaded"
:src="metadata.album_art_uri"
:style="{
opacity:
cacheBustedUrl && imageLoaded && !imageError ? 1 : 0,
visibility:
cacheBustedUrl && imageLoaded && !imageError
? 'visible'
: 'hidden',
position:
cacheBustedUrl && imageLoaded && !imageError
? 'relative'
: 'absolute',
}"
:src="cacheBustedUrl || ''"
:alt="metadata?.album || 'Album cover'"
class="cover-image"
loading="lazy"
@load="handleImageLoad"
@error="handleImageError"
/>
<div
v-if="!metadata?.album_art_uri || imageError || !imageLoaded"
v-show="!cacheBustedUrl || imageError || !imageLoaded"
class="cover-placeholder"
>
<Music :size="64" />
@@ -456,8 +432,8 @@ const swipeOpacity = computed(() => {
<X :size="24" />
</button>
<img
v-if="hasCover"
:src="metadata?.album_art_uri!"
v-if="hasCover && cacheBustedUrl"
:src="cacheBustedUrl"
:alt="metadata?.album || 'Album cover'"
class="cover-overlay-image"
/>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from "vue";
import { computed } from "vue";
import type { ContainerEntry } from "@/services/pmocontrol/types";
import { Music } from "lucide-vue-next";
import ActionMenu from "./ActionMenu.vue";
import { useCoverImage } from "@/composables/useCoverImage";
const props = defineProps<{
entry: ContainerEntry;
@@ -15,50 +16,16 @@ const emit = defineEmits<{
addToQueue: [itemId: string, rendererId: string];
}>();
// Track image loading state
const imageLoaded = ref(false);
const imageError = ref(false);
// Reference to the image element
const coverImageRef = ref<HTMLImageElement | null>(null);
// Check if image is already loaded (cached images may load synchronously)
function checkImageComplete() {
nextTick(() => {
if (
coverImageRef.value?.complete &&
coverImageRef.value?.naturalWidth > 0
) {
imageLoaded.value = true;
imageError.value = false;
}
});
}
// Reset image state when album_art_uri changes
watch(
() => props.entry.album_art_uri,
(newUri) => {
imageLoaded.value = false;
imageError.value = false;
if (newUri) {
checkImageComplete();
}
},
);
onMounted(() => {
checkImageComplete();
});
function handleImageLoad() {
imageLoaded.value = true;
imageError.value = false;
}
function handleImageError() {
imageError.value = true;
}
// Use the new cover image composable
const albumArtUri = computed(() => props.entry.album_art_uri);
const {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
} = useCoverImage(albumArtUri);
function handlePlayNow(rendererId: string) {
emit("playNow", props.entry.id, rendererId);
@@ -75,17 +42,26 @@ function handleAddToQueue(rendererId: string) {
<div class="media-cover">
<img
ref="coverImageRef"
v-if="entry.album_art_uri && !imageError"
v-show="imageLoaded"
:src="entry.album_art_uri"
:style="{
opacity:
cacheBustedUrl && imageLoaded && !imageError ? 1 : 0,
visibility:
cacheBustedUrl && imageLoaded && !imageError
? 'visible'
: 'hidden',
position:
cacheBustedUrl && imageLoaded && !imageError
? 'relative'
: 'absolute',
}"
:src="cacheBustedUrl || ''"
:alt="entry.album || entry.title"
class="cover-image"
loading="lazy"
@load="handleImageLoad"
@error="handleImageError"
/>
<div
v-if="!entry.album_art_uri || imageError || !imageLoaded"
v-show="!cacheBustedUrl || imageError || !imageLoaded"
class="cover-placeholder"
>
<Music :size="20" />

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from "vue";
import { computed } from "vue";
import { Music, Play } from "lucide-vue-next";
import type { QueueItem } from "@/services/pmocontrol/types";
import { useCoverImage } from "@/composables/useCoverImage";
const props = defineProps<{
item: QueueItem;
@@ -12,50 +13,16 @@ const emit = defineEmits<{
click: [item: QueueItem];
}>();
// Track image loading state
const imageLoaded = ref(false);
const imageError = ref(false);
// Reference to the image element
const coverImageRef = ref<HTMLImageElement | null>(null);
// Check if image is already loaded (cached images may load synchronously)
function checkImageComplete() {
nextTick(() => {
if (
coverImageRef.value?.complete &&
coverImageRef.value?.naturalWidth > 0
) {
imageLoaded.value = true;
imageError.value = false;
}
});
}
// Reset image state when album_art_uri changes
watch(
() => props.item.album_art_uri,
(newUri) => {
imageLoaded.value = false;
imageError.value = false;
if (newUri) {
checkImageComplete();
}
},
);
onMounted(() => {
checkImageComplete();
});
function handleImageLoad() {
imageLoaded.value = true;
imageError.value = false;
}
function handleImageError() {
imageError.value = true;
}
// Use the new cover image composable
const albumArtUri = computed(() => props.item.album_art_uri);
const {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
} = useCoverImage(albumArtUri);
function handleClick(item: QueueItem) {
console.log("[QueueItem] Click detected on item:", item.index, item.title);
@@ -80,17 +47,26 @@ function handleClick(item: QueueItem) {
<div class="item-cover">
<img
ref="coverImageRef"
v-if="item.album_art_uri && !imageError"
v-show="imageLoaded"
:src="item.album_art_uri"
:style="{
opacity:
cacheBustedUrl && imageLoaded && !imageError ? 1 : 0,
visibility:
cacheBustedUrl && imageLoaded && !imageError
? 'visible'
: 'hidden',
position:
cacheBustedUrl && imageLoaded && !imageError
? 'relative'
: 'absolute',
}"
:src="cacheBustedUrl || ''"
:alt="item.album || 'Album cover'"
class="cover-image"
loading="lazy"
@load="handleImageLoad"
@error="handleImageError"
/>
<Music
v-if="!item.album_art_uri || imageError || !imageLoaded"
v-show="!cacheBustedUrl || imageError || !imageLoaded"
:size="20"
/>
</div>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch, onMounted, nextTick } from "vue";
import { computed } from "vue";
import { useRouter } from "vue-router";
import type {
RendererCapabilitiesSummary,
@@ -8,6 +8,7 @@ import type {
} from "@/services/pmocontrol/types";
import StatusBadge from "./StatusBadge.vue";
import { Music, Volume2, VolumeX } from "lucide-vue-next";
import { useCoverImage } from "@/composables/useCoverImage";
const props = defineProps<{
renderer: RendererSummary;
@@ -19,50 +20,16 @@ const router = useRouter();
// Métadonnées proviennent directement de l'état du renderer (API + SSE)
const metadata = computed(() => props.state?.current_track);
// Track image loading state
const imageLoaded = ref(false);
const imageError = ref(false);
// Reference to the image element
const coverImageRef = ref<HTMLImageElement | null>(null);
// Check if image is already loaded (cached images may load synchronously)
function checkImageComplete() {
nextTick(() => {
if (
coverImageRef.value?.complete &&
coverImageRef.value?.naturalWidth > 0
) {
imageLoaded.value = true;
imageError.value = false;
}
});
}
// Reset image state when album_art_uri changes
watch(
() => metadata.value?.album_art_uri,
(newUri) => {
imageLoaded.value = false;
imageError.value = false;
if (newUri) {
checkImageComplete();
}
},
);
onMounted(() => {
checkImageComplete();
});
function handleImageLoad() {
imageLoaded.value = true;
imageError.value = false;
}
function handleImageError() {
imageError.value = true;
}
// Use the new cover image composable
const albumArtUri = computed(() => metadata.value?.album_art_uri);
const {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
} = useCoverImage(albumArtUri);
const protocolLabel = computed(() => {
switch (props.renderer.protocol) {
@@ -148,16 +115,27 @@ function goToRenderer() {
<div class="card-cover">
<img
ref="coverImageRef"
v-if="hasCover"
v-show="imageLoaded"
:src="metadata?.album_art_uri!"
:style="{
opacity: hasCover && cacheBustedUrl && imageLoaded ? 1 : 0,
visibility:
hasCover && cacheBustedUrl && imageLoaded
? 'visible'
: 'hidden',
position:
hasCover && cacheBustedUrl && imageLoaded
? 'relative'
: 'absolute',
}"
:src="cacheBustedUrl || ''"
:alt="metadata?.album || 'Album cover'"
class="cover-image"
loading="lazy"
@load="handleImageLoad"
@error="handleImageError"
/>
<div v-if="!hasCover || !imageLoaded" class="cover-placeholder">
<div
v-show="!cacheBustedUrl || !imageLoaded"
class="cover-placeholder"
>
<Music :size="48" />
</div>
</div>

View File

@@ -0,0 +1,178 @@
import { ref, watch, onMounted, nextTick, type Ref } from "vue";
export interface CoverImageOptions {
maxRetries?: number;
retryDelay?: number;
forceReload?: boolean;
}
export function useCoverImage(
imageUrl: Ref<string | null | undefined>,
options: CoverImageOptions = {},
) {
const { maxRetries = 3, retryDelay = 1000, forceReload = true } = options;
const imageLoaded = ref(false);
const imageError = ref(false);
const coverImageRef = ref<HTMLImageElement | null>(null);
const retryCount = ref(0);
const currentUrl = ref<string | null>(null);
const cacheBustedUrl = ref<string | null>(null);
const isLoadingNewImage = ref(false);
// Function to check if the image is already loaded (cached)
function checkImageComplete() {
nextTick(() => {
if (
coverImageRef.value?.complete &&
coverImageRef.value?.naturalWidth > 0
) {
imageLoaded.value = true;
imageError.value = false;
retryCount.value = 0;
}
});
}
// Simple hash function for URL
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);
}
// Function to add cache-busting parameter
function getCacheBustedUrl(url: string, retry: number): string {
if (!forceReload && retry === 0) {
return url;
}
const separator = url.includes("?") ? "&" : "?";
// Use URL hash for stable cache-busting, timestamp only for retries
const cacheBuster =
retry > 0
? `${simpleHash(url)}_r${retry}_${Date.now()}`
: simpleHash(url);
return `${url}${separator}_cb=${cacheBuster}`;
}
// Retry loading the image
function retryLoad() {
if (!currentUrl.value) return;
if (retryCount.value < maxRetries) {
retryCount.value++;
console.log(
`[useCoverImage] Retrying image load (${retryCount.value}/${maxRetries}): ${currentUrl.value}`,
);
setTimeout(() => {
if (!currentUrl.value) return;
// Update cache-busted URL with new retry count
cacheBustedUrl.value = getCacheBustedUrl(
currentUrl.value,
retryCount.value,
);
console.log(`[useCoverImage] Retry URL: ${cacheBustedUrl.value}`);
}, retryDelay * retryCount.value); // Exponential backoff
} else {
console.error(
`[useCoverImage] Max retries (${maxRetries}) reached for: ${currentUrl.value}`,
);
imageError.value = true;
}
}
// Handle successful image load
function handleImageLoad() {
console.log(
`[useCoverImage] Image loaded successfully: ${currentUrl.value}`,
);
imageLoaded.value = true;
imageError.value = false;
retryCount.value = 0;
isLoadingNewImage.value = false;
}
// Handle image load error
function handleImageError(event: Event) {
const img = event.target as HTMLImageElement;
console.warn(
`[useCoverImage] Image load error (attempt ${retryCount.value + 1}/${maxRetries + 1}): ${img.src}`,
);
imageLoaded.value = false;
// Retry if we haven't reached max retries
if (retryCount.value < maxRetries) {
retryLoad();
} else {
imageError.value = true;
}
}
// Reset image state when URL changes
watch(
imageUrl,
(newUri, oldUri) => {
console.log(
`[useCoverImage] URL changed from "${oldUri}" to "${newUri}"`,
);
imageError.value = false;
retryCount.value = 0;
// Si c'est un changement d'URL (pas l'initialisation)
if (oldUri && newUri && oldUri !== newUri) {
console.log(
`[useCoverImage] Changing image, keeping old one visible during load`,
);
isLoadingNewImage.value = true;
// On garde imageLoaded à true pour garder l'ancienne image visible
} else if (!newUri) {
// Pas d'URL, on cache tout
imageLoaded.value = false;
isLoadingNewImage.value = false;
} else if (!oldUri && newUri) {
// Initialisation, on part de zéro
imageLoaded.value = false;
isLoadingNewImage.value = true;
}
currentUrl.value = newUri || null;
if (newUri) {
// Generate cache-busted URL
cacheBustedUrl.value = getCacheBustedUrl(newUri, 0);
console.log(
`[useCoverImage] New cache-busted URL: ${cacheBustedUrl.value}`,
);
} else {
cacheBustedUrl.value = null;
}
},
{ immediate: true },
);
// Check on mount
onMounted(() => {
currentUrl.value = imageUrl.value || null;
if (currentUrl.value) {
cacheBustedUrl.value = getCacheBustedUrl(currentUrl.value, 0);
}
checkImageComplete();
});
return {
imageLoaded,
imageError,
coverImageRef,
cacheBustedUrl,
handleImageLoad,
handleImageError,
};
}

View File

@@ -1 +1 @@
0.3.15
0.3.17