From cd24ba5b0cba26f0ee2e3c002e5eb5cf278eb755 Mon Sep 17 00:00:00 2001 From: m Date: Thu, 16 Jul 2026 08:32:48 +0200 Subject: [PATCH] display pdf files --- mobile/api/client.ts | 2 + mobile/api/secureStorage.ts | 13 ++++ mobile/app/file-detail.tsx | 114 +++++++++------------------- mobile/app/file-edit.tsx | 1 + mobile/app/folder.tsx | 1 + mobile/app/index.tsx | 1 + mobile/components/FileThumbnail.tsx | 9 ++- mobile/contexts/AuthContext.tsx | 8 ++ mobile/hooks/useFiles.ts | 6 +- mobile/package-lock.json | 91 ---------------------- mobile/package.json | 1 - mobile/types/index.ts | 12 +++ 12 files changed, 84 insertions(+), 175 deletions(-) diff --git a/mobile/api/client.ts b/mobile/api/client.ts index ceca53b..68602aa 100644 --- a/mobile/api/client.ts +++ b/mobile/api/client.ts @@ -79,6 +79,7 @@ class ApiClient { const data = await response.json(); this.accessToken = data.access_token; + await tokenStorage.setAccessToken(data.access_token); await tokenStorage.setRefreshToken(data.refresh_token); return true; } catch { @@ -89,6 +90,7 @@ class ApiClient { private async clearAuth() { this.accessToken = null; + await tokenStorage.deleteAccessToken(); await tokenStorage.deleteRefreshToken(); await tokenStorage.deleteUser(); } diff --git a/mobile/api/secureStorage.ts b/mobile/api/secureStorage.ts index 4f84240..0d25275 100644 --- a/mobile/api/secureStorage.ts +++ b/mobile/api/secureStorage.ts @@ -1,9 +1,22 @@ import * as SecureStore from 'expo-secure-store'; +const ACCESS_KEY = 'vaultdrop_access_token'; const REFRESH_KEY = 'vaultdrop_refresh_token'; const USER_KEY = 'vaultdrop_user'; export const tokenStorage = { + async getAccessToken(): Promise { + return SecureStore.getItemAsync(ACCESS_KEY); + }, + + async setAccessToken(token: string): Promise { + await SecureStore.setItemAsync(ACCESS_KEY, token); + }, + + async deleteAccessToken(): Promise { + await SecureStore.deleteItemAsync(ACCESS_KEY); + }, + async getRefreshToken(): Promise { return SecureStore.getItemAsync(REFRESH_KEY); }, diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx index 92fb4d3..03c48e2 100644 --- a/mobile/app/file-detail.tsx +++ b/mobile/app/file-detail.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState } from 'react'; import { View, Text, @@ -10,12 +10,10 @@ import { ScrollView, } from 'react-native'; import { RouteProp, useRoute } from '@react-navigation/native'; -import Pdf from 'react-native-pdf'; -import { downloadAsync, documentDirectory, getInfoAsync } from 'expo-file-system/legacy'; import { useFile, useFileImage } from '../hooks/useFiles'; -import { apiClient } from '../api/client'; import { TagChip } from '../components/TagChip'; import { FileThumbnail } from '../components/FileThumbnail'; +import type { Thumbnail } from '../types'; const SCREEN_WIDTH = Dimensions.get('window').width; @@ -30,71 +28,42 @@ function DetailItem({ fileId }: { fileId: string }) { const { data: fileData } = useFile(fileId); const uri = imageData?.data?.url; const file = fileData as any; - const isPdf = file?.data?.mimeType === 'application/pdf'; - const [localPdfUri, setLocalPdfUri] = useState(null); - const [pdfLoading, setPdfLoading] = useState(false); - useEffect(() => { - if (!isPdf || !uri) return; - let cancelled = false; + const fullThumbnails: Thumbnail[] = (file?.data?.thumbnails ?? []) + .filter((t: Thumbnail) => t.resolutionLabel === 'full') + .sort((a: Thumbnail, b: Thumbnail) => a.pageNumber - b.pageNumber); - async function downloadPdf() { - if (!uri) return; - setPdfLoading(true); - const localUri = `${documentDirectory}pdf_${fileId}.pdf`; - try { - const info = await getInfoAsync(localUri); - if (info.exists && info.size > 100) { - if (!cancelled) setLocalPdfUri(localUri); - return; - } - const headers: Record = {}; - const token = apiClient.getAccessToken(); - if (token) headers['Authorization'] = `Bearer ${token}`; - await downloadAsync(uri, localUri, { headers }); - if (!cancelled) setLocalPdfUri(localUri); - } catch (e) { - console.log('PDF download error:', e); - } finally { - if (!cancelled) setPdfLoading(false); - } - } - - downloadPdf(); - return () => { cancelled = true; }; - }, [isPdf, uri, fileId]); + const hasPages = fullThumbnails.length > 0; return ( - - {isPdf ? ( - pdfLoading || !localPdfUri ? ( - - ) : ( - { - console.log(`PDF loaded: ${numberOfPages} pages`); - }} - onError={(error) => { - console.log('PDF error:', error); - }} + {hasPages ? ( + fullThumbnails.map((thumb) => ( + + - ) - ) : uri ? ( - - ) : file ? ( - - ) : ( - - )} - + + )) + ) : ( + + {uri ? ( + + ) : file ? ( + + ) : ( + + )} + + )} {imageData?.data?.name ?? file?.name ?? fileId} @@ -201,22 +170,13 @@ const styles = StyleSheet.create({ width: SCREEN_WIDTH, height: SCREEN_WIDTH, }, - pdf: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - backgroundColor: '#fff', - }, - placeholder: { - width: SCREEN_WIDTH, - height: SCREEN_WIDTH, - justifyContent: 'center', + pageImageContainer: { alignItems: 'center', - backgroundColor: '#1a1a2e', + backgroundColor: '#000', + paddingVertical: 4, }, - placeholderText: { - fontSize: 48, - color: '#1976D2', - fontWeight: '700', + pageImage: { + width: SCREEN_WIDTH, }, details: { backgroundColor: '#fff', diff --git a/mobile/app/file-edit.tsx b/mobile/app/file-edit.tsx index 6341649..e905e14 100644 --- a/mobile/app/file-edit.tsx +++ b/mobile/app/file-edit.tsx @@ -57,6 +57,7 @@ function FileEditItem({ fileId, selected, onPress }: FileEditItemProps) { ) : ( ; + const imageUri = thumbnailUrl || (uri && mimeType.startsWith('image/') ? uri : undefined); + + if (imageUri) { + return ; } return ( diff --git a/mobile/contexts/AuthContext.tsx b/mobile/contexts/AuthContext.tsx index b1fe596..a7ed630 100644 --- a/mobile/contexts/AuthContext.tsx +++ b/mobile/contexts/AuthContext.tsx @@ -28,6 +28,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (stored) { const userData = JSON.parse(stored) as User; setUser(userData); + + const accessToken = await tokenStorage.getAccessToken(); + if (accessToken) { + apiClient.setAccessToken(accessToken); + } } } catch { await tokenStorage.deleteUser(); @@ -43,6 +48,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }); apiClient.setAccessToken(response.access_token); + await tokenStorage.setAccessToken(response.access_token); await tokenStorage.setRefreshToken(response.refresh_token); await tokenStorage.setUser(JSON.stringify(response.user)); setUser(response.user); @@ -55,6 +61,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }); apiClient.setAccessToken(response.access_token); + await tokenStorage.setAccessToken(response.access_token); await tokenStorage.setRefreshToken(response.refresh_token); await tokenStorage.setUser(JSON.stringify(response.user)); setUser(response.user); @@ -69,6 +76,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { } catch {} apiClient.setAccessToken(null); + await tokenStorage.deleteAccessToken(); await tokenStorage.deleteRefreshToken(); await tokenStorage.deleteUser(); setUser(null); diff --git a/mobile/hooks/useFiles.ts b/mobile/hooks/useFiles.ts index 3c7bfed..422148b 100644 --- a/mobile/hooks/useFiles.ts +++ b/mobile/hooks/useFiles.ts @@ -8,7 +8,7 @@ export function useFiles(page: number = 1, limit: number = 20) { queryKey: ['files', page, limit], queryFn: () => apiClient.get>( - `${ENDPOINTS.FILES}?page=${page}&limit=${limit}` + `${ENDPOINTS.FILES}?page=${page}&limit=${limit}&thumbnail=thumbnail` ), }); } @@ -16,7 +16,7 @@ export function useFiles(page: number = 1, limit: number = 20) { export function useFile(id: string) { return useQuery({ queryKey: ['files', id], - queryFn: () => apiClient.get(`${ENDPOINTS.FILES}/${id}`), + queryFn: () => apiClient.get(`${ENDPOINTS.FILES}/${id}?thumbnail=thumbnail`), enabled: !!id, }); } @@ -75,7 +75,7 @@ export function useFilesByParent(parentId: string) { return useQuery({ queryKey: ['files', 'parent', parentId], queryFn: () => - apiClient.get<{ data: FileItem[] }>(`${ENDPOINTS.FOLDERS}/${parentId}/files`), + apiClient.get<{ data: FileItem[] }>(`${ENDPOINTS.FOLDERS}/${parentId}/files?thumbnail=thumbnail`), enabled: !!parentId, }); } diff --git a/mobile/package-lock.json b/mobile/package-lock.json index b22167d..45cdffd 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -24,7 +24,6 @@ "react-native-mmkv": "^4.3.2", "react-native-nitro-image": "^0.15.1", "react-native-nitro-modules": "^0.36.1", - "react-native-pdf": "^7.0.4", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", "react-native-vision-camera": "^5.1.0" @@ -1669,12 +1668,6 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/@react-native/normalize-color": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-color/-/normalize-color-2.1.0.tgz", - "integrity": "sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==", - "license": "MIT" - }, "node_modules/@react-native/normalize-colors": { "version": "0.86.0", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.86.0.tgz", @@ -2141,13 +2134,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/base-64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", - "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==", - "license": "MIT", - "peer": true - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2590,12 +2576,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2659,17 +2639,6 @@ "node": ">= 0.8" } }, - "node_modules/deprecated-react-native-prop-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", - "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", - "license": "MIT", - "dependencies": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -4829,15 +4798,6 @@ "node": "^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -5201,23 +5161,6 @@ "node": ">= 6" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/query-string": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", @@ -5350,25 +5293,6 @@ } } }, - "node_modules/react-native-blob-util": { - "version": "0.24.10", - "resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.24.10.tgz", - "integrity": "sha512-4yazgoCstXgt2/CRURtowGP3furAzbWdfMQ4jyUPDo5xtLx7KE2F/xMz3oU3shLwfL4id0iEKsBywoxvJSHPzQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "base-64": "1.0.0", - "glob": "13.0.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ronradtke" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-mmkv": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-4.3.2.tgz", @@ -5404,21 +5328,6 @@ "react-native": "*" } }, - "node_modules/react-native-pdf": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/react-native-pdf/-/react-native-pdf-7.0.4.tgz", - "integrity": "sha512-Uhxn5SSguMKvbwD50iIZfYPmYeKcX+9I3tc4J3HCbRExAN0uqXTAKx2pvbR2Y62umAj0JS274FcbSldQfoQfyg==", - "license": "MIT", - "dependencies": { - "crypto-js": "4.2.0", - "deprecated-react-native-prop-types": "^2.3.0" - }, - "peerDependencies": { - "react": "*", - "react-native": "*", - "react-native-blob-util": ">=0.13.7" - } - }, "node_modules/react-native-safe-area-context": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index 43306dd..41c37d0 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -19,7 +19,6 @@ "react-native-mmkv": "^4.3.2", "react-native-nitro-image": "^0.15.1", "react-native-nitro-modules": "^0.36.1", - "react-native-pdf": "^7.0.4", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "4.25.2", "react-native-vision-camera": "^5.1.0" diff --git a/mobile/types/index.ts b/mobile/types/index.ts index 78c9d77..e9c806c 100644 --- a/mobile/types/index.ts +++ b/mobile/types/index.ts @@ -1,3 +1,13 @@ +export interface Thumbnail { + id: string; + pageNumber: number; + resolutionLabel: string; + width: number; + height: number; + url: string; + mimeType: string; +} + export interface FileItem { id: string; name: string; @@ -10,6 +20,8 @@ export interface FileItem { isFolder: boolean; parentFileId?: string; url?: string; + thumbnailUrl?: string; + thumbnails?: Thumbnail[]; } export function isFolder(file: FileItem): boolean {