Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bookmark upload #163

Merged
merged 4 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions apps/web/components/dashboard/UploadDropzone.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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);
Expand All @@ -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({
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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>
Expand Down
15 changes: 2 additions & 13 deletions apps/web/components/dashboard/bookmarks/EditorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
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";
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";

Expand Down Expand Up @@ -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",
});
}
Expand Down
20 changes: 20 additions & 0 deletions apps/web/components/utils/BookmarkAlreadyExistsToast.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
20 changes: 20 additions & 0 deletions apps/web/lib/netscapeBookmarkParser.ts
Original file line number Diff line number Diff line change
@@ -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));
}
33 changes: 25 additions & 8 deletions docs/docs/10-import.md
Original file line number Diff line number Diff line change
@@ -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
Loading