mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-17 04:07:51 +01:00
web: fix note editor properties
This commit is contained in:
39
apps/web/package-lock.json
generated
39
apps/web/package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"@react-pdf-viewer/core": "^3.12.0",
|
"@react-pdf-viewer/core": "^3.12.0",
|
||||||
"@react-pdf-viewer/toolbar": "^3.12.0",
|
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||||
"@tanstack/react-query": "^4.29.19",
|
"@tanstack/react-query": "^4.29.19",
|
||||||
|
"@tanstack/react-virtual": "^3.0.0-beta.68",
|
||||||
"@theme-ui/color": "^0.14.7",
|
"@theme-ui/color": "^0.14.7",
|
||||||
"@theme-ui/components": "^0.14.7",
|
"@theme-ui/components": "^0.14.7",
|
||||||
"@theme-ui/core": "^0.14.7",
|
"@theme-ui/core": "^0.14.7",
|
||||||
@@ -68,7 +69,6 @@
|
|||||||
"react-modal": "3.13.1",
|
"react-modal": "3.13.1",
|
||||||
"react-qrcode-logo": "^2.2.1",
|
"react-qrcode-logo": "^2.2.1",
|
||||||
"react-scroll-sync": "^0.9.0",
|
"react-scroll-sync": "^0.9.0",
|
||||||
"react-virtuoso": "^4.4.2",
|
|
||||||
"timeago.js": "4.0.2",
|
"timeago.js": "4.0.2",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
"w3c-keyname": "^2.2.6",
|
"w3c-keyname": "^2.2.6",
|
||||||
@@ -24335,6 +24335,7 @@
|
|||||||
"async-mutex": "^0.3.2",
|
"async-mutex": "^0.3.2",
|
||||||
"dayjs": "1.11.9",
|
"dayjs": "1.11.9",
|
||||||
"entities": "^4.3.1",
|
"entities": "^4.3.1",
|
||||||
|
"fuzzyjs": "^5.0.1",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"katex": "0.16.2",
|
"katex": "0.16.2",
|
||||||
@@ -40764,6 +40765,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-virtual": {
|
||||||
|
"version": "3.0.0-beta.68",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.68.tgz",
|
||||||
|
"integrity": "sha512-YEFNCf+N3ZlNou2r4qnh+GscMe24foYEjTL05RS0ZHvah2RoUDPGuhnuedTv0z66dO2vIq6+Bl4TXatht5T7GQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/virtual-core": "3.0.0-beta.68"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/virtual-core": {
|
||||||
|
"version": "3.0.0-beta.68",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.68.tgz",
|
||||||
|
"integrity": "sha512-CnvsEJWK7cugigckt13AeY80FMzH+OMdEP0j0bS3/zjs44NiRe49x8FZC6R9suRXGMVMXtUHet0zbTp/Ec9Wfg==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@theme-ui/color": {
|
"node_modules/@theme-ui/color": {
|
||||||
"version": "0.14.7",
|
"version": "0.14.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -46406,17 +46432,6 @@
|
|||||||
"react-dom": "0.14.x || 15.x || 16.x || 17.x"
|
"react-dom": "0.14.x || 15.x || 16.x || 17.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-virtuoso": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16 || >=17 || >= 18",
|
|
||||||
"react-dom": ">=16 || >=17 || >= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"@react-pdf-viewer/core": "^3.12.0",
|
"@react-pdf-viewer/core": "^3.12.0",
|
||||||
"@react-pdf-viewer/toolbar": "^3.12.0",
|
"@react-pdf-viewer/toolbar": "^3.12.0",
|
||||||
"@tanstack/react-query": "^4.29.19",
|
"@tanstack/react-query": "^4.29.19",
|
||||||
|
"@tanstack/react-virtual": "^3.0.0-beta.68",
|
||||||
"@theme-ui/color": "^0.14.7",
|
"@theme-ui/color": "^0.14.7",
|
||||||
"@theme-ui/components": "^0.14.7",
|
"@theme-ui/components": "^0.14.7",
|
||||||
"@theme-ui/core": "^0.14.7",
|
"@theme-ui/core": "^0.14.7",
|
||||||
@@ -67,7 +68,6 @@
|
|||||||
"react-modal": "3.13.1",
|
"react-modal": "3.13.1",
|
||||||
"react-qrcode-logo": "^2.2.1",
|
"react-qrcode-logo": "^2.2.1",
|
||||||
"react-scroll-sync": "^0.9.0",
|
"react-scroll-sync": "^0.9.0",
|
||||||
"react-virtuoso": "^4.4.2",
|
|
||||||
"timeago.js": "4.0.2",
|
"timeago.js": "4.0.2",
|
||||||
"tinycolor2": "^1.6.0",
|
"tinycolor2": "^1.6.0",
|
||||||
"w3c-keyname": "^2.2.6",
|
"w3c-keyname": "^2.2.6",
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ export async function saveAttachment(hash: string) {
|
|||||||
if (!response) return;
|
if (!response) return;
|
||||||
|
|
||||||
const { attachment, key } = response;
|
const { attachment, key } = response;
|
||||||
await lazify(import("../interfaces/fs"), ({ default: FS }) =>
|
await lazify(import("../interfaces/fs"), ({ saveFile }) =>
|
||||||
FS.saveFile(attachment.metadata.hash, {
|
saveFile(attachment.metadata.hash, {
|
||||||
key,
|
key,
|
||||||
iv: attachment.iv,
|
iv: attachment.iv,
|
||||||
name: attachment.metadata.filename,
|
name: attachment.filename,
|
||||||
type: attachment.metadata.type,
|
type: attachment.mimeType,
|
||||||
isUploaded: !!attachment.dateUploaded
|
isUploaded: !!attachment.dateUploaded
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -76,12 +76,12 @@ export async function downloadAttachment<
|
|||||||
if (type === "base64" || type === "text")
|
if (type === "base64" || type === "text")
|
||||||
return (await db.attachments.read(hash, type)) as TOutputType;
|
return (await db.attachments.read(hash, type)) as TOutputType;
|
||||||
|
|
||||||
const blob = await lazify(import("../interfaces/fs"), ({ default: FS }) =>
|
const blob = await lazify(import("../interfaces/fs"), ({ decryptFile }) =>
|
||||||
FS.decryptFile(attachment.metadata.hash, {
|
decryptFile(attachment.metadata.hash, {
|
||||||
key,
|
key,
|
||||||
iv: attachment.iv,
|
iv: attachment.iv,
|
||||||
name: attachment.metadata.filename,
|
name: attachment.filename,
|
||||||
type: attachment.metadata.type,
|
type: attachment.mimeType,
|
||||||
isUploaded: !!attachment.dateUploaded
|
isUploaded: !!attachment.dateUploaded
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -95,8 +95,9 @@ export async function checkAttachment(hash: string) {
|
|||||||
if (!attachment) return { failed: "Attachment not found." };
|
if (!attachment) return { failed: "Attachment not found." };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const size = await lazify(import("../interfaces/fs"), ({ default: FS }) =>
|
const size = await lazify(
|
||||||
FS.getUploadedFileSize(hash)
|
import("../interfaces/fs"),
|
||||||
|
({ getUploadedFileSize }) => getUploadedFileSize(hash)
|
||||||
);
|
);
|
||||||
if (size <= 0) return { failed: "File length is 0." };
|
if (size <= 0) return { failed: "File length is 0." };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -104,12 +105,3 @@ export async function checkAttachment(hash: string) {
|
|||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const ABYTES = 17;
|
|
||||||
export function getTotalSize(attachments: Attachment[]) {
|
|
||||||
let size = 0;
|
|
||||||
for (const attachment of attachments) {
|
|
||||||
size += attachment.length + ABYTES;
|
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,18 +33,16 @@ import {
|
|||||||
FileWebClip,
|
FileWebClip,
|
||||||
Icon,
|
Icon,
|
||||||
Loading,
|
Loading,
|
||||||
References,
|
|
||||||
Rename,
|
Rename,
|
||||||
Reupload,
|
Reupload,
|
||||||
Uploading
|
Uploading
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { showToast } from "../../utils/toast";
|
|
||||||
import { hashNavigate } from "../../navigation";
|
import { hashNavigate } from "../../navigation";
|
||||||
import {
|
import {
|
||||||
closeOpenedDialog,
|
closeOpenedDialog,
|
||||||
showPromptDialog
|
showPromptDialog
|
||||||
} from "../../common/dialog-controller";
|
} from "../../common/dialog-controller";
|
||||||
import { store } from "../../stores/attachment-store";
|
import { store, useStore } from "../../stores/attachment-store";
|
||||||
import { db } from "../../common/db";
|
import { db } from "../../common/db";
|
||||||
import { saveAttachment } from "../../common/attachments";
|
import { saveAttachment } from "../../common/attachments";
|
||||||
import { reuploadAttachment } from "../editor/picker";
|
import { reuploadAttachment } from "../editor/picker";
|
||||||
@@ -55,11 +53,11 @@ import {
|
|||||||
WebClipMimeType,
|
WebClipMimeType,
|
||||||
PDFMimeType
|
PDFMimeType
|
||||||
} from "@notesnook/core/dist/utils/filename";
|
} from "@notesnook/core/dist/utils/filename";
|
||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||||
import { getFormattedDate } from "@notesnook/common";
|
import { getFormattedDate } from "@notesnook/common";
|
||||||
import { MenuItem } from "@notesnook/ui";
|
import { MenuItem } from "@notesnook/ui";
|
||||||
import { Attachment } from "@notesnook/core";
|
import { Attachment as AttachmentType } from "@notesnook/core";
|
||||||
|
|
||||||
const FILE_ICONS: Record<string, Icon> = {
|
const FILE_ICONS: Record<string, Icon> = {
|
||||||
"image/": FileImage,
|
"image/": FileImage,
|
||||||
@@ -86,24 +84,29 @@ type AttachmentProgressStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type AttachmentProps = {
|
type AttachmentProps = {
|
||||||
attachment: Attachment;
|
item: AttachmentType;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelected?: () => void;
|
onSelected?: () => void;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
rowRef?: React.Ref<HTMLTableRowElement>;
|
||||||
};
|
};
|
||||||
export function Attachment({
|
export function Attachment({
|
||||||
attachment,
|
item,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelected,
|
onSelected,
|
||||||
compact
|
compact,
|
||||||
|
rowRef,
|
||||||
|
style
|
||||||
}: AttachmentProps) {
|
}: AttachmentProps) {
|
||||||
const [status, setStatus] = useState<AttachmentProgressStatus>();
|
const [status, setStatus] = useState<AttachmentProgressStatus>();
|
||||||
|
const processing = useStore((store) => store.processing[item.hash]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const event = AppEventManager.subscribe(
|
const event = AppEventManager.subscribe(
|
||||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||||
(progress: any) => {
|
(progress: any) => {
|
||||||
if (progress.hash === attachment.metadata.hash) {
|
if (progress.hash === item.hash) {
|
||||||
const percent = Math.round((progress.loaded / progress.total) * 100);
|
const percent = Math.round((progress.loaded / progress.total) * 100);
|
||||||
setStatus(
|
setStatus(
|
||||||
percent < 100
|
percent < 100
|
||||||
@@ -120,18 +123,20 @@ export function Attachment({
|
|||||||
return () => {
|
return () => {
|
||||||
event.unsubscribe();
|
event.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [attachment.metadata.hash]);
|
}, [item.hash]);
|
||||||
|
|
||||||
const FileIcon = getFileIcon(attachment.metadata.type);
|
const FileIcon = getFileIcon(item.type);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as="tr"
|
as="tr"
|
||||||
sx={{ height: 30, ":hover": { bg: "hover" } }}
|
sx={{ height: 30, ":hover": { bg: "hover" } }}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Menu.openMenu(AttachmentMenuItems(attachment, status));
|
Menu.openMenu(AttachmentMenuItems(item, status));
|
||||||
}}
|
}}
|
||||||
onClick={onSelected}
|
onClick={onSelected}
|
||||||
|
style={style}
|
||||||
|
ref={rowRef}
|
||||||
>
|
>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<td>
|
<td>
|
||||||
@@ -161,7 +166,7 @@ export function Attachment({
|
|||||||
) : (
|
) : (
|
||||||
<Uploading size={16} color="accent" />
|
<Uploading size={16} color="accent" />
|
||||||
)
|
)
|
||||||
) : attachment.failed ? (
|
) : processing?.failed || item.failed ? (
|
||||||
<AttachmentError
|
<AttachmentError
|
||||||
color={"icon-error"}
|
color={"icon-error"}
|
||||||
size={16}
|
size={16}
|
||||||
@@ -171,7 +176,7 @@ export function Attachment({
|
|||||||
: attachment.failed
|
: attachment.failed
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : attachment.working ? (
|
) : processing?.working ? (
|
||||||
<Loading size={16} />
|
<Loading size={16} />
|
||||||
) : (
|
) : (
|
||||||
<FileIcon size={16} />
|
<FileIcon size={16} />
|
||||||
@@ -185,14 +190,12 @@ export function Attachment({
|
|||||||
textOverflow: "ellipsis"
|
textOverflow: "ellipsis"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{attachment.metadata.filename}
|
{item.filename}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</td>
|
</td>
|
||||||
<Text as="td" variant="body">
|
<Text as="td" variant="body">
|
||||||
{attachment.isDeleting ? (
|
{item.dateUploaded ? (
|
||||||
<Loading sx={{ flexShrink: 0 }} size={16} title={"Deleting.."} />
|
|
||||||
) : attachment.dateUploaded ? (
|
|
||||||
<DoubleCheckmark
|
<DoubleCheckmark
|
||||||
sx={{ flexShrink: 0 }}
|
sx={{ flexShrink: 0 }}
|
||||||
color={"accent"}
|
color={"accent"}
|
||||||
@@ -217,13 +220,13 @@ export function Attachment({
|
|||||||
{formatBytes(status.loaded, 1)}/{formatBytes(status.total, 1)}
|
{formatBytes(status.loaded, 1)}/{formatBytes(status.total, 1)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
formatBytes(attachment.length, compact ? 1 : 2)
|
formatBytes(item.size, compact ? 1 : 2)
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
{!compact && (
|
{!compact && (
|
||||||
<Text as="td" variant="body">
|
<Text as="td" variant="body">
|
||||||
{attachment.dateUploaded
|
{item.dateUploaded
|
||||||
? getFormattedDate(attachment.dateUploaded, "date")
|
? getFormattedDate(item.dateUploaded, "date")
|
||||||
: "-"}
|
: "-"}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@@ -232,37 +235,28 @@ export function Attachment({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentMenuItems: (
|
const AttachmentMenuItems: (
|
||||||
attachment: any,
|
attachment: AttachmentType,
|
||||||
status?: AttachmentProgressStatus
|
status?: AttachmentProgressStatus
|
||||||
) => MenuItem[] = (attachment, status) => {
|
) => MenuItem[] = (attachment, status) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "button",
|
|
||||||
key: "notes",
|
key: "notes",
|
||||||
title: "Notes",
|
type: "lazy-loader",
|
||||||
icon: References.path,
|
async items() {
|
||||||
menu: {
|
const menuItems: MenuItem[] = [];
|
||||||
items: (attachment.noteIds as string[]).reduce((prev, curr) => {
|
for await (const note of db.relations.from(attachment, "note")
|
||||||
const note = db.notes.note(curr);
|
.selector) {
|
||||||
if (!note)
|
menuItems.push({
|
||||||
prev.push({
|
type: "button",
|
||||||
type: "button",
|
key: note.id,
|
||||||
key: curr,
|
title: note.title,
|
||||||
title: `Note with id ${curr}`,
|
onClick: () => {
|
||||||
onClick: () => showToast("error", "This note does not exist.")
|
hashNavigate(`/notes/${note.id}/edit`);
|
||||||
});
|
closeOpenedDialog();
|
||||||
else
|
}
|
||||||
prev.push({
|
});
|
||||||
type: "button",
|
}
|
||||||
key: note.id,
|
return menuItems;
|
||||||
title: note.title,
|
|
||||||
onClick: () => {
|
|
||||||
hashNavigate(`/notes/${curr}/edit`);
|
|
||||||
closeOpenedDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return prev;
|
|
||||||
}, [] as MenuItem[])
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -272,7 +266,7 @@ const AttachmentMenuItems: (
|
|||||||
icon: DoubleCheckmark.path,
|
icon: DoubleCheckmark.path,
|
||||||
isDisabled: !attachment.dateUploaded,
|
isDisabled: !attachment.dateUploaded,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
await store.recheck([attachment.metadata.hash]);
|
await store.recheck([attachment.id]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -283,11 +277,11 @@ const AttachmentMenuItems: (
|
|||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const newName = await showPromptDialog({
|
const newName = await showPromptDialog({
|
||||||
title: "Rename attachment",
|
title: "Rename attachment",
|
||||||
description: attachment.metadata.filename,
|
description: attachment.filename,
|
||||||
defaultValue: attachment.metadata.filename
|
defaultValue: attachment.filename
|
||||||
});
|
});
|
||||||
if (!newName) return;
|
if (!newName) return;
|
||||||
await store.rename(attachment.metadata.hash, newName);
|
await store.rename(attachment.hash, newName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -298,8 +292,8 @@ const AttachmentMenuItems: (
|
|||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const isDownloading = status?.type === "download";
|
const isDownloading = status?.type === "download";
|
||||||
if (isDownloading) {
|
if (isDownloading) {
|
||||||
await db.fs().cancel(attachment.metadata.hash, "download");
|
await db.fs().cancel(attachment.hash, "download");
|
||||||
} else await saveAttachment(attachment.metadata.hash);
|
} else await saveAttachment(attachment.hash);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -310,12 +304,8 @@ const AttachmentMenuItems: (
|
|||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const isDownloading = status?.type === "upload";
|
const isDownloading = status?.type === "upload";
|
||||||
if (isDownloading) {
|
if (isDownloading) {
|
||||||
await db.fs().cancel(attachment.metadata.hash, "upload");
|
await db.fs().cancel(attachment.hash, "upload");
|
||||||
} else
|
} else await reuploadAttachment(attachment.type, attachment.hash);
|
||||||
await reuploadAttachment(
|
|
||||||
attachment.metadata.type,
|
|
||||||
attachment.metadata.hash
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -324,7 +314,7 @@ const AttachmentMenuItems: (
|
|||||||
variant: "dangerous",
|
variant: "dangerous",
|
||||||
title: "Delete permanently",
|
title: "Delete permanently",
|
||||||
icon: DeleteForver.path,
|
icon: DeleteForver.path,
|
||||||
onClick: () => Multiselect.deleteAttachments([attachment])
|
onClick: () => Multiselect.deleteAttachments([attachment.id])
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ function CachedRouter() {
|
|||||||
sx={{
|
sx={{
|
||||||
display: key === RouteResult.key ? "flex" : "none",
|
display: key === RouteResult.key ? "flex" : "none",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
flex: 1
|
flex: 1,
|
||||||
|
overflow: "hidden"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Component key={key} {...RouteResult.props} />
|
<Component key={key} {...RouteResult.props} />
|
||||||
|
|||||||
@@ -62,15 +62,10 @@ import {
|
|||||||
isDeleted
|
isDeleted
|
||||||
} from "@notesnook/core/dist/types";
|
} from "@notesnook/core/dist/types";
|
||||||
import { isEncryptedContent } from "@notesnook/core/dist/collections/content";
|
import { isEncryptedContent } from "@notesnook/core/dist/collections/content";
|
||||||
|
import { PreviewSession } from "./types";
|
||||||
|
|
||||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||||
|
|
||||||
type PreviewSession = {
|
|
||||||
content: { data: string; type: ContentType };
|
|
||||||
dateCreated: number;
|
|
||||||
dateEdited: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DocumentPreview = {
|
type DocumentPreview = {
|
||||||
url?: string;
|
url?: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Attachment } from "@notesnook/editor";
|
import { Attachment } from "@notesnook/editor";
|
||||||
|
import { ContentType } from "@notesnook/core";
|
||||||
|
|
||||||
export type NoteStatistics = {
|
export type NoteStatistics = {
|
||||||
words: {
|
words: {
|
||||||
@@ -37,3 +38,9 @@ export interface IEditor {
|
|||||||
attachFile: (file: Attachment) => void;
|
attachFile: (file: Attachment) => void;
|
||||||
sendAttachmentProgress: (hash: string, progress: number) => void;
|
sendAttachmentProgress: (hash: string, progress: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PreviewSession = {
|
||||||
|
content: { data: string; type: ContentType };
|
||||||
|
dateCreated: number;
|
||||||
|
dateEdited: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -17,15 +17,9 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Flex, Button } from "@theme-ui/components";
|
import { Flex, Button } from "@theme-ui/components";
|
||||||
import { Plus } from "../icons";
|
import { Plus } from "../icons";
|
||||||
import {
|
|
||||||
ItemProps,
|
|
||||||
ScrollerProps,
|
|
||||||
Virtuoso,
|
|
||||||
VirtuosoHandle
|
|
||||||
} from "react-virtuoso";
|
|
||||||
import {
|
import {
|
||||||
useStore as useSelectionStore,
|
useStore as useSelectionStore,
|
||||||
store as selectionStore
|
store as selectionStore
|
||||||
@@ -34,31 +28,17 @@ import GroupHeader from "../group-header";
|
|||||||
import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles";
|
import { DEFAULT_ITEM_HEIGHT, ListItemWrapper } from "./list-profiles";
|
||||||
import Announcements from "../announcements";
|
import Announcements from "../announcements";
|
||||||
import { ListLoader } from "../loaders/list-loader";
|
import { ListLoader } from "../loaders/list-loader";
|
||||||
import ScrollContainer from "../scroll-container";
|
import { FlexScrollContainer } from "../scroll-container";
|
||||||
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
import { useKeyboardListNavigation } from "../../hooks/use-keyboard-list-navigation";
|
||||||
import { Context } from "./types";
|
import { Context } from "./types";
|
||||||
import {
|
import {
|
||||||
VirtualizedGrouping,
|
VirtualizedGrouping,
|
||||||
GroupHeader as GroupHeaderType,
|
|
||||||
GroupingKey,
|
GroupingKey,
|
||||||
Item,
|
Item,
|
||||||
isGroupHeader
|
isGroupHeader
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
|
import { VirtualizedList } from "../virtualized-list";
|
||||||
export const CustomScrollbarsVirtualList = forwardRef<
|
import { Virtualizer } from "@tanstack/react-virtual";
|
||||||
HTMLDivElement,
|
|
||||||
ScrollerProps
|
|
||||||
>(function CustomScrollbarsVirtualList(props, ref) {
|
|
||||||
return (
|
|
||||||
<ScrollContainer
|
|
||||||
{...props}
|
|
||||||
forwardedRef={(sRef) => {
|
|
||||||
if (typeof ref === "function") ref(sRef);
|
|
||||||
else if (ref) ref.current = sRef;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
type ListContainerProps = {
|
type ListContainerProps = {
|
||||||
group?: GroupingKey;
|
group?: GroupingKey;
|
||||||
@@ -87,8 +67,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
(store) => store.toggleSelectionMode
|
(store) => store.toggleSelectionMode
|
||||||
);
|
);
|
||||||
|
|
||||||
const listRef = useRef<VirtuosoHandle>(null);
|
const listRef = useRef<Virtualizer<Element, Element>>();
|
||||||
const listContainerRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -125,7 +104,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex variant="columnFill">
|
<Flex variant="columnFill" sx={{ overflow: "hidden" }}>
|
||||||
{!props.items.ids.length && props.placeholder ? (
|
{!props.items.ids.length && props.placeholder ? (
|
||||||
<>
|
<>
|
||||||
{header}
|
{header}
|
||||||
@@ -139,29 +118,25 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<FlexScrollContainer
|
||||||
ref={listContainerRef}
|
style={{ display: "flex", flexDirection: "column", flex: 1 }}
|
||||||
variant="columnFill"
|
|
||||||
data-test-id={`${group}-list`}
|
data-test-id={`${group}-list`}
|
||||||
>
|
>
|
||||||
<Virtuoso
|
{header ? header : <Announcements />}
|
||||||
ref={listRef}
|
<VirtualizedList
|
||||||
data={items.ids}
|
virtualizerRef={listRef}
|
||||||
computeItemKey={(index) => items.getKey(index)}
|
estimatedSize={DEFAULT_ITEM_HEIGHT}
|
||||||
defaultItemHeight={DEFAULT_ITEM_HEIGHT}
|
getItemKey={(index) => items.getKey(index)}
|
||||||
totalCount={items.ids.length}
|
items={items.ids}
|
||||||
|
mode="dynamic"
|
||||||
|
tabIndex={-1}
|
||||||
onBlur={() => setFocusedGroupIndex(-1)}
|
onBlur={() => setFocusedGroupIndex(-1)}
|
||||||
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
||||||
components={{
|
itemWrapperProps={(_, index) => ({
|
||||||
Scroller: CustomScrollbarsVirtualList,
|
onFocus: () => onFocus(index),
|
||||||
Item: VirtuosoItem,
|
onMouseDown: (e) => onMouseDown(e.nativeEvent, index)
|
||||||
Header: () => (header ? header : <Announcements />)
|
})}
|
||||||
}}
|
renderItem={({ index, item }) => {
|
||||||
context={{
|
|
||||||
onMouseDown,
|
|
||||||
onFocus
|
|
||||||
}}
|
|
||||||
itemContent={(index, item) => {
|
|
||||||
if (isGroupHeader(item)) {
|
if (isGroupHeader(item)) {
|
||||||
if (!group) return null;
|
if (!group) return null;
|
||||||
return (
|
return (
|
||||||
@@ -197,8 +172,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
(v) => isGroupHeader(v) && v.title === title
|
(v) => isGroupHeader(v) && v.title === title
|
||||||
);
|
);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
listRef.current?.scrollToIndex({
|
listRef.current?.scrollToIndex(index, {
|
||||||
index,
|
|
||||||
align: "center",
|
align: "center",
|
||||||
behavior: "auto"
|
behavior: "auto"
|
||||||
});
|
});
|
||||||
@@ -220,7 +194,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</FlexScrollContainer>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{button && (
|
{button && (
|
||||||
@@ -250,29 +224,6 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
}
|
}
|
||||||
export default ListContainer;
|
export default ListContainer;
|
||||||
|
|
||||||
function VirtuosoItem({
|
|
||||||
item: _item,
|
|
||||||
context,
|
|
||||||
...props
|
|
||||||
}: ItemProps<string | GroupHeaderType> & {
|
|
||||||
context?: {
|
|
||||||
onMouseDown: (e: MouseEvent, itemIndex: number) => void;
|
|
||||||
onFocus: (itemIndex: number) => void;
|
|
||||||
};
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
onFocus={() => context?.onFocus(props["data-item-index"])}
|
|
||||||
onMouseDown={(e) =>
|
|
||||||
context?.onMouseDown(e.nativeEvent, props["data-item-index"])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the element at the specified index into view and
|
* Scroll the element at the specified index into view and
|
||||||
* wait until it renders into the DOM. This function keeps
|
* wait until it renders into the DOM. This function keeps
|
||||||
@@ -281,29 +232,28 @@ function VirtuosoItem({
|
|||||||
* 50ms interval.
|
* 50ms interval.
|
||||||
*/
|
*/
|
||||||
function waitForElement(
|
function waitForElement(
|
||||||
list: VirtuosoHandle,
|
list: Virtualizer<Element, Element>,
|
||||||
index: number,
|
index: number,
|
||||||
elementId: string,
|
elementId: string,
|
||||||
callback: (element: HTMLElement) => void
|
callback: (element: HTMLElement) => void
|
||||||
) {
|
) {
|
||||||
let waitInterval = 0;
|
let waitInterval = 0;
|
||||||
let maxAttempts = 3;
|
let maxAttempts = 3;
|
||||||
list.scrollIntoView({
|
list.scrollToIndex(index);
|
||||||
index,
|
function scrollDone() {
|
||||||
done: function scrollDone() {
|
if (!maxAttempts) return;
|
||||||
if (!maxAttempts) return;
|
clearTimeout(waitInterval);
|
||||||
clearTimeout(waitInterval);
|
|
||||||
|
|
||||||
const element = document.getElementById(elementId);
|
const element = document.getElementById(elementId);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
--maxAttempts;
|
--maxAttempts;
|
||||||
waitInterval = setTimeout(() => {
|
waitInterval = setTimeout(() => {
|
||||||
scrollDone();
|
scrollDone();
|
||||||
}, 50) as unknown as number;
|
}, 50) as unknown as number;
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
callback(element);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
callback(element);
|
||||||
|
}
|
||||||
|
scrollDone();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,121 +23,70 @@ import Tag from "../tag";
|
|||||||
import TrashItem from "../trash-item";
|
import TrashItem from "../trash-item";
|
||||||
import { db } from "../../common/db";
|
import { db } from "../../common/db";
|
||||||
import Reminder from "../reminder";
|
import Reminder from "../reminder";
|
||||||
import {
|
import { Context } from "./types";
|
||||||
Context,
|
|
||||||
TagsWithDateEdited,
|
|
||||||
WithDateEdited,
|
|
||||||
NotebooksWithDateEdited
|
|
||||||
} from "./types";
|
|
||||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||||
import {
|
import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
|
||||||
GroupingKey,
|
import { Attachment } from "../attachment";
|
||||||
Item,
|
import { isNoteResolvedData, useResolvedItem } from "./resolved-item";
|
||||||
VirtualizedGrouping,
|
|
||||||
Color,
|
|
||||||
Reminder as ReminderItem
|
|
||||||
} from "@notesnook/core";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import SubNotebook from "../sub-notebook";
|
|
||||||
|
|
||||||
const SINGLE_LINE_HEIGHT = 1.4;
|
const SINGLE_LINE_HEIGHT = 1.4;
|
||||||
const DEFAULT_LINE_HEIGHT =
|
const DEFAULT_LINE_HEIGHT =
|
||||||
(document.getElementById("p")?.clientHeight || 16) - 1;
|
(document.getElementById("p")?.clientHeight || 16) - 1;
|
||||||
export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 2 * DEFAULT_LINE_HEIGHT;
|
export const DEFAULT_ITEM_HEIGHT = SINGLE_LINE_HEIGHT * 4 * DEFAULT_LINE_HEIGHT;
|
||||||
|
|
||||||
type ListItemWrapperProps<TItem = Item> = {
|
type ListItemWrapperProps = {
|
||||||
group?: GroupingKey;
|
group?: GroupingKey;
|
||||||
items: VirtualizedGrouping<TItem>;
|
items: VirtualizedGrouping<Item>;
|
||||||
id: string;
|
id: string;
|
||||||
context?: Context;
|
context?: Context;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
|
simplified?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ListItemWrapper(props: ListItemWrapperProps) {
|
export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||||
const { id, items, group, compact, context } = props;
|
const { group, compact, context, simplified } = props;
|
||||||
const [item, setItem] = useState<Item>();
|
|
||||||
const tags = useRef<TagsWithDateEdited>();
|
|
||||||
const notebooks = useRef<NotebooksWithDateEdited>();
|
|
||||||
const reminder = useRef<ReminderItem>();
|
|
||||||
const color = useRef<Color>();
|
|
||||||
const totalNotes = useRef<number>(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const resolvedItem = useResolvedItem(props);
|
||||||
(async function () {
|
if (!resolvedItem)
|
||||||
const { item, data } = (await items.item(id, resolveItems)) || {};
|
|
||||||
if (!item) return;
|
|
||||||
if (item.type === "note" && isNoteResolvedData(data)) {
|
|
||||||
tags.current = data.tags;
|
|
||||||
notebooks.current = data.notebooks;
|
|
||||||
reminder.current = data.reminder;
|
|
||||||
color.current = data.color;
|
|
||||||
} else if (item.type === "notebook" && typeof data === "number") {
|
|
||||||
totalNotes.current = data;
|
|
||||||
} else if (item.type === "tag" && typeof data === "number") {
|
|
||||||
totalNotes.current = data;
|
|
||||||
}
|
|
||||||
setItem(item);
|
|
||||||
})();
|
|
||||||
}, [id, items]);
|
|
||||||
|
|
||||||
if (!item)
|
|
||||||
return <div style={{ height: DEFAULT_ITEM_HEIGHT, width: "100%" }} />;
|
return <div style={{ height: DEFAULT_ITEM_HEIGHT, width: "100%" }} />;
|
||||||
|
|
||||||
const { type } = item;
|
const { data, item } = resolvedItem;
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case "note": {
|
case "note": {
|
||||||
return (
|
return (
|
||||||
<Note
|
<Note
|
||||||
compact={compact}
|
compact={compact}
|
||||||
item={item}
|
item={item}
|
||||||
tags={tags.current}
|
|
||||||
color={color.current}
|
|
||||||
notebooks={notebooks.current}
|
|
||||||
reminder={reminder.current}
|
|
||||||
date={getDate(item, group)}
|
date={getDate(item, group)}
|
||||||
context={context}
|
context={context}
|
||||||
|
{...(isNoteResolvedData(data) ? data : {})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "notebook":
|
case "notebook":
|
||||||
if (context?.type === "notebook")
|
|
||||||
return (
|
|
||||||
<SubNotebook
|
|
||||||
item={item}
|
|
||||||
totalNotes={totalNotes.current}
|
|
||||||
notebookId={context.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Notebook
|
<Notebook
|
||||||
item={item}
|
item={item}
|
||||||
totalNotes={totalNotes.current}
|
totalNotes={typeof data === "number" ? data : 0}
|
||||||
date={getDate(item, group)}
|
date={getDate(item, group)}
|
||||||
|
simplified={simplified}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "trash":
|
case "trash":
|
||||||
return <TrashItem item={item} date={getDate(item, type)} />;
|
return <TrashItem item={item} date={getDate(item, group)} />;
|
||||||
case "reminder":
|
case "reminder":
|
||||||
return <Reminder item={item} />;
|
return <Reminder item={item} simplified={simplified} />;
|
||||||
case "tag":
|
case "tag":
|
||||||
return <Tag item={item} totalNotes={totalNotes.current} />;
|
return (
|
||||||
|
<Tag item={item} totalNotes={typeof data === "number" ? data : 0} />
|
||||||
|
);
|
||||||
|
case "attachment":
|
||||||
|
return <Attachment item={item} compact={compact} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function withDateEdited<
|
|
||||||
T extends { dateEdited: number } | { dateModified: number }
|
|
||||||
>(items: T[]): WithDateEdited<T> {
|
|
||||||
let latestDateEdited = 0;
|
|
||||||
items.forEach((item) => {
|
|
||||||
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
|
||||||
if (latestDateEdited < date) latestDateEdited = date;
|
|
||||||
});
|
|
||||||
return { dateEdited: latestDateEdited, items };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDate(item: Item, groupType?: GroupingKey): number {
|
function getDate(item: Item, groupType?: GroupingKey): number {
|
||||||
return (
|
return (
|
||||||
getSortValue(
|
getSortValue(
|
||||||
@@ -152,123 +101,3 @@ function getDate(item: Item, groupType?: GroupingKey): number {
|
|||||||
) || 0
|
) || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveItems(ids: string[], items: Record<string, Item>) {
|
|
||||||
const { type } = items[ids[0]];
|
|
||||||
if (type === "note") return resolveNotes(ids);
|
|
||||||
else if (type === "notebook") {
|
|
||||||
const data: Record<string, number> = {};
|
|
||||||
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
|
|
||||||
return data;
|
|
||||||
} else if (type === "tag") {
|
|
||||||
const data: Record<string, number> = {};
|
|
||||||
for (const id of ids)
|
|
||||||
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
type NoteResolvedData = {
|
|
||||||
notebooks?: NotebooksWithDateEdited;
|
|
||||||
reminder?: ReminderItem;
|
|
||||||
color?: Color;
|
|
||||||
tags?: TagsWithDateEdited;
|
|
||||||
};
|
|
||||||
async function resolveNotes(ids: string[]) {
|
|
||||||
console.time("relations");
|
|
||||||
const relations = [
|
|
||||||
...(await db.relations
|
|
||||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
|
||||||
.get()),
|
|
||||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
|
||||||
];
|
|
||||||
console.timeEnd("relations");
|
|
||||||
console.log(
|
|
||||||
relations,
|
|
||||||
ids,
|
|
||||||
await db.relations
|
|
||||||
.from({ type: "notebook", id: "6549b4c373c7f3a40852f80c" }, "note")
|
|
||||||
.get()
|
|
||||||
);
|
|
||||||
const relationIds: {
|
|
||||||
notebooks: Set<string>;
|
|
||||||
colors: Set<string>;
|
|
||||||
tags: Set<string>;
|
|
||||||
reminders: Set<string>;
|
|
||||||
} = {
|
|
||||||
colors: new Set(),
|
|
||||||
notebooks: new Set(),
|
|
||||||
tags: new Set(),
|
|
||||||
reminders: new Set()
|
|
||||||
};
|
|
||||||
|
|
||||||
const grouped: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
notebooks: string[];
|
|
||||||
color?: string;
|
|
||||||
tags: string[];
|
|
||||||
reminder?: string;
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
for (const relation of relations) {
|
|
||||||
const noteId =
|
|
||||||
relation.toType === "relation" ? relation.fromId : relation.toId;
|
|
||||||
const data = grouped[noteId] || {
|
|
||||||
notebooks: [],
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if (relation.toType === "relation" && !data.reminder) {
|
|
||||||
data.reminder = relation.fromId;
|
|
||||||
relationIds.reminders.add(relation.fromId);
|
|
||||||
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
|
|
||||||
data.notebooks.push(relation.fromId);
|
|
||||||
relationIds.notebooks.add(relation.fromId);
|
|
||||||
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
|
||||||
data.tags.push(relation.fromId);
|
|
||||||
relationIds.tags.add(relation.fromId);
|
|
||||||
} else if (relation.fromType === "color" && !data.color) {
|
|
||||||
data.color = relation.fromId;
|
|
||||||
relationIds.colors.add(relation.fromId);
|
|
||||||
}
|
|
||||||
grouped[relation.toId] = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.time("resolve");
|
|
||||||
const resolved = {
|
|
||||||
notebooks: await db.notebooks.all.records(
|
|
||||||
Array.from(relationIds.notebooks)
|
|
||||||
),
|
|
||||||
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
|
||||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
|
||||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
|
||||||
};
|
|
||||||
console.timeEnd("resolve");
|
|
||||||
|
|
||||||
const data: Record<string, NoteResolvedData> = {};
|
|
||||||
for (const noteId in grouped) {
|
|
||||||
const group = grouped[noteId];
|
|
||||||
data[noteId] = {
|
|
||||||
color: group.color ? resolved.colors[group.color] : undefined,
|
|
||||||
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
|
||||||
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
|
|
||||||
notebooks: withDateEdited(
|
|
||||||
group.notebooks.map((id) => resolved.notebooks[id])
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
|
||||||
return (
|
|
||||||
typeof data === "object" &&
|
|
||||||
!!data &&
|
|
||||||
"notebooks" in data &&
|
|
||||||
"reminder" in data &&
|
|
||||||
"color" in data &&
|
|
||||||
"tags" in data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
174
apps/web/src/components/list-container/resolved-item.tsx
Normal file
174
apps/web/src/components/list-container/resolved-item.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Color, Item, Reminder, VirtualizedGrouping } from "@notesnook/core";
|
||||||
|
import usePromise from "../../hooks/use-promise";
|
||||||
|
import {
|
||||||
|
NotebooksWithDateEdited,
|
||||||
|
TagsWithDateEdited,
|
||||||
|
WithDateEdited
|
||||||
|
} from "./types";
|
||||||
|
import { db } from "../../common/db";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type ResolvedItemProps = {
|
||||||
|
items: VirtualizedGrouping<Item>;
|
||||||
|
id: string;
|
||||||
|
children: (item: { item: Item; data: unknown }) => React.ReactNode;
|
||||||
|
};
|
||||||
|
export function ResolvedItem(props: ResolvedItemProps) {
|
||||||
|
const { id, items, children } = props;
|
||||||
|
const result = usePromise(() => items.item(id, resolveItems), [id, items]);
|
||||||
|
|
||||||
|
if (result.status !== "fulfilled" || !result.value) return null;
|
||||||
|
|
||||||
|
return <>{children(result.value)}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResolvedItem(props: Omit<ResolvedItemProps, "children">) {
|
||||||
|
const { id, items } = props;
|
||||||
|
const result = usePromise(() => items.item(id, resolveItems), [id, items]);
|
||||||
|
|
||||||
|
if (result.status !== "fulfilled" || !result.value) return null;
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withDateEdited<
|
||||||
|
T extends { dateEdited: number } | { dateModified: number }
|
||||||
|
>(items: T[]): WithDateEdited<T> {
|
||||||
|
let latestDateEdited = 0;
|
||||||
|
items.forEach((item) => {
|
||||||
|
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
||||||
|
if (latestDateEdited < date) latestDateEdited = date;
|
||||||
|
});
|
||||||
|
return { dateEdited: latestDateEdited, items };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveItems(ids: string[], items: Record<string, Item>) {
|
||||||
|
const { type } = items[ids[0]];
|
||||||
|
if (type === "note") return resolveNotes(ids);
|
||||||
|
else if (type === "notebook") {
|
||||||
|
const data: Record<string, number> = {};
|
||||||
|
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
|
||||||
|
return data;
|
||||||
|
} else if (type === "tag") {
|
||||||
|
const data: Record<string, number> = {};
|
||||||
|
for (const id of ids)
|
||||||
|
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoteResolvedData = {
|
||||||
|
notebooks?: NotebooksWithDateEdited;
|
||||||
|
reminder?: Reminder;
|
||||||
|
color?: Color;
|
||||||
|
tags?: TagsWithDateEdited;
|
||||||
|
};
|
||||||
|
async function resolveNotes(ids: string[]) {
|
||||||
|
console.time("relations");
|
||||||
|
const relations = [
|
||||||
|
...(await db.relations
|
||||||
|
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
||||||
|
.get()),
|
||||||
|
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
||||||
|
];
|
||||||
|
console.timeEnd("relations");
|
||||||
|
const relationIds: {
|
||||||
|
notebooks: Set<string>;
|
||||||
|
colors: Set<string>;
|
||||||
|
tags: Set<string>;
|
||||||
|
reminders: Set<string>;
|
||||||
|
} = {
|
||||||
|
colors: new Set(),
|
||||||
|
notebooks: new Set(),
|
||||||
|
tags: new Set(),
|
||||||
|
reminders: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
const grouped: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
notebooks: string[];
|
||||||
|
color?: string;
|
||||||
|
tags: string[];
|
||||||
|
reminder?: string;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
for (const relation of relations) {
|
||||||
|
const noteId =
|
||||||
|
relation.toType === "reminder" ? relation.fromId : relation.toId;
|
||||||
|
const data = grouped[noteId] || {
|
||||||
|
notebooks: [],
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (relation.toType === "reminder" && !data.reminder) {
|
||||||
|
data.reminder = relation.toId;
|
||||||
|
relationIds.reminders.add(relation.toId);
|
||||||
|
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
|
||||||
|
data.notebooks.push(relation.fromId);
|
||||||
|
relationIds.notebooks.add(relation.fromId);
|
||||||
|
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
||||||
|
data.tags.push(relation.fromId);
|
||||||
|
relationIds.tags.add(relation.fromId);
|
||||||
|
} else if (relation.fromType === "color" && !data.color) {
|
||||||
|
data.color = relation.fromId;
|
||||||
|
relationIds.colors.add(relation.fromId);
|
||||||
|
}
|
||||||
|
grouped[noteId] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.time("resolve");
|
||||||
|
const resolved = {
|
||||||
|
notebooks: await db.notebooks.all.records(
|
||||||
|
Array.from(relationIds.notebooks)
|
||||||
|
),
|
||||||
|
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
||||||
|
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
||||||
|
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
||||||
|
};
|
||||||
|
console.timeEnd("resolve");
|
||||||
|
|
||||||
|
const data: Record<string, NoteResolvedData> = {};
|
||||||
|
for (const noteId in grouped) {
|
||||||
|
const group = grouped[noteId];
|
||||||
|
data[noteId] = {
|
||||||
|
color: group.color ? resolved.colors[group.color] : undefined,
|
||||||
|
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
||||||
|
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
|
||||||
|
notebooks: withDateEdited(
|
||||||
|
group.notebooks.map((id) => resolved.notebooks[id])
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
||||||
|
return (
|
||||||
|
typeof data === "object" &&
|
||||||
|
!!data &&
|
||||||
|
"notebooks" in data &&
|
||||||
|
"reminder" in data &&
|
||||||
|
"color" in data &&
|
||||||
|
"tags" in data
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -183,7 +183,7 @@ function Note(props: NoteProps) {
|
|||||||
icon={Notebook}
|
icon={Notebook}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{reminder && isReminderActive(reminder) && (
|
{reminder && isReminderActive(reminder) ? (
|
||||||
<IconTag
|
<IconTag
|
||||||
icon={Reminder}
|
icon={Reminder}
|
||||||
text={getFormattedReminderTime(reminder, true)}
|
text={getFormattedReminderTime(reminder, true)}
|
||||||
@@ -197,7 +197,7 @@ function Note(props: NoteProps) {
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
footer={
|
footer={
|
||||||
|
|||||||
@@ -1,426 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of the Notesnook project (https://notesnook.com/)
|
|
||||||
|
|
||||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import {
|
|
||||||
Pin,
|
|
||||||
StarOutline,
|
|
||||||
Unlock,
|
|
||||||
Readonly,
|
|
||||||
SyncOff,
|
|
||||||
ArrowLeft,
|
|
||||||
Circle,
|
|
||||||
Checkmark,
|
|
||||||
Lock
|
|
||||||
} from "../icons";
|
|
||||||
import { Flex, Text } from "@theme-ui/components";
|
|
||||||
import { useStore, store } from "../../stores/editor-store";
|
|
||||||
import { COLORS } from "../../common/constants";
|
|
||||||
import { db } from "../../common/db";
|
|
||||||
import { useStore as useAppStore } from "../../stores/app-store";
|
|
||||||
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
|
|
||||||
import { store as noteStore } from "../../stores/note-store";
|
|
||||||
import { AnimatedFlex } from "../animated";
|
|
||||||
import Toggle from "./toggle";
|
|
||||||
import ScrollContainer from "../scroll-container";
|
|
||||||
import Vault from "../../common/vault";
|
|
||||||
import TimeAgo from "../time-ago";
|
|
||||||
import { Attachment } from "../attachment";
|
|
||||||
import { formatBytes } from "@notesnook/common";
|
|
||||||
import { getTotalSize } from "../../common/attachments";
|
|
||||||
import Notebook from "../notebook";
|
|
||||||
import Reminder from "../reminder";
|
|
||||||
import { getFormattedDate } from "@notesnook/common";
|
|
||||||
import { ScopedThemeProvider } from "../theme-provider";
|
|
||||||
|
|
||||||
const tools = [
|
|
||||||
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
|
|
||||||
{
|
|
||||||
key: "favorite",
|
|
||||||
property: "favorite",
|
|
||||||
icon: StarOutline,
|
|
||||||
label: "Favorite"
|
|
||||||
},
|
|
||||||
{ key: "lock", icon: Unlock, label: "Lock", property: "locked" },
|
|
||||||
{
|
|
||||||
key: "readonly",
|
|
||||||
icon: Readonly,
|
|
||||||
label: "Readonly",
|
|
||||||
property: "readonly"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "local-only",
|
|
||||||
icon: SyncOff,
|
|
||||||
label: "Disable sync",
|
|
||||||
property: "localOnly"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const metadataItems = [
|
|
||||||
{
|
|
||||||
key: "dateCreated",
|
|
||||||
label: "Created at",
|
|
||||||
value: (date) => getFormattedDate(date || Date.now())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "dateEdited",
|
|
||||||
label: "Last edited at",
|
|
||||||
value: (date) => (date ? getFormattedDate(date) : "never")
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function Properties(props) {
|
|
||||||
const { onOpenPreviewSession } = props;
|
|
||||||
|
|
||||||
const [versionHistory, setVersionHistory] = useState([]);
|
|
||||||
|
|
||||||
const toggleProperties = useStore((store) => store.toggleProperties);
|
|
||||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
|
||||||
const session = useStore((store) => store.session);
|
|
||||||
const attachments = useAttachmentStore((store) =>
|
|
||||||
store.attachments.filter((a) => a.noteIds.includes(session.id))
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
id: sessionId,
|
|
||||||
color,
|
|
||||||
notebooks = [],
|
|
||||||
sessionType,
|
|
||||||
dateCreated
|
|
||||||
} = session;
|
|
||||||
const isPreviewMode = sessionType === "preview";
|
|
||||||
const reminders = db.relations
|
|
||||||
.from({ id: session.id, type: "note" }, "reminder")
|
|
||||||
.resolved();
|
|
||||||
const allNotebooks = useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
...notebooks.map((ref) => db.notebooks.notebook(ref.id)?.data),
|
|
||||||
...db.relations.to({ id: sessionId, type: "note" }, "notebook")
|
|
||||||
].filter(Boolean),
|
|
||||||
[sessionId, notebooks]
|
|
||||||
);
|
|
||||||
|
|
||||||
const changeState = useCallback(
|
|
||||||
function changeState(prop) {
|
|
||||||
switch (prop) {
|
|
||||||
case "lock":
|
|
||||||
return store.get().session.locked
|
|
||||||
? noteStore.unlock(sessionId)
|
|
||||||
: noteStore.lock(sessionId);
|
|
||||||
case "readonly":
|
|
||||||
return noteStore.readonly(sessionId);
|
|
||||||
case "local-only":
|
|
||||||
return noteStore.localOnly(sessionId);
|
|
||||||
case "pin":
|
|
||||||
return noteStore.pin(sessionId);
|
|
||||||
case "favorite":
|
|
||||||
return noteStore.favorite(sessionId);
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[sessionId]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sessionId) return;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const history = await db.noteHistory.get(sessionId);
|
|
||||||
setVersionHistory(history);
|
|
||||||
})();
|
|
||||||
}, [sessionId]);
|
|
||||||
|
|
||||||
if (isFocusMode || !sessionId) return null;
|
|
||||||
return (
|
|
||||||
<AnimatedFlex
|
|
||||||
animate={{
|
|
||||||
x: 0
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 0.1,
|
|
||||||
bounceDamping: 1,
|
|
||||||
bounceStiffness: 1,
|
|
||||||
ease: "easeOut"
|
|
||||||
}}
|
|
||||||
initial={{ x: 600 }}
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
position: "absolute",
|
|
||||||
right: 0,
|
|
||||||
zIndex: 3,
|
|
||||||
height: "100%",
|
|
||||||
width: "300px",
|
|
||||||
borderLeft: "1px solid",
|
|
||||||
borderLeftColor: "border"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ScopedThemeProvider
|
|
||||||
scope="editorSidebar"
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
display: "flex",
|
|
||||||
bg: "background",
|
|
||||||
overflowY: "hidden",
|
|
||||||
overflowX: "hidden",
|
|
||||||
flexDirection: "column"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ScrollContainer>
|
|
||||||
<Card
|
|
||||||
title="Properties"
|
|
||||||
button={
|
|
||||||
<ArrowLeft
|
|
||||||
data-test-id="properties-close"
|
|
||||||
onClick={() => toggleProperties(false)}
|
|
||||||
size={18}
|
|
||||||
sx={{ mr: 1, cursor: "pointer" }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!isPreviewMode && (
|
|
||||||
<>
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<Toggle
|
|
||||||
{...tool}
|
|
||||||
key={tool.key}
|
|
||||||
toggleKey={tool.property}
|
|
||||||
onToggle={(state) => changeState(tool.key, state)}
|
|
||||||
testId={`properties-${tool.key}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{metadataItems.map((item) => (
|
|
||||||
<Flex
|
|
||||||
key={item.key}
|
|
||||||
py={2}
|
|
||||||
px={2}
|
|
||||||
sx={{
|
|
||||||
borderBottom: "1px solid var(--separator)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant="subBody" sx={{ fontSize: "body" }}>
|
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
className="selectable"
|
|
||||||
variant="subBody"
|
|
||||||
sx={{ fontSize: "body" }}
|
|
||||||
>
|
|
||||||
{item.value(session[item.key])}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
{!isPreviewMode && (
|
|
||||||
<>
|
|
||||||
<Flex
|
|
||||||
py={2}
|
|
||||||
px={2}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
justifyContent: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{COLORS.map((label) => (
|
|
||||||
<Flex
|
|
||||||
key={label}
|
|
||||||
onClick={() => noteStore.get().setColor(sessionId, label)}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
position: "relative",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
data-test-id={`properties-${label}`}
|
|
||||||
>
|
|
||||||
<Circle
|
|
||||||
size={35}
|
|
||||||
color={label.toLowerCase()}
|
|
||||||
data-test-id={`toggle-state-${
|
|
||||||
label.toLowerCase() === color?.toLowerCase()
|
|
||||||
? "on"
|
|
||||||
: "off"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
{label.toLowerCase() === color?.toLowerCase() && (
|
|
||||||
<Checkmark
|
|
||||||
color="white"
|
|
||||||
size={18}
|
|
||||||
sx={{ position: "absolute", left: "8px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
{allNotebooks?.length > 0 && (
|
|
||||||
<Card title="Notebooks">
|
|
||||||
{allNotebooks.map((notebook) => (
|
|
||||||
<Notebook
|
|
||||||
key={notebook.id}
|
|
||||||
item={notebook}
|
|
||||||
date={notebook.dateCreated}
|
|
||||||
totalNotes={0} // getTotalNotes(notebook)}
|
|
||||||
simplified
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
{reminders.length > 0 && (
|
|
||||||
<Card title="Reminders">
|
|
||||||
{reminders.map((reminder) => {
|
|
||||||
return (
|
|
||||||
<Reminder key={reminder.id} item={reminder} simplified />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{attachments.length > 0 && (
|
|
||||||
<Card
|
|
||||||
title="Attachments"
|
|
||||||
subtitle={`${attachments.length} attachments | ${formatBytes(
|
|
||||||
getTotalSize(attachments)
|
|
||||||
)} occupied`}
|
|
||||||
>
|
|
||||||
<table style={{ borderSpacing: 0 }}>
|
|
||||||
<tbody>
|
|
||||||
{attachments.map((attachment, i) => (
|
|
||||||
<Attachment
|
|
||||||
key={attachment.id}
|
|
||||||
compact
|
|
||||||
attachment={attachment}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
<Card
|
|
||||||
title="Previous Sessions"
|
|
||||||
subtitle={"Your session history is local only."}
|
|
||||||
>
|
|
||||||
{versionHistory.map((session) => {
|
|
||||||
const fromDate = getFormattedDate(session.dateCreated, "date");
|
|
||||||
const toDate = getFormattedDate(session.dateModified, "date");
|
|
||||||
const fromTime = getFormattedDate(session.dateCreated, "time");
|
|
||||||
const toTime = getFormattedDate(session.dateModified, "time");
|
|
||||||
const label = `${fromDate}, ${fromTime} — ${
|
|
||||||
fromDate !== toDate ? `${toDate}, ` : ""
|
|
||||||
}${toTime}`;
|
|
||||||
const isSelected =
|
|
||||||
isPreviewMode && session.dateCreated === dateCreated;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
key={session.id}
|
|
||||||
data-test-id={`session-item`}
|
|
||||||
py={1}
|
|
||||||
px={2}
|
|
||||||
sx={{
|
|
||||||
cursor: "pointer",
|
|
||||||
bg: isSelected ? "background-selected" : "transparent",
|
|
||||||
":hover": {
|
|
||||||
bg: isSelected ? "hover-selected" : "hover"
|
|
||||||
},
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between"
|
|
||||||
}}
|
|
||||||
title="Click to preview"
|
|
||||||
onClick={async () => {
|
|
||||||
toggleProperties(false);
|
|
||||||
const content = await db.noteHistory.content(session.id);
|
|
||||||
|
|
||||||
if (session.locked) {
|
|
||||||
await Vault.askPassword(async (password) => {
|
|
||||||
try {
|
|
||||||
const decryptedContent =
|
|
||||||
await db.vault.decryptContent(content, password);
|
|
||||||
onOpenPreviewSession({
|
|
||||||
content: decryptedContent,
|
|
||||||
dateCreated: session.dateCreated,
|
|
||||||
dateEdited: session.dateModified
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onOpenPreviewSession({
|
|
||||||
content,
|
|
||||||
dateCreated: session.dateCreated,
|
|
||||||
dateEdited: session.dateModified
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text variant={"body"} data-test-id="title">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
fontSize: "subBody",
|
|
||||||
color: "paragraph-secondary"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{session.locked && <Lock size={14} data-test-id="locked" />}
|
|
||||||
<TimeAgo
|
|
||||||
live
|
|
||||||
datetime={session.dateModified}
|
|
||||||
locale={"en_short"}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Card>
|
|
||||||
</ScrollContainer>
|
|
||||||
</ScopedThemeProvider>
|
|
||||||
</AnimatedFlex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default React.memo(Properties);
|
|
||||||
|
|
||||||
function Card({ title, subtitle, button, children }) {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
sx={{
|
|
||||||
borderRadius: "default",
|
|
||||||
flexDirection: "column"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex mx={2} mt={2} sx={{ alignItems: "center" }}>
|
|
||||||
{button}
|
|
||||||
<Text variant="subtitle">{title}</Text>
|
|
||||||
</Flex>
|
|
||||||
{subtitle && (
|
|
||||||
<Text variant="subBody" mb={1} mx={2}>
|
|
||||||
{subtitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
430
apps/web/src/components/properties/index.tsx
Normal file
430
apps/web/src/components/properties/index.tsx
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import {
|
||||||
|
Pin,
|
||||||
|
StarOutline,
|
||||||
|
Unlock,
|
||||||
|
Readonly,
|
||||||
|
SyncOff,
|
||||||
|
ArrowLeft,
|
||||||
|
Circle,
|
||||||
|
Checkmark
|
||||||
|
} from "../icons";
|
||||||
|
import { Flex, Text } from "@theme-ui/components";
|
||||||
|
import { useStore, store, EditorSession } from "../../stores/editor-store";
|
||||||
|
import { db } from "../../common/db";
|
||||||
|
import { useStore as useAppStore } from "../../stores/app-store";
|
||||||
|
import { store as noteStore } from "../../stores/note-store";
|
||||||
|
import { AnimatedFlex } from "../animated";
|
||||||
|
import Toggle from "./toggle";
|
||||||
|
import ScrollContainer from "../scroll-container";
|
||||||
|
import { getFormattedDate } from "@notesnook/common";
|
||||||
|
import { ScopedThemeProvider } from "../theme-provider";
|
||||||
|
import { PreviewSession } from "../editor/types";
|
||||||
|
import usePromise from "../../hooks/use-promise";
|
||||||
|
import { ListItemWrapper } from "../list-container/list-profiles";
|
||||||
|
import { VirtualizedList } from "../virtualized-list";
|
||||||
|
import { ResolvedItem } from "../list-container/resolved-item";
|
||||||
|
import { SessionItem } from "../session-item";
|
||||||
|
import { COLORS } from "../../common/constants";
|
||||||
|
import { DefaultColors } from "@notesnook/core";
|
||||||
|
|
||||||
|
const tools = [
|
||||||
|
{ key: "pin", property: "pinned", icon: Pin, label: "Pin" },
|
||||||
|
{
|
||||||
|
key: "favorite",
|
||||||
|
property: "favorite",
|
||||||
|
icon: StarOutline,
|
||||||
|
label: "Favorite"
|
||||||
|
},
|
||||||
|
{ key: "lock", icon: Unlock, label: "Lock", property: "locked" },
|
||||||
|
{
|
||||||
|
key: "readonly",
|
||||||
|
icon: Readonly,
|
||||||
|
label: "Readonly",
|
||||||
|
property: "readonly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "local-only",
|
||||||
|
icon: SyncOff,
|
||||||
|
label: "Disable sync",
|
||||||
|
property: "localOnly"
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type MetadataItem<T extends keyof EditorSession = keyof EditorSession> = {
|
||||||
|
key: T;
|
||||||
|
label: string;
|
||||||
|
value: (value: EditorSession[T]) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const metadataItems = [
|
||||||
|
{
|
||||||
|
key: "dateCreated",
|
||||||
|
label: "Created at",
|
||||||
|
value: (date) => getFormattedDate(date || Date.now())
|
||||||
|
} as MetadataItem<"dateCreated">,
|
||||||
|
{
|
||||||
|
key: "dateEdited",
|
||||||
|
label: "Last edited at",
|
||||||
|
value: (date) => (date ? getFormattedDate(date) : "never")
|
||||||
|
} as MetadataItem<"dateEdited">
|
||||||
|
];
|
||||||
|
|
||||||
|
type EditorPropertiesProps = {
|
||||||
|
onOpenPreviewSession: (session: PreviewSession) => void;
|
||||||
|
};
|
||||||
|
function EditorProperties(props: EditorPropertiesProps) {
|
||||||
|
const { onOpenPreviewSession } = props;
|
||||||
|
|
||||||
|
const toggleProperties = useStore((store) => store.toggleProperties);
|
||||||
|
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||||
|
const session = useStore((store) => store.session);
|
||||||
|
|
||||||
|
const { id: sessionId, sessionType, dateCreated } = session;
|
||||||
|
const isPreviewMode = sessionType === "preview";
|
||||||
|
|
||||||
|
if (isFocusMode || !sessionId) return null;
|
||||||
|
return (
|
||||||
|
<AnimatedFlex
|
||||||
|
animate={{
|
||||||
|
x: 0
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.1,
|
||||||
|
bounceDamping: 1,
|
||||||
|
bounceStiffness: 1,
|
||||||
|
ease: "easeOut"
|
||||||
|
}}
|
||||||
|
initial={{ x: 600 }}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
zIndex: 3,
|
||||||
|
height: "100%",
|
||||||
|
width: "300px",
|
||||||
|
borderLeft: "1px solid",
|
||||||
|
borderLeftColor: "border"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScopedThemeProvider
|
||||||
|
scope="editorSidebar"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
bg: "background",
|
||||||
|
overflowY: "hidden",
|
||||||
|
overflowX: "hidden",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollContainer>
|
||||||
|
<Section
|
||||||
|
title="Properties"
|
||||||
|
button={
|
||||||
|
<ArrowLeft
|
||||||
|
data-test-id="properties-close"
|
||||||
|
onClick={() => toggleProperties(false)}
|
||||||
|
size={18}
|
||||||
|
sx={{ mr: 1, cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!isPreviewMode && (
|
||||||
|
<>
|
||||||
|
{tools.map((tool) => (
|
||||||
|
<Toggle
|
||||||
|
{...tool}
|
||||||
|
key={tool.key}
|
||||||
|
toggleKey={tool.property}
|
||||||
|
onToggle={() => changeToggleState(tool.key)}
|
||||||
|
testId={`properties-${tool.key}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{metadataItems.map((item) => (
|
||||||
|
<Flex
|
||||||
|
key={item.key}
|
||||||
|
py={2}
|
||||||
|
px={2}
|
||||||
|
sx={{
|
||||||
|
borderBottom: "1px solid var(--separator)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="subBody" sx={{ fontSize: "body" }}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
className="selectable"
|
||||||
|
variant="subBody"
|
||||||
|
sx={{ fontSize: "body" }}
|
||||||
|
>
|
||||||
|
{item.value(session[item.key])}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
{!isPreviewMode && <Colors noteId={sessionId} />}
|
||||||
|
</Section>
|
||||||
|
<Notebooks noteId={sessionId} />
|
||||||
|
<Reminders noteId={sessionId} />
|
||||||
|
<Attachments noteId={sessionId} />
|
||||||
|
<SessionHistory
|
||||||
|
noteId={sessionId}
|
||||||
|
dateCreated={dateCreated || 0}
|
||||||
|
isPreviewMode={isPreviewMode}
|
||||||
|
onOpenPreviewSession={onOpenPreviewSession}
|
||||||
|
/>
|
||||||
|
</ScrollContainer>
|
||||||
|
</ScopedThemeProvider>
|
||||||
|
</AnimatedFlex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default React.memo(EditorProperties);
|
||||||
|
|
||||||
|
function Colors({ noteId }: { noteId: string }) {
|
||||||
|
const result = usePromise(async () =>
|
||||||
|
(
|
||||||
|
await db.relations.to({ id: noteId, type: "note" }, "color").resolve(1)
|
||||||
|
).at(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
py={2}
|
||||||
|
px={2}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
justifyContent: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{COLORS.map((label) => (
|
||||||
|
<Flex
|
||||||
|
key={label.key}
|
||||||
|
onClick={() => noteStore.get().setColor(label.key, noteId)}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
position: "relative",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
data-test-id={`properties-${label}`}
|
||||||
|
>
|
||||||
|
<Circle
|
||||||
|
size={35}
|
||||||
|
color={DefaultColors[label.key]}
|
||||||
|
data-test-id={`toggle-state-${
|
||||||
|
result.status === "fulfilled" &&
|
||||||
|
label.key === result.value?.colorCode
|
||||||
|
? "on"
|
||||||
|
: "off"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{result.status === "fulfilled" &&
|
||||||
|
label.key === result.value?.colorCode && (
|
||||||
|
<Checkmark
|
||||||
|
color="white"
|
||||||
|
size={18}
|
||||||
|
sx={{ position: "absolute", left: "8px" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Notebooks({ noteId }: { noteId: string }) {
|
||||||
|
const result = usePromise(() =>
|
||||||
|
db.relations
|
||||||
|
.to({ id: noteId, type: "note" }, "notebook")
|
||||||
|
.selector.sorted(db.settings.getGroupOptions("notebooks"))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status !== "fulfilled" || result.value.ids.length <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title="Notebooks">
|
||||||
|
<VirtualizedList
|
||||||
|
mode="fixed"
|
||||||
|
estimatedSize={50}
|
||||||
|
getItemKey={(index) => result.value.getKey(index)}
|
||||||
|
items={result.value.ids}
|
||||||
|
renderItem={(id) => (
|
||||||
|
<ListItemWrapper id={id as string} items={result.value} simplified />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Reminders({ noteId }: { noteId: string }) {
|
||||||
|
const result = usePromise(() =>
|
||||||
|
db.relations
|
||||||
|
.from({ id: noteId, type: "note" }, "reminder")
|
||||||
|
.selector.sorted(db.settings.getGroupOptions("reminders"))
|
||||||
|
);
|
||||||
|
if (result.status !== "fulfilled" || result.value.ids.length <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title="Reminders">
|
||||||
|
<VirtualizedList
|
||||||
|
mode="fixed"
|
||||||
|
estimatedSize={54}
|
||||||
|
getItemKey={(index) => result.value.getKey(index)}
|
||||||
|
items={result.value.ids}
|
||||||
|
renderItem={(id) => (
|
||||||
|
<ListItemWrapper id={id as string} items={result.value} simplified />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Attachments({ noteId }: { noteId: string }) {
|
||||||
|
const result = usePromise(() =>
|
||||||
|
db.attachments
|
||||||
|
.ofNote(noteId, "all")
|
||||||
|
.sorted({ sortBy: "dateCreated", sortDirection: "desc" })
|
||||||
|
);
|
||||||
|
if (result.status !== "fulfilled" || result.value.ids.length <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section title="Attachments">
|
||||||
|
{result.value.ids.map((id, index) => (
|
||||||
|
<ListItemWrapper
|
||||||
|
key={result.value.getKey(index)}
|
||||||
|
id={id as string}
|
||||||
|
items={result.value}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function SessionHistory({
|
||||||
|
noteId,
|
||||||
|
dateCreated,
|
||||||
|
isPreviewMode,
|
||||||
|
onOpenPreviewSession
|
||||||
|
}: {
|
||||||
|
noteId: string;
|
||||||
|
dateCreated: number;
|
||||||
|
isPreviewMode: boolean;
|
||||||
|
onOpenPreviewSession: (session: PreviewSession) => void;
|
||||||
|
}) {
|
||||||
|
const result = usePromise(() =>
|
||||||
|
db.noteHistory
|
||||||
|
.get(noteId)
|
||||||
|
.sorted({ sortBy: "dateModified", sortDirection: "desc" })
|
||||||
|
);
|
||||||
|
if (result.status !== "fulfilled" || result.value.ids.length <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title="Previous Sessions"
|
||||||
|
subtitle={"Your session history is local only."}
|
||||||
|
>
|
||||||
|
<VirtualizedList
|
||||||
|
mode="fixed"
|
||||||
|
estimatedSize={28}
|
||||||
|
getItemKey={(index) => result.value.getKey(index)}
|
||||||
|
items={result.value.ids}
|
||||||
|
renderItem={(id) => (
|
||||||
|
<ResolvedItem id={id as string} items={result.value}>
|
||||||
|
{({ item }) =>
|
||||||
|
item.type === "session" ? (
|
||||||
|
<SessionItem
|
||||||
|
noteId={noteId}
|
||||||
|
session={item}
|
||||||
|
dateCreated={dateCreated}
|
||||||
|
isPreviewMode={isPreviewMode}
|
||||||
|
onOpenPreviewSession={onOpenPreviewSession}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</ResolvedItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionProps = { title: string; subtitle?: string; button?: JSX.Element };
|
||||||
|
function Section({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
button,
|
||||||
|
children
|
||||||
|
}: PropsWithChildren<SectionProps>) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
borderRadius: "default",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex mx={2} mt={2} sx={{ alignItems: "center" }}>
|
||||||
|
{button}
|
||||||
|
<Text variant="subtitle">{title}</Text>
|
||||||
|
</Flex>
|
||||||
|
{subtitle && (
|
||||||
|
<Text variant="subBody" mb={1} mx={2}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeToggleState(
|
||||||
|
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite"
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
id: sessionId,
|
||||||
|
locked,
|
||||||
|
readonly,
|
||||||
|
localOnly,
|
||||||
|
pinned,
|
||||||
|
favorite
|
||||||
|
} = store.get().session;
|
||||||
|
if (!sessionId) return;
|
||||||
|
switch (prop) {
|
||||||
|
case "lock":
|
||||||
|
return locked ? noteStore.unlock(sessionId) : noteStore.lock(sessionId);
|
||||||
|
case "readonly":
|
||||||
|
return noteStore.readonly(!readonly, sessionId);
|
||||||
|
case "local-only":
|
||||||
|
return noteStore.localOnly(!localOnly, sessionId);
|
||||||
|
case "pin":
|
||||||
|
return noteStore.pin(!pinned, sessionId);
|
||||||
|
case "favorite":
|
||||||
|
return noteStore.favorite(!favorite, sessionId);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,10 +111,10 @@ export default React.memo(Reminder, (prev, next) => {
|
|||||||
return prev?.item?.title === next?.item?.title;
|
return prev?.item?.title === next?.item?.title;
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuItems: (
|
const menuItems: (reminder: ReminderType, items?: string[]) => MenuItem[] = (
|
||||||
reminder: ReminderType,
|
reminder,
|
||||||
items?: ReminderType[]
|
items = []
|
||||||
) => MenuItem[] = (reminder, items = []) => {
|
) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
|
|||||||
109
apps/web/src/components/session-item/index.tsx
Normal file
109
apps/web/src/components/session-item/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getFormattedDate } from "@notesnook/common";
|
||||||
|
import { HistorySession } from "@notesnook/core";
|
||||||
|
import { Flex, Text } from "@theme-ui/components";
|
||||||
|
import TimeAgo from "../time-ago";
|
||||||
|
import { Lock } from "../icons";
|
||||||
|
import Vault from "../../common/vault";
|
||||||
|
import { db } from "../../common/db";
|
||||||
|
import { PreviewSession } from "../editor/types";
|
||||||
|
|
||||||
|
type SessionItemProps = {
|
||||||
|
session: HistorySession;
|
||||||
|
isPreviewMode: boolean;
|
||||||
|
noteId: string;
|
||||||
|
dateCreated: number;
|
||||||
|
onOpenPreviewSession: (session: PreviewSession) => void;
|
||||||
|
};
|
||||||
|
export function SessionItem(props: SessionItemProps) {
|
||||||
|
const { session, isPreviewMode, dateCreated, noteId, onOpenPreviewSession } =
|
||||||
|
props;
|
||||||
|
const fromDate = getFormattedDate(session.dateCreated, "date");
|
||||||
|
const toDate = getFormattedDate(session.dateModified, "date");
|
||||||
|
const fromTime = getFormattedDate(session.dateCreated, "time");
|
||||||
|
const toTime = getFormattedDate(session.dateModified, "time");
|
||||||
|
const label = `${fromDate}, ${fromTime} — ${
|
||||||
|
fromDate !== toDate ? `${toDate}, ` : ""
|
||||||
|
}${toTime}`;
|
||||||
|
const isSelected = isPreviewMode && session.dateCreated === dateCreated;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
key={session.id}
|
||||||
|
data-test-id={`session-item`}
|
||||||
|
py={1}
|
||||||
|
px={2}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
bg: isSelected ? "background-selected" : "transparent",
|
||||||
|
":hover": {
|
||||||
|
bg: isSelected ? "hover-selected" : "hover"
|
||||||
|
},
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
|
title="Click to preview"
|
||||||
|
onClick={async () => {
|
||||||
|
const content = await db.noteHistory.content(session.id);
|
||||||
|
if (!content) return;
|
||||||
|
// toggleProperties(false);
|
||||||
|
if (session.locked) {
|
||||||
|
await Vault.askPassword(async (password) => {
|
||||||
|
try {
|
||||||
|
const decryptedContent = await db.vault.decryptContent(
|
||||||
|
content,
|
||||||
|
noteId,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
onOpenPreviewSession({
|
||||||
|
content: decryptedContent,
|
||||||
|
dateCreated: session.dateCreated,
|
||||||
|
dateEdited: session.dateModified
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onOpenPreviewSession({
|
||||||
|
content,
|
||||||
|
dateCreated: session.dateCreated,
|
||||||
|
dateEdited: session.dateModified
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant={"body"} data-test-id="title">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
fontSize: "subBody",
|
||||||
|
color: "paragraph-secondary"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.locked && <Lock size={14} data-test-id="locked" />}
|
||||||
|
<TimeAgo live datetime={session.dateModified} locale={"en_short"} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -66,7 +66,6 @@ export function ThemePreview(props: ThemePreviewProps) {
|
|||||||
theme.previewColors.background
|
theme.previewColors.background
|
||||||
].map((color) => (
|
].map((color) => (
|
||||||
<Circle
|
<Circle
|
||||||
key={color}
|
|
||||||
color={color}
|
color={color}
|
||||||
size={18}
|
size={18}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
121
apps/web/src/components/virtualized-grid/index.tsx
Normal file
121
apps/web/src/components/virtualized-grid/index.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { Box } from "@theme-ui/components";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type VirtualizedGridProps<T> = {
|
||||||
|
virtualizerRef?: React.MutableRefObject<
|
||||||
|
Virtualizer<Element, Element> | undefined
|
||||||
|
>;
|
||||||
|
mode?: "fixed" | "dynamic";
|
||||||
|
items: T[];
|
||||||
|
estimatedSize: number;
|
||||||
|
getItemKey: (index: number) => string;
|
||||||
|
scrollElement?: Element | null;
|
||||||
|
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
|
||||||
|
scrollMargin?: number;
|
||||||
|
columns: number;
|
||||||
|
onEndReached?: () => void;
|
||||||
|
};
|
||||||
|
export function VirtualizedGrid<T>(props: VirtualizedGridProps<T>) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
getItemKey,
|
||||||
|
scrollElement,
|
||||||
|
scrollMargin,
|
||||||
|
renderItem: Item,
|
||||||
|
estimatedSize,
|
||||||
|
mode,
|
||||||
|
virtualizerRef,
|
||||||
|
columns,
|
||||||
|
onEndReached
|
||||||
|
} = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
lanes: columns,
|
||||||
|
count: items.length,
|
||||||
|
estimateSize: () => estimatedSize,
|
||||||
|
getItemKey,
|
||||||
|
getScrollElement: () =>
|
||||||
|
scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||||
|
scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!virtualizer.scrollElement || !onEndReached) return;
|
||||||
|
function onScroll() {
|
||||||
|
if (!virtualizer.scrollElement) return;
|
||||||
|
const { clientHeight, scrollHeight, scrollTop } =
|
||||||
|
virtualizer.scrollElement;
|
||||||
|
const endThreshold = scrollHeight - clientHeight - 50;
|
||||||
|
if (scrollTop > endThreshold) {
|
||||||
|
onEndReached?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(virtualizer.scrollElement as HTMLElement)?.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
onScroll
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
(virtualizer.scrollElement as HTMLElement)?.removeEventListener(
|
||||||
|
"scroll",
|
||||||
|
onScroll
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [virtualizer.scrollElement, onEndReached]);
|
||||||
|
|
||||||
|
if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||||
|
|
||||||
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box ref={containerRef} className="Grid">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: virtualizer.getTotalSize(),
|
||||||
|
width: "100%",
|
||||||
|
position: "relative"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualItems.map((row) => (
|
||||||
|
<Box
|
||||||
|
key={row.key}
|
||||||
|
data-index={row.index}
|
||||||
|
ref={mode === "dynamic" ? virtualizer.measureElement : null}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: `${(100 / columns) * row.lane}%`,
|
||||||
|
width: `${100 / columns}%`,
|
||||||
|
height: mode === "dynamic" ? "unset" : `${row.size}px`,
|
||||||
|
transform: `translateY(${
|
||||||
|
row.start - virtualizer.options.scrollMargin
|
||||||
|
}px)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Item key={row.key} item={items[row.index]} index={row.index} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
apps/web/src/components/virtualized-list/index.tsx
Normal file
96
apps/web/src/components/virtualized-list/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { Box, BoxProps } from "@theme-ui/components";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
|
type VirtualizedListProps<T> = {
|
||||||
|
virtualizerRef?: React.MutableRefObject<
|
||||||
|
Virtualizer<Element, Element> | undefined
|
||||||
|
>;
|
||||||
|
mode?: "fixed" | "dynamic";
|
||||||
|
items: T[];
|
||||||
|
estimatedSize: number;
|
||||||
|
getItemKey: (index: number) => string;
|
||||||
|
scrollElement?: Element | null;
|
||||||
|
itemWrapperProps?: (item: T, index: number) => BoxProps;
|
||||||
|
renderItem: (props: { item: T; index: number }) => JSX.Element | null;
|
||||||
|
scrollMargin?: number;
|
||||||
|
} & BoxProps;
|
||||||
|
export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
getItemKey,
|
||||||
|
scrollElement,
|
||||||
|
scrollMargin,
|
||||||
|
renderItem: Item,
|
||||||
|
estimatedSize,
|
||||||
|
mode,
|
||||||
|
virtualizerRef,
|
||||||
|
itemWrapperProps,
|
||||||
|
...containerProps
|
||||||
|
} = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: items.length,
|
||||||
|
estimateSize: () => estimatedSize,
|
||||||
|
getItemKey,
|
||||||
|
getScrollElement: () =>
|
||||||
|
scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||||
|
scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||||
|
|
||||||
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
return (
|
||||||
|
<Box {...containerProps} ref={containerRef} className="List">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: virtualizer.getTotalSize(),
|
||||||
|
width: "100%",
|
||||||
|
position: "relative"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{virtualItems.map((row) => (
|
||||||
|
<Box
|
||||||
|
{...itemWrapperProps?.(items[row.index], row.index)}
|
||||||
|
key={row.key}
|
||||||
|
data-index={row.index}
|
||||||
|
ref={mode === "dynamic" ? virtualizer.measureElement : null}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: mode === "dynamic" ? "unset" : `${row.size}px`,
|
||||||
|
transform: `translateY(${
|
||||||
|
row.start - virtualizer.options.scrollMargin
|
||||||
|
}px)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Item key={row.key} item={items[row.index]} index={row.index} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
apps/web/src/components/virtualized-table/index.tsx
Normal file
106
apps/web/src/components/virtualized-table/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { Box } from "@theme-ui/components";
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
|
||||||
|
export type VirtualizedTableRowProps<T, C> = {
|
||||||
|
item: T;
|
||||||
|
index: number;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
rowRef?: React.Ref<HTMLTableRowElement>;
|
||||||
|
context?: C;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VirtualizedTableProps<T, C> = {
|
||||||
|
virtualizerRef?: React.MutableRefObject<
|
||||||
|
Virtualizer<Element, Element> | undefined
|
||||||
|
>;
|
||||||
|
mode?: "fixed" | "dynamic";
|
||||||
|
items: T[];
|
||||||
|
estimatedSize: number;
|
||||||
|
getItemKey: (index: number) => string;
|
||||||
|
scrollElement?: Element | null;
|
||||||
|
context?: C;
|
||||||
|
renderRow: (props: VirtualizedTableRowProps<T, C>) => JSX.Element | null;
|
||||||
|
scrollMargin?: number;
|
||||||
|
header: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
export function VirtualizedTable<T, C>(props: VirtualizedTableProps<T, C>) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
getItemKey,
|
||||||
|
scrollElement,
|
||||||
|
scrollMargin,
|
||||||
|
renderRow: Row,
|
||||||
|
estimatedSize,
|
||||||
|
mode,
|
||||||
|
virtualizerRef,
|
||||||
|
header,
|
||||||
|
style,
|
||||||
|
context
|
||||||
|
} = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: items.length,
|
||||||
|
estimateSize: () => estimatedSize,
|
||||||
|
getItemKey,
|
||||||
|
getScrollElement: () =>
|
||||||
|
scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||||
|
scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||||
|
|
||||||
|
const virtualItems = virtualizer.getVirtualItems();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={containerRef}
|
||||||
|
sx={{
|
||||||
|
height: virtualizer.getTotalSize()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<table style={style}>
|
||||||
|
<thead>{header}</thead>
|
||||||
|
<tbody>
|
||||||
|
{virtualItems.map((row, index) => (
|
||||||
|
<Row
|
||||||
|
key={row.key}
|
||||||
|
item={items[row.index]}
|
||||||
|
index={row.index}
|
||||||
|
rowRef={mode === "dynamic" ? virtualizer.measureElement : null}
|
||||||
|
context={context}
|
||||||
|
style={{
|
||||||
|
height: mode === "dynamic" ? "unset" : `${row.size}px`,
|
||||||
|
transform: `translateY(${
|
||||||
|
row.start -
|
||||||
|
index * row.size -
|
||||||
|
virtualizer.options.scrollMargin
|
||||||
|
}px)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -120,26 +120,27 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!reminderId) return;
|
if (!reminderId) return;
|
||||||
const reminder = db.reminders.reminder(reminderId);
|
db.reminders.reminder(reminderId).then((reminder) => {
|
||||||
if (!reminder) return;
|
if (!reminder) return;
|
||||||
|
|
||||||
setSelectedDays(reminder.selectedDays || []);
|
setSelectedDays(reminder.selectedDays || []);
|
||||||
setRecurringMode(reminder.recurringMode || RecurringModes.DAY);
|
setRecurringMode(reminder.recurringMode || RecurringModes.DAY);
|
||||||
setMode(reminder.mode || Modes.ONCE);
|
setMode(reminder.mode || Modes.ONCE);
|
||||||
setPriority(reminder.priority || Priorities.VIBRATE);
|
setPriority(reminder.priority || Priorities.VIBRATE);
|
||||||
setDate(dayjs(reminder.date).format("YYYY-MM-DD"));
|
setDate(dayjs(reminder.date).format("YYYY-MM-DD"));
|
||||||
setTime(dayjs(reminder.date).format("HH:mm"));
|
setTime(dayjs(reminder.date).format("HH:mm"));
|
||||||
setTitle(reminder.title);
|
setTitle(reminder.title);
|
||||||
setDescription(reminder.description);
|
setDescription(reminder.description);
|
||||||
|
});
|
||||||
}, [reminderId]);
|
}, [reminderId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!noteId) return;
|
if (!noteId) return;
|
||||||
const note = db.notes.note(noteId);
|
db.notes.note(noteId).then((note) => {
|
||||||
if (!note) return;
|
if (!note) return;
|
||||||
|
setTitle(note.title);
|
||||||
setTitle(note.title);
|
setDescription(note.headline);
|
||||||
setDescription(note.headline);
|
});
|
||||||
}, [noteId]);
|
}, [noteId]);
|
||||||
|
|
||||||
const repeatsDaily =
|
const repeatsDaily =
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import {
|
|||||||
import { store, useStore } from "../stores/attachment-store";
|
import { store, useStore } from "../stores/attachment-store";
|
||||||
import { formatBytes } from "@notesnook/common";
|
import { formatBytes } from "@notesnook/common";
|
||||||
import Dialog from "../components/dialog";
|
import Dialog from "../components/dialog";
|
||||||
import { ItemProps, TableVirtuoso } from "react-virtuoso";
|
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@@ -51,15 +50,20 @@ import NavigationItem from "../components/navigation-menu/navigation-item";
|
|||||||
import { pluralize } from "@notesnook/common";
|
import { pluralize } from "@notesnook/common";
|
||||||
import { db } from "../common/db";
|
import { db } from "../common/db";
|
||||||
import { Perform } from "../common/dialog-controller";
|
import { Perform } from "../common/dialog-controller";
|
||||||
import { CustomScrollbarsVirtualList } from "../components/list-container";
|
|
||||||
import { Attachment } from "../components/attachment";
|
import { Attachment } from "../components/attachment";
|
||||||
import { ScopedThemeProvider } from "../components/theme-provider";
|
import { ScopedThemeProvider } from "../components/theme-provider";
|
||||||
import {
|
import {
|
||||||
Attachment as AttachmentType,
|
Attachment as AttachmentType,
|
||||||
VirtualizedGrouping
|
VirtualizedGrouping
|
||||||
} from "@notesnook/core";
|
} from "@notesnook/core";
|
||||||
import usePromise from "../hooks/use-promise";
|
|
||||||
import { Multiselect } from "../common/multi-select";
|
import { Multiselect } from "../common/multi-select";
|
||||||
|
import { ResolvedItem } from "../components/list-container/resolved-item";
|
||||||
|
import {
|
||||||
|
VirtualizedTable,
|
||||||
|
VirtualizedTableRowProps
|
||||||
|
} from "../components/virtualized-table";
|
||||||
|
import { FlexScrollContainer } from "../components/scroll-container";
|
||||||
|
import usePromise from "../hooks/use-promise";
|
||||||
|
|
||||||
type ToolbarAction = {
|
type ToolbarAction = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -141,12 +145,6 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
|||||||
})();
|
})();
|
||||||
}, [allAttachments]);
|
}, [allAttachments]);
|
||||||
|
|
||||||
const totalSize = 0;
|
|
||||||
// useMemo(
|
|
||||||
// () => getTotalSize(allAttachments),
|
|
||||||
// [allAttachments]
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
@@ -157,16 +155,14 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
|||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
height: "80vw"
|
height: "80vw",
|
||||||
|
overflow: "hidden"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
totalSize={totalSize}
|
|
||||||
onDownloadAll={() => download(allAttachments?.ungrouped || [])}
|
onDownloadAll={() => download(allAttachments?.ungrouped || [])}
|
||||||
filter={(query) => {
|
filter={async (query) => {
|
||||||
// setAttachments(
|
setAttachments(await db.lookup.attachments(query));
|
||||||
// db.lookup?.attachments(db.attachments.all || [], query) || []
|
|
||||||
// );
|
|
||||||
}}
|
}}
|
||||||
counts={counts}
|
counts={counts}
|
||||||
onRouteChange={async (route) => {
|
onRouteChange={async (route) => {
|
||||||
@@ -185,41 +181,35 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
|||||||
sx={{
|
sx={{
|
||||||
bg: "background",
|
bg: "background",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
px: 4,
|
pl: 4,
|
||||||
pt: 2,
|
pt: 2,
|
||||||
overflowY: "hidden",
|
overflow: "hidden"
|
||||||
overflow: "hidden",
|
|
||||||
table: { width: "100%", tableLayout: "fixed" },
|
|
||||||
"tbody::before": {
|
|
||||||
content: `''`,
|
|
||||||
display: "block",
|
|
||||||
height: 5
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ justifyContent: "space-between" }}>
|
<FlexScrollContainer>
|
||||||
<Flex sx={{ gap: 1 }}>
|
<Flex sx={{ justifyContent: "space-between" }}>
|
||||||
{TOOLBAR_ACTIONS.map((tool) => (
|
<Flex sx={{ gap: 1 }}>
|
||||||
<Button
|
{TOOLBAR_ACTIONS.map((tool) => (
|
||||||
variant="secondary"
|
<Button
|
||||||
key={tool.title}
|
variant="secondary"
|
||||||
title={tool.title}
|
key={tool.title}
|
||||||
onClick={() =>
|
title={tool.title}
|
||||||
tool.onClick({
|
onClick={() =>
|
||||||
selected
|
tool.onClick({
|
||||||
// : attachments.filter(
|
selected
|
||||||
// (a) => selected.indexOf(a.id) > -1
|
// : attachments.filter(
|
||||||
// )
|
// (a) => selected.indexOf(a.id) > -1
|
||||||
})
|
// )
|
||||||
}
|
})
|
||||||
disabled={!selected.length}
|
}
|
||||||
sx={{ bg: "transparent", p: 1 }}
|
disabled={!selected.length}
|
||||||
>
|
sx={{ bg: "transparent", p: 1 }}
|
||||||
<tool.icon size={18} />
|
>
|
||||||
</Button>
|
<tool.icon size={18} />
|
||||||
))}
|
</Button>
|
||||||
</Flex>
|
))}
|
||||||
{/* <Button
|
</Flex>
|
||||||
|
{/* <Button
|
||||||
variant="tool"
|
variant="tool"
|
||||||
sx={{ p: 1, display: "flex" }}
|
sx={{ p: 1, display: "flex" }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -232,114 +222,113 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
|||||||
Upload
|
Upload
|
||||||
</Text>
|
</Text>
|
||||||
</Button> */}
|
</Button> */}
|
||||||
</Flex>
|
</Flex>
|
||||||
{attachments && (
|
{attachments && (
|
||||||
<TableVirtuoso
|
<VirtualizedTable
|
||||||
components={{
|
style={{ tableLayout: "fixed", borderCollapse: "collapse" }}
|
||||||
Scroller: CustomScrollbarsVirtualList,
|
header={
|
||||||
TableRow
|
<Box
|
||||||
}}
|
as="tr"
|
||||||
style={{ height: "100%" }}
|
|
||||||
computeItemKey={(index) => attachments.getKey(index)}
|
|
||||||
data={attachments.ungrouped}
|
|
||||||
context={{
|
|
||||||
isSelected: (id: string) => selected.indexOf(id) > -1,
|
|
||||||
select: (id: string) => {
|
|
||||||
setSelected((s) => {
|
|
||||||
const copy = s.slice();
|
|
||||||
const index = copy.indexOf(id);
|
|
||||||
if (index > -1) copy.splice(index, 1);
|
|
||||||
else copy.push(id);
|
|
||||||
return copy;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
attachments
|
|
||||||
}}
|
|
||||||
fixedItemHeight={30}
|
|
||||||
defaultItemHeight={30}
|
|
||||||
fixedHeaderContent={() => (
|
|
||||||
<Box
|
|
||||||
as="tr"
|
|
||||||
sx={{
|
|
||||||
height: 40,
|
|
||||||
th: { borderBottom: "1px solid var(--separator)" },
|
|
||||||
bg: "background"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
as="th"
|
|
||||||
variant="body"
|
|
||||||
sx={{
|
sx={{
|
||||||
width: 24,
|
height: 40,
|
||||||
textAlign: "left",
|
th: { borderBottom: "1px solid var(--separator)" },
|
||||||
fontWeight: "normal",
|
bg: "background"
|
||||||
mb: 2
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Label>
|
<Text
|
||||||
<Checkbox
|
as="th"
|
||||||
sx={{ width: 18, height: 18 }}
|
variant="body"
|
||||||
onChange={(e) => {
|
sx={{
|
||||||
setSelected(
|
width: 24,
|
||||||
e.currentTarget.checked ? attachments.ungrouped : []
|
textAlign: "left",
|
||||||
);
|
fontWeight: "normal",
|
||||||
}}
|
mb: 2
|
||||||
/>
|
}}
|
||||||
</Label>
|
>
|
||||||
</Text>
|
<Label>
|
||||||
{COLUMNS.map((column) =>
|
<Checkbox
|
||||||
!column.title ? (
|
sx={{ width: 18, height: 18 }}
|
||||||
<th key={column.id} />
|
onChange={(e) => {
|
||||||
) : (
|
setSelected(
|
||||||
<Box
|
e.currentTarget.checked
|
||||||
as="th"
|
? attachments.ungrouped
|
||||||
key={column.id}
|
: []
|
||||||
sx={{
|
);
|
||||||
width: column.width,
|
}}
|
||||||
cursor: "pointer",
|
/>
|
||||||
px: 1,
|
</Label>
|
||||||
mb: 2,
|
</Text>
|
||||||
":hover": { bg: "hover" }
|
{COLUMNS.map((column) =>
|
||||||
}}
|
!column.title ? (
|
||||||
onClick={() => {
|
<th key={column.id} />
|
||||||
setSortBy((sortBy) => ({
|
) : (
|
||||||
direction:
|
<Box
|
||||||
sortBy.id === column.id &&
|
as="th"
|
||||||
sortBy.direction === "asc"
|
key={column.id}
|
||||||
? "desc"
|
|
||||||
: "asc",
|
|
||||||
id: column.id
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex
|
|
||||||
sx={{
|
sx={{
|
||||||
alignItems: "center",
|
width: column.width,
|
||||||
justifyContent: "space-between"
|
cursor: "pointer",
|
||||||
|
px: 1,
|
||||||
|
mb: 2,
|
||||||
|
":hover": { bg: "hover" }
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setSortBy((sortBy) => ({
|
||||||
|
direction:
|
||||||
|
sortBy.id === column.id &&
|
||||||
|
sortBy.direction === "asc"
|
||||||
|
? "desc"
|
||||||
|
: "asc",
|
||||||
|
id: column.id
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Flex
|
||||||
variant="body"
|
sx={{
|
||||||
sx={{ textAlign: "left", fontWeight: "normal" }}
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{column.title}
|
<Text
|
||||||
</Text>
|
variant="body"
|
||||||
{sortBy.id === column.id ? (
|
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||||
sortBy.direction === "asc" ? (
|
>
|
||||||
<ChevronUp size={16} />
|
{column.title}
|
||||||
) : (
|
</Text>
|
||||||
<ChevronDown size={16} />
|
{sortBy.id === column.id ? (
|
||||||
)
|
sortBy.direction === "asc" ? (
|
||||||
) : null}
|
<ChevronUp size={16} />
|
||||||
</Flex>
|
) : (
|
||||||
</Box>
|
<ChevronDown size={16} />
|
||||||
)
|
)
|
||||||
)}
|
) : null}
|
||||||
</Box>
|
</Flex>
|
||||||
)}
|
</Box>
|
||||||
itemContent={() => <></>}
|
)
|
||||||
/>
|
)}
|
||||||
)}
|
</Box>
|
||||||
|
}
|
||||||
|
mode="fixed"
|
||||||
|
estimatedSize={30}
|
||||||
|
getItemKey={(index) => attachments.getKey(index)}
|
||||||
|
items={attachments.ungrouped}
|
||||||
|
context={{
|
||||||
|
isSelected: (id: string) => selected.indexOf(id) > -1,
|
||||||
|
select: (id: string) => {
|
||||||
|
setSelected((s) => {
|
||||||
|
const copy = s.slice();
|
||||||
|
const index = copy.indexOf(id);
|
||||||
|
if (index > -1) copy.splice(index, 1);
|
||||||
|
else copy.push(id);
|
||||||
|
return copy;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
attachments
|
||||||
|
}}
|
||||||
|
renderRow={AttachmentRow}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FlexScrollContainer>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -348,6 +337,34 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
|||||||
|
|
||||||
export default AttachmentsDialog;
|
export default AttachmentsDialog;
|
||||||
|
|
||||||
|
function AttachmentRow(
|
||||||
|
props: VirtualizedTableRowProps<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
isSelected: (id: string) => boolean;
|
||||||
|
select: (id: string) => void;
|
||||||
|
attachments: VirtualizedGrouping<AttachmentType>;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
if (!props.context) return null;
|
||||||
|
return (
|
||||||
|
<ResolvedItem id={props.item} items={props.context.attachments}>
|
||||||
|
{({ item }) =>
|
||||||
|
item.type === "attachment" ? (
|
||||||
|
<Attachment
|
||||||
|
rowRef={props.rowRef}
|
||||||
|
style={props.style}
|
||||||
|
item={item}
|
||||||
|
isSelected={props.context?.isSelected(props.item)}
|
||||||
|
onSelected={() => props.context?.select(props.item)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</ResolvedItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Route = "all" | "images" | "documents" | "videos" | "uploads" | "orphaned";
|
type Route = "all" | "images" | "documents" | "videos" | "uploads" | "orphaned";
|
||||||
|
|
||||||
const routes: { id: Route; icon: Icon; title: string }[] = [
|
const routes: { id: Route; icon: Icon; title: string }[] = [
|
||||||
@@ -388,14 +405,14 @@ type SidebarProps = {
|
|||||||
onRouteChange: (route: Route) => void;
|
onRouteChange: (route: Route) => void;
|
||||||
filter: (query: string) => void;
|
filter: (query: string) => void;
|
||||||
counts: Record<Route, number>;
|
counts: Record<Route, number>;
|
||||||
totalSize: number;
|
|
||||||
};
|
};
|
||||||
const Sidebar = memo(
|
const Sidebar = memo(
|
||||||
function Sidebar(props: SidebarProps) {
|
function Sidebar(props: SidebarProps) {
|
||||||
const { onRouteChange, filter, counts, totalSize, onDownloadAll } = props;
|
const { onRouteChange, filter, counts, onDownloadAll } = props;
|
||||||
const [route, setRoute] = useState("all");
|
const [route, setRoute] = useState("all");
|
||||||
const downloadStatus = useStore((store) => store.status);
|
const downloadStatus = useStore((store) => store.status);
|
||||||
const cancelDownload = useStore((store) => store.cancel);
|
const cancelDownload = useStore((store) => store.cancel);
|
||||||
|
const result = usePromise(() => db.attachments.totalSize());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScopedThemeProvider scope="navigationMenu" injectCssVars={false}>
|
<ScopedThemeProvider scope="navigationMenu" injectCssVars={false}>
|
||||||
@@ -410,6 +427,8 @@ const Sidebar = memo(
|
|||||||
>
|
>
|
||||||
<Flex sx={{ flexDirection: "column" }}>
|
<Flex sx={{ flexDirection: "column" }}>
|
||||||
<Input
|
<Input
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
sx={{ m: 2, mb: 0, width: "auto", bg: "background", py: "7px" }}
|
sx={{ m: 2, mb: 0, width: "auto", bg: "background", py: "7px" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -435,7 +454,11 @@ const Sidebar = memo(
|
|||||||
<Flex sx={{ pl: 2, m: 2, mt: 1, justifyContent: "space-between" }}>
|
<Flex sx={{ pl: 2, m: 2, mt: 1, justifyContent: "space-between" }}>
|
||||||
<Flex sx={{ flexDirection: "column" }}>
|
<Flex sx={{ flexDirection: "column" }}>
|
||||||
<Text variant="body">{pluralize(counts.all, "file")}</Text>
|
<Text variant="body">{pluralize(counts.all, "file")}</Text>
|
||||||
<Text variant="subBody">{formatBytes(totalSize)}</Text>
|
{result.status === "fulfilled" && (
|
||||||
|
<Text variant="subBody">
|
||||||
|
{formatBytes(result.value || 0)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -478,7 +501,6 @@ const Sidebar = memo(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
(prev, next) =>
|
(prev, next) =>
|
||||||
prev.totalSize === next.totalSize &&
|
|
||||||
prev.counts.all === next.counts.all &&
|
prev.counts.all === next.counts.all &&
|
||||||
prev.counts.documents === next.counts.documents &&
|
prev.counts.documents === next.counts.documents &&
|
||||||
prev.counts.images === next.counts.images &&
|
prev.counts.images === next.counts.images &&
|
||||||
@@ -487,36 +509,6 @@ const Sidebar = memo(
|
|||||||
prev.counts.orphaned === next.counts.orphaned
|
prev.counts.orphaned === next.counts.orphaned
|
||||||
);
|
);
|
||||||
|
|
||||||
function TableRow(
|
|
||||||
props: ItemProps<string> & {
|
|
||||||
context?: {
|
|
||||||
isSelected: (id: string) => boolean;
|
|
||||||
select: (id: string) => void;
|
|
||||||
attachments: VirtualizedGrouping<AttachmentType>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
const { context, item, ...restProps } = props;
|
|
||||||
const result = usePromise(
|
|
||||||
() => context?.attachments.item(item),
|
|
||||||
[item, context]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.status !== "fulfilled" || !result.value)
|
|
||||||
return <div {...restProps} />;
|
|
||||||
return (
|
|
||||||
<Attachment
|
|
||||||
{...restProps}
|
|
||||||
key={item}
|
|
||||||
attachment={result.value}
|
|
||||||
isSelected={context?.isSelected(item)}
|
|
||||||
onSelected={() => {
|
|
||||||
context?.select(item);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCounts(): Promise<Record<Route, number>> {
|
async function getCounts(): Promise<Record<Route, number>> {
|
||||||
return {
|
return {
|
||||||
all: await db.attachments.all.count(),
|
all: await db.attachments.all.count(),
|
||||||
|
|||||||
@@ -37,10 +37,10 @@ import {
|
|||||||
import { ThemeMetadata } from "@notesnook/themes-server";
|
import { ThemeMetadata } from "@notesnook/themes-server";
|
||||||
import { showThemeDetails } from "../../../common/dialog-controller";
|
import { showThemeDetails } from "../../../common/dialog-controller";
|
||||||
import { ThemePreview } from "../../../components/theme-preview";
|
import { ThemePreview } from "../../../components/theme-preview";
|
||||||
import { VirtuosoGrid } from "react-virtuoso";
|
|
||||||
import { Loader } from "../../../components/loader";
|
import { Loader } from "../../../components/loader";
|
||||||
import { showToast } from "../../../utils/toast";
|
import { showToast } from "../../../utils/toast";
|
||||||
import { showFilePicker, readFile } from "../../../utils/file-picker";
|
import { showFilePicker, readFile } from "../../../utils/file-picker";
|
||||||
|
import { VirtualizedGrid } from "../../../components/virtualized-grid";
|
||||||
|
|
||||||
const ThemesClient = ThemesTRPC.createClient({
|
const ThemesClient = ThemesTRPC.createClient({
|
||||||
links: [
|
links: [
|
||||||
@@ -107,6 +107,18 @@ function ThemesList() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
...darkTheme,
|
||||||
|
previewColors: getPreviewColors(darkTheme)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...lightTheme,
|
||||||
|
previewColors: getPreviewColors(lightTheme)
|
||||||
|
},
|
||||||
|
...(themes.data?.pages.flatMap((a) => a.themes) || [])
|
||||||
|
];
|
||||||
|
|
||||||
const setTheme = useCallback(
|
const setTheme = useCallback(
|
||||||
async (theme: ThemeMetadata) => {
|
async (theme: ThemeMetadata) => {
|
||||||
if (isThemeCurrentlyApplied(theme.id)) return;
|
if (isThemeCurrentlyApplied(theme.id)) return;
|
||||||
@@ -198,38 +210,22 @@ function ThemesList() {
|
|||||||
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
".virtuoso-grid-list": {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: 2
|
|
||||||
},
|
|
||||||
mt: 2
|
mt: 2
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{themes.isInitialLoading ? (
|
{themes.isInitialLoading ? (
|
||||||
<Loader title={"Loading themes..."} />
|
<Loader title={"Loading themes..."} />
|
||||||
) : (
|
) : (
|
||||||
<VirtuosoGrid
|
<VirtualizedGrid
|
||||||
customScrollParent={
|
columns={2}
|
||||||
document.getElementById("settings-scrollbar") || undefined
|
items={items}
|
||||||
}
|
getItemKey={(index) => items[index].id}
|
||||||
data={[
|
estimatedSize={285}
|
||||||
{
|
mode="dynamic"
|
||||||
...darkTheme,
|
onEndReached={() =>
|
||||||
previewColors: getPreviewColors(darkTheme)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...lightTheme,
|
|
||||||
previewColors: getPreviewColors(lightTheme)
|
|
||||||
},
|
|
||||||
...(themes.data?.pages.flatMap((a) => a.themes) || [])
|
|
||||||
]}
|
|
||||||
endReached={() =>
|
|
||||||
themes.hasNextPage ? themes.fetchNextPage() : null
|
themes.hasNextPage ? themes.fetchNextPage() : null
|
||||||
}
|
}
|
||||||
context={{ darkTheme, lightTheme, setTheme }}
|
renderItem={({ item: theme }) => (
|
||||||
computeItemKey={(_index, item) => item.id}
|
|
||||||
itemContent={(_index, theme) => (
|
|
||||||
<ThemeItem
|
<ThemeItem
|
||||||
key={theme.id}
|
key={theme.id}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|||||||
@@ -242,6 +242,8 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
id="search"
|
||||||
|
name="search"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
data-test-id="settings-search"
|
data-test-id="settings-search"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ProfileSettings: SettingsGroup[] = [
|
|||||||
key: "manage-attachments",
|
key: "manage-attachments",
|
||||||
title: "Attachments",
|
title: "Attachments",
|
||||||
description: "Manage all your attachments in one place.",
|
description: "Manage all your attachments in one place.",
|
||||||
isHidden: () => !useUserStore.getState().isLoggedIn,
|
// isHidden: () => !useUserStore.getState().isLoggedIn,
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ enum SESSION_STATES {
|
|||||||
opening = "opening"
|
opening = "opening"
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorSession = {
|
export type EditorSession = {
|
||||||
sessionType: "default" | "locked";
|
sessionType: "default" | "locked" | "preview";
|
||||||
content?: NoteContent<false>;
|
content?: NoteContent<false>;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
attachmentsLength: number;
|
attachmentsLength: number;
|
||||||
|
|||||||
@@ -46,14 +46,11 @@ export class Compressor implements ICompressor {
|
|||||||
return await desktop.compress.gunzip.query(data);
|
return await desktop.compress.gunzip.query(data);
|
||||||
|
|
||||||
await this.init();
|
await this.init();
|
||||||
|
const bytes = new Memory(Buffer.from(data, "base64"));
|
||||||
|
|
||||||
return gunzip(Buffer.from(data, "base64"))
|
const res = gunzip(bytes);
|
||||||
.copyAndDispose()
|
const text = Buffer.from(res.bytes).toString("utf-8");
|
||||||
.toString("utf-8");
|
res.free();
|
||||||
// return new Promise<string>((resolve, reject) => {
|
return text;
|
||||||
// gunzip(Buffer.from(data, "base64"), (err, data) =>
|
|
||||||
// err ? reject(err) : resolve(Buffer.from(data.buffer).toString("utf-8"))
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import Database from ".";
|
|||||||
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common";
|
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common";
|
||||||
import { tinyToTiptap } from "../migrations";
|
import { tinyToTiptap } from "../migrations";
|
||||||
import { isCipher } from "../database/crypto";
|
import { isCipher } from "../database/crypto";
|
||||||
import { EncryptedContentItem, Note } from "../types";
|
import { Note } from "../types";
|
||||||
import {
|
import {
|
||||||
isEncryptedContent,
|
isEncryptedContent,
|
||||||
isUnencryptedContent
|
isUnencryptedContent
|
||||||
@@ -110,6 +110,7 @@ export default class Vault {
|
|||||||
try {
|
try {
|
||||||
const content = await this.decryptContent(
|
const content = await this.decryptContent(
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
|
note.id,
|
||||||
oldPassword
|
oldPassword
|
||||||
);
|
);
|
||||||
contentItems.push({
|
contentItems.push({
|
||||||
@@ -239,18 +240,18 @@ export default class Vault {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async decryptContent(
|
async decryptContent(
|
||||||
encryptedContent: EncryptedContentItem,
|
encryptedContent: NoteContent<true>,
|
||||||
|
noteId: string,
|
||||||
password?: string
|
password?: string
|
||||||
) {
|
) {
|
||||||
if (!password) password = await this.getVaultPassword();
|
if (!password) password = await this.getVaultPassword();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
encryptedContent.noteId &&
|
|
||||||
typeof encryptedContent.data !== "object" &&
|
typeof encryptedContent.data !== "object" &&
|
||||||
!isCipher(encryptedContent.data)
|
!isCipher(encryptedContent.data)
|
||||||
) {
|
) {
|
||||||
await this.db.notes.add({
|
await this.db.notes.add({
|
||||||
id: encryptedContent.noteId,
|
id: noteId,
|
||||||
locked: false
|
locked: false
|
||||||
});
|
});
|
||||||
return { data: encryptedContent.data, type: encryptedContent.type };
|
return { data: encryptedContent.data, type: encryptedContent.type };
|
||||||
@@ -330,7 +331,11 @@ export default class Vault {
|
|||||||
|
|
||||||
const encryptedContent = await this.db.content.get(note.contentId);
|
const encryptedContent = await this.db.content.get(note.contentId);
|
||||||
if (!encryptedContent || !isEncryptedContent(encryptedContent)) return;
|
if (!encryptedContent || !isEncryptedContent(encryptedContent)) return;
|
||||||
const content = await this.decryptContent(encryptedContent, password);
|
const content = await this.decryptContent(
|
||||||
|
encryptedContent,
|
||||||
|
note.id,
|
||||||
|
password
|
||||||
|
);
|
||||||
|
|
||||||
if (perm) {
|
if (perm) {
|
||||||
await this.db.notes.add({
|
await this.db.notes.add({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { Attachment } from "../types";
|
|||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { FilteredSelector, SQLCollection } from "../database/sql-collection";
|
import { FilteredSelector, SQLCollection } from "../database/sql-collection";
|
||||||
import { isFalse } from "../database";
|
import { isFalse } from "../database";
|
||||||
|
import { sql } from "kysely";
|
||||||
|
|
||||||
export class Attachments implements ICollection {
|
export class Attachments implements ICollection {
|
||||||
name = "attachments";
|
name = "attachments";
|
||||||
@@ -470,6 +471,13 @@ export class Attachments implements ICollection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async totalSize(selector: FilteredSelector<Attachment> = this.all) {
|
||||||
|
const result = await selector.filter
|
||||||
|
.select((eb) => eb.fn.sum<number>(sql.raw(`size + 17`)).as("totalSize"))
|
||||||
|
.executeTakeFirst();
|
||||||
|
return result?.totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
private async encryptKey(key: SerializedKey) {
|
private async encryptKey(key: SerializedKey) {
|
||||||
const encryptionKey = await this._getEncryptionKey();
|
const encryptionKey = await this._getEncryptionKey();
|
||||||
const encryptedKey = await this.db
|
const encryptedKey = await this.db
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { isCipher } from "../database/crypto";
|
import { isCipher } from "../database/crypto";
|
||||||
import { SQLCollection } from "../database/sql-collection";
|
import { FilteredSelector, SQLCollection } from "../database/sql-collection";
|
||||||
import { HistorySession, isDeleted } from "../types";
|
import { HistorySession, isDeleted } from "../types";
|
||||||
import { makeSessionContentId } from "../utils/id";
|
import { makeSessionContentId } from "../utils/id";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
@@ -39,25 +39,31 @@ export class NoteHistory implements ICollection {
|
|||||||
await this.sessionContent.init();
|
await this.sessionContent.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(noteId: string, order: "asc" | "desc" = "desc") {
|
// async get(noteId: string, order: "asc" | "desc" = "desc") {
|
||||||
if (!noteId) return [];
|
// if (!noteId) return [];
|
||||||
|
|
||||||
// const indices = this.collection.indexer.indices;
|
// // const indices = this.collection.indexer.indices;
|
||||||
// const sessionIds = indices.filter((id) => id.startsWith(noteId));
|
// // const sessionIds = indices.filter((id) => id.startsWith(noteId));
|
||||||
// if (sessionIds.length === 0) return [];
|
// // if (sessionIds.length === 0) return [];
|
||||||
// const history = await this.getSessions(sessionIds);
|
// // const history = await this.getSessions(sessionIds);
|
||||||
|
|
||||||
// return history.sort(function (a, b) {
|
// // return history.sort(function (a, b) {
|
||||||
// return b.dateModified - a.dateModified;
|
// // return b.dateModified - a.dateModified;
|
||||||
// });
|
// // });
|
||||||
const history = await this.db
|
// const history = await this.db
|
||||||
.sql()
|
// .sql()
|
||||||
.selectFrom("notehistory")
|
// .selectFrom("notehistory")
|
||||||
.where("noteId", "==", noteId)
|
// .where("noteId", "==", noteId)
|
||||||
.orderBy(`dateModified ${order}`)
|
// .orderBy(`dateModified ${order}`)
|
||||||
.selectAll()
|
// .selectAll()
|
||||||
.execute();
|
// .execute();
|
||||||
return history as HistorySession[];
|
// return history as HistorySession[];
|
||||||
|
// }
|
||||||
|
get(noteId: string) {
|
||||||
|
return new FilteredSelector<HistorySession>(
|
||||||
|
"notehistory",
|
||||||
|
this.db.sql().selectFrom("notehistory").where("noteId", "==", noteId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(
|
async add(
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ export class SessionContent implements ICollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(sessionContentId: string) {
|
async get(
|
||||||
|
sessionContentId: string
|
||||||
|
): Promise<NoteContent<boolean> | undefined> {
|
||||||
const session = await this.collection.get(sessionContentId);
|
const session = await this.collection.get(sessionContentId);
|
||||||
if (!session || isDeleted(session)) return;
|
if (!session || isDeleted(session)) return;
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const DEFAULT_GROUP_OPTIONS = (key: GroupingKey) =>
|
|||||||
sortBy:
|
sortBy:
|
||||||
key === "trash"
|
key === "trash"
|
||||||
? "dateDeleted"
|
? "dateDeleted"
|
||||||
: key === "tags"
|
: key === "tags" || key === "reminders"
|
||||||
? "dateCreated"
|
? "dateCreated"
|
||||||
: key === "reminders"
|
: key === "reminders"
|
||||||
? "dueDate"
|
? "dueDate"
|
||||||
|
|||||||
@@ -345,31 +345,37 @@ export default class Backup {
|
|||||||
if ("sessionContentId" in item && item.type !== "session")
|
if ("sessionContentId" in item && item.type !== "session")
|
||||||
(item as any).type = "notehistory";
|
(item as any).type = "notehistory";
|
||||||
|
|
||||||
await migrateItem(
|
if (
|
||||||
item,
|
(await migrateItem(
|
||||||
version,
|
item,
|
||||||
CURRENT_DATABASE_VERSION,
|
version,
|
||||||
item.type,
|
CURRENT_DATABASE_VERSION,
|
||||||
this.db,
|
item.type,
|
||||||
"backup"
|
this.db,
|
||||||
);
|
"backup"
|
||||||
|
)) === "skip"
|
||||||
|
)
|
||||||
|
continue;
|
||||||
// since items in trash can have their own set of migrations,
|
// since items in trash can have their own set of migrations,
|
||||||
// we have to run the migration again to account for that.
|
// we have to run the migration again to account for that.
|
||||||
if (item.type === "trash" && item.itemType)
|
if (item.type === "trash" && item.itemType)
|
||||||
await migrateItem(
|
if (
|
||||||
item as unknown as Note | Notebook,
|
(await migrateItem(
|
||||||
version,
|
item as unknown as Note | Notebook,
|
||||||
CURRENT_DATABASE_VERSION,
|
version,
|
||||||
item.itemType,
|
CURRENT_DATABASE_VERSION,
|
||||||
this.db,
|
item.itemType,
|
||||||
"backup"
|
this.db,
|
||||||
);
|
"backup"
|
||||||
|
)) === "skip"
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
const itemType =
|
const itemType =
|
||||||
// colors are naively of type "tag" instead of "color" so we have to fix that.
|
// colors are naively of type "tag" instead of "color" so we have to fix that.
|
||||||
item.type === "tag" && COLORS.includes(item.title.toLowerCase())
|
item.type === "tag" && COLORS.includes(item.title.toLowerCase())
|
||||||
? "color"
|
? "color"
|
||||||
: "itemType" in item && item.itemType
|
: item.type === "trash" && "itemType" in item && item.itemType
|
||||||
? item.itemType
|
? item.itemType
|
||||||
: item.type;
|
: item.type;
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export class NNMigrationProvider implements MigrationProvider {
|
|||||||
.addColumn("salt", "text")
|
.addColumn("salt", "text")
|
||||||
.addColumn("size", "integer")
|
.addColumn("size", "integer")
|
||||||
.addColumn("alg", "text")
|
.addColumn("alg", "text")
|
||||||
.addColumn("encryptionKey", "text")
|
.addColumn("key", "text")
|
||||||
.addColumn("chunkSize", "integer")
|
.addColumn("chunkSize", "integer")
|
||||||
.addColumn("hash", "text", (c) => c.unique())
|
.addColumn("hash", "text", (c) => c.unique())
|
||||||
.addColumn("hashType", "text")
|
.addColumn("hashType", "text")
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
export { VirtualizedGrouping } from "./utils/virtualized-grouping";
|
export { VirtualizedGrouping } from "./utils/virtualized-grouping";
|
||||||
|
export { DefaultColors } from "./collections/colors";
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ type Migration = {
|
|||||||
item: MigrationItemMap[P],
|
item: MigrationItemMap[P],
|
||||||
db: Database,
|
db: Database,
|
||||||
migrationType: MigrationType
|
migrationType: MigrationType
|
||||||
) => boolean | Promise<boolean> | void;
|
) => "skip" | boolean | Promise<boolean | "skip"> | void;
|
||||||
};
|
};
|
||||||
collection?: (collection: IndexedCollection) => Promise<void> | void;
|
collection?: (collection: IndexedCollection) => Promise<void> | void;
|
||||||
};
|
};
|
||||||
@@ -196,12 +196,22 @@ const migrations: Migration[] = [
|
|||||||
.items()
|
.items()
|
||||||
.find((t) => item.title === t.title && t.id !== oldTagId))
|
.find((t) => item.title === t.title && t.id !== oldTagId))
|
||||||
)
|
)
|
||||||
return false;
|
return "skip";
|
||||||
|
|
||||||
const colorCode = ColorToHexCode[item.title];
|
const colorCode = ColorToHexCode[item.title];
|
||||||
if (colorCode) {
|
if (colorCode) {
|
||||||
|
const newColor = await db.colors.all.find((eb) =>
|
||||||
|
eb.or([eb("title", "in", [alias, item.title])])
|
||||||
|
);
|
||||||
|
if (newColor) return "skip";
|
||||||
|
|
||||||
(item as unknown as Color).type = "color";
|
(item as unknown as Color).type = "color";
|
||||||
(item as unknown as Color).colorCode = colorCode;
|
(item as unknown as Color).colorCode = colorCode;
|
||||||
|
} else {
|
||||||
|
const newTag = await db.tags.all.find((eb) =>
|
||||||
|
eb.or([eb("title", "in", [alias, item.title])])
|
||||||
|
);
|
||||||
|
if (newTag) return "skip";
|
||||||
}
|
}
|
||||||
|
|
||||||
item.title = alias || item.title;
|
item.title = alias || item.title;
|
||||||
@@ -305,6 +315,7 @@ const migrations: Migration[] = [
|
|||||||
await db.relations.add(item, { id: subNotebookId, type: "notebook" });
|
await db.relations.add(item, { id: subNotebookId, type: "notebook" });
|
||||||
}
|
}
|
||||||
delete item.topics;
|
delete item.topics;
|
||||||
|
delete item.totalNotes;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
shortcut: (item) => {
|
shortcut: (item) => {
|
||||||
@@ -409,7 +420,9 @@ export async function migrateItem<TItemType extends MigrationItemType>(
|
|||||||
|
|
||||||
const itemMigrator = migration.items[type];
|
const itemMigrator = migration.items[type];
|
||||||
if (!itemMigrator) continue;
|
if (!itemMigrator) continue;
|
||||||
if (await itemMigrator(item, database, migrationType)) {
|
const result = await itemMigrator(item, database, migrationType);
|
||||||
|
if (result === "skip") return "skip";
|
||||||
|
if (result) {
|
||||||
if (item.type && item.type !== type) type = item.type as TItemType;
|
if (item.type && item.type !== type) type = item.type as TItemType;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type SortOptions = {
|
|||||||
| "dateCreated"
|
| "dateCreated"
|
||||||
| "dateDeleted"
|
| "dateDeleted"
|
||||||
| "dateEdited"
|
| "dateEdited"
|
||||||
|
| "dateModified"
|
||||||
| "title"
|
| "title"
|
||||||
| "filename"
|
| "filename"
|
||||||
| "size"
|
| "size"
|
||||||
@@ -197,6 +198,10 @@ export interface Notebook extends BaseItem<"notebook"> {
|
|||||||
* @deprecated only kept here for migration purposes.
|
* @deprecated only kept here for migration purposes.
|
||||||
*/
|
*/
|
||||||
topics?: Topic[];
|
topics?: Topic[];
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes.
|
||||||
|
*/
|
||||||
|
totalNotes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user