header size
This commit is contained in:
+1
-1
@@ -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' }} />
|
||||||
|
|||||||
+434
-161
@@ -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,119 +125,269 @@ 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 });
|
||||||
const allImages: SelectedImage[] = fullThumbnails.map((t) => ({
|
} else if (file?.url) {
|
||||||
uri: t.url,
|
await Share.share({ url: file.url, title: fileName });
|
||||||
width: SCREEN_WIDTH,
|
}
|
||||||
height: t.height * (SCREEN_WIDTH / t.width),
|
}, [uri, fileName, file?.url]);
|
||||||
}));
|
|
||||||
return (
|
const handleDelete = useCallback(() => {
|
||||||
<Pressable
|
setOptionsVisible(false);
|
||||||
key={thumb.id}
|
Alert.alert(
|
||||||
style={styles.pageImageContainer}
|
'Supprimer',
|
||||||
onPress={() => onSelectImage?.({ images: allImages, index: thumbIndex })}
|
`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) => ({
|
||||||
|
uri: t.url,
|
||||||
|
width: SCREEN_WIDTH,
|
||||||
|
height: t.height * (SCREEN_WIDTH / t.width),
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={thumb.id}
|
||||||
|
onPress={() => onSelectImage?.({ images: allImages, index: thumbIndex })}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: thumb.url }}
|
||||||
|
style={[styles.image, { height: thumb.height * (SCREEN_WIDTH / thumb.width) }]}
|
||||||
|
contentFit="contain"
|
||||||
|
cachePolicy="memory-disk"
|
||||||
|
transition={200}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (uri) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={{ uri }}
|
||||||
|
style={[
|
||||||
|
styles.image,
|
||||||
|
imageSize
|
||||||
|
? { height: imageSize.height * SCREEN_WIDTH / imageSize.width }
|
||||||
|
: { aspectRatio: 1 },
|
||||||
|
]}
|
||||||
|
contentFit="contain"
|
||||||
|
cachePolicy="memory-disk"
|
||||||
|
transition={200}
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
return (
|
||||||
|
<View style={styles.cloudOnlyContainer}>
|
||||||
|
<FileThumbnail
|
||||||
|
thumbnailUrl={file?.thumbnailUrl}
|
||||||
|
mimeType={file?.mimeType ?? 'application/pdf'}
|
||||||
|
fileName={file?.name ?? fileId}
|
||||||
|
size={SCREEN_WIDTH * 0.5}
|
||||||
|
/>
|
||||||
|
{syncStatus === 'cloud' && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.downloadBtn}
|
||||||
|
onPress={handleDownload}
|
||||||
|
disabled={downloading}
|
||||||
>
|
>
|
||||||
<Image
|
{downloading ? (
|
||||||
source={{ uri: thumb.url }}
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
style={[styles.pageImage, { height: thumb.height * (SCREEN_WIDTH / thumb.width) }]}
|
) : (
|
||||||
resizeMode="contain"
|
<MaterialIcons name="cloud-download" size={20} color="#fff" />
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<View style={styles.imageContainer}>
|
|
||||||
{uri ? (
|
|
||||||
<Pressable
|
|
||||||
onPress={() => onSelectImage?.({
|
|
||||||
images: [{ uri, width: SCREEN_WIDTH, height: imageSize ? imageSize.height * SCREEN_WIDTH / imageSize.width : SCREEN_WIDTH }],
|
|
||||||
index: 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri }}
|
|
||||||
style={[
|
|
||||||
styles.image,
|
|
||||||
imageSize && { height: imageSize.height * SCREEN_WIDTH / imageSize.width },
|
|
||||||
]}
|
|
||||||
resizeMode="contain"
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
) : file ? (
|
|
||||||
<View style={styles.cloudOnlyContainer}>
|
|
||||||
<FileThumbnail
|
|
||||||
thumbnailUrl={file?.thumbnailUrl}
|
|
||||||
mimeType={file?.mimeType ?? 'application/pdf'}
|
|
||||||
fileName={file?.name ?? fileId}
|
|
||||||
size={SCREEN_WIDTH * 0.6}
|
|
||||||
/>
|
|
||||||
{syncStatus === 'cloud' && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.downloadBtn}
|
|
||||||
onPress={handleDownload}
|
|
||||||
disabled={downloading}
|
|
||||||
>
|
|
||||||
{downloading ? (
|
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
|
||||||
) : (
|
|
||||||
<MaterialIcons name="cloud-download" size={20} color="#fff" />
|
|
||||||
)}
|
|
||||||
<Text style={styles.downloadBtnText}>
|
|
||||||
{downloading ? 'Téléchargement...' : 'Télécharger'}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
<Text style={styles.downloadBtnText}>
|
||||||
) : (
|
{downloading ? 'Téléchargement...' : 'Télécharger'}
|
||||||
<ActivityIndicator size="large" color="#1976D2" />
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
);
|
||||||
|
}
|
||||||
|
return <ActivityIndicator size="large" color="#1976D2" />;
|
||||||
|
};
|
||||||
|
|
||||||
<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]}>
|
||||||
<SyncStatusBadge status={syncStatus} size={20} />
|
<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>
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
{(deviceFile || file?.size != null) && (
|
<View style={[styles.headerOverlay, { paddingTop: insets.top + 8 }]}>
|
||||||
<Text style={styles.meta}>
|
<Text style={styles.headerDate} numberOfLines={1}>{formattedDate}</Text>
|
||||||
Taille : {((deviceFile ? 0 : file?.size) ?? 0 / 1024).toFixed(1)} Ko
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(deviceFile?.createdAt || file?.createdAt) && (
|
<TouchableOpacity onPress={() => setOptionsVisible(true)} style={styles.headerBtn}>
|
||||||
<Text style={styles.meta}>
|
<MaterialIcons name="more-vert" size={24} color="#fff" />
|
||||||
Ajouté le {new Date(deviceFile?.createdAt ?? file?.createdAt).toLocaleDateString('fr-FR', {
|
</TouchableOpacity>
|
||||||
day: 'numeric', month: 'long', year: 'numeric',
|
</View>
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{file?.tags && file.tags.length > 0 && (
|
<GestureDetector gesture={panGesture}>
|
||||||
<View style={styles.tagsSection}>
|
<Animated.View style={[styles.panel, { paddingBottom: insets.bottom + 12 }, panelAnimatedStyle]}>
|
||||||
<Text style={styles.sectionLabel}>Tags</Text>
|
<TouchableOpacity onPress={togglePanelJS} activeOpacity={0.7}>
|
||||||
<View style={styles.tagsRow}>
|
<View style={styles.panelHandle} />
|
||||||
{file.tags.map((tag: any) => (
|
<View style={styles.panelHeader}>
|
||||||
<TagChip key={tag.id} name={tag.name} />
|
<Text style={styles.panelFileName} numberOfLines={1}>{fileName}</Text>
|
||||||
))}
|
<SyncStatusBadge status={syncStatus} size={20} />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.panelBody}>
|
||||||
|
{formattedDate ? (
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<MaterialIcons name="calendar-today" size={16} color="#888" />
|
||||||
|
<Text style={styles.metaText}>{formattedDate}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{formattedSize ? (
|
||||||
|
<View style={styles.metaRow}>
|
||||||
|
<MaterialIcons name="storage" size={16} color="#888" />
|
||||||
|
<Text style={styles.metaText}>{formattedSize}</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{file?.tags && file.tags.length > 0 && (
|
||||||
|
<View style={styles.tagsSection}>
|
||||||
|
<Text style={styles.sectionLabel}>Tags</Text>
|
||||||
|
<View style={styles.tagsRow}>
|
||||||
|
{file.tags.map((tag: any) => (
|
||||||
|
<TagChip key={tag.id} name={tag.name} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{file?.ocrText && (
|
||||||
|
<View style={styles.ocrSection}>
|
||||||
|
<Text style={styles.sectionLabel}>Texte OCR</Text>
|
||||||
|
<Text style={styles.ocrText} numberOfLines={4}>{file.ocrText}</Text>
|
||||||
|
</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>
|
||||||
</View>
|
</View>
|
||||||
)}
|
</Animated.View>
|
||||||
|
</GestureDetector>
|
||||||
|
|
||||||
{file?.ocrText && (
|
<Modal visible={optionsVisible} transparent animationType="fade" onRequestClose={() => setOptionsVisible(false)}>
|
||||||
<View style={styles.ocrSection}>
|
<TouchableOpacity style={styles.optionsOverlay} activeOpacity={1} onPress={() => setOptionsVisible(false)}>
|
||||||
<Text style={styles.sectionLabel}>Texte OCR</Text>
|
<View style={styles.optionsMenu}>
|
||||||
<Text style={styles.ocrText}>{file.ocrText}</Text>
|
<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>
|
</View>
|
||||||
)}
|
</TouchableOpacity>
|
||||||
</View>
|
</Modal>
|
||||||
</ScrollView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user