diff options
| author | MohamedBassem <me@mbassem.com> | 2024-09-21 17:49:47 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-09-21 17:51:58 +0000 |
| commit | 9dd6f216ad18c09a28eaad67411d3a0e7f57a04f (patch) | |
| tree | 708ac6b5f9fa58ec41e80ba6e775b742a78f41bc /apps | |
| parent | d62c9724b7f4cb728cd5b5496fdcc0eba8330772 (diff) | |
| download | karakeep-9dd6f216ad18c09a28eaad67411d3a0e7f57a04f.tar.zst | |
feature(web): Add support for importing bookmarks from Pocket
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/dashboard/settings/ImportExport.tsx | 115 | ||||
| -rw-r--r-- | apps/web/components/ui/progress.tsx | 27 | ||||
| -rw-r--r-- | apps/web/lib/importBookmarkParser.ts | 68 | ||||
| -rw-r--r-- | apps/web/lib/netscapeBookmarkParser.ts | 31 | ||||
| -rw-r--r-- | apps/web/package.json | 1 |
5 files changed, 175 insertions, 67 deletions
diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx index dcc3c8e8..4827df93 100644 --- a/apps/web/components/dashboard/settings/ImportExport.tsx +++ b/apps/web/components/dashboard/settings/ImportExport.tsx @@ -1,9 +1,15 @@ "use client"; +import { useState } from "react"; import { useRouter } from "next/navigation"; import FilePickerButton from "@/components/ui/file-picker-button"; +import { Progress } from "@/components/ui/progress"; import { toast } from "@/components/ui/use-toast"; -import { parseNetscapeBookmarkFile } from "@/lib/netscapeBookmarkParser"; +import { + ParsedBookmark, + parseNetscapeBookmarkFile, + parsePocketBookmarkFile, +} from "@/lib/importBookmarkParser"; import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; import { Upload } from "lucide-react"; @@ -22,6 +28,11 @@ import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; export function Import() { const router = useRouter(); + const [importProgress, setImportProgress] = useState<{ + done: number; + total: number; + } | null>(null); + const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook(); const { mutateAsync: updateBookmark } = useUpdateBookmark(); const { mutateAsync: createList } = useCreateBookmarkList(); @@ -30,12 +41,7 @@ export function Import() { const { mutateAsync: parseAndCreateBookmark } = useMutation({ mutationFn: async (toImport: { - bookmark: { - title: string; - url: string | undefined; - tags: string[]; - addDate?: number; - }; + bookmark: ParsedBookmark; listId: string; }) => { const bookmark = toImport.bookmark; @@ -57,6 +63,8 @@ export function Import() { createdAt: bookmark.addDate ? new Date(bookmark.addDate * 1000) : undefined, + }).catch(() => { + /* empty */ }) : undefined, @@ -76,31 +84,40 @@ export function Import() { }), // Update tags - updateTags({ - bookmarkId: created.id, - attach: bookmark.tags.map((t) => ({ tagName: t })), - detach: [], - }), + 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: File) => { - return await parseNetscapeBookmarkFile(file); + mutationFn: async ({ + file, + source, + }: { + file: File; + source: "html" | "pocket"; + }) => { + if (source === "html") { + return await parseNetscapeBookmarkFile(file); + } else if (source === "pocket") { + return await parsePocketBookmarkFile(file); + } else { + throw new Error("Unknown source"); + } }, onSuccess: async (resp) => { const importList = await createList({ name: `Imported Bookmarks`, icon: "⬆️", }); - - let done = 0; - const { id, update } = toast({ - description: `Processed 0 bookmarks of ${resp.length}`, - variant: "default", - }); + setImportProgress({ done: 0, total: resp.length }); const successes = []; const failed = []; @@ -120,12 +137,10 @@ export function Import() { } catch (e) { failed.push(parsedBookmark); } - - update({ - id, - description: `Processed ${done + 1} bookmarks of ${resp.length}`, - }); - done++; + setImportProgress((prev) => ({ + done: (prev?.done ?? 0) + 1, + total: resp.length, + })); } if (successes.length > 0 || alreadyExisted.length > 0) { @@ -153,16 +168,44 @@ export function Import() { }); return ( - <div> - <FilePickerButton - accept=".html" - multiple={false} - className="flex items-center gap-2" - onFileSelect={runUploadBookmarkFile} - > - <Upload /> - <p>Import Bookmarks from HTML file</p> - </FilePickerButton> + <div className="flex flex-col gap-3"> + <div className="flex flex-row gap-2"> + <FilePickerButton + accept=".html" + multiple={false} + className="flex items-center gap-2" + onFileSelect={(file) => + runUploadBookmarkFile({ file, source: "html" }) + } + > + <Upload /> + <p>Import Bookmarks from HTML file</p> + </FilePickerButton> + + <FilePickerButton + accept=".html" + multiple={false} + className="flex items-center gap-2" + onFileSelect={(file) => + runUploadBookmarkFile({ file, source: "pocket" }) + } + > + <Upload /> + <p>Import Bookmarks from Pocket export</p> + </FilePickerButton> + </div> + {importProgress && ( + <div className="flex flex-col gap-2"> + <p className="shrink-0 text-sm"> + Processed {importProgress.done} of {importProgress.total} bookmarks + </p> + <div className="w-full"> + <Progress + value={(importProgress.done * 100) / importProgress.total} + /> + </div> + </div> + )} </div> ); } diff --git a/apps/web/components/ui/progress.tsx b/apps/web/components/ui/progress.tsx new file mode 100644 index 00000000..d777b615 --- /dev/null +++ b/apps/web/components/ui/progress.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as React from "react"; +import { cn } from "@/lib/utils"; +import * as ProgressPrimitive from "@radix-ui/react-progress"; + +const Progress = React.forwardRef< + React.ElementRef<typeof ProgressPrimitive.Root>, + React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> +>(({ className, value, ...props }, ref) => ( + <ProgressPrimitive.Root + ref={ref} + className={cn( + "relative h-4 w-full overflow-hidden rounded-full bg-secondary", + className, + )} + {...props} + > + <ProgressPrimitive.Indicator + className="h-full w-full flex-1 bg-primary transition-all" + style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }} + /> + </ProgressPrimitive.Root> +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/apps/web/lib/importBookmarkParser.ts b/apps/web/lib/importBookmarkParser.ts new file mode 100644 index 00000000..e1b21a79 --- /dev/null +++ b/apps/web/lib/importBookmarkParser.ts @@ -0,0 +1,68 @@ +// Copied from https://gist.github.com/devster31/4e8c6548fd16ffb75c02e6f24e27f9b9 +import * as cheerio from "cheerio"; + +export interface ParsedBookmark { + title: string; + url?: string; + tags: string[]; + addDate?: number; +} + +export async function parseNetscapeBookmarkFile( + file: File, +): Promise<ParsedBookmark[]> { + const textContent = await file.text(); + + if (!textContent.startsWith("<!DOCTYPE NETSCAPE-Bookmark-file-1>")) { + throw Error("The uploaded html file does not seem to be a bookmark file"); + } + + const $ = cheerio.load(textContent); + + return $("a") + .map(function (_index, a) { + const $a = $(a); + const addDate = $a.attr("add_date"); + let tags: string[] = []; + try { + tags = $a.attr("tags")?.split(",") ?? []; + } catch (e) { + /* empty */ + } + return { + title: $a.text(), + url: $a.attr("href"), + tags: tags, + addDate: typeof addDate === "undefined" ? undefined : parseInt(addDate), + }; + }) + .get(); +} + +export async function parsePocketBookmarkFile( + file: File, +): Promise<ParsedBookmark[]> { + const textContent = await file.text(); + + const $ = cheerio.load(textContent); + + return $("a") + .map(function (_index, a) { + const $a = $(a); + const addDate = $a.attr("time_added"); + let tags: string[] = []; + const tagsStr = $a.attr("tags"); + try { + tags = tagsStr && tagsStr.length > 0 ? tagsStr.split(",") : []; + } catch (e) { + /* empty */ + } + return { + title: $a.text(), + url: $a.attr("href"), + tags: tags, + addDate: typeof addDate === "undefined" ? undefined : parseInt(addDate), + }; + }) + .get(); +} diff --git a/apps/web/lib/netscapeBookmarkParser.ts b/apps/web/lib/netscapeBookmarkParser.ts deleted file mode 100644 index 196c0525..00000000 --- a/apps/web/lib/netscapeBookmarkParser.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copied from https://gist.github.com/devster31/4e8c6548fd16ffb75c02e6f24e27f9b9 -import * as cheerio from "cheerio"; - -export async function parseNetscapeBookmarkFile(file: File) { - const textContent = await file.text(); - - if (!textContent.startsWith("<!DOCTYPE NETSCAPE-Bookmark-file-1>")) { - throw Error("The uploaded html file does not seem to be a bookmark file"); - } - - const $ = cheerio.load(textContent); - - return $("a") - .map(function (_index, a) { - const $a = $(a); - const addDate = $a.attr("add_date"); - let tags: string[] = []; - try { - tags = $a.attr("tags")?.split(",") ?? []; - } catch (e) { - /* empty */ - } - return { - title: $a.text(), - url: $a.attr("href"), - tags: tags, - addDate: typeof addDate === "undefined" ? undefined : parseInt(addDate), - }; - }) - .get(); -} diff --git a/apps/web/package.json b/apps/web/package.json index 491ad46d..a59d4a48 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", |
