aboutsummaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx25
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx15
-rw-r--r--apps/mobile/components/bookmarks/BookmarkLinkView.tsx3
-rw-r--r--apps/web/components/dashboard/BulkBookmarksAction.tsx2
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx170
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json7
6 files changed, 205 insertions, 17 deletions
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
index e0b592d6..4478bdda 100644
--- a/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx
@@ -14,6 +14,7 @@ import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
import FullPageError from "../FullPageError";
import FullPageSpinner from "../ui/FullPageSpinner";
import BookmarkAssetImage from "./BookmarkAssetImage";
+import { PDFViewer } from "./PDFViewer";
export function BookmarkLinkBrowserPreview({
bookmark,
@@ -33,6 +34,30 @@ export function BookmarkLinkBrowserPreview({
);
}
+export function BookmarkLinkPdfPreview({ bookmark }: { bookmark: ZBookmark }) {
+ if (bookmark.content.type !== BookmarkTypes.LINK) {
+ throw new Error("Wrong content type rendered");
+ }
+
+ const asset = bookmark.assets.find((r) => r.assetType == "pdf");
+
+ const assetSource = useAssetUrl(asset?.id ?? "");
+
+ if (!asset) {
+ return (
+ <View className="flex-1 bg-background">
+ <Text>Asset has no PDF</Text>
+ </View>
+ );
+ }
+
+ return (
+ <View className="flex flex-1">
+ <PDFViewer source={assetSource.uri ?? ""} headers={assetSource.headers} />
+ </View>
+ );
+}
+
export function BookmarkLinkReaderPreview({
bookmark,
}: {
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
index c7fd4be3..5c9955bd 100644
--- a/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx
@@ -4,7 +4,12 @@ import { ChevronDown } from "lucide-react-native";
import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks";
-export type BookmarkLinkType = "browser" | "reader" | "screenshot" | "archive";
+export type BookmarkLinkType =
+ | "browser"
+ | "reader"
+ | "screenshot"
+ | "archive"
+ | "pdf";
function getAvailableViewTypes(bookmark: ZBookmark): BookmarkLinkType[] {
if (bookmark.content.type !== BookmarkTypes.LINK) {
@@ -26,6 +31,9 @@ function getAvailableViewTypes(bookmark: ZBookmark): BookmarkLinkType[] {
) {
availableTypes.push("archive");
}
+ if (bookmark.assets.some((asset) => asset.assetType === "pdf")) {
+ availableTypes.push("pdf");
+ }
return availableTypes;
}
@@ -64,6 +72,11 @@ export default function BookmarkLinkTypeSelector({
title: "Archived Page",
state: type === "archive" ? ("on" as const) : undefined,
},
+ {
+ id: "pdf" as const,
+ title: "PDF",
+ state: type === "pdf" ? ("on" as const) : undefined,
+ },
];
const availableViewActions = viewActions.filter((action) =>
diff --git a/apps/mobile/components/bookmarks/BookmarkLinkView.tsx b/apps/mobile/components/bookmarks/BookmarkLinkView.tsx
index e8a78029..ba4d5b0c 100644
--- a/apps/mobile/components/bookmarks/BookmarkLinkView.tsx
+++ b/apps/mobile/components/bookmarks/BookmarkLinkView.tsx
@@ -1,6 +1,7 @@
import {
BookmarkLinkArchivePreview,
BookmarkLinkBrowserPreview,
+ BookmarkLinkPdfPreview,
BookmarkLinkReaderPreview,
BookmarkLinkScreenshotPreview,
} from "@/components/bookmarks/BookmarkLinkPreview";
@@ -31,5 +32,7 @@ export default function BookmarkLinkView({
return <BookmarkLinkScreenshotPreview bookmark={bookmark} />;
case "archive":
return <BookmarkLinkArchivePreview bookmark={bookmark} />;
+ case "pdf":
+ return <BookmarkLinkPdfPreview bookmark={bookmark} />;
}
}
diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx
index bad76ff9..0e74b985 100644
--- a/apps/web/components/dashboard/BulkBookmarksAction.tsx
+++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx
@@ -283,7 +283,7 @@ export default function BulkBookmarksAction() {
hidden: !isBulkEditEnabled,
},
{
- name: t("actions.download_full_page_archive"),
+ name: t("actions.preserve_offline_archive"),
icon: <FileDown size={18} />,
action: () => recrawlBookmarks(true),
isPending: recrawlBookmarkMutator.isPending,
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
index 696e1265..18ea3957 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { ChangeEvent, useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -13,11 +13,14 @@ import {
} from "@/components/ui/dropdown-menu";
import { toast } from "@/components/ui/sonner";
import { useClientConfig } from "@/lib/clientConfig";
+import useUpload from "@/lib/hooks/upload-file";
import { useTranslation } from "@/lib/i18n/client";
import {
Archive,
+ Download,
FileDown,
FileText,
+ ImagePlus,
Link,
List,
ListX,
@@ -34,13 +37,18 @@ import type {
ZBookmarkedLink,
} from "@karakeep/shared/types/bookmarks";
import {
- useRecrawlBookmark,
- useUpdateBookmark,
-} from "@karakeep/shared-react/hooks//bookmarks";
-import { useRemoveBookmarkFromList } from "@karakeep/shared-react/hooks//lists";
+ useAttachBookmarkAsset,
+ useReplaceBookmarkAsset,
+} from "@karakeep/shared-react/hooks/assets";
import { useBookmarkGridContext } from "@karakeep/shared-react/hooks/bookmark-grid-context";
import { useBookmarkListContext } from "@karakeep/shared-react/hooks/bookmark-list-context";
+import {
+ useRecrawlBookmark,
+ useUpdateBookmark,
+} from "@karakeep/shared-react/hooks/bookmarks";
+import { useRemoveBookmarkFromList } from "@karakeep/shared-react/hooks/lists";
import { BookmarkTypes } from "@karakeep/shared/types/bookmarks";
+import { getAssetUrl } from "@karakeep/shared/utils/assetUtils";
import { BookmarkedTextEditor } from "./BookmarkedTextEditor";
import DeleteBookmarkConfirmationDialog from "./DeleteBookmarkConfirmationDialog";
@@ -101,6 +109,47 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const [isTextEditorOpen, setTextEditorOpen] = useState(false);
const [isEditBookmarkDialogOpen, setEditBookmarkDialogOpen] = useState(false);
+ const bannerFileInputRef = useRef<HTMLInputElement>(null);
+
+ const { mutate: uploadAsset } = useUpload({
+ onError: (e) => {
+ toast({
+ description: e.error,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const { mutate: attachAsset, isPending: isAttaching } =
+ useAttachBookmarkAsset({
+ onSuccess: () => {
+ toast({
+ description: "Banner has been attached!",
+ });
+ },
+ onError: (e) => {
+ toast({
+ description: e.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const { mutate: replaceAsset, isPending: isReplacing } =
+ useReplaceBookmarkAsset({
+ onSuccess: () => {
+ toast({
+ description: "Banner has been replaced!",
+ });
+ },
+ onError: (e) => {
+ toast({
+ description: e.message,
+ variant: "destructive",
+ });
+ },
+ });
+
const { listId } = useBookmarkGridContext() ?? {};
const withinListContext = useBookmarkListContext();
@@ -156,6 +205,40 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
onError,
});
+ const handleBannerFileChange = (event: ChangeEvent<HTMLInputElement>) => {
+ const files = event.target.files;
+ if (files && files.length > 0) {
+ const file = files[0];
+ const existingBanner = bookmark.assets.find(
+ (asset) => asset.assetType === "bannerImage",
+ );
+
+ if (existingBanner) {
+ uploadAsset(file, {
+ onSuccess: (resp) => {
+ replaceAsset({
+ bookmarkId: bookmark.id,
+ oldAssetId: existingBanner.id,
+ newAssetId: resp.assetId,
+ });
+ },
+ });
+ } else {
+ uploadAsset(file, {
+ onSuccess: (resp) => {
+ attachAsset({
+ bookmarkId: bookmark.id,
+ asset: {
+ id: resp.assetId,
+ assetType: "bannerImage",
+ },
+ });
+ },
+ });
+ }
+ }
+ };
+
// Define action items array
const actionItems: ActionItemType[] = [
{
@@ -254,14 +337,6 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
}),
},
{
- id: "refresh",
- title: t("actions.refresh"),
- icon: <RotateCw className="mr-2 size-4" />,
- visible: isOwner && bookmark.content.type === BookmarkTypes.LINK,
- disabled: demoMode,
- onClick: () => crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }),
- },
- {
id: "offline-copies",
title: t("actions.offline_copies"),
icon: <Archive className="mr-2 size-4" />,
@@ -269,7 +344,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
items: [
{
id: "download-full-page",
- title: t("actions.download_full_page_archive"),
+ title: t("actions.preserve_offline_archive"),
icon: <FileDown className="mr-2 size-4" />,
visible: true,
disabled: demoMode,
@@ -293,6 +368,66 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
});
},
},
+ {
+ id: "download-full-page-archive",
+ title: t("actions.download_full_page_archive_file"),
+ icon: <Download className="mr-2 size-4" />,
+ visible:
+ bookmark.content.type === BookmarkTypes.LINK &&
+ !!(
+ bookmark.content.fullPageArchiveAssetId ||
+ bookmark.content.precrawledArchiveAssetId
+ ),
+ disabled: false,
+ onClick: () => {
+ const link = bookmark.content as ZBookmarkedLink;
+ const archiveAssetId =
+ link.fullPageArchiveAssetId ?? link.precrawledArchiveAssetId;
+ if (archiveAssetId) {
+ window.open(getAssetUrl(archiveAssetId), "_blank");
+ }
+ },
+ },
+ {
+ id: "download-pdf",
+ title: t("actions.download_pdf_file"),
+ icon: <Download className="mr-2 size-4" />,
+ visible: !!(bookmark.content as ZBookmarkedLink).pdfAssetId,
+ disabled: false,
+ onClick: () => {
+ const link = bookmark.content as ZBookmarkedLink;
+ if (link.pdfAssetId) {
+ window.open(getAssetUrl(link.pdfAssetId), "_blank");
+ }
+ },
+ },
+ ],
+ },
+ {
+ id: "more",
+ title: t("actions.more"),
+ icon: <MoreHorizontal className="mr-2 size-4" />,
+ visible: isOwner,
+ items: [
+ {
+ id: "refresh",
+ title: t("actions.refresh"),
+ icon: <RotateCw className="mr-2 size-4" />,
+ visible: bookmark.content.type === BookmarkTypes.LINK,
+ disabled: demoMode,
+ onClick: () =>
+ crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }),
+ },
+ {
+ id: "replace-banner",
+ title: bookmark.assets.find((a) => a.assetType === "bannerImage")
+ ? t("actions.replace_banner")
+ : t("actions.add_banner"),
+ icon: <ImagePlus className="mr-2 size-4" />,
+ visible: true,
+ disabled: demoMode || isAttaching || isReplacing,
+ onClick: () => bannerFileInputRef.current?.click(),
+ },
],
},
{
@@ -390,6 +525,13 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
})}
</DropdownMenuContent>
</DropdownMenu>
+ <input
+ type="file"
+ ref={bannerFileInputRef}
+ onChange={handleBannerFileChange}
+ className="hidden"
+ accept=".jpg,.jpeg,.png,.webp"
+ />
</>
);
}
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index 7077a36b..20040a79 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -72,8 +72,10 @@
"refresh": "Refresh",
"recrawl": "Recrawl",
"offline_copies": "Offline Copies",
- "download_full_page_archive": "Download Full Page Archive",
+ "preserve_offline_archive": "Preserve Offline Archive",
+ "download_full_page_archive_file": "Download Archive File",
"preserve_as_pdf": "Preserve as PDF",
+ "download_pdf_file": "Download PDF File",
"edit_tags": "Edit Tags",
"edit_notes": "Edit Notes",
"add_to_list": "Add to List",
@@ -101,6 +103,9 @@
"regenerate": "Regenerate",
"apply_all": "Apply All",
"ignore": "Ignore",
+ "more": "More",
+ "replace_banner": "Replace Banner",
+ "add_banner": "Add Banner",
"sort": {
"title": "Sort",
"relevant_first": "Most Relevant First",