aboutsummaryrefslogtreecommitdiff
path: root/extension/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src/components')
-rw-r--r--extension/src/components/Body.tsx60
-rw-r--r--extension/src/components/Bookmark.tsx34
-rw-r--r--extension/src/components/FolderBody.tsx72
-rw-r--r--extension/src/components/FolderButton.tsx30
-rw-r--r--extension/src/components/RadioButtonGroup.tsx37
-rw-r--r--extension/src/components/SettingsEditor.tsx105
6 files changed, 338 insertions, 0 deletions
diff --git a/extension/src/components/Body.tsx b/extension/src/components/Body.tsx
new file mode 100644
index 0000000..193e68e
--- /dev/null
+++ b/extension/src/components/Body.tsx
@@ -0,0 +1,60 @@
+import React, {useEffect, useState} from "react";
+import SettingsEditor from "./SettingsEditor.tsx";
+import imageUrl from "../assets/settings.svg"
+import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+import FolderBody from "./FolderBody.tsx";
+import {defaultSettings, ISettings, loadSettings, writeSettings} from "../Settings.ts";
+import {getBrowser} from "../main.tsx";
+
+export const Settings =
+ React.createContext<[ISettings, (arg0: ISettings) => void]>([
+ defaultSettings,
+ () => {}
+]);
+
+/**
+ * A component for the full body of the application
+ * Also stores the trees and settings
+ */
+function Body() {
+ const [settingsOpen, setSettingsOpen] = useState(false);
+ const [settings, setSettings] = useState<ISettings>(defaultSettings);
+ const [bookmarkTree, setBookmarkTree] = useState<BookmarkTreeNode[]>([])
+ const [ogBookmarkTree, setOgBookmarkTree] = useState<BookmarkTreeNode[] | null>([])
+ useEffect(() => {
+ loadSettings().then(r => {
+ setSettings(r);
+ })
+ getBrowser().bookmarks.getTree().then(t => {
+ setOgBookmarkTree(t);
+ })
+ }, [])
+ useEffect(() => {
+ writeSettings(settings);
+ if (settings?.rootFolder) {
+ getBrowser().bookmarks.getSubTree(settings.rootFolder).then(t => {
+ setBookmarkTree(t);
+ });
+ } else {
+ getBrowser().bookmarks.getTree().then(t => {
+ setBookmarkTree(t);
+ })
+ }
+ }, [settings]);
+
+ return (
+ <Settings.Provider value={[settings!, setSettings]}>
+ {(() => {switch (settings.backgroundMode) {
+ case "color": return (<style>{"body {background-color: " + settings.backgroundColor + "; }"}</style>)
+ case "image": return (<style>{"body {background-image: url(\"" + settings.backgroundImage + "\"); }"}</style>)
+ }})()}
+ <button id="settings-button" onClick={_ => setSettingsOpen(!settingsOpen)}>
+ <img alt="open settings" src={imageUrl}/>
+ </button>
+ <SettingsEditor tree={ogBookmarkTree!} isOpen={[settingsOpen, setSettingsOpen]}/>
+ {bookmarkTree[0] && (<FolderBody data={bookmarkTree[0]}/>)}
+ </Settings.Provider>
+ )
+}
+
+export default Body \ No newline at end of file
diff --git a/extension/src/components/Bookmark.tsx b/extension/src/components/Bookmark.tsx
new file mode 100644
index 0000000..75badd7
--- /dev/null
+++ b/extension/src/components/Bookmark.tsx
@@ -0,0 +1,34 @@
+import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+
+/**
+ * A component for a single bookmark
+ *
+ * @param props.data The BookmarkTreeNode with the data for the bookmark
+ */
+function Bookmark(props: {data: BookmarkTreeNode}) {
+ return(
+ <a className="bookmark draggable" href={props.data.url}>
+ <img alt="Bookmark icon" src={faviconURL(props.data.url)}></img>
+ <span>{props.data.title}</span>
+ </a>
+ );
+}
+
+/**
+ * Gets the icon for a bookmark
+ *
+ * @param u The URL of the link
+ * @return The URL of the icon
+ */
+function faviconURL(u: string | undefined) {
+ if (!u) return "";
+ u = new URL(u).hostname.toString();
+ const url = new URL('https://www.google.com/s2/favicons');
+ url.searchParams.set("sz", "256");
+ // u = u.split(".")[u.split(".").length-2] +"."+ u.split(".")[u.split(".").length-1]
+ url.searchParams.set("domain_url", u);
+ return url.toString();
+
+}
+
+export default Bookmark; \ No newline at end of file
diff --git a/extension/src/components/FolderBody.tsx b/extension/src/components/FolderBody.tsx
new file mode 100644
index 0000000..a573a6e
--- /dev/null
+++ b/extension/src/components/FolderBody.tsx
@@ -0,0 +1,72 @@
+import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+import Bookmark from "./Bookmark.tsx";
+import FolderButton from "./FolderButton.tsx";
+import {useContext} from "react";
+import {Settings} from "./Body.tsx";
+
+/**
+ * A component that displays the contents of a bookmark folder
+ *
+ * @param props.data The BookmarkTreeNode with data for the folder
+ * @constructor
+ */
+function FolderBody (props: {data: BookmarkTreeNode}) {
+ const [settings, _] = useContext(Settings)
+
+ if (!props.data.children) return;
+
+ let content = [...props.data.children].sort(getSortFunction(settings.sort))
+ if (settings.foldersFirst) {
+ let [bookmarks, folders] = separateFolders(content)
+ content = folders.concat(bookmarks)
+ }
+
+ return (
+ <div className={"folderBody"}>
+ {content.map(child =>
+ child.children
+ ? <FolderButton data={child} />
+ : <Bookmark data={child} />
+ )}
+ </div>
+ )
+}
+
+/**
+ * Gets the correct sort function based on the sort setting
+ * @param sort The sort setting state
+ * @return The corresponding sort function
+ */
+function getSortFunction(sort: "from-bookmarks" | "alphabetical" | "recent"): ((a:BookmarkTreeNode, b:BookmarkTreeNode) => number) | undefined {
+ switch (sort) {
+ case "alphabetical": return (a, b) => {
+ return a.title.localeCompare(b.title);
+ }
+ case "recent": return (a, b) => {
+ // @ts-ignore
+ return a.dateLastUsed - b.dateLastUsed
+ }
+ }
+}
+
+/**
+ * Separate the folders and the bookmarks into two separate lists
+ * @param content THe bookmark list
+ * @returns tuple in the format [bookmarks, folders]
+ */
+function separateFolders(content: BookmarkTreeNode[]) {
+ let bookmarks = [];
+ let folders = [];
+ for (let bookmarkTreeNode of content) {
+ if (bookmarkTreeNode.children) {
+ folders.push(bookmarkTreeNode)
+ } else {
+ bookmarks.push(bookmarkTreeNode)
+ }
+ }
+ return [bookmarks, folders]
+}
+
+
+
+export default FolderBody; \ No newline at end of file
diff --git a/extension/src/components/FolderButton.tsx b/extension/src/components/FolderButton.tsx
new file mode 100644
index 0000000..2d7cc9e
--- /dev/null
+++ b/extension/src/components/FolderButton.tsx
@@ -0,0 +1,30 @@
+import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+import FolderBody from "./FolderBody.tsx";
+import folderIcon from "../assets/folder.svg"
+import folderIconOpen from "../assets/folder_open.svg"
+import {useState} from "react";
+
+/**
+ * A component for the button used to open a bookmark folder.
+ * This is themed the same as a bookmark
+ *
+ * @param props.data The BookmarkTreeNode containing the data of the folder
+ */
+function FolderButton(props: {data: BookmarkTreeNode}) {
+ const [folderOpen, setFolderOpen] = useState(false);
+
+ return(
+ <>
+ <a className={"bookmark draggable"} onClick={() => setFolderOpen(!folderOpen)}>
+ <img alt="Folder icon" src={folderOpen ? folderIconOpen : folderIcon}/>
+ <span>{props.data.title}</span>
+ </a>
+ { folderOpen
+ && props.data.children
+ && props.data.children.length > 0
+ && (<FolderBody data={props.data}/>)}
+ </>
+);
+}
+
+export default FolderButton \ No newline at end of file
diff --git a/extension/src/components/RadioButtonGroup.tsx b/extension/src/components/RadioButtonGroup.tsx
new file mode 100644
index 0000000..297800d
--- /dev/null
+++ b/extension/src/components/RadioButtonGroup.tsx
@@ -0,0 +1,37 @@
+import React, {ReactElement, useEffect, useId, useState} from "react";
+
+/**
+ * A component for a group of radio buttons where only one can be selected
+ *
+ * @param props.children html <option> elements for each radio option
+ * @param props.value The option which is selected
+ * @param props.onChange A function that will be called when the selected option changes
+ */
+function RadioButtonGroup(props: { children: ReactElement<HTMLOptionElement>[], value: any, onChange?: (arg0: any) => void }) {
+ const [selected, setSelected] = useState(props.value);
+ useEffect(() => {
+ setSelected(props.value);
+ }, [props.value]);
+ useEffect(() => {
+ props.onChange && props.onChange(selected);
+ }, [selected])
+
+ return (
+ <div className="radio-group">
+ { props.children.map((item) => (
+ <label>
+ <input
+ type="radio"
+ name={useId()}
+ value={item.props.value}
+ checked={item.props.value === selected}
+ onChange={e => setSelected(e.target.value)}
+ />
+ {item.props.children.toString()}
+ </label>
+ )) }
+ </div>
+ )
+}
+
+export default RadioButtonGroup \ No newline at end of file
diff --git a/extension/src/components/SettingsEditor.tsx b/extension/src/components/SettingsEditor.tsx
new file mode 100644
index 0000000..abf9888
--- /dev/null
+++ b/extension/src/components/SettingsEditor.tsx
@@ -0,0 +1,105 @@
+import RadioButtonGroup from "./RadioButtonGroup.tsx";
+import React, {useContext} from "react";
+import imageUrl from "../assets/close.svg"
+import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+import {Settings} from "./Body.tsx";
+
+/**
+ * A component for the settings sidebar
+ *
+ * @param props.tree The full bookmarks tree (for use in the root selector)
+ * @param props.isOpen State for weather the menu is open
+ */
+function SettingsEditor(props: {tree: BookmarkTreeNode[], isOpen: [boolean, React.Dispatch<React.SetStateAction<boolean>>]}) {
+ const [settings, setSettings] = useContext(Settings)
+ const [open, setOpen] = props.isOpen;
+
+ return (
+ <div id="settings-menu" className={open ? "open" : "closed"}>
+ <button id="settings-close" onClick={_ => setOpen(false)}>
+ <img alt="close settings" src={imageUrl}/>
+ </button>
+ <h1>Settings</h1>
+
+ <h3>Sort</h3>
+ <RadioButtonGroup value={settings.sort}
+ onChange={e => {
+ setSettings({...settings, sort: e})
+ }}>
+ <option value={"from-bookmarks"}>From Bookmarks</option>
+ <option value={"alphabetical"}>Alphabetical</option>
+ <option value={"frequency"}>Frequency</option>
+ <option value={"recent"}>Recently used</option>
+ </RadioButtonGroup>
+ <br/>
+ <label>
+ <input type={"checkbox"}
+ checked={settings.foldersFirst}
+ onChange={e => setSettings({...settings, foldersFirst: e.target.checked})}/>
+ Sort Folders First
+ </label>
+
+ <h3>Background Type</h3>
+ <RadioButtonGroup value={settings.backgroundMode}
+ onChange={e => setSettings({...settings, backgroundMode: e})}>
+ <option value={"theme"}>Default</option>
+ <option value={"color"}>Solid Color</option>
+ <option value={"image"}>Image</option>
+ </RadioButtonGroup>
+
+ {(() => {
+ switch (settings.backgroundMode) {
+ case "image":
+ return (<>
+ <h3>Background Image URL</h3>
+ <input type={"url"}
+ value={settings.backgroundImage}
+ onChange={e => setSettings({...settings, backgroundImage: e.target.value})}/>
+ </>)
+ case "color":
+ return (<>
+ <h3>Background Color</h3>
+ <input type={"color"}
+ value={settings.backgroundColor}
+ onChange={e => setSettings({...settings, backgroundColor: e.target.value})}/>
+ </>)
+ }
+ })()}
+
+ <h3>Root folder</h3>
+ <select value={settings.rootFolder!}
+ onChange={e => setSettings({...settings, rootFolder: e.target.value})}>
+ {getFoldersFromTree(props.tree).map(i =>
+ <option value={i.id}>{i.title ? i.title : "Untitled (id:" + i.id + ")"}</option>
+ )}
+ </select>
+
+ {/*<br/>*/}
+ {/*<span>sort: {settings.sort}</span>*/}
+ {/*<span>rootFolder: {settings.rootFolder}</span>*/}
+ {/*<span>bgmode: {settings.backgroundMode}</span>*/}
+ </div>
+)
+}
+
+/**
+ * Walks the tree and creates a list of the folders
+ *
+ * @param tree The full tree to walk through
+ */
+function getFoldersFromTree(tree: BookmarkTreeNode[]) {
+ let folderList: BookmarkTreeNode[] = [];
+ rec(tree);
+
+ function rec(tree: BookmarkTreeNode[]) {
+ tree.forEach(item => {
+ if (item.children) {
+ folderList.push(item);
+ rec(item.children);
+ }
+ })
+ }
+ return folderList;
+}
+
+export default SettingsEditor; \ No newline at end of file