Greenlens/components/SafeImage.tsx

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',
},
});