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:
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
178
pmoapp/webapp/src/composables/useCoverImage.ts
Normal file
178
pmoapp/webapp/src/composables/useCoverImage.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user