aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/mobile/components')
-rw-r--r--apps/mobile/components/bookmarks/PDFViewer.tsx135
1 files changed, 135 insertions, 0 deletions
diff --git a/apps/mobile/components/bookmarks/PDFViewer.tsx b/apps/mobile/components/bookmarks/PDFViewer.tsx
new file mode 100644
index 00000000..24b9edfb
--- /dev/null
+++ b/apps/mobile/components/bookmarks/PDFViewer.tsx
@@ -0,0 +1,135 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
+import ReactNativeBlobUtil from "react-native-blob-util";
+import Pdf from "react-native-pdf";
+import { useQuery } from "@tanstack/react-query";
+import { useColorScheme } from "nativewind";
+
+interface PDFViewerProps {
+ source: string;
+ headers?: Record<string, string>;
+}
+
+export function PDFViewer({ source, headers }: PDFViewerProps) {
+ const [pdfRenderError, setPdfRenderError] = useState<string | null>(null);
+ const { colorScheme } = useColorScheme();
+ const isDark = colorScheme === "dark";
+ const colors = {
+ background: isDark ? "#000" : "#fff",
+ foreground: isDark ? "#fff" : "#000",
+ mutedForeground: isDark ? "#888" : "#666",
+ };
+
+ const {
+ data: localPath,
+ isLoading,
+ error: downloadError,
+ } = useQuery({
+ queryKey: ["pdf", source],
+ queryFn: async () => {
+ // Create a temporary filename
+ const fileName = `temp_${Date.now()}.pdf`;
+ const { dirs } = ReactNativeBlobUtil.fs;
+ const path = `${dirs.DocumentDir}/${fileName}`;
+
+ const response = await ReactNativeBlobUtil.config({
+ fileCache: true,
+ path,
+ }).fetch("GET", source, headers ?? {});
+ return response.path();
+ },
+ enabled: !!source,
+ });
+
+ // Merge download and render errors
+ const error = useMemo(() => {
+ if (downloadError) {
+ let errorMessage = "Failed to download PDF";
+ if (downloadError.message.includes("Network request failed")) {
+ errorMessage = "Network error. Please check your connection.";
+ } else if (
+ downloadError.message.includes("401") ||
+ downloadError.message.includes("403")
+ ) {
+ errorMessage = "Authentication failed. Please sign in again.";
+ } else if (downloadError.message.includes("404")) {
+ errorMessage = "PDF not found.";
+ }
+ return errorMessage;
+ }
+ if (pdfRenderError) {
+ return pdfRenderError;
+ }
+ return null;
+ }, [downloadError, pdfRenderError]);
+
+ // Cleanup function to remove temporary file on unmount
+ useEffect(() => {
+ return () => {
+ if (localPath) {
+ ReactNativeBlobUtil.fs.unlink(localPath).catch(() => ({}));
+ }
+ };
+ }, [source, headers]);
+
+ if (error) {
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <Text style={[styles.errorText, { color: colors.foreground }]}>
+ {error}
+ </Text>
+ </View>
+ );
+ }
+
+ if (isLoading || !localPath) {
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <View style={styles.loadingContainer}>
+ <ActivityIndicator size="large" color={colors.foreground} />
+ <Text style={[styles.loadingText, { color: colors.mutedForeground }]}>
+ Downloading PDF...
+ </Text>
+ </View>
+ </View>
+ );
+ }
+
+ return (
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
+ <Pdf
+ style={StyleSheet.absoluteFillObject}
+ source={{ uri: `file://${localPath}`, cache: true }}
+ spacing={16}
+ maxScale={3}
+ onLoadComplete={() => ({})}
+ onError={() => setPdfRenderError("Failed to render PDF")}
+ trustAllCerts={false}
+ renderActivityIndicator={() => (
+ <ActivityIndicator size="large" color={colors.foreground} />
+ )}
+ />
+ </View>
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ loadingContainer: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: "center",
+ alignItems: "center",
+ zIndex: 1,
+ },
+ loadingText: {
+ marginTop: 12,
+ fontSize: 16,
+ },
+ errorText: {
+ fontSize: 16,
+ textAlign: "center",
+ padding: 20,
+ },
+});