header size

This commit is contained in:
m
2026-07-29 20:16:20 +02:00
parent 60b549902c
commit 3c7a1d18da
3 changed files with 438 additions and 164 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ function AppNavigator() {
<Stack.Screen <Stack.Screen
name="FileDetail" name="FileDetail"
component={FileDetailScreen} component={FileDetailScreen}
options={{ title: 'Détails', headerTintColor: '#fff', headerStyle: { backgroundColor: '#000' } }} options={{ title: 'Détails', headerTintColor: '#fff', headerStyle: { backgroundColor: '#000' }, animation: 'fade' }}
/> />
<Stack.Screen name="FileEdit" component={FileEditScreen} options={{ title: 'Édition' }} /> <Stack.Screen name="FileEdit" component={FileEditScreen} options={{ title: 'Édition' }} />
<Stack.Screen name="Folder" component={FolderScreen} options={{ title: 'Dossier' }} /> <Stack.Screen name="Folder" component={FolderScreen} options={{ title: 'Dossier' }} />
+380 -107
View File
@@ -1,26 +1,33 @@
import React, { useRef, useState, useCallback, useEffect } from 'react'; import React, { useRef, useState, useCallback } from 'react';
import { import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
FlatList, FlatList,
Image,
Dimensions, Dimensions,
ActivityIndicator, ActivityIndicator,
ScrollView,
Modal, Modal,
Pressable, Pressable,
TouchableOpacity, TouchableOpacity,
Share,
Alert,
} from 'react-native'; } from 'react-native';
import { Image } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { RouteProp, useRoute } from '@react-navigation/native'; import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useFile, useDownloadFile } from '../hooks/useFiles'; import { useFile, useDownloadFile } from '../hooks/useFiles';
import { fileStore } from '../services/fileStore'; import { fileStore } from '../services/fileStore';
import { TagChip } from '../components/TagChip'; import { TagChip } from '../components/TagChip';
import { FileThumbnail } from '../components/FileThumbnail'; import { FileThumbnail } from '../components/FileThumbnail';
import { SyncStatusBadge } from '../components/SyncStatusBadge'; import { SyncStatusBadge } from '../components/SyncStatusBadge';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated';
import { ZoomableImage } from '../components/ZoomableImage'; import { ZoomableImage } from '../components/ZoomableImage';
import { apiClient } from '../api/client';
import { ENDPOINTS } from '../constants/api';
import { downloadRegistry } from '../services/downloadRegistry';
import { deleteAsync } from 'expo-file-system/legacy';
import type { Variant, SyncStatus } from '../types'; import type { Variant, SyncStatus } from '../types';
const SCREEN_WIDTH = Dimensions.get('window').width; const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -42,8 +49,13 @@ type RootStackParamList = {
type FileDetailRouteProp = RouteProp<RootStackParamList, 'FileDetail'>; type FileDetailRouteProp = RouteProp<RootStackParamList, 'FileDetail'>;
const PANEL_HEIGHT = 410;
const PANEL_HEADER_VISIBLE = 100;
function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; deviceFile?: DeviceFileParam; onSelectImage?: (state: ModalState) => void }) { function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; deviceFile?: DeviceFileParam; onSelectImage?: (state: ModalState) => void }) {
const isDevice = !!deviceFile; const isDevice = !!deviceFile;
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const localEntry = fileStore.getById(fileId); const localEntry = fileStore.getById(fileId);
const apiId = isDevice ? '' : (localEntry?.backendId ?? fileId); const apiId = isDevice ? '' : (localEntry?.backendId ?? fileId);
@@ -55,14 +67,10 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
const uri = isDevice ? deviceFile.localUri : (localEntry?.localUri ?? file?.url); const uri = isDevice ? deviceFile.localUri : (localEntry?.localUri ?? file?.url);
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null); const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
useEffect(() => {
if (!uri) { setImageSize(null); return; } const handleImageLoad = useCallback((event: { source: { width: number; height: number } }) => {
Image.getSize( setImageSize({ width: event.source.width, height: event.source.height });
uri, }, []);
(w, h) => setImageSize({ width: w, height: h }),
() => setImageSize(null),
);
}, [uri]);
const syncStatus: SyncStatus = isDevice const syncStatus: SyncStatus = isDevice
? 'local' ? 'local'
@@ -76,6 +84,26 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
const fullThumbnails = fullVariants; const fullThumbnails = fullVariants;
const hasPages = fullThumbnails.length > 0; const hasPages = fullThumbnails.length > 0;
const fileName = deviceFile?.name ?? file?.name ?? fileId;
const createdAt = deviceFile?.createdAt ?? file?.createdAt;
const formattedDate = createdAt
? new Date(createdAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })
: '';
const fileSize = file?.size ?? 0;
const formattedSize = fileSize > 0
? fileSize > 1024 * 1024
? `${(fileSize / (1024 * 1024)).toFixed(1)} Mo`
: `${(fileSize / 1024).toFixed(1)} Ko`
: '';
const [optionsVisible, setOptionsVisible] = useState(false);
const [deleting, setDeleting] = useState(false);
const panelOffset = useSharedValue(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
const panelStartY = useSharedValue(0);
const isPanelExpanded = useSharedValue(false);
const [panelExpanded, setPanelExpanded] = useState(false);
const handleDownload = useCallback(async () => { const handleDownload = useCallback(async () => {
if (!file) return; if (!file) return;
@@ -85,7 +113,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
await downloadFile.mutateAsync({ await downloadFile.mutateAsync({
id: bid, id: bid,
backendResourceId: bid, backendResourceId: bid,
name: file?.name ?? localEntry?.name ?? fileId, name: fileName,
mimeType: file?.mimeType ?? localEntry?.mimeType ?? 'application/octet-stream', mimeType: file?.mimeType ?? localEntry?.mimeType ?? 'application/octet-stream',
size: file?.size ?? localEntry?.size ?? 0, size: file?.size ?? localEntry?.size ?? 0,
createdAt: file?.createdAt ?? localEntry?.createdAt ?? new Date().toISOString(), createdAt: file?.createdAt ?? localEntry?.createdAt ?? new Date().toISOString(),
@@ -97,12 +125,90 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
} catch {} finally { } catch {} finally {
setDownloading(false); setDownloading(false);
} }
}, [file, fileId, localEntry, downloadFile]); }, [file, fileName, fileId, localEntry, downloadFile]);
return ( const handleShare = useCallback(async () => {
<ScrollView style={styles.page} contentContainerStyle={styles.pageContent}> setOptionsVisible(false);
{hasPages ? ( if (uri) {
fullThumbnails.map((thumb, thumbIndex) => { await Share.share({ url: uri, title: fileName });
} else if (file?.url) {
await Share.share({ url: file.url, title: fileName });
}
}, [uri, fileName, file?.url]);
const handleDelete = useCallback(() => {
setOptionsVisible(false);
Alert.alert(
'Supprimer',
`Supprimer "${fileName}" définitivement ?`,
[
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: async () => {
setDeleting(true);
try {
const bid = localEntry?.backendId ?? (file as any)?.backendResourceId;
if (bid) {
await apiClient.delete(`${ENDPOINTS.RESOURCES}/${bid}`);
fileStore.deleteByBackendId(bid);
} else {
fileStore.deleteById(fileId);
}
if (localEntry?.localUri) {
await deleteAsync(localEntry.localUri, { idempotent: true });
}
downloadRegistry.remove(fileId);
navigation.goBack();
} catch {} finally {
setDeleting(false);
}
},
},
],
);
}, [fileName, fileId, localEntry, file, navigation]);
const togglePanelJS = useCallback(() => {
if (isPanelExpanded.value) {
panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
isPanelExpanded.value = false;
setPanelExpanded(false);
} else {
panelOffset.value = withSpring(0);
isPanelExpanded.value = true;
setPanelExpanded(true);
}
}, []);
const panGesture = Gesture.Pan()
.onStart(() => {
panelStartY.value = panelOffset.value;
})
.onUpdate((event) => {
const offset = Math.max(0, Math.min(PANEL_HEIGHT - PANEL_HEADER_VISIBLE, panelStartY.value + event.translationY));
panelOffset.value = offset;
})
.onEnd(() => {
if (panelOffset.value > (PANEL_HEIGHT - PANEL_HEADER_VISIBLE) / 2) {
panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
isPanelExpanded.value = false;
runOnJS(setPanelExpanded)(false);
} else {
panelOffset.value = withSpring(0);
isPanelExpanded.value = true;
runOnJS(setPanelExpanded)(true);
}
});
const panelAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ translateY: panelOffset.value }],
}));
const renderImageContent = () => {
if (hasPages) {
return fullThumbnails.map((thumb, thumbIndex) => {
const allImages: SelectedImage[] = fullThumbnails.map((t) => ({ const allImages: SelectedImage[] = fullThumbnails.map((t) => ({
uri: t.url, uri: t.url,
width: SCREEN_WIDTH, width: SCREEN_WIDTH,
@@ -111,42 +217,44 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
return ( return (
<Pressable <Pressable
key={thumb.id} key={thumb.id}
style={styles.pageImageContainer}
onPress={() => onSelectImage?.({ images: allImages, index: thumbIndex })} onPress={() => onSelectImage?.({ images: allImages, index: thumbIndex })}
> >
<Image <Image
source={{ uri: thumb.url }} source={{ uri: thumb.url }}
style={[styles.pageImage, { height: thumb.height * (SCREEN_WIDTH / thumb.width) }]} style={[styles.image, { height: thumb.height * (SCREEN_WIDTH / thumb.width) }]}
resizeMode="contain" contentFit="contain"
cachePolicy="memory-disk"
transition={200}
/> />
</Pressable> </Pressable>
); );
}) });
) : ( }
<View style={styles.imageContainer}> if (uri) {
{uri ? ( return (
<Pressable
onPress={() => onSelectImage?.({
images: [{ uri, width: SCREEN_WIDTH, height: imageSize ? imageSize.height * SCREEN_WIDTH / imageSize.width : SCREEN_WIDTH }],
index: 0,
})}
>
<Image <Image
source={{ uri }} source={{ uri }}
style={[ style={[
styles.image, styles.image,
imageSize && { height: imageSize.height * SCREEN_WIDTH / imageSize.width }, imageSize
? { height: imageSize.height * SCREEN_WIDTH / imageSize.width }
: { aspectRatio: 1 },
]} ]}
resizeMode="contain" contentFit="contain"
cachePolicy="memory-disk"
transition={200}
onLoad={handleImageLoad}
/> />
</Pressable> );
) : file ? ( }
if (file) {
return (
<View style={styles.cloudOnlyContainer}> <View style={styles.cloudOnlyContainer}>
<FileThumbnail <FileThumbnail
thumbnailUrl={file?.thumbnailUrl} thumbnailUrl={file?.thumbnailUrl}
mimeType={file?.mimeType ?? 'application/pdf'} mimeType={file?.mimeType ?? 'application/pdf'}
fileName={file?.name ?? fileId} fileName={file?.name ?? fileId}
size={SCREEN_WIDTH * 0.6} size={SCREEN_WIDTH * 0.5}
/> />
{syncStatus === 'cloud' && ( {syncStatus === 'cloud' && (
<TouchableOpacity <TouchableOpacity
@@ -165,31 +273,72 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
) : ( );
<ActivityIndicator size="large" color="#1976D2" /> }
)} return <ActivityIndicator size="large" color="#1976D2" />;
</View> };
)}
<View style={styles.details}> if (deleting) {
<View style={styles.detailHeader}> return (
<Text style={styles.fileName}>{deviceFile?.name ?? file?.name ?? fileId}</Text> <View style={[styles.detailContainer, styles.center]}>
<ActivityIndicator size="large" color="#fff" />
</View>
);
}
return (
<View style={styles.detailContainer}>
<Pressable
style={StyleSheet.absoluteFill}
onPress={() => {
if (!hasPages && uri) {
const height = imageSize
? imageSize.height * SCREEN_WIDTH / imageSize.width
: SCREEN_WIDTH;
onSelectImage?.({
images: [{ uri, width: SCREEN_WIDTH, height }],
index: 0,
});
}
}}
>
<View style={styles.imageCentered}>
{renderImageContent()}
</View>
</Pressable>
<View style={[styles.headerOverlay, { paddingTop: insets.top + 8 }]}>
<Text style={styles.headerDate} numberOfLines={1}>{formattedDate}</Text>
<TouchableOpacity onPress={() => setOptionsVisible(true)} style={styles.headerBtn}>
<MaterialIcons name="more-vert" size={24} color="#fff" />
</TouchableOpacity>
</View>
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.panel, { paddingBottom: insets.bottom + 12 }, panelAnimatedStyle]}>
<TouchableOpacity onPress={togglePanelJS} activeOpacity={0.7}>
<View style={styles.panelHandle} />
<View style={styles.panelHeader}>
<Text style={styles.panelFileName} numberOfLines={1}>{fileName}</Text>
<SyncStatusBadge status={syncStatus} size={20} /> <SyncStatusBadge status={syncStatus} size={20} />
</View> </View>
</TouchableOpacity>
{(deviceFile || file?.size != null) && ( <View style={styles.panelBody}>
<Text style={styles.meta}> {formattedDate ? (
Taille : {((deviceFile ? 0 : file?.size) ?? 0 / 1024).toFixed(1)} Ko <View style={styles.metaRow}>
</Text> <MaterialIcons name="calendar-today" size={16} color="#888" />
)} <Text style={styles.metaText}>{formattedDate}</Text>
</View>
) : null}
{(deviceFile?.createdAt || file?.createdAt) && ( {formattedSize ? (
<Text style={styles.meta}> <View style={styles.metaRow}>
Ajouté le {new Date(deviceFile?.createdAt ?? file?.createdAt).toLocaleDateString('fr-FR', { <MaterialIcons name="storage" size={16} color="#888" />
day: 'numeric', month: 'long', year: 'numeric', <Text style={styles.metaText}>{formattedSize}</Text>
})} </View>
</Text> ) : null}
)}
{file?.tags && file.tags.length > 0 && ( {file?.tags && file.tags.length > 0 && (
<View style={styles.tagsSection}> <View style={styles.tagsSection}>
@@ -205,11 +354,40 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
{file?.ocrText && ( {file?.ocrText && (
<View style={styles.ocrSection}> <View style={styles.ocrSection}>
<Text style={styles.sectionLabel}>Texte OCR</Text> <Text style={styles.sectionLabel}>Texte OCR</Text>
<Text style={styles.ocrText}>{file.ocrText}</Text> <Text style={styles.ocrText} numberOfLines={4}>{file.ocrText}</Text>
</View> </View>
)} )}
<View style={styles.actionsRow}>
<TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
<MaterialIcons name="share" size={20} color="#1976D2" />
<Text style={styles.actionBtnText}>Partager</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionBtn} onPress={handleDelete}>
<MaterialIcons name="delete-outline" size={20} color="#E53935" />
<Text style={[styles.actionBtnText, { color: '#E53935' }]}>Supprimer</Text>
</TouchableOpacity>
</View>
</View>
</Animated.View>
</GestureDetector>
<Modal visible={optionsVisible} transparent animationType="fade" onRequestClose={() => setOptionsVisible(false)}>
<TouchableOpacity style={styles.optionsOverlay} activeOpacity={1} onPress={() => setOptionsVisible(false)}>
<View style={styles.optionsMenu}>
<TouchableOpacity style={styles.optionItem} onPress={handleShare}>
<MaterialIcons name="share" size={22} color="#333" />
<Text style={styles.optionText}>Partager</Text>
</TouchableOpacity>
<View style={styles.optionDivider} />
<TouchableOpacity style={styles.optionItem} onPress={handleDelete}>
<MaterialIcons name="delete-outline" size={22} color="#E53935" />
<Text style={[styles.optionText, { color: '#E53935' }]}>Supprimer</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
</Modal>
</View> </View>
</ScrollView>
); );
} }
@@ -244,6 +422,9 @@ export function FileDetailScreen() {
disableIntervalMomentum disableIntervalMomentum
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
initialScrollIndex={initialIndex} initialScrollIndex={initialIndex}
windowSize={5}
initialNumToRender={3}
maxToRenderPerBatch={3}
getItemLayout={(_, index) => ({ getItemLayout={(_, index) => ({
length: SCREEN_WIDTH, length: SCREEN_WIDTH,
offset: SCREEN_WIDTH * index, offset: SCREEN_WIDTH * index,
@@ -302,49 +483,142 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
backgroundColor: '#000', backgroundColor: '#000',
}, },
center: {
justifyContent: 'center',
alignItems: 'center',
},
pageWrapper: { pageWrapper: {
width: SCREEN_WIDTH, width: SCREEN_WIDTH,
}, },
page: { detailContainer: {
flex: 1, flex: 1,
},
pageContent: {
flexGrow: 1,
},
imageContainer: {
backgroundColor: '#000', backgroundColor: '#000',
}, },
imageCentered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
image: { image: {
width: SCREEN_WIDTH, width: SCREEN_WIDTH,
}, },
pageImageContainer: { headerOverlay: {
alignItems: 'center', position: 'absolute',
backgroundColor: '#000', top: 0,
paddingVertical: 4, left: 0,
}, right: 0,
pageImage: {
width: SCREEN_WIDTH,
},
details: {
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
marginTop: -16,
padding: 20,
},
detailHeader: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginBottom: 8, paddingHorizontal: 8,
paddingBottom: 8,
backgroundColor: 'rgba(0,0,0,0.3)',
}, },
fileName: { headerBtn: {
fontSize: 18, width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0,0,0,0.4)',
alignItems: 'center',
justifyContent: 'center',
},
headerDate: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
flex: 1,
textAlign: 'center',
marginHorizontal: 8,
},
panel: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: '#fff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 20,
paddingTop: 12,
height: PANEL_HEIGHT,
},
panelHandle: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: '#ddd',
alignSelf: 'center',
marginBottom: 12,
},
panelHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 12,
},
panelFileName: {
fontSize: 17,
fontWeight: '700', fontWeight: '700',
color: '#333', color: '#333',
flex: 1, flex: 1,
marginRight: 8, marginRight: 8,
}, },
panelBody: {
flex: 1,
},
metaRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 8,
},
metaText: {
fontSize: 14,
color: '#666',
},
tagsRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
marginTop: 8,
},
tagsSection: {
marginTop: 8,
},
sectionLabel: {
fontSize: 13,
fontWeight: '600',
color: '#333',
},
ocrSection: {
marginTop: 12,
},
ocrText: {
fontSize: 13,
color: '#555',
lineHeight: 18,
marginTop: 4,
},
actionsRow: {
flexDirection: 'row',
gap: 12,
marginTop: 16,
},
actionBtn: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
paddingVertical: 12,
borderRadius: 10,
backgroundColor: '#f5f5f5',
},
actionBtnText: {
fontSize: 15,
fontWeight: '600',
color: '#1976D2',
},
cloudOnlyContainer: { cloudOnlyContainer: {
alignItems: 'center', alignItems: 'center',
gap: 16, gap: 16,
@@ -363,34 +637,33 @@ const styles = StyleSheet.create({
fontSize: 15, fontSize: 15,
fontWeight: '600', fontWeight: '600',
}, },
meta: { optionsOverlay: {
fontSize: 14, flex: 1,
color: '#666', backgroundColor: 'rgba(0,0,0,0.5)',
marginBottom: 4, justifyContent: 'flex-end',
paddingBottom: 40,
}, },
tagsRow: { optionsMenu: {
backgroundColor: '#fff',
borderRadius: 14,
marginHorizontal: 20,
overflow: 'hidden',
},
optionItem: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', alignItems: 'center',
gap: 6, gap: 14,
marginTop: 8, paddingVertical: 16,
paddingHorizontal: 20,
}, },
tagsSection: { optionText: {
marginTop: 12, fontSize: 16,
},
sectionLabel: {
fontSize: 14,
fontWeight: '600',
color: '#333', color: '#333',
marginBottom: 4,
}, },
ocrSection: { optionDivider: {
marginTop: 16, height: 1,
}, backgroundColor: '#eee',
ocrText: { marginHorizontal: 20,
fontSize: 14,
color: '#555',
lineHeight: 20,
marginTop: 4,
}, },
pagination: { pagination: {
position: 'absolute', position: 'absolute',
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { View, Text, Image, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { Image } from 'expo-image';
import { FileItem } from '../types'; import { FileItem } from '../types';
import { TagChip } from './TagChip'; import { TagChip } from './TagChip';
@@ -21,7 +22,7 @@ export function FileCard({ file, onPress }: FileCardProps) {
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}> <TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
{imageUri && ( {imageUri && (
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
<Image source={{ uri: imageUri }} style={styles.image} resizeMode="cover" /> <Image source={{ uri: imageUri }} style={styles.image} contentFit="cover" cachePolicy="memory-disk" transition={200} />
</View> </View>
)} )}