1
0
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:
2025-12-19 23:10:28 +03:00
parent fdac0c0766
commit 9fbc152a91
16 changed files with 613 additions and 50 deletions
@@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
@@ -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;
}
}