"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 { toast } from "@/components/ui/use-toast";
import { useTranslation } from "@/lib/i18n/client";
import {
deduplicateBookmarks,
ParsedBookmark,
parseHoarderBookmarkFile,
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 (
{children}
);
}
function ExportButton() {
const { t } = useTranslation();
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"
| "hoarder"
| "linkwarden"
| "tab-session-manager";
}) => {
if (source === "html") {
return await parseNetscapeBookmarkFile(file);
} else if (source === "pocket") {
return await parsePocketBookmarkFile(file);
} else if (source === "hoarder") {
return await parseHoarderBookmarkFile(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: "hoarder" })
}
>
Import
{importProgress && (
Processed {importProgress.done} of {importProgress.total} bookmarks
)}
);
}
export default function ImportExport() {
const { t } = useTranslation();
return (
{t("settings.import.import_export_bookmarks")}
);
}