aboutsummaryrefslogtreecommitdiffstats
path: root/apps/mobile/components
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2025-08-31 16:09:12 +0100
committerMohamedBassem <me@mbassem.com>2025-08-31 16:09:12 +0100
commitbe7311a7db8c9dcc373090b06b825995a3682ee4 (patch)
tree40285d4094f00f931059f5fc73a867f0742821e8 /apps/mobile/components
parent1e0cce7e6f79ccea00fc740aabc2b05918d17984 (diff)
downloadkarakeep-be7311a7db8c9dcc373090b06b825995a3682ee4.tar.zst
fix(mobile): Fix text bookmark editor
Diffstat (limited to 'apps/mobile/components')
-rw-r--r--apps/mobile/components/bookmarks/BookmarkAssetView.tsx56
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx85
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkView.tsx35
-rw-r--r--apps/mobile/components/bookmarks/BookmarkTextView.tsx112
-rw-r--r--apps/mobile/components/bookmarks/BottomActions.tsx136
5 files changed, 424 insertions, 0 deletions
diff --git a/apps/mobile/components/bookmarks/BookmarkAssetView.tsx b/apps/mobile/components/bookmarks/BookmarkAssetView.tsx
new file mode 100644
index 00000000..5fe2f470
--- /dev/null
+++ b/apps/mobile/components/bookmarks/BookmarkAssetView.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+import { Pressable, View } from "react-native";
+import ImageView from "react-native-image-viewing";
+import BookmarkAssetImage from "@/components/bookmarks/BookmarkAssetImage";
+import { PDFViewer } from "@/components/bookmarks/PDFViewer";
+import { useAssetUrl } from "@/lib/hooks";
+
+import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
+
+interface BookmarkAssetViewProps {
+ bookmark: ZBookmark;
+}
+
+export default function BookmarkAssetView({
+ bookmark,
+}: BookmarkAssetViewProps) {
+ const [imageZoom, setImageZoom] = useState(false);
+
+ if (bookmark.content.type !== BookmarkTypes.ASSET) {
+ 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
+ visible={imageZoom}
+ imageIndex={0}
+ onRequestClose={() => setImageZoom(false)}
+ doubleTapToZoomEnabled={true}
+ images={[assetSource]}
+ />
+
+ <Pressable onPress={() => setImageZoom(true)}>
+ <BookmarkAssetImage
+ assetId={bookmark.content.assetId}
+ className="h-56 min-h-56 w-full object-cover"
+ />
+ </Pressable>
+ </View>
+ );
+}
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
new file mode 100644
index 00000000..58cbcc8d
--- /dev/null
+++ b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
@@ -0,0 +1,85 @@
+import * as Haptics from "expo-haptics";
+import { MenuView } from "@react-native-menu/menu";
+import { ChevronDown } from "lucide-react-native";
+
+import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
+
+export type BookmarkLinkType = "browser" | "reader" | "screenshot" | "archive";
+
+function getAvailableViewTypes(bookmark: ZBookmark): BookmarkLinkType[] {
+ if (bookmark.content.type !== BookmarkTypes.LINK) {
+ return [];
+ }
+
+ const availableTypes: BookmarkLinkType[] = ["browser", "reader"];
+
+ if (bookmark.assets.some((asset) => asset.assetType === "screenshot")) {
+ availableTypes.push("screenshot");
+ }
+
+ if (
+ bookmark.assets.some(
+ (asset) =>
+ asset.assetType === "precrawledArchive" ||
+ asset.assetType === "fullPageArchive",
+ )
+ ) {
+ availableTypes.push("archive");
+ }
+
+ return availableTypes;
+}
+
+interface BookmarkLinkTypeSelectorProps {
+ type: BookmarkLinkType;
+ onChange: (type: BookmarkLinkType) => void;
+ bookmark: ZBookmark;
+}
+
+export default function BookmarkLinkTypeSelector({
+ type,
+ onChange,
+ bookmark,
+}: BookmarkLinkTypeSelectorProps) {
+ const availableTypes = getAvailableViewTypes(bookmark);
+
+ const allActions = [
+ {
+ id: "reader" as const,
+ title: "Reader View",
+ state: type === "reader" ? ("on" as const) : undefined,
+ },
+ {
+ id: "browser" as const,
+ title: "Browser",
+ state: type === "browser" ? ("on" as const) : undefined,
+ },
+ {
+ id: "screenshot" as const,
+ title: "Screenshot",
+ state: type === "screenshot" ? ("on" as const) : undefined,
+ },
+ {
+ id: "archive" as const,
+ title: "Archived Page",
+ state: type === "archive" ? ("on" as const) : undefined,
+ },
+ ];
+
+ const availableActions = allActions.filter((action) =>
+ availableTypes.includes(action.id),
+ );
+
+ return (
+ <MenuView
+ onPressAction={({ nativeEvent }) => {
+ Haptics.selectionAsync();
+ onChange(nativeEvent.event as BookmarkLinkType);
+ }}
+ actions={availableActions}
+ shouldOpenOnLongPress={false}
+ >
+ <ChevronDown onPress={() => Haptics.selectionAsync()} color="gray" />
+ </MenuView>
+ );
+}
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkView.tsx b/apps/mobile/components/bookmarks/BookmarkLinkView.tsx
new file mode 100644
index 00000000..e8a78029
--- /dev/null
+++ b/apps/mobile/components/bookmarks/BookmarkLinkView.tsx
@@ -0,0 +1,35 @@
+import {
+ BookmarkLinkArchivePreview,
+ BookmarkLinkBrowserPreview,
+ BookmarkLinkReaderPreview,
+ BookmarkLinkScreenshotPreview,
+} from "@/components/bookmarks/BookmarkLinkPreview";
+
+import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
+
+import { BookmarkLinkType } from "./BookmarkLinkTypeSelector";
+
+interface BookmarkLinkViewProps {
+ bookmark: ZBookmark;
+ bookmarkPreviewType: BookmarkLinkType;
+}
+
+export default function BookmarkLinkView({
+ bookmark,
+ bookmarkPreviewType,
+}: BookmarkLinkViewProps) {
+ if (bookmark.content.type !== BookmarkTypes.LINK) {
+ throw new Error("Wrong content type rendered");
+ }
+
+ switch (bookmarkPreviewType) {
+ case "browser":
+ return <BookmarkLinkBrowserPreview bookmark={bookmark} />;
+ case "reader":
+ return <BookmarkLinkReaderPreview bookmark={bookmark} />;
+ case "screenshot":
+ return <BookmarkLinkScreenshotPreview bookmark={bookmark} />;
+ case "archive":
+ return <BookmarkLinkArchivePreview bookmark={bookmark} />;
+ }
+}
diff --git a/apps/mobile/components/bookmarks/BookmarkTextView.tsx b/apps/mobile/components/bookmarks/BookmarkTextView.tsx
new file mode 100644
index 00000000..0f7a7291
--- /dev/null
+++ b/apps/mobile/components/bookmarks/BookmarkTextView.tsx
@@ -0,0 +1,112 @@
+import { useState } from "react";
+import { Keyboard, Pressable, ScrollView, TextInput, View } from "react-native";
+import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown";
+import { Button } from "@/components/ui/Button";
+import { Text } from "@/components/ui/Text";
+import { useToast } from "@/components/ui/Toast";
+import { useColorScheme } from "nativewind";
+
+import { useUpdateBookmark } from "@karakeep/shared-react/hooks/bookmarks";
+import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
+
+interface BookmarkTextViewProps {
+ bookmark: ZBookmark;
+}
+
+export default function BookmarkTextView({ bookmark }: BookmarkTextViewProps) {
+ if (bookmark.content.type !== BookmarkTypes.TEXT) {
+ throw new Error("Wrong content type rendered");
+ }
+ const { toast } = useToast();
+ const { colorScheme } = useColorScheme();
+
+ const [isEditing, setIsEditing] = useState(false);
+ const initialText = bookmark.content.text;
+ const [content, setContent] = useState(initialText);
+
+ const { mutate, isPending } = useUpdateBookmark({
+ onError: () => {
+ toast({
+ message: "Something went wrong",
+ variant: "destructive",
+ });
+ },
+ onSuccess: () => {
+ setIsEditing(false);
+ toast({
+ message: "Text updated successfully",
+ showProgress: false,
+ });
+ },
+ });
+
+ const handleSave = () => {
+ mutate({
+ bookmarkId: bookmark.id,
+ text: content,
+ });
+ };
+
+ const handleDiscard = () => {
+ setContent(initialText);
+ setIsEditing(false);
+ Keyboard.dismiss();
+ };
+
+ if (isEditing) {
+ return (
+ <View className="flex-1 p-4">
+ <View className="flex-row justify-end gap-2 px-4 py-2">
+ <Button
+ size="sm"
+ onPress={handleDiscard}
+ disabled={isPending}
+ variant="plain"
+ >
+ <Text>Cancel</Text>
+ </Button>
+ <Button size="sm" onPress={handleSave} disabled={isPending}>
+ <Text>{isPending ? "Saving..." : "Save"}</Text>
+ </Button>
+ </View>
+
+ <TextInput
+ value={content}
+ onChangeText={setContent}
+ multiline
+ autoFocus
+ editable={!isPending}
+ placeholder="Enter your text here..."
+ placeholderTextColor={colorScheme === "dark" ? "#666" : "#999"}
+ style={{
+ flex: 1,
+ fontSize: 16,
+ lineHeight: 24,
+ color: colorScheme === "dark" ? "#fff" : "#000",
+ textAlignVertical: "top",
+ padding: 12,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: colorScheme === "dark" ? "#333" : "#ddd",
+ backgroundColor: colorScheme === "dark" ? "#111" : "#fff",
+ }}
+ />
+ </View>
+ );
+ }
+
+ return (
+ <ScrollView className="m-4 flex-1 rounded-lg border border-border bg-card p-2">
+ <Pressable onPress={() => setIsEditing(true)}>
+ <View className="min-h-[200px] rounded-xl p-4">
+ <BookmarkTextMarkdown text={content} />
+ {content.trim() === "" && (
+ <Text className="italic text-muted-foreground">
+ Tap to add text...
+ </Text>
+ )}
+ </View>
+ </Pressable>
+ </ScrollView>
+ );
+}
diff --git a/apps/mobile/components/bookmarks/BottomActions.tsx b/apps/mobile/components/bookmarks/BottomActions.tsx
new file mode 100644
index 00000000..8cfa27c9
--- /dev/null
+++ b/apps/mobile/components/bookmarks/BottomActions.tsx
@@ -0,0 +1,136 @@
+import { Alert, Linking, Pressable, View } from "react-native";
+import { useRouter } from "expo-router";
+import { TailwindResolver } from "@/components/TailwindResolver";
+import { useToast } from "@/components/ui/Toast";
+import { ClipboardList, Globe, Info, Tag, Trash2 } from "lucide-react-native";
+
+import { useDeleteBookmark } from "@karakeep/shared-react/hooks/bookmarks";
+import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
+
+interface BottomActionsProps {
+ bookmark: ZBookmark;
+}
+
+export default function BottomActions({ bookmark }: BottomActionsProps) {
+ const { toast } = useToast();
+ const router = useRouter();
+
+ const { mutate: deleteBookmark, isPending: isDeletionPending } =
+ useDeleteBookmark({
+ onSuccess: () => {
+ router.back();
+ toast({
+ message: "The bookmark has been deleted!",
+ showProgress: false,
+ });
+ },
+ onError: () => {
+ toast({
+ message: "Something went wrong",
+ variant: "destructive",
+ showProgress: false,
+ });
+ },
+ });
+
+ const deleteBookmarkAlert = () =>
+ Alert.alert(
+ "Delete bookmark?",
+ "Are you sure you want to delete this bookmark?",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Delete",
+ onPress: () => deleteBookmark({ bookmarkId: bookmark.id }),
+ style: "destructive",
+ },
+ ],
+ );
+
+ const actions = [
+ {
+ id: "lists",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <ClipboardList color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: true,
+ onClick: () =>
+ router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`),
+ disabled: false,
+ },
+ {
+ id: "tags",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <Tag color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: true,
+ onClick: () =>
+ router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`),
+ disabled: false,
+ },
+ {
+ id: "open",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <Info color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: true,
+ onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/info`),
+ disabled: false,
+ },
+ {
+ id: "delete",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <Trash2 color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: true,
+ onClick: deleteBookmarkAlert,
+ disabled: isDeletionPending,
+ },
+ {
+ id: "browser",
+ icon: (
+ <TailwindResolver
+ className="text-foreground"
+ comp={(styles) => <Globe color={styles?.color?.toString()} />}
+ />
+ ),
+ shouldRender: bookmark.content.type == BookmarkTypes.LINK,
+ onClick: () =>
+ bookmark.content.type == BookmarkTypes.LINK &&
+ Linking.openURL(bookmark.content.url),
+ disabled: false,
+ },
+ ];
+
+ return (
+ <View>
+ <View className="flex flex-row items-center justify-between px-10 pb-2 pt-4">
+ {actions.map(
+ (a) =>
+ a.shouldRender && (
+ <Pressable
+ disabled={a.disabled}
+ key={a.id}
+ onPress={a.onClick}
+ className="py-auto"
+ >
+ {a.icon}
+ </Pressable>
+ ),
+ )}
+ </View>
+ </View>
+ );
+}