mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-04-22 07:58:01 +03:00
feat: add netscape bookmark import/export #203
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { Body1, Button, makeStyles, MessageBar, MessageBarBody, Subtitle2, tokens } from "@fluentui/react-components";
|
||||
import { ArrowDownload24Regular, ArrowUpload24Regular } from "@fluentui/react-icons";
|
||||
import importBookmarks from "../utils/importBookmarks";
|
||||
import exportBookmarks from "../utils/exportBookmarks";
|
||||
|
||||
export default function BookmarksSection(): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
const dialog = useDialog();
|
||||
|
||||
const [importResult, setImportResult] = useState<number | null>(null);
|
||||
|
||||
const handleImport = (): void =>
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("features.netscape_bookmarks.import_dialog.title"),
|
||||
confirmText: i18n.t("options_page.storage.import_prompt.proceed"),
|
||||
onConfirm: () => importBookmarks().then(setImportResult),
|
||||
content: (
|
||||
<Body1 as="p">
|
||||
{ i18n.t("features.netscape_bookmarks.import_dialog.content") }
|
||||
</Body1>
|
||||
)
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={ cls.root }>
|
||||
<Subtitle2>{ i18n.t("features.netscape_bookmarks.title") }</Subtitle2>
|
||||
|
||||
{ importResult !== null &&
|
||||
<MessageBar intent={ importResult >= 0 ? "success" : "error" } layout="multiline">
|
||||
<MessageBarBody>
|
||||
{ importResult >= 0 ?
|
||||
i18n.t("features.netscape_bookmarks.import_result.success", [importResult]) :
|
||||
i18n.t("features.netscape_bookmarks.import_result.error")
|
||||
}
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
}
|
||||
|
||||
<div className={ cls.buttons }>
|
||||
<Button icon={ <ArrowDownload24Regular /> } onClick={ exportBookmarks }>
|
||||
{ i18n.t("features.netscape_bookmarks.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload24Regular /> } onClick={ handleImport }>
|
||||
{ i18n.t("features.netscape_bookmarks.import") }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: tokens.spacingVerticalMNudge
|
||||
},
|
||||
buttons:
|
||||
{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { CollectionItem, GraphicsStorage, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
|
||||
|
||||
export default function convertBookmarks(bookmarks: Bookmark[]): [CollectionItem[], GraphicsStorage, number]
|
||||
{
|
||||
let count: number = 0;
|
||||
const graphics: GraphicsStorage = {};
|
||||
const items: CollectionItem[] = [];
|
||||
const untitled: CollectionItem = {
|
||||
items: [],
|
||||
timestamp: Date.now(),
|
||||
type: "collection"
|
||||
};
|
||||
|
||||
for (const bookmark of bookmarks)
|
||||
{
|
||||
if (bookmark.type === "bookmark")
|
||||
{
|
||||
untitled.items.push(getTab(bookmark, graphics));
|
||||
count++;
|
||||
}
|
||||
else if (bookmark.type === "folder" && bookmark.children)
|
||||
{
|
||||
const collection: CollectionItem = getCollection(bookmark, graphics);
|
||||
items.push(collection);
|
||||
count += collection.items.reduce((acc, item) =>
|
||||
{
|
||||
if (item.type === "tab")
|
||||
return acc + 1;
|
||||
else if (item.type === "group")
|
||||
return acc + item.items.length;
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return [items, graphics, count];
|
||||
}
|
||||
|
||||
function getTab(bookmark: Bookmark, graphics: GraphicsStorage): TabItem
|
||||
{
|
||||
if (bookmark.icon)
|
||||
graphics[bookmark.url!] = {
|
||||
icon: bookmark.icon
|
||||
};
|
||||
|
||||
return {
|
||||
type: "tab",
|
||||
url: bookmark.url!,
|
||||
title: bookmark.title || bookmark.url!
|
||||
};
|
||||
}
|
||||
|
||||
function getCollection(bookmark: Bookmark, graphics: GraphicsStorage): CollectionItem
|
||||
{
|
||||
const collection: CollectionItem = {
|
||||
items: [],
|
||||
title: bookmark.title,
|
||||
timestamp: Date.now(),
|
||||
type: "collection"
|
||||
};
|
||||
|
||||
for (const child of bookmark.children!)
|
||||
{
|
||||
if (child.type === "bookmark")
|
||||
collection.items.push(getTab(child, graphics));
|
||||
else if (child.type === "folder" && child.children)
|
||||
collection.items.push(getGroup(child, graphics));
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
function getGroup(bookmark: Bookmark, graphics: GraphicsStorage): GroupItem
|
||||
{
|
||||
const group: GroupItem = {
|
||||
items: [],
|
||||
title: bookmark.title,
|
||||
pinned: false,
|
||||
type: "group",
|
||||
color: getRandomColor()
|
||||
};
|
||||
|
||||
for (const child of bookmark.children!)
|
||||
{
|
||||
if (child.type === "bookmark")
|
||||
group.items.push(getTab(child, graphics));
|
||||
else if (child.type === "folder")
|
||||
group.items.push(...getGroup(child, graphics).items);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
function getRandomColor(): "blue" | "cyan" | "green" | "grey" | "orange" | "pink" | "purple" | "red" | "yellow"
|
||||
{
|
||||
const colors = ["blue", "cyan", "green", "grey", "orange", "pink", "purple", "red", "yellow"] as const;
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { getCollections } from "@/features/collectionStorage";
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
|
||||
export default async function exportBookmarks(): Promise<void>
|
||||
{
|
||||
const [collections] = await getCollections();
|
||||
const lines: string[] = [
|
||||
"<!DOCTYPE NETSCAPE-Bookmark-file-1>",
|
||||
"<!-- This is an automatically generated file.",
|
||||
" It will be read and overwritten.",
|
||||
" DO NOT EDIT! -->",
|
||||
"<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">",
|
||||
"<TITLE>Bookmarks</TITLE>",
|
||||
"<H1>Bookmarks</H1>",
|
||||
"<DL><p>"
|
||||
];
|
||||
|
||||
for (const collection of collections)
|
||||
lines.push(...createFolder(collection));
|
||||
|
||||
lines.push("</DL><p>");
|
||||
|
||||
const data: string = lines.join("\n");
|
||||
|
||||
const blob: Blob = new Blob([data], { type: "text/html" });
|
||||
|
||||
const element: HTMLAnchorElement = document.createElement("a");
|
||||
element.style.display = "none";
|
||||
element.href = URL.createObjectURL(blob);
|
||||
element.setAttribute("download", "collections.html");
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
|
||||
URL.revokeObjectURL(element.href);
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
function createFolder(item: CollectionItem | GroupItem): string[]
|
||||
{
|
||||
const lines: string[] = [];
|
||||
const title: string = item.type === "collection" ?
|
||||
(item.title ?? getCollectionTitle(item)) :
|
||||
(item.pinned ? i18n.t("groups.pinned") : (item.title ?? ""));
|
||||
|
||||
lines.push(`<DT><H3>${sanitizeString(title)}</H3>`);
|
||||
lines.push("<DL><p>");
|
||||
|
||||
for (const subItem of item.items)
|
||||
{
|
||||
if (subItem.type === "tab")
|
||||
lines.push(`<DT><A HREF="${encodeURI(subItem.url)}">${sanitizeString(subItem.title || subItem.url)}</A>`);
|
||||
else if (subItem.type === "group")
|
||||
lines.push(...createFolder(subItem));
|
||||
}
|
||||
|
||||
lines.push("</DL><p>");
|
||||
return lines;
|
||||
}
|
||||
|
||||
function sanitizeString(str: string): string
|
||||
{
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { getCollections, saveCollections } from "@/features/collectionStorage";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import parse from "node-bookmarks-parser";
|
||||
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
|
||||
import convertBookmarks from "./convertBookmarks";
|
||||
|
||||
export default async function importBookmarks(): Promise<number | null>
|
||||
{
|
||||
const element: HTMLInputElement = document.createElement("input");
|
||||
element.style.display = "none";
|
||||
element.hidden = true;
|
||||
element.type = "file";
|
||||
element.accept = ".html";
|
||||
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
|
||||
await new Promise(resolve =>
|
||||
{
|
||||
const listener = () =>
|
||||
{
|
||||
element.removeEventListener("input", listener);
|
||||
resolve(null);
|
||||
};
|
||||
element.addEventListener("input", listener);
|
||||
});
|
||||
|
||||
if (!element.files || element.files.length < 1)
|
||||
return null;
|
||||
|
||||
const file: File = element.files[0];
|
||||
const content: string = await file.text();
|
||||
|
||||
document.body.removeChild(element);
|
||||
|
||||
try
|
||||
{
|
||||
const bookmarks: Bookmark[] = parse(content);
|
||||
const [data, graphics, tabCount] = convertBookmarks(bookmarks);
|
||||
const [collections, cloudIssues] = await getCollections();
|
||||
|
||||
await saveCollections([...data, ...collections], cloudIssues === null, graphics);
|
||||
sendMessage("refreshCollections", undefined);
|
||||
|
||||
return tabCount;
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
console.error("Failed to parse bookmarks file", error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user