aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkamtschatka <sschatka@gmail.com>2024-05-25 23:20:17 +0200
committerGitHub <noreply@github.com>2024-05-25 22:20:17 +0100
commit033e8a2d26bb0ecaa8301609960d35d3467a88f4 (patch)
tree8c899a081b1cdd31eb24ab851b7a5c9d1dec858e
parentbb431be02850ca37f89cc40ce958226f64fe5ad5 (diff)
downloadkarakeep-033e8a2d26bb0ecaa8301609960d35d3467a88f4.tar.zst
feature: Allow import Netscape HTML format (#163)
* [Feature request] Netscape HTML format import/export #96 added the possibility to add exported bookmarks via the webUI for ease of use * [Feature request] Netscape HTML format import/export #96 updated the documentation * Extract the parser into its own file and reuse the existing bookmark upload logic --------- Co-authored-by: kamtschatka <simon.schatka@gmx.at> Co-authored-by: MohamedBassem <me@mbassem.com>
-rw-r--r--apps/web/components/dashboard/UploadDropzone.tsx55
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx15
-rw-r--r--apps/web/components/utils/BookmarkAlreadyExistsToast.tsx20
-rw-r--r--apps/web/lib/netscapeBookmarkParser.ts20
-rw-r--r--docs/docs/10-import.md33
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