aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--extension/src/Settings.ts2
-rw-r--r--extension/src/assets/delete.svg1
-rw-r--r--extension/src/assets/drag.svg1
-rw-r--r--extension/src/assets/edit_off.svg1
-rw-r--r--extension/src/assets/more.svg1
-rw-r--r--extension/src/components/BMEditor.tsx55
-rw-r--r--extension/src/components/Body.tsx42
-rw-r--r--extension/src/components/Bookmark.tsx266
-rw-r--r--extension/src/components/ContextMenu.tsx51
-rw-r--r--extension/src/components/DropTargets.tsx52
-rw-r--r--extension/src/components/FolderBody.tsx46
-rw-r--r--extension/src/components/FolderButton.tsx105
-rw-r--r--extension/src/components/SettingsEditor.tsx36
-rw-r--r--extension/src/index.css113
14 files changed, 557 insertions, 215 deletions
diff --git a/extension/src/Settings.ts b/extension/src/Settings.ts
index 853058f..a3271d6 100644
--- a/extension/src/Settings.ts
+++ b/extension/src/Settings.ts
@@ -9,6 +9,7 @@ export interface ISettings {
foregroundColor: string
editMode: boolean
rootFolder: string | null
+ keepFoldersOpen: boolean
}
export let defaultSettings: ISettings = {
@@ -20,6 +21,7 @@ export let defaultSettings: ISettings = {
foregroundColor: "#FFFFFF",
editMode: false,
rootFolder: '0',
+ keepFoldersOpen: false
}
export function loadSettings(): Promise<ISettings> {
diff --git a/extension/src/assets/delete.svg b/extension/src/assets/delete.svg
new file mode 100644
index 0000000..dee0e4b
--- /dev/null
+++ b/extension/src/assets/delete.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm80-160h80v-360h-80v360Zm160 0h80v-360h-80v360Z"/></svg> \ No newline at end of file
diff --git a/extension/src/assets/drag.svg b/extension/src/assets/drag.svg
new file mode 100644
index 0000000..69c53ec
--- /dev/null
+++ b/extension/src/assets/drag.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M360-160q-33 0-56.5-23.5T280-240q0-33 23.5-56.5T360-320q33 0 56.5 23.5T440-240q0 33-23.5 56.5T360-160Zm240 0q-33 0-56.5-23.5T520-240q0-33 23.5-56.5T600-320q33 0 56.5 23.5T680-240q0 33-23.5 56.5T600-160ZM360-400q-33 0-56.5-23.5T280-480q0-33 23.5-56.5T360-560q33 0 56.5 23.5T440-480q0 33-23.5 56.5T360-400Zm240 0q-33 0-56.5-23.5T520-480q0-33 23.5-56.5T600-560q33 0 56.5 23.5T680-480q0 33-23.5 56.5T600-400ZM360-640q-33 0-56.5-23.5T280-720q0-33 23.5-56.5T360-800q33 0 56.5 23.5T440-720q0 33-23.5 56.5T360-640Zm240 0q-33 0-56.5-23.5T520-720q0-33 23.5-56.5T600-800q33 0 56.5 23.5T680-720q0 33-23.5 56.5T600-640Z"/></svg> \ No newline at end of file
diff --git a/extension/src/assets/edit_off.svg b/extension/src/assets/edit_off.svg
new file mode 100644
index 0000000..7a347a4
--- /dev/null
+++ b/extension/src/assets/edit_off.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor"><path d="M622-453 453-622l195-195q12-12 26.5-17.5T705-840q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L622-453ZM792-56 509-338 290-120H120v-169l219-219L56-792l57-57 736 736-57 57Zm-88-592 56-56-56-56-56 56 56 56Z"/></svg> \ No newline at end of file
diff --git a/extension/src/assets/more.svg b/extension/src/assets/more.svg
new file mode 100644
index 0000000..ac7a32e
--- /dev/null
+++ b/extension/src/assets/more.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M480-160q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm0-240q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0-240q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Z"/></svg> \ No newline at end of file
diff --git a/extension/src/components/BMEditor.tsx b/extension/src/components/BMEditor.tsx
new file mode 100644
index 0000000..1ac72c6
--- /dev/null
+++ b/extension/src/components/BMEditor.tsx
@@ -0,0 +1,55 @@
+import React, {useContext, useEffect, useState} from "react";
+import CloseIcon from "../assets/close.svg?react"
+import {ActiveEdit} from "./Body.tsx";
+import {getBrowser} from "../main.tsx";
+import RadioButtonGroup from "./RadioButtonGroup.tsx";
+
+
+function BMEditor() {
+ const [activeEdit, setActiveEdit] = useContext(ActiveEdit);
+
+ const [iconOptions, setIconOptions] = useState<string[]>([]);
+
+ useEffect(() => {
+ if (!activeEdit) return;
+ getBrowser().storage.local.get("icon-aval-"+activeEdit.id).then( r => {
+ setIconOptions(r["icon-aval-"+activeEdit.id]);
+ });
+ }, [activeEdit]);
+
+ if (!activeEdit) return;
+
+ let isFolder = activeEdit && activeEdit.children && activeEdit.children.length > 0;
+ return (
+ <div id="settings-menu" className={activeEdit ? "open" : "closed"}>
+ <button id="settings-close" onClick={_ => setActiveEdit(null)}>
+ <CloseIcon/>
+ </button>
+
+ <h1>Edit {isFolder ? "Folder" : "Bookmark"}</h1>
+
+ <h3>Name</h3>
+ <input type={"text"} defaultValue={activeEdit?.title} onChange={e => {
+ getBrowser().bookmarks.update(activeEdit!.id, {title: e.target.value})
+ }}/>
+
+ {!isFolder && (<>
+ <h3>URL</h3>
+ <input type={"url"} defaultValue={activeEdit?.url} onChange={e => {
+ getBrowser().bookmarks.update(activeEdit!.id, {url: e.target.value})
+ }}/>
+ </>)}
+
+ <h3>Icon</h3>
+ {/*<RadioButtonGroup value={undefined}>*/}
+ {iconOptions && iconOptions.map(s =>
+ // <option value={s}>
+ <img src={s}/>
+ // </option>
+ )}
+ {/*</RadioButtonGroup>*/}
+ </div>
+ );
+}
+
+export default BMEditor; \ No newline at end of file
diff --git a/extension/src/components/Body.tsx b/extension/src/components/Body.tsx
index 70f8fce..e4c371b 100644
--- a/extension/src/components/Body.tsx
+++ b/extension/src/components/Body.tsx
@@ -1,11 +1,13 @@
import React, {useEffect, useState} from "react";
import SettingsEditor from "./SettingsEditor.tsx";
import SettingsIcon from "../assets/settings.svg?react";
-import EditIcon from "../assets/move.svg?react";
+import EditIcon from "../assets/edit.svg?react";
import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
import FolderBody from "./FolderBody.tsx";
import {defaultSettings, ISettings, loadSettings, writeSettings} from "../Settings.ts";
import {getBrowser} from "../main.tsx";
+import BMEditor from "./BMEditor.tsx";
+import EditOffIcon from "../assets/edit_off.svg?react";
export const Settings =
React.createContext<[ISettings, (arg0: ISettings) => void]>([
@@ -19,58 +21,58 @@ export const ActiveDrag =
() => {}
])
+export const ActiveEdit =
+ React.createContext<[BookmarkTreeNode | null, (arg0: BookmarkTreeNode | null) => void]>([
+ null,
+ () => {}
+ ])
+
/**
* 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 [selectedBookmarkTree, setSelectedBookmarkTree] = useState<BookmarkTreeNode[]>([])
- const [fullBookmarkTree, setFullBookmarkTree] = useState<BookmarkTreeNode[] | null>([])
+ const [settings, setSettings] = useState<ISettings | undefined>(undefined);
const [activeDrag, setActiveDrag] = useState<BookmarkTreeNode | null>(null);
+ const [activeEdit, setActiveEdit] = useState<BookmarkTreeNode | null>(null);
useEffect(() => {
loadSettings().then(r => {
setSettings(r);
})
- getBrowser().bookmarks.getTree().then(t => {
- setFullBookmarkTree(t);
- })
}, [])
useEffect(() => {
- writeSettings(settings);
- if (settings.rootFolder) {
- getBrowser().bookmarks.getSubTree(settings.rootFolder).then(t => {
- setSelectedBookmarkTree(t);
- });
- } else {
- getBrowser().bookmarks.getTree().then(t => {
- setSelectedBookmarkTree(t);
- })
+ if (settings) {
+ writeSettings(settings);
}
}, [settings]);
+ if (!settings) return;
+
return (
<Settings.Provider value={[settings!, setSettings]}>
<ActiveDrag.Provider value={[activeDrag, setActiveDrag]}>
+ <ActiveEdit.Provider value={[activeEdit, setActiveEdit]}>
{(() => {switch (settings.backgroundMode) {
case "color": return (<style>{"body {background-color: " + settings.backgroundColor + "; }"}</style>)
case "image": return (<style>{"body {background-image: url(\"" + settings.backgroundImage + "\"); }"}</style>)
}})()}
<style>{"body > .folderBody, a {color: " + settings.foregroundColor + "; }"}</style>
<div id={"action-area"}>
- {settings.editMode && <span>Move mode: Drag bookmarks to change order</span>}
+ {settings.editMode && <span>Edit mode: Drag bookmarks to change order</span>}
<button onClick={_ => setSettings({...settings, editMode: !settings.editMode})}>
- <EditIcon className={settings.editMode? "enabled" : ""}/>
+ {settings.editMode ? <EditIcon/> : <EditOffIcon/>}
</button>
<button onClick={_ => setSettingsOpen(!settingsOpen)}>
<SettingsIcon/>
</button>
</div>
- <SettingsEditor tree={fullBookmarkTree!} isOpen={[settingsOpen, setSettingsOpen]}/>
- {selectedBookmarkTree[0] && (<FolderBody data={selectedBookmarkTree[0]}/>)}
+ <SettingsEditor isOpen={[settingsOpen, setSettingsOpen]}/>
+ <BMEditor/>
+ <FolderBody id={settings.rootFolder || '0'}/>
+ </ActiveEdit.Provider>
</ActiveDrag.Provider>
</Settings.Provider>
)
diff --git a/extension/src/components/Bookmark.tsx b/extension/src/components/Bookmark.tsx
index 5a406eb..e378b0f 100644
--- a/extension/src/components/Bookmark.tsx
+++ b/extension/src/components/Bookmark.tsx
@@ -1,42 +1,63 @@
import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
-import React, {SyntheticEvent, useEffect} from "react";
+import React, {SyntheticEvent, useContext, useEffect, useState} from "react";
import {getBrowser} from "../main.tsx";
-import {ActiveDrag, Settings} from "./Body.tsx";
+import {ActiveDrag, ActiveEdit, Settings} from "./Body.tsx";
import ColorThief from 'colorthief'
-import CreateFolderIcon from "../assets/create_folder.svg?react"
-import react from "@vitejs/plugin-react";
+import DropTargets from "./DropTargets.tsx";
+import ContextMenu from "./ContextMenu.tsx";
/**
* A component for a single bookmark
- *
- * @param props.data The BookmarkTreeNode with the data for the bookmark
*/
-function Bookmark(props: {data: BookmarkTreeNode}) {
+function Bookmark(props: {id: string}) {
+ let [settings] = React.useContext(Settings);
+ let [activeDrag, setActiveDrag] = React.useContext(ActiveDrag);
+ const [, setActiveEdit] = useContext(ActiveEdit)
+
let [favicon, setFavicon] = React.useState<string | null>(null);
let [iconMode, setIconMode] = React.useState<"large" | "small" | "letter">("letter");
- let [settings, setSettings] = React.useContext(Settings);
let [bgColor, setBgColor] = React.useState<[number, number, number] | null>(null)
let [bgColorPriority, setBgColorPriority] = React.useState(0);
- let [activeDrag, setActiveDrag] = React.useContext(ActiveDrag);
- let [dropRight, setDropRight] = React.useState(false);
- let [dropLeft, setDropLeft] = React.useState(false);
- let [dropCenter, setDropCenter] = React.useState(false);
- let [thisDragged, setThisDragged] = React.useState(false);
+ const [bmData, setBmData] = useState<BookmarkTreeNode | undefined>()
+ const [renameMode, setRenameMode] = useState(false);
+
+ useEffect(() => {
+ getBrowser().bookmarks.get(props.id).then(r => {
+ setBmData(r[0]);
+ })
+
+ getBrowser().bookmarks.onChanged.addListener((id: string) => {
+ if (id !== props.id) return;
+ getBrowser().bookmarks.get(props.id).then(r => {
+ setBmData(r[0]);
+ })
+ })
+ }, []);
useEffect(() => {
- faviconURL(props.data).then(r => {
+ if (!bmData) return;
+ faviconURL(bmData).then(r => {
if (r) {
setFavicon(r)
setIconMode("small");
}
})
- }, []);
+ }, [bmData]);
useEffect(() => {
- setDropLeft(false);
- setDropRight(false);
- setDropCenter(false);
- }, [activeDrag]);
+ let evl = () => {
+ console.log("clicked")
+ renameMode && setRenameMode(false);
+ console.log("evl unregistered")
+ document.body.removeEventListener('click', evl);
+ }
+ if (renameMode) {
+ console.log("evl registered")
+ document.body.addEventListener('click', evl);
+ }
+ }, [renameMode]);
+
+ if (!bmData) return;
function handleImageLoad(e: SyntheticEvent<HTMLImageElement, Event>) {
if (e.currentTarget.naturalWidth >= 75 || favicon!.startsWith("data:image/svg+xml")) {
@@ -47,133 +68,94 @@ function Bookmark(props: {data: BookmarkTreeNode}) {
}
}
- function handleDragStart(e: React.DragEvent<HTMLAnchorElement>) {
- // e.dataTransfer.setData("text/bm-id", props.data.id);
- // setActiveDrag(true);
- console.log("data", e.dataTransfer.getData("text/bm-id").toString())
- }
+ // Dragging
+ const handleDrag = () => {
+ setActiveDrag(bmData);
+ };
- function handleDrag(e: React.DragEvent<HTMLAnchorElement>) {
- // e.dataTransfer.setData("text/bm-id", props.data.id);
- setActiveDrag(props.data);
- setThisDragged(true);
- // e.dataTransfer.dropEffect = "move";
- }
-
- function handleDragEnd() {
+ const handleDragEnd = () => {
setActiveDrag(null);
- setThisDragged(false);
- }
+ };
+
+ // Dropping
+ const handleDropLeft = () => {
+ console.log("drop left bm")
+ getBrowser().bookmarks.move(activeDrag!.id, {
+ parentId: bmData.parentId,
+ index: bmData.index
+ })
+ location.reload()
+ };
+
+ const handleDropRight = () => {
+ console.log("drop right bm")
+ getBrowser().bookmarks.move(activeDrag!.id, {
+ parentId: bmData.parentId,
+ index: (bmData.index! + 1)
+ })
+ location.reload();
+ };
+
+ const handleDropCenter = () => {
+ console.log("drop center bm")
+ chrome.bookmarks.create({
+ // type: "folder",
+ parentId: bmData.parentId,
+ index: bmData.index,
+ title: "New Folder"
+ }).then(r => {
+ getBrowser().bookmarks.move(bmData.id, {parentId: r.id});
+ getBrowser().bookmarks.move(activeDrag!.id, {parentId: r.id});
+ location.reload()
+ })
+ };
+
+ const handleDelete = () => {
+ getBrowser().bookmarks.remove(bmData.id);
+ location.reload();
+ };
+
+ const handleEdit = (e: React.MouseEvent<HTMLButtonElement>) => {
+ e.preventDefault()
+ setRenameMode(true)
+ // setActiveEdit(bmData);
+ };
return(
<div className={"bookmark"}>
- <a draggable={settings.editMode} href={props.data.url} onDragStart={handleDragStart} onDrag={handleDrag} onDragEnd={handleDragEnd}>
- <div className={"icon-box " + (iconMode)} style={bgColor ? {"--icon-bg": `rgba(${bgColor[0]}, ${bgColor[1]}, ${bgColor[2]}, 0.2)`} as React.CSSProperties : undefined}>
- {(() => { switch (iconMode) {
- case "letter": {
- let url = new URL(props.data.url!);
- if (bgColorPriority < 1) {
- setBgColor(hashStringToColor(url.hostname));
- setBgColorPriority(1);
+ <a href={bmData.url} draggable={settings.editMode} onDrag={handleDrag} onDragEnd={handleDragEnd}>
+ <div className={"icon-box " + (iconMode)}
+ style={bgColor ? {"--icon-bg": `rgba(${bgColor[0]}, ${bgColor[1]}, ${bgColor[2]}, 0.2)`} as React.CSSProperties : undefined}>
+ {(() => {
+ switch (iconMode) {
+ case "letter": {
+ let url = new URL(bmData.url!);
+ if (bgColorPriority < 1) {
+ setBgColor(hashStringToColor(url.hostname));
+ setBgColorPriority(1);
+ }
+ return (<span className={"letter"}>{url.hostname.charAt(0)}</span>)
+ }
+ case "small": {
+ return (<img alt="Bookmark icon" src={favicon!} onLoad={handleImageLoad}/>)
+ }
+ case "large": {
+ return (<img alt="Bookmark icon" src={favicon!}/>)
}
- return (<span className={"letter"}>{url.hostname.charAt(0)}</span>)
- }
- case "small": {
- return (<img alt="Bookmark icon" src={favicon!} onLoad={handleImageLoad}/>)
- }
- case "large": {
- return(<img alt="Bookmark icon" src={favicon!}/>)
}
- }})()}
+ })()}
</div>
- <span>{props.data.title}</span>
+ {renameMode
+ ? <input type={'text'}
+ defaultValue={bmData.title}
+ onChange={e => {
+ getBrowser().bookmarks.update(props.id, {title: e.target.value})
+ }}/>
+ : <span>{bmData.title}</span>}
</a>
- {activeDrag && !thisDragged && (
- <div className={"drop-targets"}>
- <div
- className={"left"}
- onDragOver={e => {
- e.preventDefault()
- setDropLeft(true)
- }}
- onDragEnter={e =>{
- e.preventDefault()
- }}
- onDragLeave={_ => {
- setDropLeft(false)
- }}
- onDrop={e => {
- getBrowser().bookmarks.move(activeDrag.id, {
- parentId: props.data.parentId,
- index: props.data.index
- })
- location.reload()
- }}
- style={dropLeft ? undefined : {opacity: 0}}
- // hidden={!dropLeft}
- >
- <div/>
- </div>
- <div
- className={"right"}
- onDragOver={e => {
- e.preventDefault();
- setDropRight(true);
- }}
- onDragEnter={e => {
- e.preventDefault();
- }}
- onDragLeave={_ => {
- setDropRight(false)
- }}
- onDrop={e => {
- getBrowser().bookmarks.move(activeDrag.id, {
- parentId: props.data.parentId,
- index: (props.data.index! + 1)
- })
- location.reload()
- e.preventDefault()
- }}
- style={dropRight ? undefined : {opacity: 0}}
- // hidden={!dropRight}
- >
- <div/>
- </div>
- <div
- className={"center"}
- onDragOver={e => {
- e.preventDefault()
- setDropCenter(true)
- // console.log("dropped")
- }}
- onDragEnter={e => {
- e.preventDefault()
- // console.log("enter")
- }}
- onDragLeave={_ => {
- setDropCenter(false)
- // console.log("exit")
- }}
- onDrop={e => {
- e.preventDefault();
- chrome.bookmarks.create({
- // type: "folder",
- parentId: props.data.parentId,
- index: props.data.index,
- title: "New Folder"
- }).then(r => {
- getBrowser().bookmarks.move(props.data.id, {parentId: r.id});
- getBrowser().bookmarks.move(activeDrag?.id, {parentId: r.id});
- location.reload()
- })
- }}
- style={dropCenter ? undefined : {opacity: 0}}
- // hidden={!dropCenter}
- >
- <CreateFolderIcon/>
- </div>
- </div>
- )}
+ {settings.editMode && <ContextMenu onEdit={handleEdit} onDelete={handleDelete}/>}
+ {activeDrag && activeDrag !== bmData &&
+ <DropTargets onDropLeft={handleDropLeft} onDropRight={handleDropRight} onDropCenter={handleDropCenter}/>}
</div>
);
}
@@ -211,7 +193,7 @@ function toDataURL(url:string):string {
}
function djb2(str: string){
- var hash = 5381;
+ let hash = 5381;
for (var i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
}
@@ -219,10 +201,10 @@ function djb2(str: string){
}
function hashStringToColor(str: string): [number, number, number] {
- var hash = djb2(str);
- var r = (hash & 0xFF0000) >> 16;
- var g = (hash & 0x00FF00) >> 8;
- var b = hash & 0x0000FF;
+ let hash = djb2(str);
+ let r = (hash & 0xFF0000) >> 16;
+ let g = (hash & 0x00FF00) >> 8;
+ let b = hash & 0x0000FF;
return [r, g, b];
}
diff --git a/extension/src/components/ContextMenu.tsx b/extension/src/components/ContextMenu.tsx
new file mode 100644
index 0000000..058e9ba
--- /dev/null
+++ b/extension/src/components/ContextMenu.tsx
@@ -0,0 +1,51 @@
+import DeleteIcon from "../assets/delete.svg?react";
+import EditIcon from "../assets/edit.svg?react";
+import DragIcon from "../assets/drag.svg?react";
+import MoreIcon from "../assets/more.svg?react";
+import React, {useEffect, useState} from "react";
+
+function ContextMenu(props: {onEdit: (e: React.MouseEvent<HTMLButtonElement>) => void, onDelete: (e: React.MouseEvent<HTMLButtonElement>) => void}) {
+ const [open, setOpen] = useState(false)
+
+ useEffect(() => {
+ let evl = () => {
+ console.log("clicked")
+ open && setOpen(false);
+ console.log("evl unregistered")
+ document.body.removeEventListener('click', evl);
+ }
+ if (open) {
+ console.log("evl registered")
+ document.body.addEventListener('click', evl);
+ }
+ }, [open]);
+
+ return (
+ <div className={"overflow"}>
+ <button onClick={e => {
+ e.preventDefault();
+ setOpen(!open);
+ }}>
+ <MoreIcon/>
+ </button>
+ {open && <div className={"context-menu"}>
+ <button onClick={e => {
+ setOpen(false);
+ props.onEdit(e);
+ }}>
+ <EditIcon />
+ Edit
+ </button>
+ <button className={"del"} onClick={e => {
+ setOpen(false);
+ props.onDelete(e);
+ }}>
+ <DeleteIcon/>
+ Delete
+ </button>
+ </div>}
+ </div>
+ )
+}
+
+export default ContextMenu; \ No newline at end of file
diff --git a/extension/src/components/DropTargets.tsx b/extension/src/components/DropTargets.tsx
new file mode 100644
index 0000000..297f366
--- /dev/null
+++ b/extension/src/components/DropTargets.tsx
@@ -0,0 +1,52 @@
+import React, {useEffect} from "react";
+import {ActiveDrag} from "./Body.tsx";
+import CreateFolderIcon from "../assets/create_folder.svg?react"
+
+function DropTarget(props: {children: React.ReactNode, className: string, onDrop: () => void}) {
+ let [drop, setDrop] = React.useState(false);
+ let [activeDrag, _] = React.useContext(ActiveDrag);
+
+ useEffect(() => {
+ setDrop(false);
+ }, [activeDrag]);
+
+ function handleDragOver(e: React.DragEvent<HTMLDivElement>) {
+ e.preventDefault()
+ setDrop(true)
+ }
+
+ function handleDragLeave(e: React.DragEvent<HTMLDivElement>) {
+ setDrop(false)
+ }
+
+ function handleDrop(e: React.DragEvent<HTMLDivElement>) {
+ e.preventDefault();
+ props.onDrop();
+ }
+
+ return (
+ <div className={props.className} style={drop ? undefined : {opacity: 0}}
+ onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}>
+ {props.children}
+ </div>
+ );
+}
+
+function DropTargets(props: { onDropLeft: () => void, onDropRight: () => void, onDropCenter: () => void }) {
+
+ return (
+ <div className={"drop-targets"}>
+ <DropTarget className={"left"} onDrop={props.onDropLeft}>
+ <div/>
+ </DropTarget>
+ <DropTarget className={"right"} onDrop={props.onDropRight}>
+ <div/>
+ </DropTarget>
+ <DropTarget className={"center"} onDrop={props.onDropCenter}>
+ <CreateFolderIcon/>
+ </DropTarget>
+ </div>
+ );
+}
+
+export default DropTargets; \ No newline at end of file
diff --git a/extension/src/components/FolderBody.tsx b/extension/src/components/FolderBody.tsx
index a573a6e..e75fcc7 100644
--- a/extension/src/components/FolderBody.tsx
+++ b/extension/src/components/FolderBody.tsx
@@ -1,32 +1,48 @@
import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
+import {ReactElement, useContext, useEffect, useState} from "react";
+import {Settings} from "./Body.tsx";
+import {getBrowser} from "../main.tsx";
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)
+function FolderBody (props: {id: string}) {
+
+ const [settings, ] = useContext(Settings)
- if (!props.data.children) return;
+ const [children, setChildren] = useState<BookmarkTreeNode[]>([])
- let content = [...props.data.children].sort(getSortFunction(settings.sort))
- if (settings.foldersFirst) {
- let [bookmarks, folders] = separateFolders(content)
- content = folders.concat(bookmarks)
+ const updateBookmarks = () => {
+ getBrowser().bookmarks.getSubTree(props.id).then(r => {
+ let content = [...r[0].children!].sort(getSortFunction(settings.sort))
+ if (settings.foldersFirst) {
+ let [bookmarks, folders] = separateFolders(content)
+ content = folders.concat(bookmarks)
+ }
+ setChildren(content);
+ })
}
+ useEffect(() => {
+ updateBookmarks();
+ // getBrowser().bookmarks.onRemoved.addListener((id: string, moveInfo) => {
+ // if (moveInfo.parentId !== props.id) return;
+ // updateBookmarks();
+ // })
+ }, []);
+
+ useEffect(() => {
+ updateBookmarks();
+ }, [settings]);
+
return (
<div className={"folderBody"}>
- {content.map(child =>
+ {children.map(child =>
child.children
- ? <FolderButton data={child} />
- : <Bookmark data={child} />
+ ? <FolderButton id={child.id} />
+ : <Bookmark id={child.id} />
)}
</div>
)
diff --git a/extension/src/components/FolderButton.tsx b/extension/src/components/FolderButton.tsx
index 2b59d56..374982a 100644
--- a/extension/src/components/FolderButton.tsx
+++ b/extension/src/components/FolderButton.tsx
@@ -2,34 +2,113 @@ import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
import FolderBody from "./FolderBody.tsx";
import FolderIcon from "../assets/folder.svg?react"
import FolderIconOpen from "../assets/folder_open.svg?react"
-import {useState} from "react";
-import bookmark from "./Bookmark.tsx";
+import React, {useEffect, useState} from "react";
+import DropTargets from "./DropTargets.tsx";
+import {ActiveDrag, ActiveEdit, Settings} from "./Body.tsx";
+import {getBrowser} from "../main.tsx";
+import ContextMenu from "./ContextMenu.tsx";
/**
* 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);
+function FolderButton(props: {id: string}) {
+ let [settings, ] = React.useContext(Settings);
+ let [activeDrag, setActiveDrag] = React.useContext(ActiveDrag);
+ let [, setActiveEdit] = React.useContext(ActiveEdit)
+
+ const [folderOpen, setFolderOpen] = useState<undefined | boolean>(undefined);
+ const [bmData, setBmData] = useState<BookmarkTreeNode | undefined>()
+
+ useEffect(() => {
+ console.log(props.id+" state="+folderOpen)
+ getBrowser().storage.local.set({['keepopen-'+props.id]: folderOpen})
+ }, [folderOpen]);
+
+ useEffect(() => {
+ if (settings.keepFoldersOpen) {
+ getBrowser().storage.local.get('keepopen-' + props.id).then(r => {
+ setFolderOpen(r['keepopen-' + props.id] == true);
+ })
+ } else {
+ setFolderOpen(false);
+ }
+ getBrowser().bookmarks.get(props.id).then(r => {
+ setBmData(r[0])
+ })
+ }, []);
+
+ if (!bmData) return;
+
+ // Dragging
+ const handleDragStart = (e: React.DragEvent<HTMLAnchorElement>) => {
+ e.dataTransfer.setData("sowgro", "placeholder")
+ };
+
+ const handleDrag = () => {
+ setActiveDrag(bmData);
+ };
+
+ const handleDragEnd = () => {
+ console.log("drop end")
+ setActiveDrag(null);
+ };
+
+ // Dropping
+ const handleDropLeft = () => {
+ console.log("drop left folder")
+ getBrowser().bookmarks.move(activeDrag!.id, {
+ parentId: bmData.parentId,
+ index: bmData.index
+ })
+ location.reload()
+ };
+
+ const handleDropRight = () => {
+ console.log("drop right folder")
+ getBrowser().bookmarks.move(activeDrag!.id, {
+ parentId: bmData.parentId,
+ index: (bmData.index! + 1)
+ })
+ location.reload();
+ };
+
+ const handleDropCenter = () => {
+ console.log("drop center folder")
+ getBrowser().bookmarks.move(activeDrag!.id, {
+ parentId: bmData.id
+ });
+ location.reload()
+ };
+
+ const handleDelete = () => {
+ getBrowser().bookmarks.removeTree(bmData.id);
+ location.reload();
+ };
+
+ const handleEdit = (e: React.MouseEvent<HTMLButtonElement>) => {
+ e.preventDefault();
+ setActiveEdit(bmData);
+ };
return(
<>
<div className={"bookmark"}>
- <a onClick={() => setFolderOpen(!folderOpen)}>
+ <a onClick={() => setFolderOpen(!folderOpen)} draggable={settings.editMode} onDrag={handleDrag}
+ onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
<div className="icon-box">
{folderOpen ? <FolderIconOpen/> : <FolderIcon/>}
</div>
- <span>{props.data.title}</span>
+ <span>{bmData.title}</span>
</a>
+ {settings.editMode && <ContextMenu onEdit={handleEdit} onDelete={handleDelete}/>}
+ {activeDrag && activeDrag !== bmData &&
+ <DropTargets onDropLeft={handleDropLeft} onDropRight={handleDropRight} onDropCenter={handleDropCenter}/>}
</div>
- {folderOpen
- && props.data.children
- && props.data.children.length > 0
- && (<FolderBody data={props.data}/>)}
+ {folderOpen && /*bmData.children && bmData.children.length > 0 &&*/
+ <FolderBody id={bmData.id}/>}
</>
-);
+ );
}
export default FolderButton \ No newline at end of file
diff --git a/extension/src/components/SettingsEditor.tsx b/extension/src/components/SettingsEditor.tsx
index d11158e..2ad5104 100644
--- a/extension/src/components/SettingsEditor.tsx
+++ b/extension/src/components/SettingsEditor.tsx
@@ -1,5 +1,5 @@
import RadioButtonGroup from "./RadioButtonGroup.tsx";
-import React, {useContext} from "react";
+import React, {useContext, useEffect, useState} from "react";
import CloseIcon from "../assets/close.svg?react"
import BookmarkTreeNode = browser.bookmarks.BookmarkTreeNode;
import {Settings} from "./Body.tsx";
@@ -11,9 +11,16 @@ import {getBrowser} from "../main.tsx";
* @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>>]}) {
+function SettingsEditor(props: {isOpen: [boolean, React.Dispatch<React.SetStateAction<boolean>>]}) {
const [settings, setSettings] = useContext(Settings)
const [open, setOpen] = props.isOpen;
+ const [folders, setFolders] = useState<BookmarkTreeNode[] | undefined>()
+
+ useEffect(() => {
+ getFoldersFromTree().then(r => setFolders(r));
+ }, []);
+
+ if (!folders) return;
return (
<div id="settings-menu" className={open ? "open" : "closed"}>
@@ -74,13 +81,31 @@ function SettingsEditor(props: {tree: BookmarkTreeNode[], isOpen: [boolean, Rea
<h3>Root folder</h3>
<select value={settings.rootFolder!}
onChange={e => setSettings({...settings, rootFolder: e.target.value})}>
- {getFoldersFromTree(props.tree).map(i =>
+ {folders.map(i =>
<option value={i.id}>{i.title ? i.title : "Untitled (id:" + i.id + ")"}</option>
)}
</select>
<h3>Icon Cache</h3>
- <button onClick={_ => getBrowser().storage.local.clear()}>Clear Icon Cache</button>
+ <button className={"default"} onClick={_ => getBrowser().storage.local.clear()}>Clear Icon Cache</button>
+
+ {/*<h3>Editing</h3>*/}
+ {/*<label>*/}
+ {/* <input type={"checkbox"}*/}
+ {/* checked={!settings.editMode}*/}
+ {/* onChange={e => setSettings({...settings, editMode: !e.target.checked})}*/}
+ {/* />*/}
+ {/* Prevent editing of bookmarks*/}
+ {/*</label>*/}
+
+ <h3>Open Folders</h3>
+ <label>
+ <input type={"checkbox"}
+ checked={settings.keepFoldersOpen}
+ onChange={e => setSettings({...settings, keepFoldersOpen: e.target.checked})}
+ />
+ Keep folders open
+ </label>
</div>
)
@@ -91,7 +116,8 @@ function SettingsEditor(props: {tree: BookmarkTreeNode[], isOpen: [boolean, Rea
*
* @param tree The full tree to walk through
*/
-function getFoldersFromTree(tree: BookmarkTreeNode[]) {
+async function getFoldersFromTree() {
+ let tree = await getBrowser().bookmarks.getTree();
let folderList: BookmarkTreeNode[] = [];
rec(tree);
diff --git a/extension/src/index.css b/extension/src/index.css
index f240f82..72f29db 100644
--- a/extension/src/index.css
+++ b/extension/src/index.css
@@ -79,8 +79,9 @@ body > .folderBody {
/* Bookmark */
.bookmark {
- padding: 10px;
+ margin: 10px;
position: relative;
+ border-radius: 10px;
}
.bookmark > a {
@@ -89,7 +90,6 @@ body > .folderBody {
width: 125px;
padding: 10px;
user-select: none;
- border-radius: 10px;
}
.icon-box {
@@ -141,7 +141,7 @@ body > .folderBody {
font-size: 12px;
}
-.bookmark > a:hover {
+.bookmark:hover {
background-color: rgba(0, 0, 0, 0.3);
}
@@ -162,6 +162,8 @@ a {
z-index: 2;
}
+
+
#action-area {
position: absolute;
top: 0;
@@ -172,22 +174,22 @@ a {
display: flex;
flex-direction: row;
- button {
- border-style: none;
- background: none;
- color: white;
- }
-
> * {
padding: 5px;
display: flex;
justify-content: center;
align-items: center;
}
+}
- button:hover {
- background-color: rgba(0, 0, 0, 0.3);
- }
+button:not(.default) {
+ border-style: none;
+ background: none;
+ color: white;
+}
+
+button:hover:not(.default) {
+ background-color: rgba(0, 0, 0, 0.3);
}
/* Drop targets */
@@ -208,12 +210,13 @@ a {
.drop-targets > .left {
position: absolute;
- left: -2px;
+ left: -12px;
height: 100%;
width: 30px;
display: flex;
justify-content: flex-start;
align-items: center;
+ /*background-color: rgba(0, 255, 255, 0.5);*/
> div {
background-color: white;
@@ -224,12 +227,13 @@ a {
.drop-targets > .right {
position: absolute;
- right: -2px;
+ right: -12px;
height: 100%;
width: 30px;
display: flex;
justify-content: flex-end;
align-items: center;
+ /*background-color: rgba(0, 255, 255, 0.5);*/
> div {
background-color: white;
@@ -242,11 +246,12 @@ a {
position: absolute;
top: 0;
bottom: 0;
- left: 30px;
- right: 30px;
+ left: 20px;
+ right: 20px;
display: flex;
justify-content: center;
align-items: center;
+ /*background-color: rgba(255, 0, 106, 0.5);*/
> span {
/*color: aqua;*/
@@ -258,13 +263,13 @@ a {
}
> svg {
- background-color: #FFF;
- width: 50px;
- height: 50px;
+ background-color: rgba(0, 0, 0, 0.5);
+ width: 75px;
+ height: 75px;
border-radius: 10px;
padding: 10px;
margin-bottom: 14px;
- fill: black;
+ fill: #ffffff;
}
}
@@ -273,4 +278,72 @@ a {
fill: black;
}
+.overflow {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ display: none;
+
+ > button {
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+ padding: 2px;
+ display: flex;
+ /*aspect-ratio: 1 / 1;*/
+ }
+}
+
+.bookmark:hover .overflow, .overflow:hover, .overflow:has(.context-menu) {
+ display: flex;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0 }
+}
+
+.context-menu {
+ display: flex;
+ flex-direction: column;
+ background-color: black;
+ color: white;
+ fill: white;
+ position: absolute;
+ top: 30px;
+ left: -30px;
+ right: -30px;
+ /* width: 50px; */
+ border-radius: 5px;
+ animation: fadeIn;
+ animation-duration: 0.2s;
+ padding: 5px 0;
+ gap: 2px;
+
+ button {
+ display: flex;
+ flex-direction: row;
+ height: 30px;
+ padding: 7px;
+ align-items: center;
+ gap: 2px;
+ justify-content: flex-start;
+ font-size: 15px;
+ }
+
+ svg {
+ max-height: 100%;
+ }
+
+ button:hover {
+ background-color: #242424;
+ }
+
+ .del {
+ color: #d00000;
+ fill: #d00000;
+ }
+}
+
+
+