From 79c743722da0841717d73aceb10400ed43835c82 Mon Sep 17 00:00:00 2001 From: m Date: Tue, 14 Jul 2026 10:49:17 +0200 Subject: [PATCH] visualize pdf --- mobile/app/file-detail.tsx | 60 ++++++++++++++++++++++++- mobile/package-lock.json | 91 ++++++++++++++++++++++++++++++++++++++ mobile/package.json | 1 + 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/mobile/app/file-detail.tsx b/mobile/app/file-detail.tsx index e8e6223..92fb4d3 100644 --- a/mobile/app/file-detail.tsx +++ b/mobile/app/file-detail.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { View, Text, @@ -10,7 +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'; @@ -27,11 +30,59 @@ 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; + + 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]); return ( - {uri ? ( + {isPdf ? ( + pdfLoading || !localPdfUri ? ( + + ) : ( + { + console.log(`PDF loaded: ${numberOfPages} pages`); + }} + onError={(error) => { + console.log('PDF error:', error); + }} + /> + ) + ) : uri ? ( ) : file ? ( = 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", @@ -2134,6 +2141,13 @@ "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", @@ -2576,6 +2590,12 @@ "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", @@ -2639,6 +2659,17 @@ "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", @@ -4798,6 +4829,15 @@ "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", @@ -5161,6 +5201,23 @@ "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", @@ -5293,6 +5350,25 @@ } } }, + "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", @@ -5328,6 +5404,21 @@ "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 41c37d0..43306dd 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -19,6 +19,7 @@ "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"