diff --git a/mobile/src/hooks/useDocumentActions.ts b/mobile/src/hooks/useDocumentActions.ts new file mode 100644 index 0000000..1bd958c --- /dev/null +++ b/mobile/src/hooks/useDocumentActions.ts @@ -0,0 +1,49 @@ +import { Alert } from 'react-native'; +import { useDocumentStore } from '../store/useDocumentStore'; +import { useAuthStore } from '../store/useAuthStore'; + +export const useDocumentActions = () => { + const { renameDocument, removeDocument } = useDocumentStore(); + const { user } = useAuthStore(); + + const handleRename = (oldName: string) => { + Alert.prompt( + 'Rename Document', + 'Enter a new name for the document.', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Save', + onPress: (newName) => { + if (newName && newName !== oldName.replace('.pdf', '') && user?.user.id) { + renameDocument(user.user.id, oldName, newName); + } + }, + }, + ], + 'plain-text', + oldName.replace('.pdf', '') + ); + }; + + const handleDelete = (fileName: string) => { + Alert.alert( + 'Delete Document', + 'Are you sure you want to delete this document?', + [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Delete', + style: 'destructive', + onPress: () => { + if (user?.user.id) { + removeDocument(user.user.id, fileName); + } + } + }, + ] + ); + }; + + return { handleRename, handleDelete }; +}; diff --git a/mobile/src/hooks/useDocumentScanner.ts b/mobile/src/hooks/useDocumentScanner.ts new file mode 100644 index 0000000..c83242e --- /dev/null +++ b/mobile/src/hooks/useDocumentScanner.ts @@ -0,0 +1,89 @@ +import { useState } from 'react'; +import { Alert } from 'react-native'; +import DocumentScanner from 'react-native-document-scanner-plugin'; +import * as Print from 'expo-print'; +import * as FileSystem from 'expo-file-system/legacy'; +import { useDocumentStore } from '../store/useDocumentStore'; +import { useAuthStore } from '../store/useAuthStore'; + +export const useDocumentScanner = () => { + const { addDocument } = useDocumentStore(); + const { user } = useAuthStore(); + const [isScanning, setIsScanning] = useState(false); + + const createPdfFromImages = async (imageUris: string[]) => { + try { + const imageSources = await Promise.all( + imageUris.map(async (uri) => { + const fileUri = uri.startsWith('file://') ? uri : `file://${uri}`; + const base64 = await FileSystem.readAsStringAsync(fileUri, { + encoding: FileSystem.EncodingType.Base64, + }); + return `data:image/jpeg;base64,${base64}`; + }) + ); + + const htmlContent = ` + + + + ${imageSources.map(src => ` +
+ +
+ `).join('')} + + + `; + + const { uri } = await Print.printToFileAsync({ html: htmlContent }); + return uri; + } catch (error) { + console.error('Failed to create PDF', error); + throw error; + } + }; + + const scanDocument = async () => { + if (!user?.user.id) { + Alert.alert('Error', 'You must be logged in to scan documents.'); + return; + } + + setIsScanning(true); + try { + const { scannedImages } = await DocumentScanner.scanDocument(); + + if (scannedImages && scannedImages.length > 0) { + Alert.prompt( + 'Name Your Document', + 'Enter a name for your new document.', + [ + { text: 'Cancel', style: 'cancel', onPress: () => {} }, + { + text: 'Save', + onPress: async (fileName) => { + if (fileName) { + try { + const pdfUri = await createPdfFromImages(scannedImages); + await addDocument(user.user.id, pdfUri, fileName); + } catch (e) { + Alert.alert('Error', 'Could not save document.'); + } + } + }, + }, + ], + 'plain-text', + `Scan_${Date.now()}` + ); + } + } catch (error) { + console.error('Scanning failed', error); + } finally { + setIsScanning(false); + } + }; + + return { isScanning, scanDocument }; +}; diff --git a/mobile/src/screens/DocumentViewScreen.tsx b/mobile/src/screens/DocumentViewScreen.tsx new file mode 100644 index 0000000..15a499d --- /dev/null +++ b/mobile/src/screens/DocumentViewScreen.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { View, StyleSheet, useColorScheme, Text, ActivityIndicator } from 'react-native'; +import Pdf from 'react-native-pdf'; +import { getThemeStyles } from '../theme/styles'; + +export default function DocumentViewScreen({ route }: any) { + const { uri } = route.params; + const colorScheme = useColorScheme(); + const isDark = colorScheme === 'dark'; + const themeStyles = getThemeStyles(isDark); + + // Use the URI directly. + // Note: If filenames have spaces, they should ideally be encoded, + // but for local file:// URIs, react-native-pdf often handles them as they are. + // If issues persist with spaces, try encodeURI(uri). + const source = React.useMemo(() => ({ + uri: uri, + cache: true + }), [uri]); + + return ( + + { + console.log(`Number of pages: ${numberOfPages}`); + }} + onPageChanged={(page, numberOfPages) => { + console.log(`Current page: ${page}`); + }} + onError={(error) => { + console.error('PDF Load Error:', error); + }} + onPressLink={(uri) => { + console.log(`Link pressed: ${uri}`); + }} + style={styles.pdf} + trustAllCerts={false} + renderActivityIndicator={() => } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'flex-start', + alignItems: 'center', + }, + pdf: { + flex: 1, + width: '100%', + }, +}); diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index 111230f..f2179a2 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -1,43 +1,95 @@ -import React from 'react'; -import { View, Text, TouchableOpacity, useColorScheme } from 'react-native'; -import { getThemeStyles, commonStyles } from '../theme/styles'; +import React, { useEffect } from 'react'; +import { View, Text, useColorScheme, FlatList, TouchableOpacity, ActivityIndicator } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from '../navigation/AppNavigator'; +import { getThemeStyles, commonStyles, homeStyles } from '../theme/styles'; +import { useDocumentStore } from '../store/useDocumentStore'; import { useAuthStore } from '../store/useAuthStore'; +import { useDocumentScanner } from '../hooks/useDocumentScanner'; +import { useDocumentActions } from '../hooks/useDocumentActions'; +import { COLORS } from '../theme/colors'; export default function HomeScreen() { - const { user, logout } = useAuthStore(); + const navigation = useNavigation>(); const colorScheme = useColorScheme(); const isDark = colorScheme === 'dark'; const themeStyles = getThemeStyles(isDark); + const themeColors = isDark ? COLORS.DARK : COLORS.LIGHT; - if (!user) return null; // Should not happen if navigation logic is correct + const { user } = useAuthStore(); + const { documents, loadDocuments, isLoading } = useDocumentStore(); + const { isScanning, scanDocument } = useDocumentScanner(); + const { handleRename, handleDelete } = useDocumentActions(); - return ( - - Welcome Back! - - - - - {user.user?.email?.charAt(0).toUpperCase() || 'U'} - - - - Logged in as: - {user.user?.email} - + useEffect(() => { + if (user?.user?.id) { + loadDocuments(user.user.id); + } + }, [user]); + + const renderItem = ({ item }: { item: any }) => ( + navigation.navigate('DocumentView', { uri: item.uri })} activeOpacity={0.7}> + + + - - - Your API Token: - - {user.token} + + + {item.name.replace('.pdf', '')} + + + {new Date(item.timestamp).toLocaleDateString()} + handleRename(item.name)} style={homeStyles.deleteBtn}> + + + handleDelete(item.name)} style={homeStyles.deleteBtn}> + + + + ); - - Sign Out + return ( + + {documents.length === 0 ? ( + + + No documents yet. + + Tap the + button to scan your first document. + + + ) : ( + item.name} + contentContainerStyle={homeStyles.listContent} + refreshing={isLoading} + onRefresh={() => user?.user.id && loadDocuments(user.user.id)} + /> + )} + + {isScanning && ( + + + Processing... + + )} + + + ); } + + diff --git a/mobile/src/services/DocumentService.ts b/mobile/src/services/DocumentService.ts new file mode 100644 index 0000000..bfd6eec --- /dev/null +++ b/mobile/src/services/DocumentService.ts @@ -0,0 +1,101 @@ +import * as FileSystem from 'expo-file-system/legacy'; + +const getDocumentDir = (userId: string | number) => `${FileSystem.documentDirectory}documents/user_${userId}/`; + +export interface ScannedDocument { + uri: string; + name: string; + timestamp: number; + size?: number; +} + +export const DocumentService = { + // Ensure the documents directory exists for the specific user + init: async (userId: string | number) => { + const dir = getDocumentDir(userId); + const dirInfo = await FileSystem.getInfoAsync(dir); + if (!dirInfo.exists) { + await FileSystem.makeDirectoryAsync(dir, { intermediates: true }); + } + }, + + // Save a scanned file from a temporary URI to permanent storage + saveDocument: async (userId: string | number, tempUri: string, fileName: string): Promise => { + await DocumentService.init(userId); + + const dir = getDocumentDir(userId); + const timestamp = Date.now(); + const destination = `${dir}${fileName}.pdf`; + + await FileSystem.moveAsync({ + from: tempUri, + to: destination, + }); + + const fileInfo = await FileSystem.getInfoAsync(destination); + + return { + uri: destination, + name: `${fileName}.pdf`, + timestamp, + size: fileInfo.exists ? fileInfo.size : 0, + }; + }, + + // Get a list of all saved documents for a user + getDocuments: async (userId: string | number): Promise => { + await DocumentService.init(userId); + + const dir = getDocumentDir(userId); + const files = await FileSystem.readDirectoryAsync(dir); + + const docs: ScannedDocument[] = await Promise.all( + files.map(async (fileName) => { + const uri = `${dir}${fileName}`; + const info = await FileSystem.getInfoAsync(uri); + // Extract timestamp from filename if possible, else use modification time + const match = fileName.match(/scan_(\d+)/); + const timestamp = match ? parseInt(match[1]) : (info.exists ? info.modificationTime || 0 : 0) * 1000; + + return { + uri, + name: fileName, + timestamp, + size: info.exists ? info.size : 0, + }; + }) + ); + + // Sort by newest first + return docs.sort((a, b) => b.timestamp - a.timestamp); + }, + + // Delete a document + deleteDocument: async (userId: string | number, fileName: string) => { + const dir = getDocumentDir(userId); + const uri = `${dir}${fileName}`; + await FileSystem.deleteAsync(uri, { idempotent: true }); + }, + + // Rename a document + renameDocument: async (userId: string | number, oldName: string, newName: string): Promise => { + const dir = getDocumentDir(userId); + const oldUri = `${dir}${oldName}`; + const newUri = `${dir}${newName}.pdf`; // Assuming pdf extension + + await FileSystem.moveAsync({ + from: oldUri, + to: newUri, + }); + + const fileInfo = await FileSystem.getInfoAsync(newUri); + const timestamp = fileInfo.exists ? (fileInfo.modificationTime || 0) * 1000 : Date.now(); + + return { + uri: newUri, + name: `${newName}.pdf`, + timestamp, + size: fileInfo.exists ? fileInfo.size : 0, + }; + }, +}; diff --git a/mobile/src/store/useDocumentStore.ts b/mobile/src/store/useDocumentStore.ts new file mode 100644 index 0000000..8427647 --- /dev/null +++ b/mobile/src/store/useDocumentStore.ts @@ -0,0 +1,65 @@ +import { create } from 'zustand'; +import { DocumentService, ScannedDocument } from '../services/DocumentService'; + +interface DocumentState { + documents: ScannedDocument[]; + isLoading: boolean; + loadDocuments: (userId: string | number) => Promise; + addDocument: (userId: string | number, tempUri: string, fileName: string) => Promise; + removeDocument: (userId: string | number, fileName: string) => Promise; + renameDocument: (userId: string | number, oldName: string, newName: string) => Promise; +} + +export const useDocumentStore = create((set, get) => ({ + documents: [], + isLoading: false, + + loadDocuments: async (userId: string | number) => { + set({ isLoading: true }); + try { + const docs = await DocumentService.getDocuments(userId); + set({ documents: docs }); + } catch (error) { + console.error('Failed to load documents', error); + set({ documents: [] }); + } finally { + set({ isLoading: false }); + } + }, + + addDocument: async (userId: string | number, tempUri: string, fileName: string) => { + try { + const newDoc = await DocumentService.saveDocument(userId, tempUri, fileName); + set((state) => ({ + documents: [newDoc, ...state.documents].sort((a, b) => b.timestamp - a.timestamp), + })); + } catch (error) { + console.error('Failed to save document', error); + throw error; + } + }, + + removeDocument: async (userId: string | number, fileName: string) => { + try { + await DocumentService.deleteDocument(userId, fileName); + set((state) => ({ + documents: state.documents.filter((doc) => doc.name !== fileName), + })); + } catch (error) { + console.error('Failed to delete document', error); + throw error; + } + }, + + renameDocument: async (userId: string | number, oldName: string, newName: string) => { + try { + const updatedDoc = await DocumentService.renameDocument(userId, oldName, newName); + set((state) => ({ + documents: state.documents.map((doc) => (doc.name === oldName ? updatedDoc : doc)), + })); + } catch (error) { + console.error('Failed to rename document', error); + throw error; + } + }, +}));