diff --git a/apps/web/src/components/editor/index.js b/apps/web/src/components/editor/index.js index 971137f3a..10f8bc8af 100644 --- a/apps/web/src/components/editor/index.js +++ b/apps/web/src/components/editor/index.js @@ -170,7 +170,7 @@ function Editor({ noteId, nonce }) { - + ); } diff --git a/apps/web/src/components/icons/index.js b/apps/web/src/components/icons/index.js index d1ce92f49..6e21482e6 100644 --- a/apps/web/src/components/icons/index.js +++ b/apps/web/src/components/icons/index.js @@ -58,7 +58,7 @@ export const Plus = createIcon(Icons.mdiPlus); export const Note = createIcon(Icons.mdiHomeVariantOutline); export const Minus = createIcon(Icons.mdiMinus); export const Notebook = createIcon(Icons.mdiBookOutline); -export const Notebook2 = createIcon(Icons.mdiBookOutline); +export const Notebook2 = createIcon(Icons.mdiNotebookOutline); export const ArrowLeft = createIcon(Icons.mdiArrowLeft); export const ArrowRight = createIcon(Icons.mdiArrowRight); export const ArrowDown = createIcon(Icons.mdiArrowDown); @@ -128,8 +128,8 @@ export const Error = createIcon(Icons.mdiAlertCircle); export const Warn = createIcon(Icons.mdiAlert); export const Info = createIcon(Icons.mdiInformation); -export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOff); -export const ToggleChecked = createIcon(Icons.mdiToggleSwitch); +export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOffOutline); +export const ToggleChecked = createIcon(Icons.mdiToggleSwitchOutline); export const Backup = createIcon(Icons.mdiBackupRestore); export const Buy = createIcon(Icons.mdiCurrencyUsdCircleOutline); @@ -179,3 +179,6 @@ export const Twitter = createIcon(Icons.mdiTwitter); export const Reddit = createIcon(Icons.mdiReddit); export const Dismiss = createIcon(Icons.mdiClose); + +export const File = createIcon(Icons.mdiFileOutline); +export const Download = createIcon(Icons.mdiArrowDown); diff --git a/apps/web/src/components/properties/index.js b/apps/web/src/components/properties/index.js index cc84661c8..a52f39fc2 100644 --- a/apps/web/src/components/properties/index.js +++ b/apps/web/src/components/properties/index.js @@ -1,33 +1,37 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import * as Icon from "../icons"; -import { Flex, Text, Button } from "rebass"; +import { Flex, Text, Button, Box } from "rebass"; import { useStore } from "../../stores/editor-store"; -import { COLORS } from "../../common"; +import { AppEventManager, AppEvents, COLORS } from "../../common"; import { db } from "../../common/db"; import { useStore as useAppStore } from "../../stores/app-store"; import Animated from "../animated"; import Toggle from "./toggle"; -import { showMoveNoteDialog } from "../../common/dialog-controller"; import { navigate } from "../../navigation"; +import IconTag from "../icon-tag"; +import FileSaver from "file-saver"; const tools = [ - { key: "pinned", icons: { on: Icon.PinFilled, off: Icon.Pin }, label: "Pin" }, + { key: "pinned", icon: Icon.Pin, label: "Pin" }, { key: "favorite", - icons: { on: Icon.Star, off: Icon.StarOutline }, + icon: Icon.StarOutline, label: "Favorite", }, - { key: "locked", icons: { on: Icon.Lock, off: Icon.Unlock }, label: "Lock" }, + { key: "locked", icon: Icon.Unlock, label: "Lock" }, ]; -function Properties() { - const color = useStore((store) => store.session.color); - const toggleLocked = useStore((store) => store.toggleLocked); - const sessionId = useStore((store) => store.session.id); - const notebooks = useStore((store) => store.session.notebooks); - const setSession = useStore((store) => store.setSession); - const setColor = useStore((store) => store.setColor); +function Properties({ noteId }) { + const [attachmentsStatus, setAttachmentsStatus] = useState({}); const arePropertiesVisible = useStore((store) => store.arePropertiesVisible); + const color = useStore((store) => store.session.color); + const notebooks = useStore((store) => store.session.notebooks); + const attachments = useStore((store) => store.session.attachments); + + const toggleLocked = useStore((store) => store.toggleLocked); + const setSession = useStore((store) => store.setSession); + const sessionId = useStore((store) => store.session.id); + const setColor = useStore((store) => store.setColor); const toggleProperties = useStore((store) => store.toggleProperties); const isFocusMode = useAppStore((store) => store.isFocusMode); @@ -44,183 +48,328 @@ function Properties() { [setSession, toggleLocked] ); - return ( - !isFocusMode && ( - <> - - - - Properties - toggleProperties()} - sx={{ - color: "red", - height: 24, - ":active": { color: "darkRed" }, - }} - > - - - - {sessionId ? ( - <> - - {tools.map((tool, _) => ( - changeState(tool.key, state)} - testId={`properties-${tool.key}`} - /> - ))} - - - {COLORS.map((label) => ( - setColor(label)} - sx={{ cursor: "pointer" }} - mt={4} - data-test-id={`properties-${label}`} - > - - - - {label} - - - {label.toLowerCase() === color?.toLowerCase() && ( - - )} - - ))} - - {notebooks?.length && ( - <> - - Referenced in {notebooks.length} notebook(s): - - {notebooks.map((ref) => { - const notebook = db.notebooks.notebook(ref.id); - if (!notebook) return null; - const topics = ref.topics.reduce((topics, topicId) => { - const topic = notebook.topics.topic(topicId); - if (!!topic && !!topic._topic) - topics.push(topic._topic); - return topics; - }, []); + useEffect(() => { + const event = AppEventManager.subscribe( + AppEvents.UPDATE_ATTACHMENT_PROGRESS, + ({ hash, type, total, loaded }) => { + if (!attachments.find((a) => a.metadata.hash === hash)) return; + setAttachmentsStatus((status) => { + const copy = { ...status }; + copy[hash] = { + type, + progress: Math.round((loaded / total) * 100), + }; + return copy; + }); + } + ); + return () => { + event.unsubscribe(); + }; + }, [attachments]); - return ( - - { - navigate(`/notebooks/${notebook.data.id}`); - }} - mb={1} - > - - - {notebook.title} - - - {topics.map((topic) => ( - { - navigate( - `/notebooks/${notebook.data.id}/${topic.id}` - ); - }} - > - - - {topic.title} - - - ))} - - ); - })} - - )} - - - ) : ( - + + + {tools.map((tool, _) => ( + changeState(tool.key, state)} + testId={`properties-${tool.key}`} + /> + ))} + + {COLORS.map((label) => ( + setColor(label)} + sx={{ + cursor: "pointer", + position: "relative", + }} + data-test-id={`properties-${label}`} > - Start writing to make a new note. - - )} + + {label.toLowerCase() === color?.toLowerCase() && ( + + )} + + ))} - - - ) + + {notebooks?.length && ( + + {notebooks.map((ref) => { + const notebook = db.notebooks.notebook(ref.id); + if (!notebook) return null; + const topics = ref.topics.reduce((topics, topicId) => { + const topic = notebook.topics.topic(topicId); + if (!!topic && !!topic._topic) topics.push(topic._topic); + return topics; + }, []); + + return ( + { + navigate(`/notebooks/${notebook.data.id}`); + }} + > + + + {notebook.title} + + + + {topics.map((topic) => ( + { + e.stopPropagation(); + navigate( + `/notebooks/${notebook.data.id}/${topic.id}` + ); + }} + /> + ))} + + + ); + })} + + )} + {attachments.length > 0 && ( + + {attachments.map((attachment) => { + const attachmentStatus = + attachmentsStatus[attachment.metadata.hash]; + return ( + + + + {formatFilename(attachment.metadata.filename)} + + {attachmentStatus && ( + + )} + + + {formatBytes(attachment.length, 1)} + + + {attachmentStatus ? ( + + ) : ( + + )} + + + ); + })} + + )} + + ); } export default React.memo(Properties); + +function Card({ title, children }) { + return ( + + + {title} + + {children} + + ); +} + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return "0B"; + + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"]; + + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i]; +} + +function formatFilename(filename) { + const MAX_LENGTH = 28; + if (filename.length > MAX_LENGTH) { + return ( + filename.substr(0, MAX_LENGTH / 2) + + "..." + + filename.substr(-(MAX_LENGTH / 3)) + ); + } + return filename; +} diff --git a/apps/web/src/components/properties/toggle.css b/apps/web/src/components/properties/toggle.css new file mode 100644 index 000000000..2b151f0f6 --- /dev/null +++ b/apps/web/src/components/properties/toggle.css @@ -0,0 +1,34 @@ +.react-toggle { + display: flex; + align-items: center; +} + +.react-toggle-thumb { + box-shadow: none; +} + +.react-toggle-track { + width: 30px; + height: 18px; +} + +.react-toggle-thumb { + width: 16px; + height: 16px; + top: 0px; + left: 1px; + margin-top: 1px; +} + +.react-toggle--checked .react-toggle-thumb { + left: 18px; + border-color: var(--primary); +} + +.react-toggle:active:not(.react-toggle--disabled) .react-toggle-thumb { + box-shadow: none; +} + +.react-toggle--focus .react-toggle-thumb { + box-shadow: none; +} diff --git a/apps/web/src/components/properties/toggle.js b/apps/web/src/components/properties/toggle.js index ee5e96ba4..1e00f4806 100644 --- a/apps/web/src/components/properties/toggle.js +++ b/apps/web/src/components/properties/toggle.js @@ -1,24 +1,37 @@ import React from "react"; import { Flex, Text } from "rebass"; import { useStore } from "../../stores/editor-store"; +import ReactToggle from "react-toggle"; +import "react-toggle/style.css"; +import { Label } from "@rebass/forms"; +import "./toggle.css"; function Toggle(props) { - const { icons, label, onToggle, toggleKey } = props; + const { icon: ToggleIcon, label, onToggle, toggleKey } = props; const isOn = useStore((store) => store.session[toggleKey]); return ( onToggle(!isOn)} data-test-id={props.testId} > - {isOn ? : } - + + {label} + ); } diff --git a/apps/web/src/stores/editor-store.js b/apps/web/src/stores/editor-store.js index 388200f94..37e3bd529 100644 --- a/apps/web/src/stores/editor-store.js +++ b/apps/web/src/stores/editor-store.js @@ -30,6 +30,7 @@ const getDefaultSession = () => { color: undefined, dateEdited: 0, totalWords: 0, + attachments: [], content: { type: "tiny", data: "", @@ -102,6 +103,7 @@ class EditorStore extends BaseStore { content: content || defaultSession.content, totalWords: state.session.totalWords, state: SESSION_STATES.new, + attachments: db.attachments.get(note.id) || [], }; }); appStore.setIsEditorOpen(true); @@ -146,6 +148,7 @@ class EditorStore extends BaseStore { state.session.title = note.title; state.session.isSaving = false; state.session.notebooks = note.notebooks; + state.session.attachments = db.attachments.get(note.id) || []; }); noteStore.refresh();