"use client"; import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { buttonVariants } from "@/components/ui/button"; import FilePickerButton from "@/components/ui/file-picker-button"; import { Progress } from "@/components/ui/progress"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { deduplicateBookmarks, ParsedBookmark, parseKarakeepBookmarkFile, parseLinkwardenBookmarkFile, parseNetscapeBookmarkFile, parseOmnivoreBookmarkFile, parsePocketBookmarkFile, parseTabSessionManagerStateFile, } from "@/lib/importBookmarkParser"; import { cn } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { Download, Upload } from "lucide-react"; import { useCreateBookmarkWithPostHook, useUpdateBookmarkTags, } from "@karakeep/shared-react/hooks/bookmarks"; import { useAddBookmarkToList, useCreateBookmarkList, } from "@karakeep/shared-react/hooks/lists"; import { limitConcurrency } from "@karakeep/shared/concurrency"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { Card, CardContent } from "../ui/card"; function ImportCard({ text, description, children, }: { text: string; description: string; children: React.ReactNode; }) { return (

{text}

{description}

{children}
); } function ExportButton() { const { t } = useTranslation(); const [format, setFormat] = useState<"json" | "netscape">("json"); return (

Export File

{t("settings.import.export_links_and_notes")}

Export

); } export function ImportExportRow() { const { t } = useTranslation(); const router = useRouter(); const [importProgress, setImportProgress] = useState<{ done: number; total: number; } | null>(null); const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); const { mutateAsync: createList } = useCreateBookmarkList(); const { mutateAsync: addToList } = useAddBookmarkToList(); const { mutateAsync: updateTags } = useUpdateBookmarkTags(); const { mutateAsync: parseAndCreateBookmark } = useMutation({ mutationFn: async (toImport: { bookmark: ParsedBookmark; listId: string; }) => { const bookmark = toImport.bookmark; if (bookmark.content === undefined) { throw new Error("Content is undefined"); } const created = await createBookmark({ title: bookmark.title, createdAt: bookmark.addDate ? new Date(bookmark.addDate * 1000) : undefined, note: bookmark.notes, ...(bookmark.content.type === BookmarkTypes.LINK ? { type: BookmarkTypes.LINK, url: bookmark.content.url, } : { type: BookmarkTypes.TEXT, text: bookmark.content.text, }), }); await Promise.all([ // Add to import list addToList({ bookmarkId: created.id, listId: toImport.listId, }).catch((e) => { if ( e instanceof TRPCClientError && e.message.includes("already in the list") ) { /* empty */ } else { throw e; } }), // Update tags bookmark.tags.length > 0 ? updateTags({ bookmarkId: created.id, attach: bookmark.tags.map((t) => ({ tagName: t })), detach: [], }) : undefined, ]); return created; }, }); const { mutateAsync: runUploadBookmarkFile } = useMutation({ mutationFn: async ({ file, source, }: { file: File; source: | "html" | "pocket" | "omnivore" | "karakeep" | "linkwarden" | "tab-session-manager"; }) => { if (source === "html") { return await parseNetscapeBookmarkFile(file); } else if (source === "pocket") { return await parsePocketBookmarkFile(file); } else if (source === "karakeep") { return await parseKarakeepBookmarkFile(file); } else if (source === "omnivore") { return await parseOmnivoreBookmarkFile(file); } else if (source === "linkwarden") { return await parseLinkwardenBookmarkFile(file); } else if (source === "tab-session-manager") { return await parseTabSessionManagerStateFile(file); } else { throw new Error("Unknown source"); } }, onSuccess: async (parsedBookmarks) => { if (parsedBookmarks.length === 0) { toast({ description: "No bookmarks found in the file." }); return; } const importList = await createList({ name: t("settings.import.imported_bookmarks"), icon: "⬆️", }); const finalBookmarksToImport = deduplicateBookmarks(parsedBookmarks); setImportProgress({ done: 0, total: finalBookmarksToImport.length }); const importPromises = finalBookmarksToImport.map( (bookmark) => () => parseAndCreateBookmark({ bookmark: bookmark, listId: importList.id, }).then( (value) => { setImportProgress((prev) => { const newDone = (prev?.done ?? 0) + 1; return { done: newDone, total: finalBookmarksToImport.length, }; }); return { status: "fulfilled" as const, value }; }, () => { setImportProgress((prev) => { const newDone = (prev?.done ?? 0) + 1; return { done: newDone, total: finalBookmarksToImport.length, }; }); return { status: "rejected" as const }; }, ), ); const CONCURRENCY_LIMIT = 20; const resultsPromises = limitConcurrency( importPromises, CONCURRENCY_LIMIT, ); const results = await Promise.all(resultsPromises); let successes = 0; let failures = 0; let alreadyExisted = 0; for (const result of results) { if (result.status === "fulfilled") { if (result.value.alreadyExists) { alreadyExisted++; } else { successes++; } } else { failures++; } } if (successes > 0 || alreadyExisted > 0) { toast({ description: `Imported ${successes} bookmarks and skipped ${alreadyExisted} bookmarks that already existed`, variant: "default", }); } if (failures > 0) { toast({ description: `Failed to import ${failures} bookmarks. Check console for details.`, variant: "destructive", }); } router.push(`/dashboard/lists/${importList.id}`); }, onError: (error) => { setImportProgress(null); // Clear progress on initial parsing error toast({ description: error.message, variant: "destructive", }); }, }); return (
runUploadBookmarkFile({ file, source: "html" }) } >

Import

runUploadBookmarkFile({ file, source: "pocket" }) } >

Import

runUploadBookmarkFile({ file, source: "omnivore" }) } >

Import

runUploadBookmarkFile({ file, source: "linkwarden" }) } >

Import

runUploadBookmarkFile({ file, source: "tab-session-manager" }) } >

Import

runUploadBookmarkFile({ file, source: "karakeep" }) } >

Import

{importProgress && (

Processed {importProgress.done} of {importProgress.total} bookmarks

)}
); } export default function ImportExport() { const { t } = useTranslation(); return (

{t("settings.import.import_export_bookmarks")}

); }