diff --git a/mobile/App.tsx b/mobile/App.tsx
index d0c8dc5..38e2ba8 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -9,6 +9,7 @@ import { ScanScreen } from './app/scan';
import { SearchScreen } from './app/search';
import { BatchReviewScreen } from './app/batch-review';
import { PendingReviewScreen } from './app/pending-review';
+import { FileDetailScreen } from './app/file-detail';
const Stack = createNativeStackNavigator();
const queryClient = new QueryClient();
@@ -29,6 +30,11 @@ export default function App() {
+
diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx
new file mode 100644
index 0000000..c0314de
--- /dev/null
+++ b/mobile/app/file-detail.tsx
@@ -0,0 +1,216 @@
+import React, { useRef, useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ FlatList,
+ Image,
+ Dimensions,
+ ActivityIndicator,
+ ScrollView,
+} from 'react-native';
+import { RouteProp, useRoute } from '@react-navigation/native';
+import { useFile, useFileImage } from '../hooks/useFiles';
+import { TagChip } from '../components/TagChip';
+
+const SCREEN_WIDTH = Dimensions.get('window').width;
+
+type RootStackParamList = {
+ FileDetail: { fileIds: string[]; initialIndex: number };
+};
+
+type FileDetailRouteProp = RouteProp;
+
+function DetailItem({ fileId }: { fileId: string }) {
+ const { data: imageData, isLoading: imageLoading } = useFileImage(fileId);
+ const { data: fileData } = useFile(fileId);
+ const uri = imageData?.data?.url;
+ const file = fileData as any;
+
+ return (
+
+
+ {imageLoading ? (
+
+ ) : uri ? (
+
+ ) : (
+
+ PDF
+
+ )}
+
+
+
+ {imageData?.data?.name ?? file?.name ?? fileId}
+
+ {imageData?.data?.size != null && (
+
+ Taille : {(imageData.data.size / 1024).toFixed(1)} Ko
+
+ )}
+
+ {file?.createdAt && (
+
+ Ajouté le {new Date(file.createdAt).toLocaleDateString('fr-FR', {
+ day: 'numeric', month: 'long', year: 'numeric',
+ })}
+
+ )}
+
+ {file?.tags && file.tags.length > 0 && (
+
+ Tags
+
+ {file.tags.map((tag: any) => (
+
+ ))}
+
+
+ )}
+
+ {file?.ocrText && (
+
+ Texte OCR
+ {file.ocrText}
+
+ )}
+
+
+ );
+}
+
+export function FileDetailScreen() {
+ const route = useRoute();
+ const { fileIds, initialIndex } = route.params;
+
+ const flatListRef = useRef(null);
+ const [currentIndex, setCurrentIndex] = useState(initialIndex);
+
+ return (
+
+ item}
+ horizontal
+ pagingEnabled
+ showsHorizontalScrollIndicator={false}
+ initialScrollIndex={initialIndex}
+ getItemLayout={(_, index) => ({
+ length: SCREEN_WIDTH,
+ offset: SCREEN_WIDTH * index,
+ index,
+ })}
+ onMomentumScrollEnd={(e) => {
+ const index = Math.round(e.nativeEvent.contentOffset.x / SCREEN_WIDTH);
+ setCurrentIndex(index);
+ }}
+ renderItem={({ item }) => (
+
+
+
+ )}
+ />
+
+
+
+ {currentIndex + 1} / {fileIds.length}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#000',
+ },
+ pageWrapper: {
+ width: SCREEN_WIDTH,
+ },
+ page: {
+ flex: 1,
+ },
+ pageContent: {
+ flexGrow: 1,
+ },
+ imageContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#000',
+ },
+ image: {
+ width: SCREEN_WIDTH,
+ height: SCREEN_WIDTH,
+ },
+ placeholder: {
+ width: SCREEN_WIDTH,
+ height: SCREEN_WIDTH,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#1a1a2e',
+ },
+ placeholderText: {
+ fontSize: 48,
+ color: '#1976D2',
+ fontWeight: '700',
+ },
+ details: {
+ backgroundColor: '#fff',
+ borderTopLeftRadius: 16,
+ borderTopRightRadius: 16,
+ marginTop: -16,
+ padding: 20,
+ },
+ fileName: {
+ fontSize: 18,
+ fontWeight: '700',
+ color: '#333',
+ marginBottom: 8,
+ },
+ meta: {
+ fontSize: 14,
+ color: '#666',
+ marginBottom: 4,
+ },
+ tagsRow: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 6,
+ marginTop: 8,
+ },
+ tagsSection: {
+ marginTop: 12,
+ },
+ sectionLabel: {
+ fontSize: 14,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 4,
+ },
+ ocrSection: {
+ marginTop: 16,
+ },
+ ocrText: {
+ fontSize: 14,
+ color: '#555',
+ lineHeight: 20,
+ marginTop: 4,
+ },
+ pagination: {
+ position: 'absolute',
+ bottom: 16,
+ alignSelf: 'center',
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ borderRadius: 12,
+ paddingHorizontal: 12,
+ paddingVertical: 4,
+ },
+ paginationText: {
+ color: '#fff',
+ fontSize: 13,
+ },
+});
diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx
index ff784df..3f1abec 100644
--- a/mobile/app/index.tsx
+++ b/mobile/app/index.tsx
@@ -14,6 +14,7 @@ type RootStackParamList = {
Home: undefined;
Upload: undefined;
Scan: undefined;
+ FileDetail: { fileIds: string[]; initialIndex: number };
};
type NavigationProp = NativeStackNavigationProp;
@@ -51,13 +52,13 @@ function formatDateLabel(key: string): string {
return key.charAt(0).toUpperCase() + key.slice(1);
}
-function FileGridItem({ file }: { file: FileItem }) {
+function FileGridItem({ file, onPress }: { file: FileItem; onPress?: () => void }) {
const { data, isLoading } = useFileImage(file.id);
const uri = data?.data?.url;
return (
-
+
{isLoading ? (
@@ -70,7 +71,7 @@ function FileGridItem({ file }: { file: FileItem }) {
)}
{file.name}
-
+
);
}
@@ -101,6 +102,12 @@ export function HomeScreen() {
const groups = groupByDate(filteredFiles);
const groupKeys = Object.keys(groups);
+ const fileIdToIndex = useMemo(() => {
+ const map = new Map();
+ filteredFiles.forEach((f, i) => map.set(f.id, i));
+ return map;
+ }, [filteredFiles]);
+
if (isLoading) {
return (
@@ -154,7 +161,16 @@ export function HomeScreen() {
{formatDateLabel(dateKey)}
{groupFiles.map((file) => (
-
+
+ navigation.navigate('FileDetail', {
+ fileIds: filteredFiles.map((f) => f.id),
+ initialIndex: fileIdToIndex.get(file.id) ?? 0,
+ })
+ }
+ />
))}