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;
+ }
+ },
+}));