diff options
Diffstat (limited to 'extension/src/components')
-rw-r--r-- | extension/src/components/Body.tsx | 60 | ||||
-rw-r--r-- | extension/src/components/Bookmark.tsx | 34 | ||||
-rw-r--r-- | extension/src/components/FolderBody.tsx | 72 | ||||
-rw-r--r-- | extension/src/components/FolderButton.tsx | 30 | ||||
-rw-r--r-- | extension/src/components/RadioButtonGroup.tsx | 37 | ||||
-rw-r--r-- | extension/src/components/SettingsEditor.tsx | 105 |
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 |