feat: improve properties UI and add attachments section

This commit is contained in:
thecodrr
2021-10-04 14:05:29 +05:00
parent 2fa455fc55
commit 533509ffb0
6 changed files with 401 additions and 199 deletions

View File

@@ -170,7 +170,7 @@ function Editor({ noteId, nonce }) {
</Suspense> </Suspense>
</Animated.Flex> </Animated.Flex>
</Flex> </Flex>
<Properties /> <Properties noteId={noteId} />
</Flex> </Flex>
); );
} }

View File

@@ -58,7 +58,7 @@ export const Plus = createIcon(Icons.mdiPlus);
export const Note = createIcon(Icons.mdiHomeVariantOutline); export const Note = createIcon(Icons.mdiHomeVariantOutline);
export const Minus = createIcon(Icons.mdiMinus); export const Minus = createIcon(Icons.mdiMinus);
export const Notebook = createIcon(Icons.mdiBookOutline); 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 ArrowLeft = createIcon(Icons.mdiArrowLeft);
export const ArrowRight = createIcon(Icons.mdiArrowRight); export const ArrowRight = createIcon(Icons.mdiArrowRight);
export const ArrowDown = createIcon(Icons.mdiArrowDown); export const ArrowDown = createIcon(Icons.mdiArrowDown);
@@ -128,8 +128,8 @@ export const Error = createIcon(Icons.mdiAlertCircle);
export const Warn = createIcon(Icons.mdiAlert); export const Warn = createIcon(Icons.mdiAlert);
export const Info = createIcon(Icons.mdiInformation); export const Info = createIcon(Icons.mdiInformation);
export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOff); export const ToggleUnchecked = createIcon(Icons.mdiToggleSwitchOffOutline);
export const ToggleChecked = createIcon(Icons.mdiToggleSwitch); export const ToggleChecked = createIcon(Icons.mdiToggleSwitchOutline);
export const Backup = createIcon(Icons.mdiBackupRestore); export const Backup = createIcon(Icons.mdiBackupRestore);
export const Buy = createIcon(Icons.mdiCurrencyUsdCircleOutline); export const Buy = createIcon(Icons.mdiCurrencyUsdCircleOutline);
@@ -179,3 +179,6 @@ export const Twitter = createIcon(Icons.mdiTwitter);
export const Reddit = createIcon(Icons.mdiReddit); export const Reddit = createIcon(Icons.mdiReddit);
export const Dismiss = createIcon(Icons.mdiClose); export const Dismiss = createIcon(Icons.mdiClose);
export const File = createIcon(Icons.mdiFileOutline);
export const Download = createIcon(Icons.mdiArrowDown);

View File

