aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx22
-rw-r--r--apps/mobile/components/bookmarks/PDFViewer.tsx135
-rw-r--r--apps/mobile/package.json1
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",