diff options
| -rw-r--r-- | apps/web/components/dashboard/UploadDropzone.tsx | 55 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/EditorCard.tsx | 15 | ||||
| -rw-r--r-- | apps/web/components/utils/BookmarkAlreadyExistsToast.tsx | 20 | ||||
| -rw-r--r-- | apps/web/lib/netscapeBookmarkParser.ts | 20 | ||||
| -rw-r--r-- | docs/docs/10-import.md | 33 |
5 files changed, 113 insertions, 30 deletions
diff --git a/apps/web/components/dashboard/UploadDropzone.tsx b/apps/web/components/dashboard/UploadDropzone.tsx index be30a77f..60398ede 100644 --- a/apps/web/components/dashboard/UploadDropzone.tsx +++ b/apps/web/components/dashboard/UploadDropzone.tsx @@ -1,6 +1,7 @@ "use client"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; +import { parseNetscapeBookmarkFile } from "@/lib/netscapeBookmarkParser"; import { cn } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; import { TRPCClientError } from "@trpc/client"; @@ -14,19 +15,26 @@ import { import LoadingSpinner from "../ui/spinner"; import { toast } from "../ui/use-toast"; +import BookmarkAlreadyExistsToast from "../utils/BookmarkAlreadyExistsToast"; -function useUploadAsset({ onComplete }: { onComplete: () => void }) { +function useUploadAsset() { const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook({ - onSuccess: () => { - toast({ description: "Bookmark uploaded" }); - onComplete(); + onSuccess: (resp) => { + if (resp.alreadyExists) { + toast({ + description: <BookmarkAlreadyExistsToast bookmarkId={resp.id} />, + variant: "default", + }); + } else { + toast({ description: "Bookmark uploaded" }); + } }, onError: () => { toast({ description: "Something went wrong", variant: "destructive" }); }, }); - const { mutateAsync: runUpload } = useMutation({ + const { mutateAsync: runUploadAsset } = useMutation({ mutationFn: async (file: File) => { const formData = new FormData(); formData.append("file", file); @@ -53,7 +61,35 @@ function useUploadAsset({ onComplete }: { onComplete: () => void }) { }, }); - return runUpload; + const { mutateAsync: runUploadBookmarkFile } = useMutation({ + mutationFn: async (file: File) => { + return await parseNetscapeBookmarkFile(file); + }, + onSuccess: async (resp) => { + return Promise.all( + resp.map((url) => + createBookmark({ type: "link", url: url.toString() }), + ), + ); + }, + onError: (error) => { + toast({ + description: error.message, + variant: "destructive", + }); + }, + }); + + return useCallback( + (file: File) => { + if (file.type === "text/html") { + return runUploadBookmarkFile(file); + } else { + return runUploadAsset(file); + } + }, + [runUploadAsset, runUploadBookmarkFile], + ); } function useUploadAssets({ @@ -65,7 +101,7 @@ function useUploadAssets({ onFileError: (name: string, e: Error) => void; onAllUploaded: () => void; }) { - const runUpload = useUploadAsset({ onComplete: onFileUpload }); + const runUpload = useUploadAsset(); return async (files: File[]) => { if (files.length == 0) { @@ -74,6 +110,7 @@ function useUploadAssets({ for (const file of files) { try { await runUpload(file); + onFileUpload(); } catch (e) { if (e instanceof TRPCClientError || e instanceof Error) { onFileError(file.name, e); @@ -137,7 +174,7 @@ export default function UploadDropzone({ </div> ) : ( <p className="text-2xl font-bold text-gray-700"> - Drop Your Image + Drop Your Image / Bookmark file </p> )} </div> diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index 8425f669..44d68378 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -1,6 +1,5 @@ import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form"; import React, { useEffect, useImperativeHandle, useRef } from "react"; -import Link from "next/link"; import { ActionButton } from "@/components/ui/action-button"; import { Form, FormControl, FormItem } from "@/components/ui/form"; import InfoTooltip from "@/components/ui/info-tooltip"; @@ -8,11 +7,11 @@ import MultipleChoiceDialog from "@/components/ui/multiple-choice-dialog"; import { Separator } from "@/components/ui/separator"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/components/ui/use-toast"; +import BookmarkAlreadyExistsToast from "@/components/utils/BookmarkAlreadyExistsToast"; import { useClientConfig } from "@/lib/clientConfig"; import { useBookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ExternalLink } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -64,17 +63,7 @@ export default function EditorCard({ className }: { className?: string }) { onSuccess: (resp) => { if (resp.alreadyExists) { toast({ - description: ( - <div className="flex items-center gap-1"> - Bookmark already exists. - <Link - className="flex underline-offset-4 hover:underline" - href={`/dashboard/preview/${resp.id}`} - > - Open <ExternalLink className="ml-1 size-4" /> - </Link> - </div> - ), + description: <BookmarkAlreadyExistsToast bookmarkId={resp.id} />, variant: "default", }); } diff --git a/apps/web/components/utils/BookmarkAlreadyExistsToast.tsx b/apps/web/components/utils/BookmarkAlreadyExistsToast.tsx new file mode 100644 index 00000000..66bdb17a --- /dev/null +++ b/apps/web/components/utils/BookmarkAlreadyExistsToast.tsx @@ -0,0 +1,20 @@ +import Link from "next/link"; +import { ExternalLink } from "lucide-react"; + +export default function BookmarkAlreadyExistsToast({ + bookmarkId, +}: { + bookmarkId: string; +}) { + return ( + <div className="flex items-center gap-1"> + Bookmark already exists. + <Link + className="flex underline-offset-4 hover:underline" + href={`/dashboard/preview/${bookmarkId}`} + > + Open <ExternalLink className="ml-1 size-4" /> + </Link> + </div> + ); +} diff --git a/apps/web/lib/netscapeBookmarkParser.ts b/apps/web/lib/netscapeBookmarkParser.ts new file mode 100644 index 00000000..ac5f3ec2 --- /dev/null +++ b/apps/web/lib/netscapeBookmarkParser.ts @@ -0,0 +1,20 @@ +function extractUrls(html: string): string[] { + const regex = /<a\s+(?:[^>]*?\s+)?href="(http[^"]*)"/gi; + let match; + const urls = []; + + while ((match = regex.exec(html)) !== null) { + urls.push(match[1]); + } + + return urls; +} + +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"); + } + + return extractUrls(textContent).map((url) => new URL(url)); +} diff --git a/docs/docs/10-import.md b/docs/docs/10-import.md index f0e6ee35..14c59034 100644 --- a/docs/docs/10-import.md +++ b/docs/docs/10-import.md @@ -1,28 +1,45 @@ # Importing Bookmarks +## Import using the WebUI + +Hoarder supports importing bookmarks using the Netscape HTML Format. + +Simply open the WebUI of your Hoarder instance and drag and drop the bookmarks file into the UI. + +:::info +All the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import! +::: + +## Import using the CLI + :::warning -Currently importing bookmarks requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though. +Importing bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though. ::: -## Import from Chrome +### Import from Chrome -- Open Chrome and go to `chrome://bookmarks` -- Click on the three dots on the top right corner and choose `Export bookmarks` -- This will download an html file with all of your bookmarks. +- First follow the steps below to export your bookmarks from Chrome - To extract the links from this html file, you can run this simple bash one liner (if on windows, you might need to use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)): `cat <file_path> | grep HREF | sed 's/.*HREF="\([^"]*\)".*/\1/' > all_links.txt`. - This will create a file `all_links.txt` with all of your bookmarks one per line. - To import them, we'll use the [hoarder cli](https://docs.hoarder.app/command-line). You'll need a Hoarder API key for that. - -Run the following command to import all the links from `all_links.txt`: +- Run the following command to import all the links from `all_links.txt`: ``` cat all_links.txt | xargs -I{} hoarder --api-key <key> --server-addr <addr> bookmarks add --link {} ``` -## Import from other platforms +### Import from other platforms If you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [hoarder cli](https://docs.hoarder.app/command-line): ``` cat all_links.txt | xargs -I{} hoarder --api-key <key> --server-addr <addr> bookmarks add --link {} ``` + +## Exporting Bookmarks from Chrome + +- Open Chrome and go to `chrome://bookmarks` +- Click on the three dots on the top right corner and choose `Export bookmarks` +- This will download an html file with all of your bookmarks. + +You can use this file to import the bookmarks using the UI or CLI method described above
\ No newline at end of file |
