135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
import React from 'react';
|
|
import { Image, ImageProps, StyleSheet, Text, View } from 'react-native';
|
|
import {
|
|
DEFAULT_PLANT_IMAGE_URI,
|
|
getCategoryFallbackImage,
|
|
getPlantImageSourceFallbackUri,
|
|
getWikimediaFilePathFromThumbnailUrl,
|
|
resolveImageUri,
|
|
tryResolveImageUri,
|
|
} from '../utils/imageUri';
|
|
|
|
type SafeImageFallbackMode = 'category' | 'default' | 'none';
|
|
|
|
interface SafeImageProps extends Omit<ImageProps, 'source'> {
|
|
uri?: string | null;
|
|
categories?: string[];
|
|
fallbackMode?: SafeImageFallbackMode;
|
|
placeholderLabel?: string;
|
|
}
|
|
|
|
const getPlaceholderInitial = (label?: string): string => {
|
|
if (!label) return '?';
|
|
const trimmed = label.trim();
|
|
if (!trimmed) return '?';
|
|
return trimmed.charAt(0).toUpperCase();
|
|
};
|
|
|
|
export const SafeImage: React.FC<SafeImageProps> = ({
|
|
uri,
|
|
categories,
|
|
fallbackMode = 'category',
|
|
placeholderLabel,
|
|
onError,
|
|
style,
|
|
...props
|
|
}) => {
|
|
const categoryFallback = categories && categories.length > 0
|
|
? getCategoryFallbackImage(categories)
|
|
: DEFAULT_PLANT_IMAGE_URI;
|
|
const selectedFallback = fallbackMode === 'category'
|
|
? categoryFallback
|
|
: DEFAULT_PLANT_IMAGE_URI;
|
|
|
|
const [resolvedUri, setResolvedUri] = React.useState<string>(() => {
|
|
const strictResolved = tryResolveImageUri(uri);
|
|
if (strictResolved) return strictResolved;
|
|
return fallbackMode === 'none' ? '' : selectedFallback;
|
|
});
|
|
const [showPlaceholder, setShowPlaceholder] = React.useState<boolean>(() => {
|
|
if (fallbackMode !== 'none') return false;
|
|
return !tryResolveImageUri(uri);
|
|
});
|
|
const [retryCount, setRetryCount] = React.useState(0);
|
|
const lastAttemptUri = React.useRef<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
const strictResolved = tryResolveImageUri(uri);
|
|
setResolvedUri(strictResolved || (fallbackMode === 'none' ? '' : selectedFallback));
|
|
setShowPlaceholder(fallbackMode === 'none' && !strictResolved);
|
|
setRetryCount(0);
|
|
lastAttemptUri.current = strictResolved;
|
|
}, [uri, fallbackMode, selectedFallback]);
|
|
|
|
if (fallbackMode === 'none' && showPlaceholder) {
|
|
return (
|
|
<View style={[styles.placeholder, style]}>
|
|
<Text style={styles.placeholderText}>{getPlaceholderInitial(placeholderLabel)}</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Image
|
|
{...props}
|
|
style={style}
|
|
source={{
|
|
uri: resolvedUri || selectedFallback
|
|
}}
|
|
onError={(event) => {
|
|
onError?.(event);
|
|
|
|
const currentUri = resolvedUri || selectedFallback;
|
|
|
|
// Smart Retry Logic for Wikimedia (first failure only)
|
|
if (retryCount === 0 && currentUri.includes('upload.wikimedia.org')) {
|
|
const fileName = getWikimediaFilePathFromThumbnailUrl(currentUri);
|
|
if (fileName) {
|
|
const redirectUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(fileName)}`;
|
|
setRetryCount(1);
|
|
setResolvedUri(redirectUrl);
|
|
lastAttemptUri.current = redirectUrl;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const sourceFallbackUri = getPlantImageSourceFallbackUri(uri);
|
|
if (sourceFallbackUri && sourceFallbackUri !== currentUri && lastAttemptUri.current !== sourceFallbackUri) {
|
|
setResolvedUri(sourceFallbackUri);
|
|
lastAttemptUri.current = sourceFallbackUri;
|
|
return;
|
|
}
|
|
|
|
// If we get here, either it wasn't a Wikimedia URL, or filename extraction failed,
|
|
// or the retry itself failed.
|
|
if (fallbackMode === 'none') {
|
|
setShowPlaceholder(true);
|
|
return;
|
|
}
|
|
|
|
setResolvedUri((current) => {
|
|
if (current === DEFAULT_PLANT_IMAGE_URI) return current;
|
|
if (current === selectedFallback) return DEFAULT_PLANT_IMAGE_URI;
|
|
return selectedFallback;
|
|
});
|
|
|
|
// Prevent infinite loops if fallbacks also fail
|
|
setRetryCount(current => current + 1);
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
placeholder: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: '#e7ece8',
|
|
},
|
|
placeholderText: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
color: '#5f6f63',
|
|
},
|
|
});
|