From be7311a7db8c9dcc373090b06b825995a3682ee4 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sun, 31 Aug 2025 16:09:12 +0100 Subject: fix(mobile): Fix text bookmark editor --- .../components/bookmarks/BookmarkAssetView.tsx | 56 +++++++++ .../bookmarks/BookmarkLinkTypeSelector.tsx | 85 +++++++++++++ .../components/bookmarks/BookmarkLinkView.tsx | 35 ++++++ .../components/bookmarks/BookmarkTextView.tsx | 112 +++++++++++++++++ apps/mobile/components/bookmarks/BottomActions.tsx | 136 +++++++++++++++++++++ 5 files changed, 424 insertions(+) create mode 100644 apps/mobile/components/bookmarks/BookmarkAssetView.tsx create mode 100644 apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx create mode 100644 apps/mobile/components/bookmarks/BookmarkLinkView.tsx create mode 100644 apps/mobile/components/bookmarks/BookmarkTextView.tsx create mode 100644 apps/mobile/components/bookmarks/BottomActions.tsx (limited to 'apps/mobile/components') 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 ( + + + + ); + } + + // Handle image assets as before + return ( + + setImageZoom(false)} + doubleTapToZoomEnabled={true} + images={[assetSource]} + /> + + setImageZoom(true)}> + + + + ); +} 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 ( + { + Haptics.selectionAsync(); + onChange(nativeEvent.event as BookmarkLinkType); + }} + actions={availableActions} + shouldOpenOnLongPress={false} + > + Haptics.selectionAsync()} color="gray" /> + + ); +} 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 ; + case "reader": + return ; + case "screenshot": + return ; + case "archive": + return ; + } +} 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 ( + + + + + + + + + ); + } + + return ( + + setIsEditing(true)}> + + + {content.trim() === "" && ( + + Tap to add text... + + )} + + + + ); +} 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: ( + } + /> + ), + shouldRender: true, + onClick: () => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`), + disabled: false, + }, + { + id: "tags", + icon: ( + } + /> + ), + shouldRender: true, + onClick: () => + router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`), + disabled: false, + }, + { + id: "open", + icon: ( + } + /> + ), + shouldRender: true, + onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/info`), + disabled: false, + }, + { + id: "delete", + icon: ( + } + /> + ), + shouldRender: true, + onClick: deleteBookmarkAlert, + disabled: isDeletionPending, + }, + { + id: "browser", + icon: ( + } + /> + ), + shouldRender: bookmark.content.type == BookmarkTypes.LINK, + onClick: () => + bookmark.content.type == BookmarkTypes.LINK && + Linking.openURL(bookmark.content.url), + disabled: false, + }, + ]; + + return ( + + + {actions.map( + (a) => + a.shouldRender && ( + + {a.icon} + + ), + )} + + + ); +} -- cgit v1.2.3-70-g09d2