@@ -1,33 +1,37 @@
import React, { useCallback } from "react"; import React, { useCallback, useEffect, useState } from "react";
import * as Icon from "../icons"; 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 { useStore } from "../../stores/editor-store";
import { COLORS } from "../../common"; import { AppEventManager, AppEvents, COLORS } from "../../common";
import { db } from "../../common/db"; import { db } from "../../common/db";
import { useStore as useAppStore } from "../../stores/app-store"; import { useStore as useAppStore } from "../../stores/app-store";
import Animated from "../animated"; import Animated from "../animated";
import Toggle from "./toggle"; import Toggle from "./toggle";
import { showMoveNoteDialog } from "../../common/dialog-controller";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import IconTag from "../icon-tag";
import FileSaver from "file-saver";
const tools = [ const tools = [
{ key: "pinned", icons: { on: Icon.PinFilled, off: Icon.Pin }, label: "Pin" }, { key: "pinned", icon: Icon.Pin, label: "Pin" },
{ {
key: "favorite", key: "favorite",
icons: { on: Icon.Star, off: Icon.StarOutline }, icon: Icon.StarOutline,
label: "Favorite", label: "Favorite",
}, },
{ key: "locked", icons: { on: Icon.Lock, off: Icon.Unlock }, label: "Lock" }, { key: "locked", icon: Icon.Unlock, label: "Lock" },
]; ];
function Properties() { function Properties({ noteId }) {
const color = useStore((store) => store.session.color); const [attachmentsStatus, setAttachmentsStatus] = useState({});
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);
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible); 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 toggleProperties = useStore((store) => store.toggleProperties);
const isFocusMode = useAppStore((store) => store.isFocusMode); const isFocusMode = useAppStore((store) => store.isFocusMode);
@@ -44,183 +48,328 @@ function Properties() {
[setSession, toggleLocked] [setSession, toggleLocked]
); );
return ( useEffect(() => {
!isFocusMode && ( const event = AppEventManager.subscribe(
<> AppEvents.UPDATE_ATTACHMENT_PROGRESS,
<Animated.Flex ({ hash, type, total, loaded }) => {
animate={{ if (!attachments.find((a) => a.metadata.hash === hash)) return;
x: arePropertiesVisible ? 0 : 800, setAttachmentsStatus((status) => {
display: arePropertiesVisible ? "flex" : "none", const copy = { ...status };
}} copy[hash] = {
transition={{ type,
duration: 0.3, progress: Math.round((loaded / total) * 100),
bounceDamping: 1, };
bounceStiffness: 1, return copy;
ease: "easeOut", });
}} }
initial={false} );
style={{ return () => {
position: "absolute", event.unsubscribe();
right: 0, };
zIndex: 3, }, [attachments]);
height: "100%",
}}
>
<Flex
sx={{
overflowY: "auto",
overflowX: "hidden",
height: "100%",
width: "300px",
borderLeft: "1px solid",
borderLeftColor: "border",
}}
flexDirection="column"
bg="background"
px={3}
py={0}
>
<Text
variant="title"
my={2}
alignItems="center"
justifyContent="space-between"
sx={{ display: "flex" }}
>
Properties
<Text
data-test-id="properties-close"
as="span"
onClick={() => toggleProperties()}
sx={{
color: "red",
height: 24,
":active": { color: "darkRed" },
}}
>
<Icon.Close />
</Text>
</Text>
{sessionId ? (
<>
<Flex>
{tools.map((tool, _) => (
<Toggle
{...tool}
key={tool.key}
toggleKey={tool.key}
onToggle={(state) => changeState(tool.key, state)}
testId={`properties-${tool.key}`}
/>
))}
</Flex>
<Flex flexDirection="column">
{COLORS.map((label) => (
<Flex
key={label}
justifyContent="space-between"
alignItems="center"
onClick={() => setColor(label)}
sx={{ cursor: "pointer" }}
mt={4}
data-test-id={`properties-${label}`}
>
<Flex key={label} alignItems="center">
<Icon.Circle size={14} color={label.toLowerCase()} />
<Text ml={1} color="text" variant="body">
{label}
</Text>
</Flex>
{label.toLowerCase() === color?.toLowerCase() && (
<Icon.Checkmark
color="primary"
size={20}
data-test-id={`properties-${label}-check`}
/>
)}
</Flex>
))}
</Flex>
{notebooks?.length && (
<>
<Text variant="subtitle" mt={4} mb={1}>
Referenced in {notebooks.length} notebook(s):
</Text>
{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 ( if (isFocusMode || !sessionId) return null;
<Flex flexDirection="column" my={1}> return (
<Flex <>
onClick={() => { <Animated.Flex
navigate(`/notebooks/${notebook.data.id}`); animate={{
}} x: arePropertiesVisible ? 0 : 800,
mb={1} display: arePropertiesVisible ? "flex" : "none",
> }}
<Icon.Notebook size={12} /> transition={{
<Text duration: 0.3,
variant="body" bounceDamping: 1,
ml={1} bounceStiffness: 1,
sx={{ cursor: "pointer" }} ease: "easeOut",
> }}
{notebook.title} initial={false}
</Text> sx={{
</Flex> position: "absolute",
{topics.map((topic) => ( right: 0,
<Flex zIndex: 3,
mb={1} height: "100%",
ml={2} width: "300px",
onClick={() => { borderLeft: "1px solid",
navigate( borderLeftColor: "border",
`/notebooks/${notebook.data.id}/${topic.id}` overflowY: "auto",
); overflowX: "hidden",
}} }}
> flexDirection="column"
<Icon.Topic size={12} /> bg="background"
<Text // px={2}
variant="body" >
ml={1} <Card title="Properties">
sx={{ cursor: "pointer" }} {tools.map((tool, _) => (
> <Toggle
{topic.title} {...tool}
</Text> key={tool.key}
</Flex> toggleKey={tool.key}
))} onToggle={(state) => changeState(tool.key, state)}
</Flex> testId={`properties-${tool.key}`}
); />
})} ))}
</> <Flex
)} py={2}
<Button px={2}
variant="secondary" sx={{
onClick={async () => { cursor: "pointer",
await showMoveNoteDialog([sessionId]); }}
}} justifyContent="center"
data-test-id="properties-add-to-nb" >
mt={notebooks?.length ? 0 : 3} {COLORS.map((label) => (
> <Flex
Add to notebook key={label}
</Button> justifyContent="space-between"
</> alignItems="center"
) : ( onClick={() => setColor(label)}
<Text sx={{
variant="body" cursor: "pointer",
sx={{ justifySelf: "center", alignSelf: "center" }} position: "relative",
}}
data-test-id={`properties-${label}`}
> >
Start writing to make a new note. <Icon.Circle size={35} color={label.toLowerCase()} />
</Text> {label.toLowerCase() === color?.toLowerCase() && (
)} <Icon.Checkmark
color="static"
size={18}
sx={{ position: "absolute", left: "8px" }}
data-test-id={`properties-${label}-check`}
/>
)}
</Flex>
))}
</Flex> </Flex>
</Animated.Flex> </Card>
</> {notebooks?.length && (
) <Card title="Referenced In">
{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 (
<Flex
py={2}
px={2}
sx={{
borderBottom: "1px solid var(--border)",
":last-of-type": { borderBottom: "none" },
cursor: "pointer",
":hover": {
bg: "hover",
},
}}
flexDirection="column"
onClick={() => {
navigate(`/notebooks/${notebook.data.id}`);
}}
>
<Text variant="body" display="flex" alignItems="center">
<Icon.Notebook size={13} sx={{ flexShrink: 0, mr: 1 }} />
{notebook.title}
</Text>
<Flex
sx={{
flexWrap: "wrap",
}}
mt="2.5px"
>
{topics.map((topic) => (
<IconTag
title={topic.title}
text={topic.title}
key={topic.id}
icon={Icon.Topic}
onClick={(e) => {
e.stopPropagation();
navigate(
`/notebooks/${notebook.data.id}/${topic.id}`
);
}}
/>
))}
</Flex>
</Flex>
);
})}
</Card>
)}
{attachments.length > 0 && (
<Card title="Attachments">
{attachments.map((attachment) => {
const attachmentStatus =
attachmentsStatus[attachment.metadata.hash];
return (
<Flex
//py={2}
py={0}
px={2}
sx={{
borderBottom: "1px solid var(--border)",
":last-of-type": { borderBottom: "none" },
":hover .attachment-actions": {
display: "flex",
},
":hover .attachment-size": {
display: "none",
},
}}
title={attachment.metadata.filename}
alignItems="center"
justifyContent="space-between"
>
<Flex flexDirection="column">
<Text
variant="body"
sx={{
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{formatFilename(attachment.metadata.filename)}
</Text>
{attachmentStatus && (
<Box
sx={{
my: 1,
bg: "primary",
height: "2px",
width: `${attachmentStatus.progress}%`,
}}
/>
)}
</Flex>
<Text
className="attachment-size"
variant="subBody"
flexShrink={0}
p={1}
m={1}
>
{formatBytes(attachment.length, 1)}
</Text>
<Box display="none" className="attachment-actions">
{attachmentStatus ? (
<Button
title="Cancel download"
variant="tool"
p={1}
m={1}
bg="transparent"
sx={{ ":hover": { bg: "hover" } }}
onClick={async () => {
await db.fs.cancel(
attachment.metadata.hash,
"download"
);
}}
>
<Icon.Close size={16} />
</Button>
) : (
<Button
title="Download attachment"
variant="tool"
p={1}
m={1}
bg="transparent"
sx={{ ":hover": { bg: "hover" } }}
onClick={async () => {
await db.fs.downloadFile(
attachment.metadata.hash,
attachment.metadata.hash
);
const data = await db.fs.readEncrypted(
attachment.metadata.hash,
await db.user.getEncryptionKey(),
{
iv: attachment.iv,
salt: attachment.salt,
length: attachment.length,
alg: attachment.alg,
outputType: "uint8array",
}
);
// download(
// attachment.metadata.filename,
// data,
// attachment.metadata.type
// );
FileSaver.saveAs(
new Blob([data], {
type: attachment.metadata.type,
}),
attachment.metadata.filename
);
}}
>
<Icon.Download size={16} />
</Button>
)}
</Box>
</Flex>
);
})}
</Card>
)}
</Animated.Flex>
</>
); );
} }
export default React.memo(Properties); export default React.memo(Properties);
function Card({ title, children }) {
return (
<Flex
flexDirection="column"
//mx={1}
//mt={2}
sx={{
//border: "1px solid var(--border)",
borderRadius: "default",
}}
>
<Text
variant="subtitle"
fontSize="subtitle"
mx={2}
my={2}
color="fontTertiary"
>
{title}
</Text>
{children}
</Flex>
);
}
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;
}

