diff options
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx | 22 | ||||
| -rw-r--r-- | apps/mobile/components/bookmarks/PDFViewer.tsx | 135 | ||||
| -rw-r--r-- | apps/mobile/package.json | 1 |
3 files changed, 156 insertions, 2 deletions
diff --git a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx index 01ee61de..1cf2ad3d 100644 --- a/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx +++ b/apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx @@ -18,6 +18,7 @@ import { BookmarkLinkScreenshotPreview, } from "@/components/bookmarks/BookmarkLinkPreview"; import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown"; +import { PDFViewer } from "@/components/bookmarks/PDFViewer"; import FullPageError from "@/components/FullPageError"; import { TailwindResolver } from "@/components/TailwindResolver"; import { Button } from "@/components/ui/Button"; @@ -91,6 +92,7 @@ function BookmarkLinkTypeSelector({ function BottomActions({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); const router = useRouter(); + const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { @@ -304,6 +306,20 @@ function BookmarkAssetView({ bookmark }: { bookmark: ZBookmark }) { throw new Error("Wrong content type rendered"); } const assetSource = useAssetUrl(bookmark.content.assetId); + + // Check if this is a PDF asset + if (bookmark.content.assetType === "pdf") { + return ( + <View className="flex flex-1"> + <PDFViewer + source={assetSource.uri ?? ""} + headers={assetSource.headers} + /> + </View> + ); + } + + // Handle image assets as before return ( <View className="flex flex-1 gap-2"> <ImageView @@ -327,6 +343,7 @@ function BookmarkAssetView({ bookmark }: { bookmark: ZBookmark }) { export default function ListView() { const { slug } = useLocalSearchParams(); const { colorScheme } = useColorScheme(); + const isDark = colorScheme === "dark"; const [bookmarkLinkType, setBookmarkLinkType] = useState<BookmarkLinkType>("reader"); @@ -380,10 +397,11 @@ export default function ListView() { headerTitle: title ?? "", headerBackTitle: "Back", headerTransparent: false, - headerTintColor: colorScheme === "dark" ? "#ffffff" : undefined, + headerShown: true, headerStyle: { - backgroundColor: colorScheme === "dark" ? "#000000" : undefined, + backgroundColor: isDark ? "#000" : "#fff", }, + headerTintColor: isDark ? "#fff" : "#000", headerRight: () => bookmark.content.type === BookmarkTypes.LINK ? ( <BookmarkLinkTypeSelector 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, + }, +}); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 4ce6a718..4902249a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -48,6 +48,7 @@ "react-native-gesture-handler": "~2.21.2", "react-native-image-viewing": "^0.2.2", "react-native-markdown-display": "^7.0.2", + "react-native-pdf": "^6.7.7", "react-native-reanimated": "^3.16.2", "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0", |
