162 lines
4.3 KiB
TypeScript
162 lines
4.3 KiB
TypeScript
|
|
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++;
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
if (!currentUrl.value) return;
|
||
|
|
|
||
|
|
// Update cache-busted URL with new retry count
|
||
|
|
cacheBustedUrl.value = getCacheBustedUrl(
|
||
|
|
currentUrl.value,
|
||
|
|
retryCount.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() {
|
||
|
|
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) => {
|
||
|
|
imageError.value = false;
|
||
|
|
retryCount.value = 0;
|
||
|
|
|
||
|
|
// Si c'est un changement d'URL (pas l'initialisation)
|
||
|
|
if (oldUri && newUri && oldUri !== newUri) {
|
||
|
|
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);
|
||
|
|
} 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,
|
||
|
|
};
|
||
|
|
}
|