Skip to content
Open
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
4 changes: 4 additions & 0 deletions entrypoints/options/hooks/useOptionsStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@ export const useOptionsStyles = makeStyles({
flexFlow: "column",
alignItems: "flex-start",
gap: tokens.spacingVerticalSNudge
},
messageBar:
{
flexShrink: 0
}
});
88 changes: 49 additions & 39 deletions entrypoints/options/layouts/StorageSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useDialog } from "@/contexts/DialogProvider";
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useStorageInfo from "@/hooks/useStorageInfo";
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components";
import { Button, Divider, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Subtitle2, Switch } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
import { Unwatch } from "wxt/utils/storage";
import { useOptionsStyles } from "../hooks/useOptionsStyles";
import exportData from "../utils/exportData";
import importData from "../utils/importData";
import BookmarksSection from "@/features/netscapeBookmarks/layouts/BookmarksSection";

export default function StorageSection(): React.ReactElement
{
Expand Down Expand Up @@ -78,28 +79,7 @@ export default function StorageSection(): React.ReactElement

return (
<>
<div className={ cls.group }>
<Switch
checked={ isThumbnailCaptureEnabled ?? true }
disabled={ isThumbnailCaptureEnabled === null }
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
label={ {
children: (_: any, props: LabelProps) =>
<InfoLabel
{ ...props }
label={ i18n.t("options_page.storage.thumbnail_capture") }
info={
<p>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</p>
} />
} } />

<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
{ i18n.t("options_page.storage.clear_thumbnails.action") }
</Button>
</div>
<Subtitle2>{ i18n.t("options_page.storage.manage_title") }</Subtitle2>

{ isCloudDisabled === false &&
<Field
Expand All @@ -111,11 +91,24 @@ export default function StorageSection(): React.ReactElement
</Field>
}

{ isCloudDisabled === true &&
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
{ i18n.t("options_page.storage.enable") }
</Button>
}
<div className={ cls.horizontalButtons }>
{ isCloudDisabled === true &&
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
{ i18n.t("options_page.storage.enable") }
</Button>
}

{ isCloudDisabled === false &&
<div className={ cls.horizontalButtons }>
<Button
appearance="subtle" className={ dangerCls.buttonSubtle }
onClick={ handleDisableCloud }
>
{ i18n.t("options_page.storage.disable") }
</Button>
</div>
}
</div>

<div className={ cls.horizontalButtons }>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
Expand All @@ -127,7 +120,7 @@ export default function StorageSection(): React.ReactElement
</div>

{ importResult !== null &&
<MessageBar intent={ importResult ? "success" : "error" }>
<MessageBar intent={ importResult ? "success" : "error" } className={ cls.messageBar }>
<MessageBarBody>
{ importResult === true ?
i18n.t("options_page.storage.import_results.success") :
Expand All @@ -137,16 +130,33 @@ export default function StorageSection(): React.ReactElement
</MessageBar>
}

{ isCloudDisabled === false &&
<div className={ cls.horizontalButtons }>
<Button
appearance="subtle" className={ dangerCls.buttonSubtle }
onClick={ handleDisableCloud }
>
{ i18n.t("options_page.storage.disable") }
</Button>
</div>
}
<Divider />
<Subtitle2>{ i18n.t("options_page.storage.thumbnails_title") }</Subtitle2>
<div className={ cls.group }>
<Switch
checked={ isThumbnailCaptureEnabled ?? true }
disabled={ isThumbnailCaptureEnabled === null }
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
label={ {
children: (_: any, props: LabelProps) =>
<InfoLabel
{ ...props }
label={ i18n.t("options_page.storage.thumbnail_capture") }
info={
<p>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</p>
} />
} } />

<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
{ i18n.t("options_page.storage.clear_thumbnails.action") }
</Button>
</div>

<Divider />
<BookmarksSection />
</>
);
}
66 changes: 66 additions & 0 deletions features/netscapeBookmarks/layouts/BookmarksSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useDialog } from "@/contexts/DialogProvider";
import { Body1, Button, makeStyles, MessageBar, MessageBarBody, Subtitle2, tokens } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular } 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={ <ArrowDownload20Regular /> } onClick={ exportBookmarks }>
{ i18n.t("features.netscape_bookmarks.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } 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
}
});
104 changes: 104 additions & 0 deletions features/netscapeBookmarks/utils/convertBookmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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")
{
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);
}
}

if (untitled.items.length > 0)
items.unshift(untitled);

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"
};

if (bookmark.children)
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()
};

if (bookmark.children)
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)];
}
69 changes: 69 additions & 0 deletions features/netscapeBookmarks/utils/exportBookmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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).replace(/"/g, "%22")}">${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;")
.replace(/"/g, "&quot;");
}
Loading
Loading