Initial commit for Greenlens
This commit is contained in:
134
components/SafeImage.tsx
Normal file
134
components/SafeImage.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user