View File

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

View File

@@ -1,24 +1,37 @@
import React from "react"; import React from "react";
import { Flex, Text } from "rebass"; import { Flex, Text } from "rebass";
import { useStore } from "../../stores/editor-store"; 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) { function Toggle(props) {
const { icons, label, onToggle, toggleKey } = props; const { icon: ToggleIcon, label, onToggle, toggleKey } = props;
const isOn = useStore((store) => store.session[toggleKey]); const isOn = useStore((store) => store.session[toggleKey]);
return ( return (
<Flex <Flex
variant="columnCenter" alignItems="center"
width="33%" justifyContent="space-between"
py={2} py={2}
mr={1} px={2}
sx={{ borderRadius: "default", cursor: "pointer" }} sx={{
borderBottom: "1px solid var(--border)",
cursor: "pointer",
}}
onClick={() => onToggle(!isOn)} onClick={() => onToggle(!isOn)}
data-test-id={props.testId} data-test-id={props.testId}
> >
{isOn ? <icons.on color="primary" /> : <icons.off />} <Text
<Text mt={1} variant="body" color={isOn ? "primary" : "text"}> display="flex"
alignItems="center"
variant="body"
color={isOn ? "primary" : "text"}
>
<ToggleIcon size={13} sx={{ flexShrink: 0, mr: 1 }} />
{label} {label}
</Text> </Text>
<ReactToggle size={20} defaultChecked={isOn} icons={false} />
</Flex> </Flex>
); );
} }

View File

@@ -30,6 +30,7 @@ const getDefaultSession = () => {
color: undefined, color: undefined,
dateEdited: 0, dateEdited: 0,
totalWords: 0, totalWords: 0,
attachments: [],
content: { content: {
type: "tiny", type: "tiny",
data: "", data: "",
@@ -102,6 +103,7 @@ class EditorStore extends BaseStore {
content: content || defaultSession.content, content: content || defaultSession.content,
totalWords: state.session.totalWords, totalWords: state.session.totalWords,
state: SESSION_STATES.new, state: SESSION_STATES.new,
attachments: db.attachments.get(note.id) || [],
}; };
}); });
appStore.setIsEditorOpen(true); appStore.setIsEditorOpen(true);
@@ -146,6 +148,7 @@ class EditorStore extends BaseStore {
state.session.title = note.title; state.session.title = note.title;
state.session.isSaving = false; state.session.isSaving = false;
state.session.notebooks = note.notebooks; state.session.notebooks = note.notebooks;
state.session.attachments = db.attachments.get(note.id) || [];
}); });
noteStore.refresh(); noteStore.refresh();