mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 11:47:54 +01:00
web: add support for tabs
This commit is contained in:
104
apps/web/package-lock.json
generated
104
apps/web/package-lock.json
generated
@@ -49,6 +49,7 @@
|
||||
"cronosjs": "^1.7.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "1.11.9",
|
||||
"diffblazer": "^1.0.1",
|
||||
"electron-trpc": "0.5.2",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"fflate": "^0.8.0",
|
||||
@@ -73,6 +74,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-freeze": "^1.0.3",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-modal": "3.16.1",
|
||||
@@ -101,6 +103,7 @@
|
||||
"@types/react-avatar-editor": "^13.0.2",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@types/react-scroll-sync": "^0.9.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@vitejs/plugin-react-swc": "3.3.2",
|
||||
@@ -31955,7 +31958,7 @@
|
||||
},
|
||||
"../desktop": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.4-beta",
|
||||
"version": "3.0.6-beta",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -35022,8 +35025,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.24.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
|
||||
"integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.24.0"
|
||||
},
|
||||
@@ -35061,8 +35063,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-plugin-utils": {
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
|
||||
"integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -35132,8 +35133,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
|
||||
"integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -36125,8 +36125,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime": {
|
||||
"version": "7.24.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.3.tgz",
|
||||
"integrity": "sha512-J0BuRPNlNqlMTRJ72eVptpt9VcInbxO6iP3jaxr+1NPhC0UkKL+6oeX6VXMEYdADnuqmMmsBspt4d5w8Y/TCbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.24.3",
|
||||
"@babel/helper-plugin-utils": "^7.24.0",
|
||||
@@ -36144,8 +36143,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime/node_modules/@babel/helper-define-polyfill-provider": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
|
||||
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.22.6",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -36159,8 +36157,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-corejs3": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz",
|
||||
"integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
||||
"core-js-compat": "^3.36.1"
|
||||
@@ -36171,8 +36168,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-runtime/node_modules/babel-plugin-polyfill-regenerator": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz",
|
||||
"integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.1"
|
||||
},
|
||||
@@ -36464,8 +36460,7 @@
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
|
||||
"integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.23.4",
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
@@ -38092,9 +38087,8 @@
|
||||
},
|
||||
"node_modules/@types/react-avatar-editor": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
|
||||
"integrity": "sha512-vnGU4sx5TDB9JMCuw+kB3+jfDNMhVlpBbgMRZG9NzVnaBb5xUjVV4VmHdD2O8gj/pb5GN+QXKk7jan09aMjG2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -38115,6 +38109,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-scroll-sync": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-scroll-sync/-/react-scroll-sync-0.9.0.tgz",
|
||||
"integrity": "sha512-DFJzqtF0lMUNxsKZ9aoFS+LGieGLXWQVWvCMTZWKm/qcGz+GCNTqwdHsnyWW3yy/aNSUxlXCAWdHaemGaES6lQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"dev": true,
|
||||
@@ -38709,8 +38712,7 @@
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||
"version": "0.4.10",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz",
|
||||
"integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.6",
|
||||
"@babel/helper-define-polyfill-provider": "^0.6.1",
|
||||
@@ -38722,8 +38724,7 @@
|
||||
},
|
||||
"node_modules/babel-plugin-polyfill-corejs2/node_modules/@babel/helper-define-polyfill-provider": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz",
|
||||
"integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-compilation-targets": "^7.22.6",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -38874,8 +38875,6 @@
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
|
||||
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -38890,6 +38889,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001587",
|
||||
"electron-to-chromium": "^1.4.668",
|
||||
@@ -39020,8 +39020,6 @@
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001599",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
|
||||
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -39035,7 +39033,8 @@
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
]
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas": {
|
||||
"version": "2.11.2",
|
||||
@@ -39253,8 +39252,7 @@
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
|
||||
"integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.23.0"
|
||||
},
|
||||
@@ -39533,6 +39531,32 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffblazer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/diffblazer/-/diffblazer-1.0.1.tgz",
|
||||
"integrity": "sha512-aBQUrBdpDYqqVf/8tlLh0JVRw/IBFlLQR7Wgn3/8kO54cq+HRcPuT/JUbbm/136hutuRFXzWERDIS/lX3HViYg==",
|
||||
"dependencies": {
|
||||
"htmlparser2": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/diffblazer/node_modules/htmlparser2": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
|
||||
"integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.1.0",
|
||||
"entities": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
@@ -39637,8 +39661,7 @@
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.713",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.713.tgz",
|
||||
"integrity": "sha512-vDarADhwntXiULEdmWd77g2dV6FrNGa8ecAC29MZ4TwPut2fvosD0/5sJd1qWNNe8HcJFAC+F5Lf9jW1NPtWmw=="
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-trpc": {
|
||||
"version": "0.5.2",
|
||||
@@ -42830,8 +42853,7 @@
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
@@ -43390,8 +43412,7 @@
|
||||
},
|
||||
"node_modules/react-avatar-editor": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-avatar-editor/-/react-avatar-editor-13.0.2.tgz",
|
||||
"integrity": "sha512-a4ajbi7lwDh98kgEtSEeKMu0vs0CHTczkq4Xcxr1EiwMFH1GlgHCEtwGU8q/H5W8SeLnH4KPK8LUjEEaZXklxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -43457,6 +43478,17 @@
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-freeze": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz",
|
||||
"integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.4.1",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"cronosjs": "^1.7.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"dayjs": "1.11.9",
|
||||
"diffblazer": "^1.0.1",
|
||||
"electron-trpc": "0.5.2",
|
||||
"event-source-polyfill": "^1.0.25",
|
||||
"fflate": "^0.8.0",
|
||||
@@ -71,6 +72,7 @@
|
||||
"react-dom": "18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-error-boundary": "^4.0.12",
|
||||
"react-freeze": "^1.0.3",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
"react-modal": "3.16.1",
|
||||
@@ -99,6 +101,7 @@
|
||||
"@types/react-avatar-editor": "^13.0.2",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-modal": "3.16.3",
|
||||
"@types/react-scroll-sync": "^0.9.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@vitejs/plugin-react-swc": "3.3.2",
|
||||
|
||||
@@ -22,7 +22,7 @@ import { useStore } from "./stores/app-store";
|
||||
import { useStore as useUserStore } from "./stores/user-store";
|
||||
import { useStore as useThemeStore } from "./stores/theme-store";
|
||||
import { useStore as useAttachmentStore } from "./stores/attachment-store";
|
||||
import { useStore as useEditorStore } from "./stores/editor-store";
|
||||
import { useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { resetNotices, scheduleBackups } from "./common/notices";
|
||||
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
||||
@@ -244,7 +244,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
onData(itemType) {
|
||||
switch (itemType) {
|
||||
case "note":
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true });
|
||||
useEditorStore.getState().newSession();
|
||||
break;
|
||||
case "notebook":
|
||||
hashNavigate("/notebooks/create", { replace: true });
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Dialogs } from "../dialogs";
|
||||
import { store as tagStore } from "../stores/tag-store";
|
||||
import { store as appStore } from "../stores/app-store";
|
||||
import { store as editorStore } from "../stores/editor-store";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { db } from "./db";
|
||||
import { showToast } from "../utils/toast";
|
||||
@@ -378,7 +378,7 @@ export function showEditTagDialog(tag: Tag) {
|
||||
await db.tags.add({ id: tag.id, title });
|
||||
showToast("success", "Tag edited!");
|
||||
tagStore.refresh();
|
||||
editorStore.refreshTags();
|
||||
useEditorStore.getState().refreshTags();
|
||||
noteStore.refresh();
|
||||
appStore.refreshNavItems();
|
||||
perform(true);
|
||||
|
||||
@@ -43,12 +43,12 @@ import { FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
|
||||
import { User } from "@notesnook/core/dist/api/user-manager";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
|
||||
@@ -18,9 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { immerable, setAutoFreeze } from "immer";
|
||||
import { create } from "zustand";
|
||||
import { subscribeWithSelector } from "zustand/middleware";
|
||||
import {
|
||||
subscribeWithSelector,
|
||||
persist,
|
||||
PersistOptions
|
||||
} from "zustand/middleware";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
import { GetState, SetState } from "../stores";
|
||||
import { GetState, IStore, SetState } from "../stores";
|
||||
setAutoFreeze(false);
|
||||
|
||||
export function createStore<T>(
|
||||
@@ -38,9 +42,33 @@ export function createStore<T>(
|
||||
})
|
||||
)
|
||||
);
|
||||
// return Object.defineProperty(store as CustomStore<T>, "get", {
|
||||
// get: () => store.getState()
|
||||
// });
|
||||
return [store, store.getState()] as const;
|
||||
}
|
||||
|
||||
export function createPersistedStore<T extends object>(
|
||||
Store: IStore<T>,
|
||||
options: PersistOptions<T, Partial<T>>
|
||||
) {
|
||||
const store = create<
|
||||
T,
|
||||
[
|
||||
["zustand/persist", Partial<T>],
|
||||
["zustand/subscribeWithSelector", never],
|
||||
["zustand/immer", never]
|
||||
]
|
||||
>(
|
||||
persist(
|
||||
subscribeWithSelector(
|
||||
immer((set, get) => {
|
||||
const store = new Store(set, get);
|
||||
(store as any)[immerable] = true;
|
||||
return store;
|
||||
})
|
||||
),
|
||||
options
|
||||
)
|
||||
);
|
||||
|
||||
return [store, store.getState()] as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
Reupload,
|
||||
Uploading
|
||||
} from "../icons";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
import {
|
||||
closeOpenedDialog,
|
||||
showPromptDialog
|
||||
@@ -58,6 +57,7 @@ import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Attachment as AttachmentType } from "@notesnook/core";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
|
||||
const FILE_ICONS: Record<string, Icon> = {
|
||||
"image/": FileImage,
|
||||
@@ -247,7 +247,7 @@ const AttachmentMenuItems: (
|
||||
key: note.id,
|
||||
title: note.title,
|
||||
onClick: () => {
|
||||
hashNavigate(`/notes/${note.id}/edit`);
|
||||
useEditorStore.getState().openSession(note);
|
||||
closeOpenedDialog();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,49 +17,62 @@ 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 { Flex, Button, Text } from "@theme-ui/components";
|
||||
import { Flex, Button, Text, FlexProps } from "@theme-ui/components";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
|
||||
function ContentToggle(props) {
|
||||
type ContentToggle = {
|
||||
isSelected: boolean;
|
||||
isOtherSelected: boolean;
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
dateEdited: number;
|
||||
resolveConflict: (options: { saveCopy: boolean }) => void;
|
||||
readonly: boolean;
|
||||
sx: FlexProps["sx"];
|
||||
};
|
||||
function ContentToggle(props: ContentToggle) {
|
||||
const {
|
||||
isSelected,
|
||||
isOtherSelected,
|
||||
onToggle,
|
||||
sx,
|
||||
label,
|
||||
dateEdited,
|
||||
resolveConflict
|
||||
resolveConflict,
|
||||
readonly,
|
||||
sx
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Flex sx={{ ...sx, flexDirection: "column" }}>
|
||||
<Flex>
|
||||
{isOtherSelected && (
|
||||
{!readonly && (
|
||||
<Flex>
|
||||
{isOtherSelected && (
|
||||
<Button
|
||||
variant="accent"
|
||||
mr={2}
|
||||
onClick={() => resolveConflict({ saveCopy: true })}
|
||||
p={1}
|
||||
px={2}
|
||||
>
|
||||
Save copy
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="accent"
|
||||
mr={2}
|
||||
onClick={() => resolveConflict({ saveCopy: true })}
|
||||
variant={isOtherSelected ? "error" : "accent"}
|
||||
onClick={() => {
|
||||
if (isOtherSelected) {
|
||||
resolveConflict({ saveCopy: false });
|
||||
} else {
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
p={1}
|
||||
px={2}
|
||||
>
|
||||
Save copy
|
||||
{isSelected ? "Undo" : isOtherSelected ? "Discard" : "Keep"}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={isOtherSelected ? "error" : "accent"}
|
||||
onClick={() => {
|
||||
if (isOtherSelected) {
|
||||
resolveConflict({ saveCopy: false });
|
||||
} else {
|
||||
onToggle();
|
||||
}
|
||||
}}
|
||||
p={1}
|
||||
px={2}
|
||||
>
|
||||
{isSelected ? "Undo" : isOtherSelected ? "Discard" : "Keep"}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
<Text variant="subBody" mt={1}>
|
||||
{label} | {getFormattedDate(dateEdited)}
|
||||
</Text>
|
||||
@@ -1,296 +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 { useState, useEffect, useCallback } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Loading, ImageDownload } from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
import { store as notesStore } from "../../stores/note-store";
|
||||
import { db } from "../../common/db";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useEditorStore } from "../../stores/editor-store";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
|
||||
import { Editor } from "../editor";
|
||||
|
||||
function DiffViewer(props) {
|
||||
const { noteId } = props;
|
||||
|
||||
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
|
||||
const sync = useAppStore((store) => store.sync);
|
||||
const openSession = useEditorStore((store) => store.openSession);
|
||||
const [conflictedNote, setConflictedNote] = useState();
|
||||
const [remoteContent, setRemoteContent] = useState();
|
||||
const [localContent, setLocalContent] = useState();
|
||||
const [isDownloadingImages, setIsDownloadingImages] = useState(false);
|
||||
const [htmlDiff, setHtmlDiff] = useState({});
|
||||
const [selectedContent, setSelectedContent] = useState(-1);
|
||||
|
||||
const resolveConflict = useCallback(
|
||||
async ({ toKeep, toCopy, toKeepDateEdited, dateResolved }) => {
|
||||
if (!conflictedNote) return;
|
||||
|
||||
await db.notes.add({
|
||||
id: conflictedNote.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: conflictedNote.contentId,
|
||||
data: toKeep,
|
||||
type: "tiptap",
|
||||
dateResolved,
|
||||
conflicted: false,
|
||||
sessionId: Date.now()
|
||||
});
|
||||
|
||||
if (toCopy) {
|
||||
const toCopyContent = {
|
||||
data: toCopy,
|
||||
type: "tiptap"
|
||||
};
|
||||
await db.notes.add({
|
||||
content: toCopyContent,
|
||||
title: conflictedNote.title + " (COPY)"
|
||||
});
|
||||
}
|
||||
|
||||
await notesStore.refresh();
|
||||
hashNavigate(`/notes/${conflictedNote.id}/edit`, { replace: true });
|
||||
|
||||
const conflictsCount = await db.notes.conflicted.count();
|
||||
if (conflictsCount) {
|
||||
showToast(
|
||||
"success",
|
||||
`Conflict resolved. ${conflictsCount} conflicts left.`
|
||||
);
|
||||
} else {
|
||||
showToast("success", "All conflicts resolved. Starting sync.");
|
||||
await sync();
|
||||
}
|
||||
},
|
||||
[conflictedNote, sync]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
let note = await db.notes.note(noteId);
|
||||
if (!note) {
|
||||
hashNavigate(`/notes/create`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await openSession(noteId);
|
||||
setIsEditorOpen(true);
|
||||
|
||||
setConflictedNote(note);
|
||||
|
||||
const content = await db.content.get(note.contentId);
|
||||
if (!content.conflicted)
|
||||
return resolveConflict({
|
||||
toKeep: content.data,
|
||||
toKeepDateEdited: content.dateEdited
|
||||
});
|
||||
|
||||
setLocalContent({ ...content, conflicted: false });
|
||||
setRemoteContent(content.conflicted);
|
||||
|
||||
setHtmlDiff({ before: content.data, after: content.conflicted.data });
|
||||
})();
|
||||
}, [noteId]);
|
||||
|
||||
if (!conflictedNote || !localContent || !remoteContent) return null;
|
||||
return (
|
||||
<Flex
|
||||
className="diffviewer"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
mt={2}
|
||||
variant="heading"
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{conflictedNote.title}
|
||||
</Text>
|
||||
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setIsDownloadingImages(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.before,
|
||||
type: localContent.type
|
||||
}),
|
||||
db.content.downloadMedia(noteId, {
|
||||
data: htmlDiff.after,
|
||||
type: remoteContent.type
|
||||
})
|
||||
]);
|
||||
} finally {
|
||||
setIsDownloadingImages(false);
|
||||
}
|
||||
}}
|
||||
disabled={isDownloadingImages}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
{isDownloadingImages ? (
|
||||
<Loading size={18} />
|
||||
) : (
|
||||
<ImageDownload size={18} />
|
||||
)}
|
||||
<Text
|
||||
ml={1}
|
||||
sx={{ fontSize: "body", display: ["none", "block", "block"] }}
|
||||
>
|
||||
{isDownloadingImages ? "Downloading..." : "Load images"}
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
<ScrollSync>
|
||||
<Flex
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: ["column", "column", "row"],
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
className="firstEditor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"]
|
||||
}}
|
||||
>
|
||||
<ContentToggle
|
||||
label="Current note"
|
||||
dateEdited={localContent.dateEdited}
|
||||
isSelected={selectedContent === 0}
|
||||
isOtherSelected={selectedContent === 1}
|
||||
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
toKeep: remoteContent.data,
|
||||
toCopy: saveCopy ? localContent.data : null,
|
||||
toKeepDateEdited: localContent.dateEdited,
|
||||
dateResolved: remoteContent.dateModified
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex
|
||||
sx={{
|
||||
px: 2,
|
||||
overflowY: "auto",
|
||||
flex: 1,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderRightWidth: [0, 0, 1],
|
||||
borderBottomWidth: [1, 1, 0],
|
||||
borderColor: "border"
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
content={() => htmlDiff.before}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</Flex>
|
||||
<Flex
|
||||
className="secondEditor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"]
|
||||
}}
|
||||
>
|
||||
<ContentToggle
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
toKeep: localContent.data,
|
||||
toCopy: saveCopy ? remoteContent.data : null,
|
||||
toKeepDateEdited: remoteContent.dateEdited,
|
||||
dateResolved: remoteContent.dateModified
|
||||
});
|
||||
}}
|
||||
label="Incoming note"
|
||||
isSelected={selectedContent === 1}
|
||||
isOtherSelected={selectedContent === 0}
|
||||
dateEdited={remoteContent.dateEdited}
|
||||
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1,
|
||||
pt: [1, 1, 0]
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex sx={{ px: 2, overflow: "auto" }}>
|
||||
<Editor
|
||||
content={() => htmlDiff.after}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ScrollSync>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiffViewer;
|
||||
394
apps/web/src/components/diff-viewer/index.tsx
Normal file
394
apps/web/src/components/diff-viewer/index.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
/*
|
||||
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 { useState } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Loading, ImageDownload, Copy, Restore } from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
import { store as notesStore } from "../../stores/note-store";
|
||||
import { db } from "../../common/db";
|
||||
import {
|
||||
ConflictedEditorSession,
|
||||
useEditorStore
|
||||
} from "../../stores/editor-store";
|
||||
import { ScrollSync, ScrollSyncPane } from "react-scroll-sync";
|
||||
import { Editor } from "../editor";
|
||||
import { ContentItem, Note } from "@notesnook/core";
|
||||
import { UnlockView } from "../unlock";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
|
||||
type DiffViewerProps = { session: ConflictedEditorSession };
|
||||
function DiffViewer(props: DiffViewerProps) {
|
||||
const { session } = props;
|
||||
|
||||
const [isDownloadingImages, setIsDownloadingImages] = useState(false);
|
||||
const [selectedContent, setSelectedContent] = useState(-1);
|
||||
|
||||
const [content, setContent] = useState(session.content);
|
||||
const [conflictedContent, setConflictedContent] = useState(
|
||||
content.conflicted
|
||||
);
|
||||
|
||||
if (!conflictedContent) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className="diffviewer"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{session.note.title}
|
||||
</Text>
|
||||
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
|
||||
{!content.locked && !conflictedContent.locked && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
setIsDownloadingImages(true);
|
||||
try {
|
||||
await Promise.all([
|
||||
db.content.downloadMedia(session.id, {
|
||||
data: content.data,
|
||||
type: content.type
|
||||
}),
|
||||
db.content.downloadMedia(session.id, {
|
||||
data: conflictedContent.data,
|
||||
type: conflictedContent.type
|
||||
})
|
||||
]);
|
||||
} finally {
|
||||
setIsDownloadingImages(false);
|
||||
}
|
||||
}}
|
||||
disabled={isDownloadingImages}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
{isDownloadingImages ? (
|
||||
<Loading size={18} />
|
||||
) : (
|
||||
<ImageDownload size={18} />
|
||||
)}
|
||||
<Text
|
||||
ml={1}
|
||||
sx={{ fontSize: "body", display: ["none", "block", "block"] }}
|
||||
>
|
||||
{isDownloadingImages ? "Downloading..." : "Load images"}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{session.type === "diff" ? (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const { closeSessions, openSession } =
|
||||
useEditorStore.getState();
|
||||
|
||||
await db.noteHistory.restore(session.id);
|
||||
|
||||
closeSessions(session.id, session.note.id);
|
||||
|
||||
await notesStore.refresh();
|
||||
await openSession(session.note.id, true);
|
||||
}}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<Restore size={18} />
|
||||
<Text ml={1}>Restore this version</Text>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const { closeSessions, openSession } =
|
||||
useEditorStore.getState();
|
||||
|
||||
const noteId = await createCopy(session.note, content);
|
||||
|
||||
closeSessions(session.id);
|
||||
|
||||
await notesStore.refresh();
|
||||
await openSession(noteId);
|
||||
}}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<Copy size={18} />
|
||||
<Text ml={1}>Save a copy</Text>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
<ScrollSync>
|
||||
<Flex
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: ["column", "column", "row"],
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
className="firstEditor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"]
|
||||
}}
|
||||
>
|
||||
{content.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(content.dateEdited)}
|
||||
subtitle="Please enter the password to view this version"
|
||||
buttonTitle="Unlock"
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
content,
|
||||
session.note.id,
|
||||
password
|
||||
);
|
||||
setContent({
|
||||
...content,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
label={
|
||||
session.type === "diff" ? "Older version" : "Current note"
|
||||
}
|
||||
readonly={session.type === "diff"}
|
||||
dateEdited={content.dateEdited}
|
||||
isSelected={selectedContent === 0}
|
||||
isOtherSelected={selectedContent === 1}
|
||||
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep: content.data,
|
||||
toCopy: saveCopy ? conflictedContent : undefined,
|
||||
toKeepDateEdited: content.dateEdited,
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollSyncPane>
|
||||
<Flex
|
||||
sx={{
|
||||
px: 2,
|
||||
overflowY: "auto",
|
||||
flex: 1,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderRightWidth: [0, 0, 1],
|
||||
borderBottomWidth: [1, 1, 0],
|
||||
borderColor: "border"
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
id={content.id}
|
||||
content={() => content.data}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex
|
||||
className="secondEditor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"],
|
||||
borderLeft: conflictedContent.locked
|
||||
? "1px solid var(--border)"
|
||||
: "none"
|
||||
}}
|
||||
>
|
||||
{conflictedContent.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(conflictedContent.dateEdited)}
|
||||
subtitle="Please enter the password to view this version"
|
||||
buttonTitle="Unlock"
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
conflictedContent,
|
||||
session.note.id,
|
||||
password
|
||||
);
|
||||
setConflictedContent({
|
||||
...conflictedContent,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
readonly={session.type === "diff"}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep: conflictedContent.data,
|
||||
toCopy: saveCopy ? content : undefined,
|
||||
toKeepDateEdited: conflictedContent.dateEdited,
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
}}
|
||||
label={
|
||||
session.type === "diff"
|
||||
? "Current version"
|
||||
: "Incoming note"
|
||||
}
|
||||
isSelected={selectedContent === 1}
|
||||
isOtherSelected={selectedContent === 0}
|
||||
dateEdited={conflictedContent.dateEdited}
|
||||
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1,
|
||||
pt: [1, 1, 0]
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex sx={{ px: 2, overflow: "auto" }}>
|
||||
<Editor
|
||||
id={`${conflictedContent.id}-conflicted`}
|
||||
content={() => conflictedContent.data}
|
||||
nonce={0}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ScrollSync>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiffViewer;
|
||||
|
||||
async function resolveConflict({
|
||||
note,
|
||||
toKeep,
|
||||
toCopy,
|
||||
toKeepDateEdited,
|
||||
dateResolved
|
||||
}: {
|
||||
note: Note;
|
||||
toKeep: string;
|
||||
toCopy?: ContentItem;
|
||||
toKeepDateEdited: number;
|
||||
dateResolved?: number;
|
||||
}) {
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
dateEdited: toKeepDateEdited,
|
||||
conflicted: false
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: toKeep,
|
||||
type: "tiptap",
|
||||
dateResolved,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
|
||||
if (toCopy) {
|
||||
await createCopy(note, toCopy);
|
||||
}
|
||||
|
||||
await notesStore.refresh();
|
||||
useEditorStore.getState().openSession(note.id, true);
|
||||
}
|
||||
|
||||
async function createCopy(note: Note, content: ContentItem) {
|
||||
if (content.locked) {
|
||||
const contentId = await db.content.add({
|
||||
locked: true,
|
||||
data: content.data,
|
||||
type: content.type,
|
||||
noteId: note.id
|
||||
});
|
||||
return await db.notes.add({
|
||||
contentId,
|
||||
title: note.title + " (COPY)"
|
||||
});
|
||||
} else {
|
||||
return await db.notes.add({
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: content.data
|
||||
},
|
||||
title: note.title + " (COPY)"
|
||||
});
|
||||
}
|
||||
}
|
||||
671
apps/web/src/components/editor/action-bar.tsx
Normal file
671
apps/web/src/components/editor/action-bar.tsx
Normal file
@@ -0,0 +1,671 @@
|
||||
/*
|
||||
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 { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Cross,
|
||||
ExitFullscreen,
|
||||
FocusMode,
|
||||
Fullscreen,
|
||||
Lock,
|
||||
NormalMode,
|
||||
Note,
|
||||
Pin,
|
||||
Properties,
|
||||
Search,
|
||||
Unlock
|
||||
} from "../icons";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import {
|
||||
SessionType,
|
||||
isLockedSession,
|
||||
useEditorStore
|
||||
} from "../../stores/editor-store";
|
||||
import { Menu } from "../../hooks/use-menu";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useEditorManager, useSearch } from "./manager";
|
||||
|
||||
export function EditorActionBar() {
|
||||
// const editorMargins = useEditorStore((store) => store.editorMargins);
|
||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const activeSession = useEditorStore((store) =>
|
||||
store.activeSessionId ? store.getSession(store.activeSessionId) : undefined
|
||||
);
|
||||
const { toggleSearch } = useSearch();
|
||||
|
||||
const tools = [
|
||||
// {
|
||||
// title: editorMargins ? "Disable editor margins" : "Enable editor margins",
|
||||
// icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
|
||||
// enabled: true,
|
||||
// onClick: () => useEditorStore.getState().toggleEditorMargins()
|
||||
// },
|
||||
{
|
||||
title: isFocusMode ? "Normal mode" : "Focus mode",
|
||||
icon: isFocusMode ? FocusMode : NormalMode,
|
||||
enabled: true,
|
||||
hideOnMobile: true,
|
||||
onClick: () => {
|
||||
useAppStore.getState().toggleFocusMode();
|
||||
if (document.fullscreenElement) exitFullscreen();
|
||||
const id = useEditorStore.getState().activeSessionId;
|
||||
const editor = id && useEditorManager.getState().getEditor(id);
|
||||
if (editor) editor.editor?.focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
||||
icon: isFullscreen ? ExitFullscreen : Fullscreen,
|
||||
enabled: true,
|
||||
hidden: !isFocusMode,
|
||||
hideOnMobile: true,
|
||||
onClick: () => {
|
||||
if (isFullscreen) {
|
||||
exitFullscreen();
|
||||
} else {
|
||||
enterFullscreen(document.documentElement);
|
||||
}
|
||||
setIsFullscreen((s) => !s);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
icon: Search,
|
||||
enabled:
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "readonly",
|
||||
onClick: toggleSearch
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
icon: Properties,
|
||||
enabled:
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "readonly" &&
|
||||
activeSession.type !== "locked" &&
|
||||
!isFocusMode,
|
||||
onClick: () => useEditorStore.getState().toggleProperties()
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex sx={{ mb: 2, gap: 2 }}>
|
||||
<TabStrip />
|
||||
<Flex
|
||||
bg="background"
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
mr: 2
|
||||
}}
|
||||
>
|
||||
{tools.map((tool) => (
|
||||
<Button
|
||||
data-test-id={tool.title}
|
||||
disabled={!tool.enabled}
|
||||
variant="secondary"
|
||||
title={tool.title}
|
||||
key={tool.title}
|
||||
sx={{
|
||||
display: [
|
||||
tool.hideOnMobile ? "none" : "flex",
|
||||
tool.hidden ? "none" : "flex"
|
||||
],
|
||||
borderRadius: 0,
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={tool.onClick}
|
||||
>
|
||||
<tool.icon size={18} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function TabStrip() {
|
||||
const sessions = useEditorStore((store) => store.sessions);
|
||||
const activeSessionId = useEditorStore((store) => store.activeSessionId);
|
||||
|
||||
return (
|
||||
<ScrollContainer
|
||||
className="tabsScroll"
|
||||
suppressScrollY
|
||||
style={{ flex: 1 }}
|
||||
trackStyle={() => ({
|
||||
backgroundColor: "transparent",
|
||||
pointerEvents: "none"
|
||||
})}
|
||||
thumbStyle={() => ({ height: 3 })}
|
||||
onWheel={(e) => {
|
||||
const scrollcontainer = document.querySelector(".tabsScroll");
|
||||
if (!scrollcontainer) return;
|
||||
if (e.deltaY > 0) scrollcontainer.scrollLeft += 100;
|
||||
else if (e.deltaY < 0) scrollcontainer.scrollLeft -= 100;
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flex: 1,
|
||||
my: 1,
|
||||
ml: 1,
|
||||
gap: 1,
|
||||
height: 32
|
||||
}}
|
||||
onDoubleClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
useEditorStore.getState().newSession();
|
||||
}}
|
||||
>
|
||||
{sessions.map((session, i) => (
|
||||
<Tab
|
||||
key={session.id}
|
||||
title={
|
||||
session.title ||
|
||||
("note" in session ? session.note.title : "Untitled")
|
||||
}
|
||||
isTemporary={!!session.preview}
|
||||
isActive={session.id === activeSessionId}
|
||||
isPinned={!!session.pinned}
|
||||
isLocked={isLockedSession(session)}
|
||||
type={session.type}
|
||||
index={i}
|
||||
onKeepOpen={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.updateSession(
|
||||
session.id,
|
||||
[session.type],
|
||||
(s) => (s.preview = true)
|
||||
)
|
||||
}
|
||||
onFocus={() => {
|
||||
if (session.id !== activeSessionId) {
|
||||
useEditorStore.getState().openSession(session.id);
|
||||
}
|
||||
}}
|
||||
onMove={(from, to) => {
|
||||
if (from === to) return;
|
||||
|
||||
useEditorStore.setState((state) => {
|
||||
const direction =
|
||||
to === 0 ? "start" : from > to ? "left" : "right";
|
||||
const [fromTab] = state.sessions.splice(from, 1);
|
||||
const newIndex =
|
||||
direction === "start" || direction === "right" ? to : to - 1;
|
||||
|
||||
// unpin the tab if it is moved.
|
||||
if (fromTab.pinned) fromTab.pinned = false;
|
||||
// if the tab where this tab is being dropped is pinned,
|
||||
// let's pin our tab too.
|
||||
if (state.sessions[to].pinned) fromTab.pinned = true;
|
||||
|
||||
state.sessions.splice(newIndex, 0, fromTab);
|
||||
});
|
||||
}}
|
||||
onClose={() => useEditorStore.getState().closeSessions(session.id)}
|
||||
onCloseAll={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions.filter((s) => !s.pinned).map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseOthers={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s) => s.id !== session.id && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheRight={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index > i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onCloseToTheLeft={() =>
|
||||
useEditorStore
|
||||
.getState()
|
||||
.closeSessions(
|
||||
...sessions
|
||||
.filter((s, index) => index < i && !s.pinned)
|
||||
.map((s) => s.id)
|
||||
)
|
||||
}
|
||||
onPin={() => {
|
||||
useEditorStore.setState((state) => {
|
||||
let to = state.sessions.findLastIndex((a) => a.pinned);
|
||||
if (to === -1) to = 0;
|
||||
const [fromTab] = state.sessions.splice(i, 1);
|
||||
fromTab.pinned = !fromTab.pinned;
|
||||
// preview tabs can never be pinned.
|
||||
if (fromTab.pinned) fromTab.preview = false;
|
||||
state.sessions.splice(to + 1, 0, fromTab);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const dragState: { element?: HTMLElement | null; index: number } = {
|
||||
element: undefined,
|
||||
index: -1
|
||||
};
|
||||
|
||||
type TabProps = {
|
||||
title: string;
|
||||
index: number;
|
||||
isActive: boolean;
|
||||
isTemporary: boolean;
|
||||
isPinned: boolean;
|
||||
isLocked: boolean;
|
||||
type: SessionType;
|
||||
onKeepOpen: () => void;
|
||||
onFocus: () => void;
|
||||
onClose: () => void;
|
||||
onCloseOthers: () => void;
|
||||
onCloseToTheRight: () => void;
|
||||
onCloseToTheLeft: () => void;
|
||||
onCloseAll: () => void;
|
||||
onPin: () => void;
|
||||
onMove: (from: number, to: number) => void;
|
||||
};
|
||||
function Tab(props: TabProps) {
|
||||
const {
|
||||
title,
|
||||
index,
|
||||
isActive,
|
||||
isTemporary,
|
||||
isPinned,
|
||||
isLocked,
|
||||
type,
|
||||
onKeepOpen,
|
||||
onFocus,
|
||||
onClose,
|
||||
onCloseAll,
|
||||
onCloseOthers,
|
||||
onCloseToTheRight,
|
||||
onCloseToTheLeft,
|
||||
onMove,
|
||||
onPin
|
||||
} = props;
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const Icon = isLocked ? (type === "locked" ? Lock : Unlock) : Note;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
className="tab"
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: "7px",
|
||||
bg: isDragOver
|
||||
? "shade"
|
||||
: isActive
|
||||
? "background"
|
||||
: "background-secondary",
|
||||
// borderTopLeftRadius: "default",
|
||||
// borderTopRightRadius: "default",
|
||||
// borderBottom: isActive ? "none" : "1px solid var(--border)",
|
||||
border: "1px solid",
|
||||
borderColor: isActive ? "border" : "transparent",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
":hover": {
|
||||
"& .closeTabButton": {
|
||||
visibility: "visible"
|
||||
},
|
||||
bg: isActive ? "background" : "hover"
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
Menu.openMenu([
|
||||
{ type: "button", title: "Close", key: "close", onClick: onClose },
|
||||
{
|
||||
type: "button",
|
||||
title: "Close others",
|
||||
key: "close-others",
|
||||
onClick: onCloseOthers
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: "Close to the right",
|
||||
key: "close-to-the-right",
|
||||
onClick: onCloseToTheRight
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: "Close to the left",
|
||||
key: "close-to-the-left",
|
||||
onClick: onCloseToTheLeft
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: "Close all",
|
||||
key: "close-all",
|
||||
onClick: onCloseAll
|
||||
},
|
||||
{ type: "separator", key: "sep" },
|
||||
{
|
||||
type: "button",
|
||||
key: "keep-open",
|
||||
title: "Keep open",
|
||||
onClick: onKeepOpen,
|
||||
isDisabled: !isTemporary
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "pin",
|
||||
title: "Pin",
|
||||
onClick: onPin,
|
||||
isChecked: isPinned,
|
||||
icon: Pin.path
|
||||
}
|
||||
]);
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isTemporary) onKeepOpen();
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFocus();
|
||||
}}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
onFocus();
|
||||
e.target.style.cursor = "grabbing";
|
||||
dragState.element = e.target;
|
||||
dragState.index = index;
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
e.target.style.cursor = "pointer";
|
||||
dragState.element = null;
|
||||
dragState.index = -1;
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
if (e.target === dragState.element) return;
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
if (e.target === dragState.element) return;
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (e.target === dragState.element) return;
|
||||
setIsDragOver(false);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
setIsDragOver(false);
|
||||
onMove(dragState.index, index);
|
||||
}}
|
||||
>
|
||||
<Flex mr={1}>
|
||||
<Icon size={16} color={isActive ? "accent" : "icon"} />
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflowX: "hidden",
|
||||
pointerEvents: "none",
|
||||
fontStyle: isTemporary ? "italic" : "normal",
|
||||
maxWidth: 120
|
||||
}}
|
||||
ml={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Flex>
|
||||
{isPinned ? (
|
||||
<Pin
|
||||
sx={{
|
||||
":hover": { bg: "border" },
|
||||
borderRadius: "default",
|
||||
flexShrink: 0
|
||||
}}
|
||||
size={14}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onPin();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Cross
|
||||
sx={{
|
||||
visibility: isActive ? "visible" : "hidden",
|
||||
":hover": { bg: "border" },
|
||||
borderRadius: "default",
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
className="closeTabButton"
|
||||
size={16}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
// import { useEffect, useMemo, useState } from "react";
|
||||
// import { Button, Flex, Text } from "@theme-ui/components";
|
||||
// import {
|
||||
// Published,
|
||||
// Publish,
|
||||
// EditorNormalWidth,
|
||||
// EditorFullWidth,
|
||||
// ThemeIcon,
|
||||
// FocusMode,
|
||||
// NormalMode,
|
||||
// ExitFullscreen,
|
||||
// Fullscreen,
|
||||
// Search,
|
||||
// Undo,
|
||||
// Redo,
|
||||
// Properties,
|
||||
// ArrowLeft
|
||||
// } from "../icons";
|
||||
// import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
// import { useStore as useMonographStore } from "../../stores/monograph-store";
|
||||
// import { useStore, store } from "../../stores/editor-store";
|
||||
// import { showToast } from "../../utils/toast";
|
||||
// import { AnimatedInput } from "../animated";
|
||||
// import { showPublishView } from "../publish-view";
|
||||
// import { db } from "../../common/db";
|
||||
// import { useEditorInstance, useHistory, useSearch } from "./manager";
|
||||
// import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
|
||||
// // TODO: this needs to be cleaned up!
|
||||
// function Toolbar() {
|
||||
// const sessionId = useStore((store) => store.session.id);
|
||||
// const isDeleted = useStore((store) => store.session.isDeleted);
|
||||
// const isLocked = useStore((store) => store.session.locked);
|
||||
// const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
// const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
// const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
|
||||
// const toggleProperties = useStore((store) => store.toggleProperties);
|
||||
// const toggleEditorMargins = useStore((store) => store.toggleEditorMargins);
|
||||
// const clearSession = useStore((store) => store.clearSession);
|
||||
// const title = useStore((store) => store.session.title);
|
||||
// const theme = useThemeStore((store) => store.colorScheme);
|
||||
// const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
|
||||
// const [isTitleVisible, setIsTitleVisible] = useState(false);
|
||||
|
||||
// const monographs = useMonographStore((store) => store.monographs);
|
||||
// const { canRedo, canUndo, redo, undo } = useHistory();
|
||||
// const { toggleSearch } = useSearch();
|
||||
// const editor = useEditorInstance();
|
||||
|
||||
// const isNotePublished = useMemo(
|
||||
// () => sessionId && db.monographs.isPublished(sessionId),
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// [sessionId, monographs]
|
||||
// );
|
||||
|
||||
// useEffect(() => {
|
||||
// const editorScroll = document.querySelector(".editorScroll");
|
||||
// if (!editorScroll) return;
|
||||
|
||||
// function onScroll(e) {
|
||||
// const hideOffset = document.querySelector(".editorTitle").scrollHeight;
|
||||
// if (e.target.scrollTop > hideOffset && !isTitleVisible)
|
||||
// setIsTitleVisible(e.target.scrollTop > hideOffset && !isTitleVisible);
|
||||
// else if (e.target.scrollTop <= hideOffset && isTitleVisible)
|
||||
// setIsTitleVisible(false);
|
||||
// }
|
||||
// editorScroll.addEventListener("scroll", onScroll);
|
||||
// return () => {
|
||||
// editorScroll.removeEventListener("scroll", onScroll);
|
||||
// };
|
||||
// }, [isTitleVisible]);
|
||||
|
||||
// const tools = useMemo(
|
||||
// () => [
|
||||
// {
|
||||
// title: isNotePublished ? "Published" : "Publish",
|
||||
// icon: isNotePublished ? Published : Publish,
|
||||
// hidden: !sessionId || isDeleted,
|
||||
// enabled: !isLocked,
|
||||
// onClick: () => showPublishView(store.get().session.id, "top")
|
||||
// }
|
||||
// ],
|
||||
// [sessionId, isLocked, isNotePublished, isDeleted]
|
||||
// );
|
||||
|
||||
// return (
|
||||
// <Flex mx={2} my={1} sx={{ justifyContent: "space-between" }}>
|
||||
// <Flex sx={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
|
||||
// <ArrowLeft
|
||||
// sx={{
|
||||
// display: ["block", "none", "none"],
|
||||
// flexShrink: 0
|
||||
// }}
|
||||
// size={24}
|
||||
// onClick={() => {
|
||||
// if (store.get().session.id) showToast("success", "Note saved!");
|
||||
// if (isFocusMode) toggleFocusMode();
|
||||
// clearSession();
|
||||
// }}
|
||||
// />
|
||||
// <AnimatedInput
|
||||
// variant="clean"
|
||||
// ml={[2, 2, 0]}
|
||||
// initial={{
|
||||
// opacity: isTitleVisible ? 1 : 0,
|
||||
// zIndex: isTitleVisible ? 1 : -1
|
||||
// }}
|
||||
// animate={{
|
||||
// opacity: isTitleVisible ? 1 : 0,
|
||||
// zIndex: isTitleVisible ? 1 : -1
|
||||
// }}
|
||||
// transition={{ duration: 0.5 }}
|
||||
// defaultValue={title}
|
||||
// onChange={(e) => {
|
||||
// AppEventManager.publish(AppEvents.changeNoteTitle, {
|
||||
// title: e.target.value,
|
||||
// preventSave: false
|
||||
// });
|
||||
// }}
|
||||
// sx={{
|
||||
// flex: 1,
|
||||
// fontWeight: "heading",
|
||||
// fontSize: "heading",
|
||||
// color: "paragraph",
|
||||
// p: 0,
|
||||
// pl: 4,
|
||||
// borderWidth: 0,
|
||||
// borderRadius: "default",
|
||||
// textOverflow: "ellipsis",
|
||||
// whiteSpace: "nowrap",
|
||||
// overflow: "hidden"
|
||||
// }}
|
||||
// />
|
||||
// </Flex>
|
||||
|
||||
// <Flex sx={{ gap: 1 }}>
|
||||
// {tools.map((tool) => (
|
||||
// <Button
|
||||
// key={tool.title}
|
||||
// variant="secondary"
|
||||
// data-test-id={tool.title}
|
||||
// disabled={!tool.enabled}
|
||||
// title={tool.title}
|
||||
// sx={{
|
||||
// display: [
|
||||
// tool.hideOnMobile ? "none" : "flex",
|
||||
// tool.hidden ? "none" : "flex"
|
||||
// ],
|
||||
// color: "paragraph",
|
||||
// flexDirection: "row",
|
||||
// flexShrink: 0,
|
||||
// alignItems: "center"
|
||||
// }}
|
||||
// onClick={tool.onClick}
|
||||
// >
|
||||
// <tool.icon size={18} />
|
||||
// <Text
|
||||
// variant="body"
|
||||
// ml={1}
|
||||
// sx={{ display: ["none", "none", "block"] }}
|
||||
// >
|
||||
// {tool.title}
|
||||
// </Text>
|
||||
// </Button>
|
||||
// ))}
|
||||
// </Flex>
|
||||
// </Flex>
|
||||
// );
|
||||
// }
|
||||
// export default Toolbar;
|
||||
|
||||
function enterFullscreen(elem: HTMLElement) {
|
||||
elem.requestFullscreen();
|
||||
}
|
||||
|
||||
function exitFullscreen() {
|
||||
if (!document.fullscreenElement) return;
|
||||
document.exitFullscreen();
|
||||
}
|
||||
@@ -1,160 +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 { useCallback, useEffect, useRef } from "react";
|
||||
import { IEditor, NoteStatistics } from "./types";
|
||||
import createStore from "../../common/store";
|
||||
import BaseStore from "../../stores";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import type { ToolbarDefinition } from "@notesnook/editor";
|
||||
import Config from "../../utils/config";
|
||||
|
||||
type EditorConfig = { fontFamily: string; fontSize: number };
|
||||
type EditorSubState = {
|
||||
editor?: IEditor;
|
||||
canUndo?: boolean;
|
||||
canRedo?: boolean;
|
||||
searching?: boolean;
|
||||
toolbarConfig?: ToolbarDefinition;
|
||||
editorConfig: EditorConfig;
|
||||
statistics?: NoteStatistics;
|
||||
};
|
||||
|
||||
class EditorContext extends BaseStore<EditorContext> {
|
||||
subState: EditorSubState = {
|
||||
editorConfig: Config.get("editorConfig", {
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: 16
|
||||
})
|
||||
};
|
||||
|
||||
configure = (
|
||||
partial:
|
||||
| Partial<EditorSubState>
|
||||
| ((oldState: EditorSubState) => Partial<EditorSubState>)
|
||||
) => {
|
||||
this.set((state) => {
|
||||
const newPartialState =
|
||||
typeof partial === "function" ? partial(state.subState) : partial;
|
||||
state.subState = { ...state.subState, ...newPartialState };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const [useEditorContext] = createStore<EditorContext>(
|
||||
(set, get) => new EditorContext(set, get)
|
||||
);
|
||||
|
||||
export function useEditorInstance() {
|
||||
const editor = useEditorContext((store) => store.subState.editor);
|
||||
const editorRef = useRef(editor);
|
||||
useEffect(() => {
|
||||
editorRef.current = editor;
|
||||
}, [editor]);
|
||||
return editorRef;
|
||||
}
|
||||
|
||||
export function useConfigureEditor() {
|
||||
return useEditorContext((store) => store.configure);
|
||||
}
|
||||
|
||||
export const configureEditor = (
|
||||
partial:
|
||||
| Partial<EditorSubState>
|
||||
| ((oldState: EditorSubState) => Partial<EditorSubState>)
|
||||
) => useEditorContext.getState().configure(partial);
|
||||
|
||||
export function useHistory() {
|
||||
return useEditorContext(
|
||||
(store) =>
|
||||
({
|
||||
canUndo: store.subState.canUndo,
|
||||
canRedo: store.subState.canRedo,
|
||||
undo: store.subState.editor?.undo,
|
||||
redo: store.subState.editor?.redo
|
||||
} as const),
|
||||
shallow
|
||||
);
|
||||
}
|
||||
|
||||
export function useSearch() {
|
||||
const isSearching = useEditorContext((store) => store.subState.searching);
|
||||
const configure = useEditorContext((store) => store.configure);
|
||||
const toggleSearch = useCallback(
|
||||
() => configure({ searching: !isSearching }),
|
||||
[isSearching, configure]
|
||||
);
|
||||
return { isSearching, toggleSearch };
|
||||
}
|
||||
|
||||
export function useToolbarConfig() {
|
||||
const toolbarConfig = useEditorContext(
|
||||
(store) => store.subState.toolbarConfig
|
||||
);
|
||||
const configure = useEditorContext((store) => store.configure);
|
||||
const setToolbarConfig = useCallback(
|
||||
(config: ToolbarDefinition) => configure({ toolbarConfig: config }),
|
||||
[configure]
|
||||
);
|
||||
return { toolbarConfig, setToolbarConfig };
|
||||
}
|
||||
|
||||
export function useNoteStatistics(): NoteStatistics {
|
||||
return useEditorContext(
|
||||
(store) =>
|
||||
store.subState.statistics || {
|
||||
words: { total: 0 }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEditorConfig() {
|
||||
const editorConfig = useEditorContext((store) => store.subState.editorConfig);
|
||||
return { editorConfig, setEditorConfig };
|
||||
}
|
||||
|
||||
export const editorConfig = () =>
|
||||
useEditorContext.getState().subState.editorConfig;
|
||||
|
||||
export const setEditorConfig = (config: Partial<EditorConfig>) => {
|
||||
const oldConfig = editorConfig();
|
||||
if (oldConfig)
|
||||
Config.set("editorConfig", {
|
||||
...oldConfig,
|
||||
...config
|
||||
});
|
||||
|
||||
useEditorContext.getState().configure({
|
||||
editorConfig: {
|
||||
...oldConfig,
|
||||
...config
|
||||
}
|
||||
});
|
||||
};
|
||||
export const onEditorConfigChange = (
|
||||
selector: (editorConfig: EditorConfig) => any,
|
||||
listener: (
|
||||
selectedState: EditorConfig,
|
||||
previousSelectedState: EditorConfig
|
||||
) => void
|
||||
) =>
|
||||
useEditorContext.subscribe(
|
||||
(s) => selector(s.subState.editorConfig),
|
||||
listener
|
||||
);
|
||||
@@ -18,11 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { useMemo } from "react";
|
||||
import { useStore } from "../../stores/editor-store";
|
||||
import { SaveState, useEditorStore } from "../../stores/editor-store";
|
||||
import { Loading, Saved, NotSaved } from "../icons";
|
||||
import { useNoteStatistics } from "./context";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { useNoteStatistics } from "./manager";
|
||||
|
||||
const SAVE_STATE_ICON_MAP = {
|
||||
"-1": NotSaved,
|
||||
@@ -31,18 +29,14 @@ const SAVE_STATE_ICON_MAP = {
|
||||
};
|
||||
|
||||
function EditorFooter() {
|
||||
const { words } = useNoteStatistics();
|
||||
const dateEdited = useStore((store) => store.session.dateEdited);
|
||||
const id = useStore((store) => store.session.id);
|
||||
const saveState = useStore(
|
||||
(store) => store.session.saveState
|
||||
) as keyof typeof SAVE_STATE_ICON_MAP;
|
||||
const SaveStateIcon = useMemo(
|
||||
() => SAVE_STATE_ICON_MAP[saveState],
|
||||
[saveState]
|
||||
const activeSessionId = useEditorStore((store) => store.activeSessionId);
|
||||
const { words } = useNoteStatistics(activeSessionId || "unknown");
|
||||
const saveState = useEditorStore(
|
||||
(store) => store.getActiveSession(["default", "unlocked"])?.saveState
|
||||
);
|
||||
const SaveStateIcon = saveState ? SAVE_STATE_ICON_MAP[saveState] : null;
|
||||
|
||||
if (!id) return null;
|
||||
if (!activeSessionId) return null;
|
||||
return (
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
<Text
|
||||
@@ -55,23 +49,13 @@ function EditorFooter() {
|
||||
{words.total + " words"}
|
||||
{words.selected ? ` (${words.selected} selected)` : ""}
|
||||
</Text>
|
||||
<Text
|
||||
className="selectable"
|
||||
variant="subBody"
|
||||
mr={2}
|
||||
sx={{ color: "paragraph" }}
|
||||
data-test-id="editor-date-edited"
|
||||
title={dateEdited?.toString()}
|
||||
>
|
||||
{getFormattedDate(dateEdited || Date.now())}
|
||||
</Text>
|
||||
{SaveStateIcon && (
|
||||
<SaveStateIcon
|
||||
size={13}
|
||||
color={
|
||||
saveState === 1
|
||||
saveState === SaveState.Saved
|
||||
? "accent"
|
||||
: saveState === "-1"
|
||||
: saveState === SaveState.NotSaved
|
||||
? "red"
|
||||
: "paragraph"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useStore } from "../../stores/editor-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { useStore as useTagStore } from "../../stores/tag-store";
|
||||
import { useStore as useNoteStore } from "../../stores/note-store";
|
||||
import { Input } from "@theme-ui/components";
|
||||
@@ -30,26 +30,17 @@ import { useMenuTrigger } from "../../hooks/use-menu";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { navigate } from "../../navigation";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { usePromise } from "@notesnook/common";
|
||||
|
||||
type HeaderProps = { readonly: boolean };
|
||||
type HeaderProps = { readonly: boolean; id: string };
|
||||
function Header(props: HeaderProps) {
|
||||
const { readonly } = props;
|
||||
const id = useStore((store) => store.session.id);
|
||||
const tags = useStore((store) => store.tags);
|
||||
const refreshTags = useStore((store) => store.refreshTags);
|
||||
const { readonly, id } = props;
|
||||
const tags = useEditorStore((store) => store.getSession(id)?.tags || []);
|
||||
const refreshTags = useEditorStore((store) => store.refreshTags);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
refreshTags();
|
||||
}, [id, refreshTags]);
|
||||
|
||||
const defaultTags = usePromise(() =>
|
||||
db.tags.all
|
||||
.limit(10)
|
||||
.items(undefined, { sortBy: "dateCreated", sortDirection: "desc" })
|
||||
);
|
||||
|
||||
const setTag = useCallback(
|
||||
async function (noteId: string, tags: Tag[], value: string) {
|
||||
const oldTag = tags.find((t) => t.title === value);
|
||||
@@ -62,76 +53,76 @@ function Header(props: HeaderProps) {
|
||||
{ type: "note", id: noteId }
|
||||
);
|
||||
await useTagStore.getState().refresh();
|
||||
if (defaultTags.status === "fulfilled") defaultTags.refresh();
|
||||
}
|
||||
await refreshTags();
|
||||
await useNoteStore.getState().refresh();
|
||||
},
|
||||
[refreshTags, defaultTags]
|
||||
[refreshTags]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{id && (
|
||||
<Flex
|
||||
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
|
||||
data-test-id="tags"
|
||||
>
|
||||
{tags?.map((tag) => (
|
||||
<IconTag
|
||||
testId={`tag`}
|
||||
key={tag.id}
|
||||
text={tag.title}
|
||||
icon={TagIcon}
|
||||
onClick={() => navigate(`/tags/${tag.id}`)}
|
||||
onDismiss={
|
||||
readonly ? undefined : () => setTag(id, tags, tag.title)
|
||||
<Flex
|
||||
sx={{ lineHeight: 2.5, alignItems: "center", flexWrap: "wrap" }}
|
||||
data-test-id="tags"
|
||||
>
|
||||
{tags?.map((tag) => (
|
||||
<IconTag
|
||||
testId={`tag`}
|
||||
key={tag.id}
|
||||
text={tag.title}
|
||||
icon={TagIcon}
|
||||
onClick={() => navigate(`/tags/${tag.id}`)}
|
||||
onDismiss={readonly ? undefined : () => setTag(id, tags, tag.title)}
|
||||
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
|
||||
/>
|
||||
))}
|
||||
{!readonly && tags ? (
|
||||
<Autosuggest
|
||||
sessionId={id}
|
||||
filter={(query) => db.lookup.tags(query).items(10)}
|
||||
toMenuItems={(filtered, reset, query) => {
|
||||
const items: MenuItem[] = [];
|
||||
const isExactMatch =
|
||||
!!query && filtered.some((item) => item.title === query);
|
||||
if (query && !isExactMatch) {
|
||||
items.push({
|
||||
type: "button",
|
||||
key: "new",
|
||||
title: `Create "${query}" tag`,
|
||||
icon: Plus.path,
|
||||
onClick: () => setTag(id, tags, query).finally(reset)
|
||||
});
|
||||
}
|
||||
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
|
||||
/>
|
||||
))}
|
||||
{!readonly && tags && defaultTags.status === "fulfilled" ? (
|
||||
<Autosuggest
|
||||
sessionId={id}
|
||||
filter={(query) => db.lookup.tags(query).items(10)}
|
||||
toMenuItems={(filtered, reset, query) => {
|
||||
const items: MenuItem[] = [];
|
||||
const isExactMatch =
|
||||
!!query && filtered.some((item) => item.title === query);
|
||||
if (query && !isExactMatch) {
|
||||
items.push({
|
||||
type: "button",
|
||||
key: "new",
|
||||
title: `Create "${query}" tag`,
|
||||
icon: Plus.path,
|
||||
onClick: () => setTag(id, tags, query).finally(reset)
|
||||
});
|
||||
}
|
||||
|
||||
if (filtered.length > 0) {
|
||||
items.push(
|
||||
...filtered.map((item) => ({
|
||||
type: "button" as const,
|
||||
key: item.id,
|
||||
title: item.title,
|
||||
icon: TagIcon.path,
|
||||
onClick: () => setTag(id, tags, item.title).finally(reset)
|
||||
}))
|
||||
);
|
||||
}
|
||||
if (filtered.length > 0) {
|
||||
items.push(
|
||||
...filtered.map((item) => ({
|
||||
type: "button" as const,
|
||||
key: item.id,
|
||||
title: item.title,
|
||||
icon: TagIcon.path,
|
||||
onClick: () => setTag(id, tags, item.title).finally(reset)
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
}}
|
||||
onAdd={(value) => setTag(id, tags, value)}
|
||||
onRemove={() => {
|
||||
if (tags.length <= 0) return;
|
||||
setTag(id, tags, tags[tags.length - 1].title);
|
||||
}}
|
||||
defaultItems={defaultTags.value}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
)}
|
||||
return items;
|
||||
}}
|
||||
onAdd={(value) => setTag(id, tags, value)}
|
||||
onRemove={() => {
|
||||
if (tags.length <= 0) return;
|
||||
setTag(id, tags, tags[tags.length - 1].title);
|
||||
}}
|
||||
defaultItems={() =>
|
||||
db.tags.all.limit(10).items(undefined, {
|
||||
sortBy: "dateCreated",
|
||||
sortDirection: "desc"
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -143,7 +134,7 @@ type AutosuggestProps<T> = {
|
||||
onRemove: () => void;
|
||||
onAdd: (text: string) => void;
|
||||
toMenuItems: (filtered: T[], reset: () => void, query?: string) => MenuItem[];
|
||||
defaultItems: T[];
|
||||
defaultItems: () => Promise<T[]>;
|
||||
};
|
||||
export function Autosuggest<T>(props: AutosuggestProps<T>) {
|
||||
const { sessionId, filter, onRemove, onAdd, defaultItems, toMenuItems } =
|
||||
@@ -207,14 +198,14 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
|
||||
}}
|
||||
placeholder="Add a tag..."
|
||||
data-test-id="editor-tag-input"
|
||||
onFocus={() => {
|
||||
onFocus={async () => {
|
||||
const text = getInputValue();
|
||||
if (!text) onOpenMenu(defaultItems);
|
||||
if (!text) onOpenMenu(await defaultItems());
|
||||
else closeMenu();
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
const text = getInputValue();
|
||||
if (!text) onOpenMenu(defaultItems);
|
||||
if (!text) onOpenMenu(await defaultItems());
|
||||
else closeMenu();
|
||||
}}
|
||||
onChange={async (e) => {
|
||||
@@ -225,7 +216,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
|
||||
}
|
||||
onOpenMenu(await filter(value));
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown={async (e) => {
|
||||
const text = getInputValue();
|
||||
if (e.key === "Enter" && !!text && isOpen && !arrowDown.current) {
|
||||
onAdd(text);
|
||||
@@ -239,7 +230,7 @@ export function Autosuggest<T>(props: AutosuggestProps<T>) {
|
||||
e.stopPropagation();
|
||||
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||
arrowDown.current = true;
|
||||
if (e.key === "ArrowDown" && !text) onOpenMenu(defaultItems);
|
||||
if (e.key === "ArrowDown" && !text) onOpenMenu(await defaultItems());
|
||||
|
||||
e.preventDefault();
|
||||
} else if (e.key === "Tab") {
|
||||
|
||||
@@ -19,28 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
useRef,
|
||||
PropsWithChildren,
|
||||
Suspense
|
||||
} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
|
||||
import { Box, Flex, Progress, Text } from "@theme-ui/components";
|
||||
import Properties from "../properties";
|
||||
import { useStore, store as editorstore } from "../../stores/editor-store";
|
||||
import { useEditorStore, SaveState } from "../../stores/editor-store";
|
||||
import {
|
||||
useStore as useAppStore,
|
||||
store as appstore
|
||||
} from "../../stores/app-store";
|
||||
import Toolbar from "./toolbar";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { FlexScrollContainer } from "../scroll-container";
|
||||
import Tiptap from "./tiptap";
|
||||
import Tiptap, { OnChangeHandler } from "./tiptap";
|
||||
import Header from "./header";
|
||||
import { Attachment } from "../icons";
|
||||
import { useEditorInstance } from "./context";
|
||||
import { attachFiles, AttachmentProgress, insertAttachments } from "./picker";
|
||||
import { useEditorManager } from "./manager";
|
||||
import { saveAttachment, downloadAttachment } from "../../common/attachments";
|
||||
import { EV, EVENTS } from "@notesnook/core/dist/common";
|
||||
import { db } from "../../common/db";
|
||||
@@ -48,20 +46,22 @@ import useMobile from "../../hooks/use-mobile";
|
||||
import Titlebox from "./title-box";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
import Config from "../../utils/config";
|
||||
import { AnimatedFlex } from "../animated";
|
||||
import { EditorLoader } from "../loaders/editor-loader";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { Lightbox } from "../lightbox";
|
||||
import { Allotment } from "allotment";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { debounce, getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
ContentType,
|
||||
Item,
|
||||
MaybeDeletedItem,
|
||||
isDeleted
|
||||
} from "@notesnook/core/dist/types";
|
||||
import { debounce, debounceWithId } from "@notesnook/common";
|
||||
import { PreviewSession } from "./types";
|
||||
import { Freeze } from "react-freeze";
|
||||
import { EditorActionBar } from "./action-bar";
|
||||
import { UnlockView } from "../unlock";
|
||||
import DiffViewer from "../diff-viewer";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
@@ -70,45 +70,87 @@ type DocumentPreview = {
|
||||
hash: string;
|
||||
};
|
||||
|
||||
function onEditorChange(
|
||||
noteId: string | undefined,
|
||||
function saveContent(
|
||||
noteId: string,
|
||||
sessionId: string,
|
||||
content: string,
|
||||
ignoreEdit: boolean
|
||||
ignoreEdit: boolean,
|
||||
content: () => string
|
||||
) {
|
||||
if (!content) return;
|
||||
|
||||
editorstore.get().saveSessionContent(noteId, sessionId, ignoreEdit, {
|
||||
console.log("SAVE to note");
|
||||
useEditorStore.getState().saveSessionContent(noteId, sessionId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
data: content()
|
||||
});
|
||||
}
|
||||
const deferredSave = debounceWithId(saveContent, 100);
|
||||
|
||||
export default function EditorManager({
|
||||
noteId,
|
||||
nonce
|
||||
}: {
|
||||
noteId?: string | number;
|
||||
nonce?: string;
|
||||
}) {
|
||||
const isNewSession = !!nonce && !noteId;
|
||||
const isOldSession = !nonce && !!noteId;
|
||||
export default function TabsView() {
|
||||
const sessions = useEditorStore((store) => store.sessions);
|
||||
const activeSessionId = useEditorStore((store) => store.activeSessionId);
|
||||
|
||||
// the only state that changes. Everything else is
|
||||
// stored in refs. Update this value to trigger an
|
||||
// update.
|
||||
const [timestamp, setTimestamp] = useState<number>(0);
|
||||
useEffect(() => {
|
||||
if (!activeSessionId) return;
|
||||
// if the session isn't yet rendered, do nothing.
|
||||
const activeSession = useEditorStore.getState().getSession(activeSessionId);
|
||||
if (!activeSession?.needsHydration)
|
||||
useEditorManager.getState().editors[activeSessionId]?.editor?.focus();
|
||||
}, [activeSessionId]);
|
||||
console.log("TabsView", sessions, activeSessionId);
|
||||
return (
|
||||
<>
|
||||
<EditorActionBar />
|
||||
{sessions.map((session) => (
|
||||
<Freeze key={session.id} freeze={session.id !== activeSessionId}>
|
||||
{session.needsHydration ? null : session.type === "locked" ? (
|
||||
<UnlockView
|
||||
buttonTitle="Open note"
|
||||
subtitle="Please enter the password to unlock this note."
|
||||
title={session.note.title}
|
||||
unlock={async (password) => {
|
||||
const note = await db.vault.open(session.id, password);
|
||||
if (!note) throw new Error("note with this id does not exist.");
|
||||
|
||||
const lastSavedTime = useRef<number>(0);
|
||||
useEditorStore.getState().addSession({
|
||||
type: "default",
|
||||
id: session.id,
|
||||
note: session.note,
|
||||
saveState: SaveState.Saved,
|
||||
sessionId: `${Date.now()}`,
|
||||
pinned: session.pinned,
|
||||
preview: session.preview,
|
||||
content: note.content
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : session.type === "conflicted" || session.type === "diff" ? (
|
||||
<DiffViewer session={session} />
|
||||
) : (
|
||||
<MemoizedEditorView id={session.id} />
|
||||
)}
|
||||
</Freeze>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MemoizedEditorView = React.memo(
|
||||
EditorView,
|
||||
(prev, next) => prev.id === next.id
|
||||
);
|
||||
function EditorView({ id }: { id: string }) {
|
||||
const lastSavedTime = useRef<number>(Date.now());
|
||||
const [docPreview, setDocPreview] = useState<DocumentPreview>();
|
||||
|
||||
const previewSession = useRef<PreviewSession>();
|
||||
const [dropRef, overlayRef] = useDragOverlay();
|
||||
const editorInstance = useEditorInstance();
|
||||
|
||||
const arePropertiesVisible = useStore((store) => store.arePropertiesVisible);
|
||||
const toggleProperties = useStore((store) => store.toggleProperties);
|
||||
const isReadonly = useStore((store) => store.session.readonly);
|
||||
const arePropertiesVisible = useEditorStore(
|
||||
(store) => store.arePropertiesVisible
|
||||
);
|
||||
const toggleProperties = useEditorStore((store) => store.toggleProperties);
|
||||
const isReadonly = useEditorStore(
|
||||
(store) => store.getSession(id, ["default"])?.note?.readonly
|
||||
);
|
||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
const isPreviewSession = !!previewSession.current;
|
||||
|
||||
@@ -119,7 +161,13 @@ export default function EditorManager({
|
||||
const event = db.eventManager.subscribe(
|
||||
EVENTS.syncItemMerged,
|
||||
async (item?: MaybeDeletedItem<Item>) => {
|
||||
const session = useEditorStore
|
||||
.getState()
|
||||
.getSession(id, ["unlocked", "default"]);
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (
|
||||
!editor ||
|
||||
!session?.note ||
|
||||
!item ||
|
||||
isDeleted(item) ||
|
||||
(item.type !== "tiptap" && item.type !== "note") ||
|
||||
@@ -129,11 +177,11 @@ export default function EditorManager({
|
||||
)
|
||||
return;
|
||||
|
||||
const { id, contentId, locked } = editorstore.get().session;
|
||||
const { contentId, locked } = session.note;
|
||||
const isContent = item.type === "tiptap" && item.id === contentId;
|
||||
const isNote = item.type === "note" && item.id === id;
|
||||
|
||||
if (id && isContent && editorInstance.current) {
|
||||
if (id && isContent) {
|
||||
let content: string | null = null;
|
||||
if (locked && item.locked) {
|
||||
const result = await db.vault
|
||||
@@ -142,11 +190,13 @@ export default function EditorManager({
|
||||
if (result) content = result.data;
|
||||
else EV.publish(EVENTS.vaultLocked);
|
||||
}
|
||||
editorInstance.current.updateContent(item.data as string);
|
||||
editor.updateContent(item.data as string);
|
||||
} else if (isNote) {
|
||||
if (!locked && item.locked) return EV.publish(EVENTS.vaultLocked);
|
||||
|
||||
editorstore.get().updateSession(item);
|
||||
useEditorStore
|
||||
.getState()
|
||||
.updateSession(id, ["default"], { note: item });
|
||||
if (item.title)
|
||||
AppEventManager.publish(AppEvents.changeNoteTitle, {
|
||||
title: item.title,
|
||||
@@ -158,32 +208,30 @@ export default function EditorManager({
|
||||
return () => {
|
||||
event.unsubscribe();
|
||||
};
|
||||
}, [editorInstance, isPreviewSession]);
|
||||
}, [id, isPreviewSession]);
|
||||
|
||||
const openSession = useCallback(async (noteId: string) => {
|
||||
await editorstore.get().openSession(noteId);
|
||||
previewSession.current = undefined;
|
||||
// const openSession = useCallback(async (noteId: string) => {
|
||||
// await useEditorStore.getState().openSession(noteId);
|
||||
// previewSession.current = undefined;
|
||||
|
||||
lastSavedTime.current = Date.now();
|
||||
setTimestamp(Date.now());
|
||||
}, []);
|
||||
// lastSavedTime.current = Date.now();
|
||||
// setTimestamp(Date.now());
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNewSession) return;
|
||||
// useEffect(() => {
|
||||
// if (!isNewSession) return;
|
||||
|
||||
(async function () {
|
||||
await editorstore.newSession(nonce);
|
||||
// editorstore.newSession();
|
||||
|
||||
lastSavedTime.current = 0;
|
||||
setTimestamp(Date.now());
|
||||
})();
|
||||
}, [isNewSession, nonce]);
|
||||
// lastSavedTime.current = 0;
|
||||
// setTimestamp(Date.now());
|
||||
// }, [isNewSession, nonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOldSession || typeof noteId === "number") return;
|
||||
// useEffect(() => {
|
||||
// if (!isOldSession || typeof noteId === "number") return;
|
||||
|
||||
openSession(noteId);
|
||||
}, [noteId]);
|
||||
// openSession(noteId);
|
||||
// }, [noteId]);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider scope="editor" sx={{ flex: 1 }}>
|
||||
@@ -206,23 +254,32 @@ export default function EditorManager({
|
||||
background: "background"
|
||||
}}
|
||||
>
|
||||
{previewSession.current && noteId && (
|
||||
{/* {previewSession.current && (
|
||||
<PreviewModeNotice
|
||||
{...previewSession.current}
|
||||
onDiscard={() =>
|
||||
typeof noteId === "string" && openSession(noteId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
<Editor
|
||||
id={noteId}
|
||||
nonce={timestamp}
|
||||
id={id}
|
||||
nonce={1}
|
||||
content={() =>
|
||||
previewSession.current?.content.data ||
|
||||
editorstore.get().session?.content?.data
|
||||
useEditorStore.getState().getSession(id, ["default"])?.content
|
||||
?.data
|
||||
}
|
||||
onPreviewDocument={(url) => setDocPreview(url)}
|
||||
onContentChange={() => (lastSavedTime.current = Date.now())}
|
||||
onSave={(content, ignoreEdit) => {
|
||||
const session = useEditorStore
|
||||
.getState()
|
||||
.getSession(id, ["default"]);
|
||||
console.log("ON SAVE", session);
|
||||
if (!session) return;
|
||||
deferredSave(id, id, session.sessionId, ignoreEdit, content);
|
||||
}}
|
||||
options={{
|
||||
readonly: isReadonly || isPreviewSession,
|
||||
onRequestFocus: () => toggleProperties(false),
|
||||
@@ -231,15 +288,8 @@ export default function EditorManager({
|
||||
}}
|
||||
/>
|
||||
|
||||
{arePropertiesVisible && (
|
||||
<Properties
|
||||
onOpenPreviewSession={async (session: PreviewSession) => {
|
||||
previewSession.current = session;
|
||||
setTimestamp(Date.now());
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DropZone overlayRef={overlayRef} />
|
||||
{arePropertiesVisible && <Properties id={id} />}
|
||||
<DropZone id={id} overlayRef={overlayRef} />
|
||||
</Flex>
|
||||
</Allotment.Pane>
|
||||
{docPreview && (
|
||||
@@ -331,16 +381,24 @@ type EditorOptions = {
|
||||
onRequestFocus?: () => void;
|
||||
};
|
||||
type EditorProps = {
|
||||
id?: string | number;
|
||||
id: string;
|
||||
content: () => string | undefined;
|
||||
nonce?: number;
|
||||
options?: EditorOptions;
|
||||
onContentChange?: () => void;
|
||||
onSave?: OnChangeHandler;
|
||||
onPreviewDocument?: (preview: DocumentPreview) => void;
|
||||
};
|
||||
export function Editor(props: EditorProps) {
|
||||
const { id, content, nonce, options, onContentChange, onPreviewDocument } =
|
||||
props;
|
||||
const {
|
||||
id,
|
||||
content,
|
||||
onSave,
|
||||
nonce,
|
||||
options,
|
||||
onContentChange,
|
||||
onPreviewDocument
|
||||
} = props;
|
||||
const { readonly, headless, isMobile } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
@@ -349,13 +407,12 @@ export function Editor(props: EditorProps) {
|
||||
};
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const editor = useEditorInstance();
|
||||
|
||||
useEffect(() => {
|
||||
const event = AppEventManager.subscribe(
|
||||
AppEvents.UPDATE_ATTACHMENT_PROGRESS,
|
||||
({ hash, loaded, total }: AttachmentProgress) => {
|
||||
editor.current?.sendAttachmentProgress(
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
editor?.sendAttachmentProgress(
|
||||
hash,
|
||||
Math.round((loaded / total) * 100)
|
||||
);
|
||||
@@ -365,24 +422,28 @@ export function Editor(props: EditorProps) {
|
||||
return () => {
|
||||
event.unsubscribe();
|
||||
};
|
||||
}, [editor]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EditorChrome isLoading={isLoading} {...props}>
|
||||
<Tiptap
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
nonce={nonce}
|
||||
readonly={readonly}
|
||||
toolbarContainerId={headless ? undefined : "editorToolbar"}
|
||||
content={content}
|
||||
downloadOptions={{
|
||||
corsHost: Config.get("corsProxy", "https://cors.notesnook.com")
|
||||
}}
|
||||
onLoad={() => {
|
||||
if (nonce && nonce > 0) setIsLoading(false);
|
||||
restoreSelection(id);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
onSelectionChange={({ from, to }) =>
|
||||
Config.set(`${id}:selection`, { from, to })
|
||||
}
|
||||
onContentChange={onContentChange}
|
||||
onChange={onEditorChange}
|
||||
onChange={onSave}
|
||||
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
|
||||
onPreviewAttachment={async (data) => {
|
||||
const { hash } = data;
|
||||
@@ -418,11 +479,12 @@ export function Editor(props: EditorProps) {
|
||||
}
|
||||
}}
|
||||
onInsertAttachment={async (type) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
const mime = type === "file" ? "*/*" : "image/*";
|
||||
const attachments = await insertAttachments(mime);
|
||||
if (!attachments) return;
|
||||
for (const attachment of attachments) {
|
||||
editor.current?.attachFile(attachment);
|
||||
editor?.attachFile(attachment);
|
||||
}
|
||||
}}
|
||||
onGetAttachmentData={(attachment) => {
|
||||
@@ -433,13 +495,19 @@ export function Editor(props: EditorProps) {
|
||||
);
|
||||
}}
|
||||
onAttachFiles={async (files) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
const result = await attachFiles(files);
|
||||
if (!result) return;
|
||||
result.forEach((attachment) =>
|
||||
editor.current?.attachFile(attachment)
|
||||
);
|
||||
result.forEach((attachment) => editor?.attachFile(attachment));
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{headless ? null : (
|
||||
<>
|
||||
<Titlebox id={id} readonly={readonly || false} />
|
||||
<Header id={id} readonly={readonly || false} />
|
||||
</>
|
||||
)}
|
||||
</Tiptap>
|
||||
</EditorChrome>
|
||||
);
|
||||
}
|
||||
@@ -447,17 +515,16 @@ export function Editor(props: EditorProps) {
|
||||
function EditorChrome(
|
||||
props: PropsWithChildren<EditorProps & { isLoading: boolean }>
|
||||
) {
|
||||
const { options, children, isLoading } = props;
|
||||
const { readonly, focusMode, headless, onRequestFocus, isMobile } =
|
||||
options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
isMobile: false
|
||||
};
|
||||
const editorMargins = useStore((store) => store.editorMargins);
|
||||
const { id, options, children } = props;
|
||||
const { focusMode, headless, onRequestFocus, isMobile } = options || {
|
||||
headless: false,
|
||||
readonly: false,
|
||||
focusMode: false,
|
||||
isMobile: false
|
||||
};
|
||||
const editorMargins = useEditorStore((store) => store.editorMargins);
|
||||
const editorContainerRef = useRef<HTMLElement>(null);
|
||||
const editorScrollRef = useRef<HTMLElement>(null);
|
||||
const editorScrollRef = useRef<HTMLElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorScrollRef.current) return;
|
||||
@@ -493,7 +560,7 @@ function EditorChrome(
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
{/* {isLoading ? (
|
||||
<AnimatedFlex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
@@ -508,13 +575,27 @@ function EditorChrome(
|
||||
>
|
||||
<EditorLoader />
|
||||
</AnimatedFlex>
|
||||
) : null}
|
||||
) : null} */}
|
||||
|
||||
<Toolbar />
|
||||
{/* <Toolbar /> */}
|
||||
<FlexScrollContainer
|
||||
scrollRef={editorScrollRef}
|
||||
className="editorScroll"
|
||||
style={{ display: "flex", flexDirection: "column", flex: 1 }}
|
||||
id={`${id}_editorScroll`}
|
||||
scrollRef={(ref) => {
|
||||
editorScrollRef.current = ref || undefined;
|
||||
restoreScrollPosition(id);
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1
|
||||
// minHeight: Config.get(`${id}:scroll-position`, 0) + 100
|
||||
}}
|
||||
onScroll={debounce((e) => {
|
||||
if (e.target instanceof HTMLElement) {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
Config.set(`${id}:scroll-position`, scrollTop);
|
||||
}
|
||||
}, 500)}
|
||||
>
|
||||
<Flex
|
||||
ref={editorContainerRef}
|
||||
@@ -529,34 +610,35 @@ function EditorChrome(
|
||||
pr={6}
|
||||
onClick={onRequestFocus}
|
||||
>
|
||||
{!isMobile && (
|
||||
<Box
|
||||
id="editorToolbar"
|
||||
sx={{
|
||||
display: readonly ? "none" : "flex",
|
||||
bg: "background",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
mb: 1,
|
||||
zIndex: 2
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Titlebox readonly={readonly || false} />
|
||||
<Header readonly={readonly || false} />
|
||||
<AnimatedFlex
|
||||
{/* {!isMobile && (
|
||||
|
||||
)} */}
|
||||
{/* <Box
|
||||
id={`${id}_toolbar`}
|
||||
sx={{
|
||||
minHeight: 34,
|
||||
display: readonly ? "none" : "flex",
|
||||
bg: "background",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
mb: 1,
|
||||
zIndex: 2
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* <AnimatedFlex
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: isLoading ? 0 : 1 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{children}
|
||||
</AnimatedFlex>
|
||||
> */}
|
||||
{children}
|
||||
{/* </AnimatedFlex> */}
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
{isMobile && (
|
||||
<Box
|
||||
id="editorToolbar"
|
||||
id={`${id}_toolbar`}
|
||||
sx={{
|
||||
display: "flex",
|
||||
bg: "background",
|
||||
@@ -572,68 +654,68 @@ function EditorChrome(
|
||||
);
|
||||
}
|
||||
|
||||
type PreviewModeNoticeProps = PreviewSession & {
|
||||
onDiscard: () => void;
|
||||
};
|
||||
function PreviewModeNotice(props: PreviewModeNoticeProps) {
|
||||
const { dateCreated, dateEdited, content, onDiscard } = props;
|
||||
const disablePreviewMode = useCallback(
|
||||
async (cancelled: boolean) => {
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
if (!cancelled) {
|
||||
await editorstore.saveSessionContent(id, sessionId, false, content);
|
||||
}
|
||||
onDiscard();
|
||||
},
|
||||
[onDiscard, content]
|
||||
);
|
||||
// type PreviewModeNoticeProps = PreviewSession & {
|
||||
// onDiscard: () => void;
|
||||
// };
|
||||
// function PreviewModeNotice(props: PreviewModeNoticeProps) {
|
||||
// const { dateCreated, dateEdited, content, onDiscard } = props;
|
||||
// const disablePreviewMode = useCallback(
|
||||
// async (cancelled: boolean) => {
|
||||
// const { id, sessionId } = useEditorStore.getState().session;
|
||||
// if (!cancelled) {
|
||||
// await editorstore.saveSessionContent(id, sessionId, content);
|
||||
// }
|
||||
// onDiscard();
|
||||
// },
|
||||
// [onDiscard, content]
|
||||
// );
|
||||
|
||||
return (
|
||||
<Flex
|
||||
bg="var(--background-secondary)"
|
||||
p={2}
|
||||
sx={{ alignItems: "center", justifyContent: "space-between" }}
|
||||
data-test-id="preview-notice"
|
||||
>
|
||||
<Flex mr={4} sx={{ flexDirection: "column" }}>
|
||||
<Text variant={"subtitle"}>Preview</Text>
|
||||
<Text variant={"body"}>
|
||||
You are previewing note version edited from{" "}
|
||||
{getFormattedDate(dateCreated, "date-time")} to{" "}
|
||||
{getFormattedDate(dateEdited, "date-time")}.
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Button
|
||||
data-test-id="preview-notice-cancel"
|
||||
variant={"secondary"}
|
||||
mr={1}
|
||||
px={4}
|
||||
onClick={() => disablePreviewMode(true)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="accent"
|
||||
data-test-id="preview-notice-restore"
|
||||
px={4}
|
||||
onClick={async () => {
|
||||
await disablePreviewMode(false);
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
// return (
|
||||
// <Flex
|
||||
// bg="var(--background-secondary)"
|
||||
// p={2}
|
||||
// sx={{ alignItems: "center", justifyContent: "space-between" }}
|
||||
// data-test-id="preview-notice"
|
||||
// >
|
||||
// <Flex mr={4} sx={{ flexDirection: "column" }}>
|
||||
// <Text variant={"subtitle"}>Preview</Text>
|
||||
// <Text variant={"body"}>
|
||||
// You are previewing note version edited from{" "}
|
||||
// {getFormattedDate(dateCreated, "date-time")} to{" "}
|
||||
// {getFormattedDate(dateEdited, "date-time")}.
|
||||
// </Text>
|
||||
// </Flex>
|
||||
// <Flex>
|
||||
// <Button
|
||||
// data-test-id="preview-notice-cancel"
|
||||
// variant={"secondary"}
|
||||
// mr={1}
|
||||
// px={4}
|
||||
// onClick={() => disablePreviewMode(true)}
|
||||
// >
|
||||
// Cancel
|
||||
// </Button>
|
||||
// <Button
|
||||
// variant="accent"
|
||||
// data-test-id="preview-notice-restore"
|
||||
// px={4}
|
||||
// onClick={async () => {
|
||||
// await disablePreviewMode(false);
|
||||
// }}
|
||||
// >
|
||||
// Restore
|
||||
// </Button>
|
||||
// </Flex>
|
||||
// </Flex>
|
||||
// );
|
||||
// }
|
||||
|
||||
type DropZoneProps = {
|
||||
id: string;
|
||||
overlayRef: React.MutableRefObject<HTMLElement | undefined>;
|
||||
};
|
||||
function DropZone(props: DropZoneProps) {
|
||||
const { overlayRef } = props;
|
||||
const editor = useEditorInstance();
|
||||
const { overlayRef, id } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -650,12 +732,14 @@ function DropZone(props: DropZoneProps) {
|
||||
display: "none"
|
||||
}}
|
||||
onDrop={async (e) => {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
if (!editor || !e.dataTransfer.files?.length) return;
|
||||
e.preventDefault();
|
||||
|
||||
(await attachFiles(Array.from(e.dataTransfer.files)))?.forEach(
|
||||
(attachment) => editor.current?.attachFile(attachment)
|
||||
);
|
||||
const attachments = await attachFiles(Array.from(e.dataTransfer.files));
|
||||
for (const attachment of attachments || []) {
|
||||
editor.attachFile(attachment);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
@@ -728,3 +812,22 @@ function isFile(e: DragEvent) {
|
||||
e.dataTransfer.types?.some((a) => a === "Files"))
|
||||
);
|
||||
}
|
||||
|
||||
function restoreScrollPosition(id: string) {
|
||||
const scrollContainer = document.getElementById(`${id}_editorScroll`);
|
||||
const scrollPosition = Config.get(`${id}:scroll-position`, 0);
|
||||
if (scrollContainer) {
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollContainer.scrollHeight < scrollPosition)
|
||||
scrollContainer.style.minHeight = `${scrollPosition + 100}px`;
|
||||
scrollContainer.scrollTop = scrollPosition;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restoreSelection(id: string) {
|
||||
const editor = useEditorManager.getState().getEditor(id)?.editor;
|
||||
editor?.focus({
|
||||
position: Config.get(`${id}:selection`, { from: 0, to: 0 })
|
||||
});
|
||||
}
|
||||
|
||||
159
apps/web/src/components/editor/manager.ts
Normal file
159
apps/web/src/components/editor/manager.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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 { useCallback } from "react";
|
||||
import { IEditor, NoteStatistics } from "./types";
|
||||
import createStore from "../../common/store";
|
||||
import BaseStore from "../../stores";
|
||||
import type { ToolbarDefinition } from "@notesnook/editor";
|
||||
import Config from "../../utils/config";
|
||||
import { getCurrentPreset } from "../../common/toolbar-config";
|
||||
|
||||
type EditorConfig = { fontFamily: string; fontSize: number };
|
||||
type EditorState = {
|
||||
selection?: { from: number; to: number };
|
||||
scrollPosition?: number;
|
||||
};
|
||||
type EditorContext = {
|
||||
editor?: IEditor;
|
||||
canUndo?: boolean;
|
||||
canRedo?: boolean;
|
||||
statistics?: NoteStatistics;
|
||||
};
|
||||
|
||||
class EditorManager extends BaseStore<EditorManager> {
|
||||
toolbarConfig?: ToolbarDefinition;
|
||||
editorConfig: EditorConfig = Config.get("editorConfig", {
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: 16
|
||||
});
|
||||
searching?: boolean;
|
||||
editors: Record<string, EditorContext> = {};
|
||||
|
||||
getEditor = (id: string): EditorContext | undefined => {
|
||||
return this.get().editors[id];
|
||||
};
|
||||
|
||||
setEditor = (id: string, editor?: EditorContext) => {
|
||||
this.set((state) => {
|
||||
if (!editor) delete state.editors[id];
|
||||
else state.editors[id] = editor;
|
||||
});
|
||||
};
|
||||
|
||||
updateEditor = (
|
||||
id: string,
|
||||
partial:
|
||||
| Partial<EditorContext>
|
||||
| ((oldState: EditorContext) => Partial<EditorContext>)
|
||||
) => {
|
||||
this.set((state) => {
|
||||
const newPartialState =
|
||||
typeof partial === "function" ? partial(state.editors[id]) : partial;
|
||||
state.editors[id] = { ...state.editors[id], ...newPartialState };
|
||||
});
|
||||
};
|
||||
|
||||
setEditorConfig = (config: Partial<EditorConfig>) => {
|
||||
const oldConfig = this.get().editorConfig;
|
||||
this.set({
|
||||
editorConfig: Config.set("editorConfig", {
|
||||
...oldConfig,
|
||||
...config
|
||||
})
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const [useEditorManager] = createStore<EditorManager>(
|
||||
(set, get) => new EditorManager(set, get)
|
||||
);
|
||||
|
||||
export { useEditorManager };
|
||||
|
||||
// export function useEditorInstance(id: string) {
|
||||
// const editor = useEditorContext((store) => store.subState.editors[id]);
|
||||
// const editorRef = useRef(editor);
|
||||
// useEffect(() => {
|
||||
// editorRef.current = editor;
|
||||
// }, [editor]);
|
||||
// return editorRef;
|
||||
// }
|
||||
// export const editorInstance = (id: string) =>
|
||||
// useEditorManager.getState().editors[id];
|
||||
|
||||
// export function useConfigureEditor() {
|
||||
// return useEditorContext((store) => store.configure);
|
||||
// }
|
||||
|
||||
// export const configureEditor = (
|
||||
// partial:
|
||||
// | Partial<EditorSubState>
|
||||
// | ((oldState: EditorSubState) => Partial<EditorSubState>)
|
||||
// ) => useEditorContext.getState().configure(partial);
|
||||
|
||||
export function useEditor(id: string) {
|
||||
return useEditorManager((store) => store.editors[id]);
|
||||
}
|
||||
|
||||
export function useSearch() {
|
||||
const isSearching = useEditorManager((store) => store.searching);
|
||||
const toggleSearch = useCallback(
|
||||
() => useEditorManager.setState({ searching: !isSearching }),
|
||||
[isSearching]
|
||||
);
|
||||
return { isSearching, toggleSearch };
|
||||
}
|
||||
|
||||
export function useToolbarConfig() {
|
||||
const toolbarConfig =
|
||||
useEditorManager((store) => store.toolbarConfig) ||
|
||||
getCurrentPreset().tools;
|
||||
const setToolbarConfig = useCallback(
|
||||
(config: ToolbarDefinition) =>
|
||||
useEditorManager.setState({ toolbarConfig: config }),
|
||||
[]
|
||||
);
|
||||
return { toolbarConfig, setToolbarConfig };
|
||||
}
|
||||
|
||||
export function useNoteStatistics(id: string): NoteStatistics {
|
||||
return useEditorManager(
|
||||
(store) =>
|
||||
store.editors[id]?.statistics || {
|
||||
words: { total: 0 }
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function useEditorConfig() {
|
||||
const editorConfig = useEditorManager((store) => store.editorConfig);
|
||||
const setEditorConfig = useEditorManager((store) => store.setEditorConfig);
|
||||
return { editorConfig, setEditorConfig };
|
||||
}
|
||||
|
||||
export const editorConfig = () => useEditorManager.getState().editorConfig;
|
||||
|
||||
export const onEditorConfigChange = (
|
||||
selector: (editorConfig: EditorConfig) => any,
|
||||
listener: (
|
||||
selectedState: EditorConfig,
|
||||
previousSelectedState: EditorConfig
|
||||
) => void
|
||||
) => useEditorManager.subscribe((s) => selector(s.editorConfig), listener);
|
||||
@@ -38,37 +38,41 @@ import {
|
||||
Attachment
|
||||
} from "@notesnook/editor";
|
||||
import { Box, Flex } from "@theme-ui/components";
|
||||
import { PropsWithChildren, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
PropsWithChildren,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef
|
||||
} from "react";
|
||||
import { IEditor } from "./types";
|
||||
import {
|
||||
useConfigureEditor,
|
||||
useSearch,
|
||||
useEditorConfig,
|
||||
useToolbarConfig,
|
||||
configureEditor,
|
||||
useEditorConfig
|
||||
} from "./context";
|
||||
useEditorManager
|
||||
} from "./manager";
|
||||
import { createPortal } from "react-dom";
|
||||
import { getCurrentPreset } from "../../common/toolbar-config";
|
||||
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showBuyDialog } from "../../common/dialog-controller";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { debounce, debounceWithId } from "@notesnook/common";
|
||||
import { store as editorstore } from "../../stores/editor-store";
|
||||
import { debounce } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
|
||||
type OnChangeHandler = (
|
||||
id: string | undefined,
|
||||
sessionId: string,
|
||||
content: string,
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
ignoreEdit: boolean
|
||||
) => void;
|
||||
type TipTapProps = {
|
||||
editorContainer: HTMLElement;
|
||||
id: string;
|
||||
editorContainer: () => HTMLElement | undefined;
|
||||
onLoad?: () => void;
|
||||
onChange?: OnChangeHandler;
|
||||
onContentChange?: () => void;
|
||||
onSelectionChange?: (range: { from: number; to: number }) => void;
|
||||
onInsertAttachment?: (type: AttachmentType) => void;
|
||||
onDownloadAttachment?: (attachment: Attachment) => void;
|
||||
onPreviewAttachment?: (attachment: Attachment) => void;
|
||||
@@ -76,7 +80,6 @@ type TipTapProps = {
|
||||
onAttachFiles?: (files: File[]) => void;
|
||||
onFocus?: () => void;
|
||||
content?: () => string | undefined;
|
||||
toolbarContainerId?: string;
|
||||
readonly?: boolean;
|
||||
nonce?: number;
|
||||
isMobile?: boolean;
|
||||
@@ -85,35 +88,24 @@ type TipTapProps = {
|
||||
fontFamily: string;
|
||||
};
|
||||
|
||||
const SAVE_INTERVAL = IS_TESTING ? 100 : 300;
|
||||
|
||||
function save(
|
||||
sessionId: string,
|
||||
noteId: string | undefined,
|
||||
editor: Editor,
|
||||
content: Fragment,
|
||||
preventSave: boolean,
|
||||
ignoreEdit: boolean,
|
||||
onChange?: OnChangeHandler
|
||||
) {
|
||||
configureEditor({
|
||||
function updateWordCount(id: string, content: () => Fragment) {
|
||||
const fragment = content();
|
||||
useEditorManager.getState().updateEditor(id, {
|
||||
statistics: {
|
||||
words: {
|
||||
total: countWords(content.textBetween(0, content.size, "\n", " ")),
|
||||
total: countWords(fragment.textBetween(0, fragment.size, "\n", " ")),
|
||||
selected: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (preventSave) return;
|
||||
const html = getHTMLFromFragment(content, editor.schema);
|
||||
onChange?.(noteId, sessionId, html, ignoreEdit);
|
||||
}
|
||||
|
||||
const deferredSave = debounceWithId(save, SAVE_INTERVAL);
|
||||
const deferredUpdateWordCount = debounce(updateWordCount, 1000);
|
||||
|
||||
function TipTap(props: TipTapProps) {
|
||||
const {
|
||||
id,
|
||||
onSelectionChange,
|
||||
onLoad,
|
||||
onChange,
|
||||
onInsertAttachment,
|
||||
@@ -124,7 +116,6 @@ function TipTap(props: TipTapProps) {
|
||||
onContentChange,
|
||||
onFocus = () => {},
|
||||
content,
|
||||
toolbarContainerId,
|
||||
editorContainer,
|
||||
readonly,
|
||||
nonce,
|
||||
@@ -135,7 +126,7 @@ function TipTap(props: TipTapProps) {
|
||||
} = props;
|
||||
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const configure = useConfigureEditor();
|
||||
// const configure = useConfigureEditor();
|
||||
const doubleSpacedLines = useSettingsStore(
|
||||
(store) => store.doubleSpacedParagraphs
|
||||
);
|
||||
@@ -187,7 +178,7 @@ function TipTap(props: TipTapProps) {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
isMobile: isMobile || false,
|
||||
element: editorContainer,
|
||||
element: editorContainer(),
|
||||
editable: !readonly,
|
||||
content: content?.(),
|
||||
autofocus: "start",
|
||||
@@ -198,11 +189,11 @@ function TipTap(props: TipTapProps) {
|
||||
editor.commands.focus("start", { scrollIntoView: true });
|
||||
oldNonce.current = nonce;
|
||||
|
||||
configure({
|
||||
console.log("on create new editor");
|
||||
useEditorManager.getState().setEditor(id, {
|
||||
editor: toIEditor(editor as Editor),
|
||||
canRedo: editor.can().redo(),
|
||||
canUndo: editor.can().undo(),
|
||||
toolbarConfig: (await getCurrentPreset()).tools,
|
||||
statistics: {
|
||||
words: {
|
||||
total: getTotalWords(editor as Editor),
|
||||
@@ -215,32 +206,23 @@ function TipTap(props: TipTapProps) {
|
||||
onUpdate: ({ editor, transaction }) => {
|
||||
onContentChange?.();
|
||||
|
||||
const preventSave = transaction.getMeta("preventSave") as boolean;
|
||||
deferredUpdateWordCount(id, () => editor.state.doc.content);
|
||||
|
||||
const preventSave = transaction?.getMeta("preventSave") as boolean;
|
||||
const ignoreEdit = transaction.getMeta("ignoreEdit") as boolean;
|
||||
const { id, sessionId } = editorstore.get().session;
|
||||
const content = editor.state.doc.content;
|
||||
deferredSave(
|
||||
sessionId,
|
||||
sessionId,
|
||||
id,
|
||||
editor as Editor,
|
||||
content,
|
||||
preventSave || !editor.isEditable,
|
||||
ignoreEdit,
|
||||
onChange
|
||||
if (preventSave || !editor.isEditable || !onChange) return;
|
||||
|
||||
console.log("CHANGING", onChange);
|
||||
onChange(
|
||||
() => getHTMLFromFragment(editor.state.doc.content, editor.schema),
|
||||
ignoreEdit
|
||||
);
|
||||
},
|
||||
onDestroy: () => {
|
||||
configure({
|
||||
editor: undefined,
|
||||
canRedo: false,
|
||||
canUndo: false,
|
||||
searching: false,
|
||||
statistics: undefined
|
||||
});
|
||||
useEditorManager.getState().setEditor(id);
|
||||
},
|
||||
onTransaction: ({ editor }) => {
|
||||
configure({
|
||||
useEditorManager.getState().updateEditor(id, {
|
||||
canRedo: editor.can().redo(),
|
||||
canUndo: editor.can().undo()
|
||||
});
|
||||
@@ -250,7 +232,8 @@ function TipTap(props: TipTapProps) {
|
||||
},
|
||||
onSelectionUpdate: debounce(({ editor, transaction }) => {
|
||||
const isEmptySelection = transaction.selection.empty;
|
||||
configure((old) => {
|
||||
if (onSelectionChange) onSelectionChange(transaction.selection);
|
||||
useEditorManager.getState().updateEditor(id, (old) => {
|
||||
const oldSelected = old.statistics?.words?.selected;
|
||||
const oldWords = old.statistics?.words.total || 0;
|
||||
if (isEmptySelection)
|
||||
@@ -326,91 +309,104 @@ function TipTap(props: TipTapProps) {
|
||||
[toggleSearch, editor?.storage.searchreplace?.isSearching]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainer) return;
|
||||
const currentEditor = editor;
|
||||
function onClick(e: MouseEvent) {
|
||||
if (e.target !== editorContainer || !currentEditor?.state.selection.empty)
|
||||
return;
|
||||
// useEffect(() => {
|
||||
// if (!editorContainer) return;
|
||||
// const currentEditor = editor;
|
||||
// function onClick(e: MouseEvent) {
|
||||
// if (e.target !== editorContainer || !currentEditor?.state.selection.empty)
|
||||
// return;
|
||||
|
||||
const lastNode = currentEditor?.state.doc.lastChild;
|
||||
const isLastNodeParagraph = lastNode?.type.name === "paragraph";
|
||||
const isEmpty = lastNode?.nodeSize === 2;
|
||||
if (isLastNodeParagraph && isEmpty) currentEditor?.commands.focus("end");
|
||||
else {
|
||||
currentEditor
|
||||
?.chain()
|
||||
.insertContentAt(currentEditor?.state.doc.nodeSize - 2, "<p></p>")
|
||||
.focus("end")
|
||||
.run();
|
||||
}
|
||||
}
|
||||
editorContainer.addEventListener("click", onClick);
|
||||
return () => {
|
||||
editorContainer.removeEventListener("click", onClick);
|
||||
};
|
||||
}, [editor, editorContainer]);
|
||||
// const lastNode = currentEditor?.state.doc.lastChild;
|
||||
// const isLastNodeParagraph = lastNode?.type.name === "paragraph";
|
||||
// const isEmpty = lastNode?.nodeSize === 2;
|
||||
// if (isLastNodeParagraph && isEmpty) currentEditor?.commands.focus("end");
|
||||
// else {
|
||||
// currentEditor
|
||||
// ?.chain()
|
||||
// .insertContentAt(currentEditor?.state.doc.nodeSize - 2, "<p></p>")
|
||||
// .focus("end")
|
||||
// .run();
|
||||
// }
|
||||
// }
|
||||
// editorContainer.addEventListener("click", onClick);
|
||||
// return () => {
|
||||
// editorContainer.removeEventListener("click", onClick);
|
||||
// };
|
||||
// }, [editor, editorContainer]);
|
||||
|
||||
if (!toolbarContainerId) return null;
|
||||
console.log("RENDERING TIPTAP");
|
||||
|
||||
if (readonly) return null;
|
||||
return (
|
||||
<>
|
||||
<Portal containerId={toolbarContainerId}>
|
||||
<ScopedThemeProvider scope="editorToolbar" sx={{ width: "100%" }}>
|
||||
<Toolbar
|
||||
editor={editor}
|
||||
location={isMobile ? "bottom" : "top"}
|
||||
tools={toolbarConfig}
|
||||
defaultFontFamily={fontFamily}
|
||||
defaultFontSize={fontSize}
|
||||
/>
|
||||
</ScopedThemeProvider>
|
||||
</Portal>
|
||||
<ScopedThemeProvider scope="editorToolbar" sx={{ width: "100%" }}>
|
||||
<Toolbar
|
||||
editor={editor}
|
||||
location={isMobile ? "bottom" : "top"}
|
||||
tools={toolbarConfig}
|
||||
defaultFontFamily={fontFamily}
|
||||
defaultFontSize={fontSize}
|
||||
/>
|
||||
</ScopedThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TiptapWrapper(
|
||||
props: Omit<
|
||||
TipTapProps,
|
||||
"editorContainer" | "theme" | "fontSize" | "fontFamily"
|
||||
props: PropsWithChildren<
|
||||
Omit<TipTapProps, "editorContainer" | "theme" | "fontSize" | "fontFamily">
|
||||
>
|
||||
) {
|
||||
const colorScheme = useThemeStore((store) => store.colorScheme);
|
||||
const theme = useThemeStore((store) =>
|
||||
colorScheme === "dark" ? store.darkTheme : store.lightTheme
|
||||
store.colorScheme === "dark" ? store.darkTheme : store.lightTheme
|
||||
);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorContainerRef = useRef<HTMLDivElement>();
|
||||
const { editorConfig } = useEditorConfig();
|
||||
useEffect(() => {
|
||||
setIsReady(true);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (
|
||||
!containerRef.current ||
|
||||
!editorContainerRef.current ||
|
||||
editorContainerRef.current.parentElement === containerRef.current
|
||||
)
|
||||
return;
|
||||
containerRef.current.appendChild(editorContainerRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorContainerRef.current) return;
|
||||
editorContainerRef.current.style.color =
|
||||
theme.scopes.editor?.primary?.paragraph ||
|
||||
theme.scopes.base.primary.paragraph;
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<PortalProvider>
|
||||
<Flex sx={{ flex: 1, flexDirection: "column" }}>
|
||||
{isReady && editorContainerRef.current ? (
|
||||
<TipTap
|
||||
{...props}
|
||||
editorContainer={editorContainerRef.current}
|
||||
fontFamily={editorConfig.fontFamily}
|
||||
fontSize={editorConfig.fontSize}
|
||||
/>
|
||||
) : null}
|
||||
<Box
|
||||
ref={editorContainerRef}
|
||||
className="selectable"
|
||||
style={{
|
||||
flex: 1,
|
||||
cursor: "text",
|
||||
color:
|
||||
<Flex ref={containerRef} sx={{ flex: 1, flexDirection: "column" }}>
|
||||
<TipTap
|
||||
{...props}
|
||||
editorContainer={() => {
|
||||
if (editorContainerRef.current) return editorContainerRef.current;
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.id = "editor-container";
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.color =
|
||||
theme.scopes.editor?.primary?.paragraph ||
|
||||
theme.scopes.base.primary.paragraph, // TODO!
|
||||
paddingBottom: 150,
|
||||
fontSize: editorConfig.fontSize,
|
||||
fontFamily: getFontById(editorConfig.fontFamily)?.font
|
||||
theme.scopes.base.primary.paragraph;
|
||||
editorContainer.style.paddingBottom = `150px`;
|
||||
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
|
||||
editorContainer.style.fontFamily =
|
||||
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
|
||||
editorContainerRef.current = editorContainer;
|
||||
return editorContainer;
|
||||
}}
|
||||
fontFamily={editorConfig.fontFamily}
|
||||
fontSize={editorConfig.fontSize}
|
||||
/>
|
||||
{props.children}
|
||||
</Flex>
|
||||
</PortalProvider>
|
||||
);
|
||||
@@ -429,10 +425,14 @@ function Portal(props: PropsWithChildren<{ containerId?: string }>) {
|
||||
|
||||
function toIEditor(editor: Editor): IEditor {
|
||||
return {
|
||||
focus: ({ position, scrollIntoView } = {}) =>
|
||||
editor.current?.commands.focus(position, {
|
||||
scrollIntoView
|
||||
}),
|
||||
focus: ({ position, scrollIntoView } = {}) => {
|
||||
if (typeof position === "object")
|
||||
editor.current?.chain().focus().setTextSelection(position).run();
|
||||
else
|
||||
editor.current?.commands.focus(position, {
|
||||
scrollIntoView
|
||||
});
|
||||
},
|
||||
undo: () => editor.current?.commands.undo(),
|
||||
redo: () => editor.current?.commands.redo(),
|
||||
updateContent: (content) => {
|
||||
|
||||
@@ -19,24 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Input } from "@theme-ui/components";
|
||||
import { useStore, store } from "../../stores/editor-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { debounceWithId } from "@notesnook/common";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
import { useEditorConfig } from "./context";
|
||||
import { useEditorConfig } from "./manager";
|
||||
import { getFontById } from "@notesnook/editor";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
|
||||
type TitleBoxProps = {
|
||||
id: string;
|
||||
readonly: boolean;
|
||||
};
|
||||
|
||||
function TitleBox(props: TitleBoxProps) {
|
||||
const { readonly } = props;
|
||||
const { readonly, id } = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const id = useStore((store) => store.session.id);
|
||||
// const id = useStore((store) => store.session.id);
|
||||
const isMobile = useMobile();
|
||||
const isTablet = useTablet();
|
||||
const { editorConfig } = useEditorConfig();
|
||||
@@ -61,7 +61,10 @@ function TitleBox(props: TitleBoxProps) {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { title } = useStore.getState().session;
|
||||
const session = useEditorStore.getState().getSession(id);
|
||||
if (!session || !session.note) return;
|
||||
|
||||
const { title } = session.note;
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.value = title || "";
|
||||
updateFontSize(title?.length || 0);
|
||||
@@ -72,27 +75,27 @@ function TitleBox(props: TitleBoxProps) {
|
||||
updateFontSize(inputRef.current.value.length);
|
||||
}, [isTablet, isMobile, updateFontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = AppEventManager.subscribe(
|
||||
AppEvents.changeNoteTitle,
|
||||
({ preventSave, title }: { title: string; preventSave: boolean }) => {
|
||||
if (!inputRef.current) return;
|
||||
withSelectionPersist(
|
||||
inputRef.current,
|
||||
(input) => (input.value = title)
|
||||
);
|
||||
updateFontSize(title.length);
|
||||
if (!preventSave) {
|
||||
const { sessionId, id } = store.get().session;
|
||||
debouncedOnTitleChange(sessionId, id, title);
|
||||
}
|
||||
}
|
||||
);
|
||||
// useEffect(() => {
|
||||
// const { unsubscribe } = AppEventManager.subscribe(
|
||||
// AppEvents.changeNoteTitle,
|
||||
// ({ preventSave, title }: { title: string; preventSave: boolean }) => {
|
||||
// if (!inputRef.current) return;
|
||||
// withSelectionPersist(
|
||||
// inputRef.current,
|
||||
// (input) => (input.value = title)
|
||||
// );
|
||||
// updateFontSize(title.length);
|
||||
// if (!preventSave) {
|
||||
// const { sessionId, id } = store.get().session;
|
||||
// debouncedOnTitleChange(sessionId, id, title);
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
// return () => {
|
||||
// unsubscribe();
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<Input
|
||||
@@ -115,13 +118,12 @@ function TitleBox(props: TitleBoxProps) {
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const { sessionId, id } = store.get().session;
|
||||
e.target.value = replaceDateTime(
|
||||
e.target.value,
|
||||
dateFormat,
|
||||
timeFormat
|
||||
);
|
||||
debouncedOnTitleChange(sessionId, id, e.target.value);
|
||||
debouncedOnTitleChange(id, id, e.target.value);
|
||||
updateFontSize(e.target.value.length);
|
||||
}}
|
||||
/>
|
||||
@@ -132,8 +134,8 @@ export default React.memo(TitleBox, (prevProps, nextProps) => {
|
||||
return prevProps.readonly === nextProps.readonly;
|
||||
});
|
||||
|
||||
function onTitleChange(noteId: string | undefined, title: string) {
|
||||
store.get().setTitle(noteId, title);
|
||||
function onTitleChange(noteId: string, title: string) {
|
||||
useEditorStore.getState().setTitle(noteId, title);
|
||||
}
|
||||
|
||||
const debouncedOnTitleChange = debounceWithId(onTitleChange, 100);
|
||||
|
||||
@@ -1,359 +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 { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import {
|
||||
Published,
|
||||
Publish,
|
||||
EditorNormalWidth,
|
||||
EditorFullWidth,
|
||||
ThemeIcon,
|
||||
FocusMode,
|
||||
NormalMode,
|
||||
ExitFullscreen,
|
||||
Fullscreen,
|
||||
Search,
|
||||
Undo,
|
||||
Redo,
|
||||
Properties,
|
||||
ArrowLeft
|
||||
} from "../icons";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { useStore as useMonographStore } from "../../stores/monograph-store";
|
||||
import { useStore, store } from "../../stores/editor-store";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { AnimatedInput } from "../animated";
|
||||
import { showPublishView } from "../publish-view";
|
||||
import { db } from "../../common/db";
|
||||
import { useEditorInstance, useHistory, useSearch } from "./context";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
|
||||
// TODO: this needs to be cleaned up!
|
||||
function Toolbar() {
|
||||
const sessionId = useStore((store) => store.session.id);
|
||||
const isDeleted = useStore((store) => store.session.isDeleted);
|
||||
const isLocked = useStore((store) => store.session.locked);
|
||||
const readonly = useStore((store) => store.session.readonly);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
|
||||
const toggleProperties = useStore((store) => store.toggleProperties);
|
||||
const toggleEditorMargins = useStore((store) => store.toggleEditorMargins);
|
||||
const editorMargins = useStore((store) => store.editorMargins);
|
||||
const clearSession = useStore((store) => store.clearSession);
|
||||
const title = useStore((store) => store.session.title);
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
|
||||
const [isTitleVisible, setIsTitleVisible] = useState(false);
|
||||
const onScrollTitleRef = useRef(null);
|
||||
|
||||
const monographs = useMonographStore((store) => store.monographs);
|
||||
const { canRedo, canUndo, redo, undo } = useHistory();
|
||||
const { toggleSearch } = useSearch();
|
||||
const editor = useEditorInstance();
|
||||
|
||||
const isNotePublished = useMemo(
|
||||
() => sessionId && db.monographs.isPublished(sessionId),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sessionId, monographs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const editorScroll = document.querySelector(".editorScroll");
|
||||
if (!editorScroll) return;
|
||||
|
||||
function onScroll(e) {
|
||||
const hideOffset = document.querySelector(".editorTitle").scrollHeight;
|
||||
if (e.target.scrollTop > hideOffset && !isTitleVisible)
|
||||
setIsTitleVisible(e.target.scrollTop > hideOffset && !isTitleVisible);
|
||||
else if (e.target.scrollTop <= hideOffset && isTitleVisible)
|
||||
setIsTitleVisible(false);
|
||||
}
|
||||
editorScroll.addEventListener("scroll", onScroll);
|
||||
return () => {
|
||||
editorScroll.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [isTitleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onScrollTitleRef.current) onScrollTitleRef.current.value = title;
|
||||
}, [title]);
|
||||
|
||||
const tools = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: isNotePublished ? "Published" : "Publish",
|
||||
icon: isNotePublished ? Published : Publish,
|
||||
hidden: !sessionId || isDeleted,
|
||||
enabled: !isLocked,
|
||||
onClick: () => showPublishView(store.get().session, "top")
|
||||
}
|
||||
],
|
||||
[sessionId, isLocked, isNotePublished, isDeleted]
|
||||
);
|
||||
|
||||
const inlineTools = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: editorMargins
|
||||
? "Disable editor margins"
|
||||
: "Enable editor margins",
|
||||
icon: editorMargins ? EditorNormalWidth : EditorFullWidth,
|
||||
enabled: true,
|
||||
onClick: () => toggleEditorMargins()
|
||||
},
|
||||
{
|
||||
title: theme === "dark" ? "Light mode" : "Dark mode",
|
||||
icon: ThemeIcon,
|
||||
hidden: !isFocusMode,
|
||||
enabled: true,
|
||||
onClick: () => toggleNightMode()
|
||||
},
|
||||
{
|
||||
title: isFocusMode ? "Normal mode" : "Focus mode",
|
||||
icon: isFocusMode ? FocusMode : NormalMode,
|
||||
enabled: true,
|
||||
hideOnMobile: true,
|
||||
onClick: () => {
|
||||
toggleFocusMode();
|
||||
if (isFullscreen) {
|
||||
exitFullscreen(document);
|
||||
setIsFullscreen(false);
|
||||
}
|
||||
if (editor) editor.current.focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: isFullscreen ? "Exit fullscreen" : "Enter fullscreen",
|
||||
icon: isFullscreen ? ExitFullscreen : Fullscreen,
|
||||
enabled: true,
|
||||
hidden: !isFocusMode,
|
||||
hideOnMobile: true,
|
||||
onClick: () => {
|
||||
if (isFullscreen) {
|
||||
exitFullscreen(document);
|
||||
} else {
|
||||
enterFullscreen(document.documentElement);
|
||||
}
|
||||
setIsFullscreen((s) => !s);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Search",
|
||||
icon: Search,
|
||||
enabled: true,
|
||||
hidden: !sessionId || isDeleted,
|
||||
onClick: () => toggleSearch()
|
||||
},
|
||||
{
|
||||
title: "Undo",
|
||||
icon: Undo,
|
||||
enabled: canUndo,
|
||||
hidden: !sessionId || isDeleted,
|
||||
onClick: () => undo()
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
icon: Redo,
|
||||
enabled: canRedo,
|
||||
hidden: !sessionId || isDeleted,
|
||||
onClick: () => redo()
|
||||
},
|
||||
{
|
||||
title: "Properties",
|
||||
icon: Properties,
|
||||
enabled: true,
|
||||
hidden: !sessionId || isFocusMode || isDeleted,
|
||||
onClick: toggleProperties
|
||||
}
|
||||
],
|
||||
[
|
||||
editorMargins,
|
||||
toggleEditorMargins,
|
||||
editor,
|
||||
undo,
|
||||
redo,
|
||||
isFullscreen,
|
||||
canRedo,
|
||||
canUndo,
|
||||
toggleFocusMode,
|
||||
toggleProperties,
|
||||
isFocusMode,
|
||||
theme,
|
||||
toggleNightMode,
|
||||
sessionId,
|
||||
toggleSearch,
|
||||
isDeleted
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex mx={2} my={1} sx={{ justifyContent: "space-between" }}>
|
||||
<Flex sx={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
|
||||
<ArrowLeft
|
||||
sx={{
|
||||
display: ["block", "none", "none"],
|
||||
flexShrink: 0
|
||||
}}
|
||||
size={24}
|
||||
onClick={() => {
|
||||
if (store.get().session.id) showToast("success", "Note saved!");
|
||||
if (isFocusMode) toggleFocusMode();
|
||||
clearSession();
|
||||
}}
|
||||
/>
|
||||
<AnimatedInput
|
||||
ref={onScrollTitleRef}
|
||||
variant="clean"
|
||||
ml={[2, 2, 0]}
|
||||
initial={{
|
||||
opacity: isTitleVisible ? 1 : 0,
|
||||
zIndex: isTitleVisible ? 1 : -1
|
||||
}}
|
||||
animate={{
|
||||
opacity: isTitleVisible ? 1 : 0,
|
||||
zIndex: isTitleVisible ? 1 : -1
|
||||
}}
|
||||
transition={{ duration: 0.5 }}
|
||||
defaultValue={title}
|
||||
readOnly={readonly}
|
||||
onChange={(e) => {
|
||||
AppEventManager.publish(AppEvents.changeNoteTitle, {
|
||||
title: e.target.value,
|
||||
preventSave: false
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
flex: 1,
|
||||
fontWeight: "heading",
|
||||
fontSize: "heading",
|
||||
color: "paragraph",
|
||||
p: 0,
|
||||
pl: 4,
|
||||
borderWidth: 0,
|
||||
borderRadius: "default",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
{tools.map((tool) => (
|
||||
<Button
|
||||
key={tool.title}
|
||||
variant="secondary"
|
||||
data-test-id={tool.title}
|
||||
disabled={!tool.enabled}
|
||||
title={tool.title}
|
||||
sx={{
|
||||
display: [
|
||||
tool.hideOnMobile ? "none" : "flex",
|
||||
tool.hidden ? "none" : "flex"
|
||||
],
|
||||
color: "paragraph",
|
||||
flexDirection: "row",
|
||||
flexShrink: 0,
|
||||
alignItems: "center"
|
||||
}}
|
||||
onClick={tool.onClick}
|
||||
>
|
||||
<tool.icon size={18} />
|
||||
<Text
|
||||
variant="body"
|
||||
ml={1}
|
||||
sx={{ display: ["none", "none", "block"] }}
|
||||
>
|
||||
{tool.title}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
<Flex
|
||||
bg="background"
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
overflow: "hidden",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end"
|
||||
}}
|
||||
>
|
||||
{inlineTools.map((tool) => (
|
||||
<Button
|
||||
data-test-id={tool.title}
|
||||
disabled={!tool.enabled}
|
||||
variant="secondary"
|
||||
title={tool.title}
|
||||
key={tool.title}
|
||||
sx={{
|
||||
display: [
|
||||
tool.hideOnMobile ? "none" : "flex",
|
||||
tool.hidden ? "none" : "flex"
|
||||
],
|
||||
borderRadius: 0,
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={tool.onClick}
|
||||
>
|
||||
<tool.icon size={18} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default Toolbar;
|
||||
|
||||
/* View in fullscreen */
|
||||
function enterFullscreen(elem) {
|
||||
// go full-screen
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) {
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
/* Close fullscreen */
|
||||
function exitFullscreen(elem) {
|
||||
if (
|
||||
!document.fullscreenElement &&
|
||||
!document.webkitFullscreenElement &&
|
||||
!document.mozFullScreenElement
|
||||
)
|
||||
return;
|
||||
|
||||
// exit full-screen
|
||||
if (elem.exitFullscreen) {
|
||||
elem.exitFullscreen();
|
||||
} else if (elem.webkitExitFullscreen) {
|
||||
elem.webkitExitFullscreen();
|
||||
} else if (elem.mozCancelFullScreen) {
|
||||
elem.mozCancelFullScreen();
|
||||
} else if (elem.msExitFullscreen) {
|
||||
elem.msExitFullscreen();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export type NoteStatistics = {
|
||||
|
||||
export interface IEditor {
|
||||
focus: (options?: {
|
||||
position?: "start" | "end";
|
||||
position?: "start" | "end" | { from: number; to: number };
|
||||
scrollIntoView?: boolean;
|
||||
}) => void;
|
||||
undo: () => void;
|
||||
|
||||
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import useHashRoutes from "../../hooks/use-hash-routes";
|
||||
import hashroutes from "../../navigation/hash-routes";
|
||||
import TabsView from "../editor";
|
||||
|
||||
function HashRouter() {
|
||||
const routeResult = useHashRoutes(hashroutes);
|
||||
return React.isValidElement(routeResult) ? routeResult : null;
|
||||
useHashRoutes(hashroutes);
|
||||
return <TabsView />; // React.isValidElement(routeResult) ? routeResult : null;
|
||||
}
|
||||
export default React.memo(HashRouter, () => true);
|
||||
|
||||
@@ -62,12 +62,12 @@ import {
|
||||
} from "../../common/dialog-controller";
|
||||
import { store, useStore } from "../../stores/note-store";
|
||||
import { store as userstore } from "../../stores/user-store";
|
||||
import { store as editorStore } from "../../stores/editor-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { store as tagStore } from "../../stores/tag-store";
|
||||
import { useStore as useMonographStore } from "../../stores/monograph-store";
|
||||
import { db } from "../../common/db";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { hashNavigate, navigate } from "../../navigation";
|
||||
import { navigate } from "../../navigation";
|
||||
import { showPublishView } from "../publish-view";
|
||||
import IconTag from "../icon-tag";
|
||||
import { exportNote, exportNotes, exportToPDF } from "../../common/export";
|
||||
@@ -112,7 +112,7 @@ function Note(props: NoteProps) {
|
||||
} = props;
|
||||
const note = item;
|
||||
|
||||
const isOpened = useStore((store) => store.selectedNote === note.id);
|
||||
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
|
||||
const primary: SchemeColors = color ? color.colorCode : "accent-selected";
|
||||
|
||||
return (
|
||||
@@ -138,15 +138,7 @@ function Note(props: NoteProps) {
|
||||
}}
|
||||
context={{ color, locked }}
|
||||
menuItems={menuItems}
|
||||
onClick={async () => {
|
||||
if (note.conflicted) {
|
||||
hashNavigate(`/notes/${note.id}/conflict`, { replace: true });
|
||||
} else if (locked) {
|
||||
hashNavigate(`/notes/${note.id}/unlock`, { replace: true });
|
||||
} else {
|
||||
hashNavigate(`/notes/${note.id}/edit`, { replace: true });
|
||||
}
|
||||
}}
|
||||
onClick={() => useEditorStore.getState().openSession(note)}
|
||||
header={
|
||||
<Flex
|
||||
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
|
||||
@@ -716,7 +708,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.to({ id, type: "note" }, "tag").unlink();
|
||||
}
|
||||
tagStore.get().refresh();
|
||||
await editorStore.get().refreshTags();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
},
|
||||
@@ -737,7 +729,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.add(tag, { id, type: "note" });
|
||||
}
|
||||
await tagStore.get().refresh();
|
||||
await editorStore.get().refreshTags();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
});
|
||||
@@ -758,7 +750,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.unlink(tag, { id, type: "note" });
|
||||
}
|
||||
await tagStore.get().refresh();
|
||||
await editorStore.get().refreshTags();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,7 +29,11 @@ import {
|
||||
Checkmark
|
||||
} from "../icons";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { useStore, store, EditorSession } from "../../stores/editor-store";
|
||||
import {
|
||||
useEditorStore,
|
||||
ReadonlyEditorSession,
|
||||
DefaultEditorSession
|
||||
} 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";
|
||||
@@ -38,7 +42,6 @@ import Toggle from "./toggle";
|
||||
import ScrollContainer from "../scroll-container";
|
||||
import { ResolvedItem, getFormattedDate, usePromise } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { PreviewSession } from "../editor/types";
|
||||
import { ListItemWrapper } from "../list-container/list-profiles";
|
||||
import { VirtualizedList } from "../virtualized-list";
|
||||
import { SessionItem } from "../session-item";
|
||||
@@ -67,10 +70,10 @@ const tools = [
|
||||
}
|
||||
] as const;
|
||||
|
||||
type MetadataItem<T extends keyof EditorSession = keyof EditorSession> = {
|
||||
type MetadataItem<T extends "dateCreated" | "dateEdited"> = {
|
||||
key: T;
|
||||
label: string;
|
||||
value: (value: EditorSession[T]) => string;
|
||||
value: (value: number) => string;
|
||||
};
|
||||
|
||||
const metadataItems = [
|
||||
@@ -87,19 +90,18 @@ const metadataItems = [
|
||||
];
|
||||
|
||||
type EditorPropertiesProps = {
|
||||
onOpenPreviewSession: (session: PreviewSession) => void;
|
||||
id: string;
|
||||
};
|
||||
function EditorProperties(props: EditorPropertiesProps) {
|
||||
const { onOpenPreviewSession } = props;
|
||||
const { id } = props;
|
||||
|
||||
const toggleProperties = useStore((store) => store.toggleProperties);
|
||||
const toggleProperties = useEditorStore((store) => store.toggleProperties);
|
||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
const session = useStore((store) => store.session);
|
||||
const session = useEditorStore((store) =>
|
||||
store.getSession(id, ["default", "unlocked", "readonly"])
|
||||
);
|
||||
|
||||
const { id: sessionId, sessionType, dateCreated } = session;
|
||||
const isPreviewMode = sessionType === "preview";
|
||||
|
||||
if (isFocusMode || !sessionId) return null;
|
||||
if (isFocusMode || !session) return null;
|
||||
return (
|
||||
<AnimatedFlex
|
||||
animate={{
|
||||
@@ -116,7 +118,7 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
zIndex: 3,
|
||||
zIndex: 1,
|
||||
height: "100%",
|
||||
width: "300px",
|
||||
borderLeft: "1px solid",
|
||||
@@ -146,19 +148,18 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!isPreviewMode && (
|
||||
<>
|
||||
{tools.map((tool) => (
|
||||
<Toggle
|
||||
{...tool}
|
||||
key={tool.key}
|
||||
toggleKey={tool.property}
|
||||
onToggle={() => changeToggleState(tool.key)}
|
||||
testId={`properties-${tool.key}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{tools.map((tool) => (
|
||||
<Toggle
|
||||
{...tool}
|
||||
key={tool.key}
|
||||
isOn={!!session.note[tool.property]}
|
||||
onToggle={() => changeToggleState(tool.key, session)}
|
||||
testId={`properties-${tool.key}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
{metadataItems.map((item) => (
|
||||
<Flex
|
||||
key={item.key}
|
||||
@@ -178,21 +179,16 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
variant="subBody"
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
{item.value(session[item.key])}
|
||||
{item.value(session.note[item.key])}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
{!isPreviewMode && <Colors noteId={sessionId} />}
|
||||
<Colors noteId={id} color={session.color} />
|
||||
</Section>
|
||||
<Notebooks noteId={sessionId} />
|
||||
<Reminders noteId={sessionId} />
|
||||
<Attachments noteId={sessionId} />
|
||||
<SessionHistory
|
||||
noteId={sessionId}
|
||||
dateCreated={dateCreated || 0}
|
||||
isPreviewMode={isPreviewMode}
|
||||
onOpenPreviewSession={onOpenPreviewSession}
|
||||
/>
|
||||
<Notebooks noteId={id} />
|
||||
<Reminders noteId={id} />
|
||||
<Attachments noteId={id} />
|
||||
<SessionHistory noteId={id} />
|
||||
</ScrollContainer>
|
||||
</ScopedThemeProvider>
|
||||
</AnimatedFlex>
|
||||
@@ -200,8 +196,7 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
}
|
||||
export default React.memo(EditorProperties);
|
||||
|
||||
function Colors({ noteId }: { noteId: string }) {
|
||||
const color = useStore((store) => store.color);
|
||||
function Colors({ noteId, color }: { noteId: string; color?: string }) {
|
||||
const result = usePromise(() => db.colors.all.items(), [color]);
|
||||
return (
|
||||
<Flex
|
||||
@@ -314,6 +309,7 @@ function Attachments({ noteId }: { noteId: string }) {
|
||||
getItemKey={(index) => result.value.key(index)}
|
||||
items={result.value.placeholders}
|
||||
header={<></>}
|
||||
headerSize={0}
|
||||
renderRow={({ index }) => (
|
||||
<ResolvedItem index={index} type="attachment" items={result.value}>
|
||||
{({ item }) => <ListItemWrapper item={item} compact />}
|
||||
@@ -323,17 +319,7 @@ function Attachments({ noteId }: { noteId: string }) {
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
function SessionHistory({
|
||||
noteId,
|
||||
dateCreated,
|
||||
isPreviewMode,
|
||||
onOpenPreviewSession
|
||||
}: {
|
||||
noteId: string;
|
||||
dateCreated: number;
|
||||
isPreviewMode: boolean;
|
||||
onOpenPreviewSession: (session: PreviewSession) => void;
|
||||
}) {
|
||||
function SessionHistory({ noteId }: { noteId: string }) {
|
||||
const result = usePromise(() =>
|
||||
db.noteHistory
|
||||
.get(noteId)
|
||||
@@ -353,15 +339,7 @@ function SessionHistory({
|
||||
items={result.value.placeholders}
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem type="session" index={index} items={result.value}>
|
||||
{({ item }) => (
|
||||
<SessionItem
|
||||
noteId={noteId}
|
||||
session={item}
|
||||
dateCreated={dateCreated}
|
||||
isPreviewMode={isPreviewMode}
|
||||
onOpenPreviewSession={onOpenPreviewSession}
|
||||
/>
|
||||
)}
|
||||
{({ item }) => <SessionItem noteId={noteId} session={item} />}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
/>
|
||||
@@ -398,7 +376,8 @@ function Section({
|
||||
}
|
||||
|
||||
function changeToggleState(
|
||||
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite"
|
||||
prop: "lock" | "readonly" | "local-only" | "pin" | "favorite",
|
||||
session: ReadonlyEditorSession | DefaultEditorSession
|
||||
) {
|
||||
const {
|
||||
id: sessionId,
|
||||
@@ -407,7 +386,7 @@ function changeToggleState(
|
||||
localOnly,
|
||||
pinned,
|
||||
favorite
|
||||
} = store.get().session;
|
||||
} = session.note;
|
||||
if (!sessionId) return;
|
||||
switch (prop) {
|
||||
case "lock":
|
||||
|
||||
@@ -18,11 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Flex, Switch, Text } from "@theme-ui/components";
|
||||
import { useStore } from "../../stores/editor-store";
|
||||
import { Icon } from "../icons";
|
||||
|
||||
type ToggleProps = {
|
||||
icon: Icon;
|
||||
label: string;
|
||||
onToggle: (toggleState: boolean) => void;
|
||||
isOn: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
function Toggle(props: ToggleProps) {
|
||||
const { icon: ToggleIcon, label, onToggle, isOn } = props;
|
||||
|
||||
function Toggle(props) {
|
||||
const { icon: ToggleIcon, label, onToggle, toggleKey } = props;
|
||||
const isOn = useStore((store) => store.session[toggleKey]);
|
||||
return (
|
||||
<Flex
|
||||
py={2}
|
||||
@@ -17,33 +17,20 @@ 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 { getFormattedHistorySessionDate } 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";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
|
||||
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;
|
||||
const { session, noteId } = props;
|
||||
const label = getFormattedHistorySessionDate(session);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -53,45 +40,17 @@ export function SessionItem(props: SessionItemProps) {
|
||||
px={2}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
bg: isSelected ? "background-selected" : "transparent",
|
||||
bg: "transparent",
|
||||
":hover": {
|
||||
bg: isSelected ? "hover-selected" : "hover"
|
||||
bg: "hover"
|
||||
},
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
title="Click to preview"
|
||||
onClick={async () => {
|
||||
const content = await db.noteHistory.content(session.id);
|
||||
if (!content) return;
|
||||
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: {
|
||||
data: content.data as string,
|
||||
type: content.type
|
||||
},
|
||||
dateCreated: session.dateCreated,
|
||||
dateEdited: session.dateModified
|
||||
});
|
||||
}
|
||||
}}
|
||||
onClick={() =>
|
||||
useEditorStore.getState().openDiffSession(noteId, session.id)
|
||||
}
|
||||
>
|
||||
<Text variant={"body"} data-test-id="title">
|
||||
{label}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Text } from "@theme-ui/components";
|
||||
import { store as appStore } from "../../stores/app-store";
|
||||
import { store as tagStore } from "../../stores/tag-store";
|
||||
import { store as noteStore } from "../../stores/note-store";
|
||||
import { store as editorStore } from "../../stores/editor-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { db } from "../../common/db";
|
||||
import { Edit, Shortcut, DeleteForver } from "../icons";
|
||||
import { showToast } from "../../utils/toast";
|
||||
@@ -99,7 +99,7 @@ const menuItems: (tag: Tag, ids?: string[]) => MenuItem[] = (tag, ids = []) => {
|
||||
await db.tags.remove(...ids);
|
||||
showToast("success", `${pluralize(ids.length, "tag")} deleted`);
|
||||
await appStore.refreshNavItems();
|
||||
await editorStore.refreshTags();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await tagStore.refresh();
|
||||
await noteStore.refresh();
|
||||
},
|
||||
|
||||
@@ -25,16 +25,15 @@ import { Flex, Text } from "@theme-ui/components";
|
||||
import TimeAgo from "../time-ago";
|
||||
import { pluralize, toTitleCase } from "@notesnook/common";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { hashNavigate } from "../../navigation";
|
||||
import { useStore } from "../../stores/note-store";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { TrashItem } from "@notesnook/core/dist/types";
|
||||
import { db } from "../../common/db";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
|
||||
type TrashItemProps = { item: TrashItem; date: number };
|
||||
function TrashItem(props: TrashItemProps) {
|
||||
const { item, date } = props;
|
||||
const isOpened = useStore((store) => store.selectedNote === item.id);
|
||||
const isOpened = useEditorStore((store) => store.activeSessionId === item.id);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -59,9 +58,7 @@ function TrashItem(props: TrashItemProps) {
|
||||
menuItems={menuItems}
|
||||
onClick={async () => {
|
||||
if (item.itemType === "note")
|
||||
(await db.vaults.itemExists({ id: item.id, type: "note" }))
|
||||
? showToast("error", "Locked notes cannot be previewed in trash.")
|
||||
: hashNavigate(`/notes/${item.id}/edit`, { replace: true });
|
||||
useEditorStore.getState().openSession(item);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,43 +17,32 @@ 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 { useRef, useState, useCallback, useEffect } from "react";
|
||||
import { useRef, useState, useCallback } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Lock } from "../icons";
|
||||
import { db } from "../../common/db";
|
||||
import { useStore as useEditorStore } from "../../stores/editor-store";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import Field from "../field";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { ErrorText } from "../error-text";
|
||||
import { usePromise } from "@notesnook/common";
|
||||
|
||||
type UnlockProps = {
|
||||
noteId: string;
|
||||
type UnlockViewProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonTitle: string;
|
||||
unlock: (password: string) => Promise<void>;
|
||||
};
|
||||
function Unlock(props: UnlockProps) {
|
||||
const { noteId } = props;
|
||||
|
||||
export function UnlockView(props: UnlockViewProps) {
|
||||
const { title, subtitle, buttonTitle, unlock } = props;
|
||||
const [isWrong, setIsWrong] = useState(false);
|
||||
const [isUnlocking, setIsUnlocking] = useState(false);
|
||||
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const note = usePromise(() => db.notes.note(noteId), [noteId]);
|
||||
const openLockedSession = useEditorStore((store) => store.openLockedSession);
|
||||
const openSession = useEditorStore((store) => store.openSession);
|
||||
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
|
||||
|
||||
const submit = useCallback(async () => {
|
||||
console.log("HELO", passwordRef.current);
|
||||
if (!passwordRef.current) return;
|
||||
if (!passwordRef.current?.value) return;
|
||||
setIsUnlocking(true);
|
||||
const password = passwordRef.current.value;
|
||||
try {
|
||||
if (!password) return;
|
||||
const note = await db.vault.open(noteId, password);
|
||||
if (!note) return;
|
||||
openLockedSession(note);
|
||||
await unlock(password);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
@@ -61,20 +50,13 @@ function Unlock(props: UnlockProps) {
|
||||
) {
|
||||
setIsWrong(true);
|
||||
} else {
|
||||
showToast("error", "Cannot unlock note: " + e);
|
||||
showToast("error", "Could not unlock: " + e);
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
setIsUnlocking(false);
|
||||
}
|
||||
}, [setIsWrong, noteId, openLockedSession]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await openSession(noteId);
|
||||
setIsEditorOpen(true);
|
||||
})();
|
||||
}, [openSession, setIsEditorOpen, noteId]);
|
||||
}, [setIsWrong, unlock]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -101,9 +83,7 @@ function Unlock(props: UnlockProps) {
|
||||
mt={25}
|
||||
sx={{ fontSize: 36, textAlign: "center" }}
|
||||
>
|
||||
{note.status === "fulfilled" && note.value
|
||||
? note.value.title
|
||||
: "Open note"}
|
||||
{title}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text
|
||||
@@ -116,14 +96,14 @@ function Unlock(props: UnlockProps) {
|
||||
color: "var(--paragraph-secondary)"
|
||||
}}
|
||||
>
|
||||
Please enter the password to unlock this note.
|
||||
{subtitle}
|
||||
</Text>
|
||||
<Field
|
||||
id="vaultPassword"
|
||||
data-test-id="unlock-note-password"
|
||||
inputRef={passwordRef}
|
||||
autoFocus
|
||||
sx={{ width: ["95%", "95%", "30%"] }}
|
||||
sx={{ width: ["95%", "95%", "max(30%, 400px)"] }}
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
onKeyUp={async (e) => {
|
||||
@@ -145,9 +125,8 @@ function Unlock(props: UnlockProps) {
|
||||
await submit();
|
||||
}}
|
||||
>
|
||||
{isUnlocking ? "Unlocking..." : "Open note"}
|
||||
{isUnlocking ? "Unlocking..." : buttonTitle}
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default Unlock;
|
||||
|
||||
@@ -28,7 +28,7 @@ import { db } from "../common/db";
|
||||
import Dialog from "../components/dialog";
|
||||
import { useStore, store } from "../stores/tag-store";
|
||||
import { store as notestore } from "../stores/note-store";
|
||||
import { store as editorStore } from "../stores/editor-store";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { FilteredList } from "../components/filtered-list";
|
||||
import { ItemReference, Tag } from "@notesnook/core/dist/types";
|
||||
@@ -101,7 +101,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
|
||||
else await db.relations.unlink(tagRef, noteRef);
|
||||
}
|
||||
}
|
||||
await editorStore.get().refreshTags();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
await notestore.get().refresh();
|
||||
onClose(true);
|
||||
|
||||
@@ -49,7 +49,7 @@ import { createPortal } from "react-dom";
|
||||
import { getId } from "@notesnook/core/dist/utils/id";
|
||||
import { Label } from "@theme-ui/components";
|
||||
import { db } from "../../../common/db";
|
||||
import { useToolbarConfig } from "../../../components/editor/context";
|
||||
import { useToolbarConfig } from "../../../components/editor/manager";
|
||||
import {
|
||||
getAllPresets,
|
||||
getCurrentPreset,
|
||||
|
||||
@@ -21,8 +21,8 @@ import { SettingsGroup } from "./types";
|
||||
import {
|
||||
editorConfig,
|
||||
onEditorConfigChange,
|
||||
setEditorConfig
|
||||
} from "../../components/editor/context";
|
||||
useEditorManager
|
||||
} from "../../components/editor/manager";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { getFonts } from "@notesnook/editor";
|
||||
import { useSpellChecker } from "../../hooks/use-spell-checker";
|
||||
@@ -74,7 +74,9 @@ symbols (e.g. 202305261253)`,
|
||||
})),
|
||||
selectedOption: () => editorConfig().fontFamily,
|
||||
onSelectionChanged: (value) => {
|
||||
setEditorConfig({ fontFamily: value });
|
||||
useEditorManager
|
||||
.getState()
|
||||
.setEditorConfig({ fontFamily: value });
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -93,7 +95,8 @@ symbols (e.g. 202305261253)`,
|
||||
max: 120,
|
||||
min: 8,
|
||||
defaultValue: () => editorConfig().fontSize,
|
||||
onChange: (value) => setEditorConfig({ fontSize: value })
|
||||
onChange: (value) =>
|
||||
useEditorManager.getState().setEditorConfig({ fontSize: value })
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ export function useIsUserPremium() {
|
||||
}
|
||||
|
||||
export function isUserPremium(user?: User) {
|
||||
return true;
|
||||
if (IS_TESTING) return true;
|
||||
if (!user) user = userstore.get().user;
|
||||
if (!user) return false;
|
||||
|
||||
@@ -31,19 +31,12 @@ import {
|
||||
showAddNotebookDialog,
|
||||
showEditNotebookDialog
|
||||
} from "../common/dialog-controller";
|
||||
import { closeOpenedDialog } from "../common/dialog-controller";
|
||||
import RouteContainer from "../components/route-container";
|
||||
import DiffViewer from "../components/diff-viewer";
|
||||
import Unlock from "../components/unlock";
|
||||
import { store as editorStore } from "../stores/editor-store";
|
||||
import { isMobile } from "../utils/dimensions";
|
||||
import { hashNavigate } from ".";
|
||||
import Editor from "../components/editor";
|
||||
import { defineRoutes } from "./types";
|
||||
|
||||
const hashroutes = defineRoutes({
|
||||
"/": () => {
|
||||
return !editorStore.get().session.state && <Editor nonce={"-1"} />;
|
||||
// return <Editor nonce={"-1"} />;
|
||||
},
|
||||
"/email/verify": () => {
|
||||
showEmailVerificationDialog().then(afterAction);
|
||||
@@ -63,45 +56,47 @@ const hashroutes = defineRoutes({
|
||||
"/tags/create": () => {
|
||||
showCreateTagDialog().then(afterAction);
|
||||
},
|
||||
"/notes/create": () => {
|
||||
closeOpenedDialog();
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true });
|
||||
},
|
||||
"/notes/create/:nonce": ({ nonce }) => {
|
||||
closeOpenedDialog();
|
||||
return <Editor nonce={nonce} />;
|
||||
},
|
||||
"/notes/:noteId/edit": ({ noteId }) => {
|
||||
closeOpenedDialog();
|
||||
|
||||
return <Editor noteId={noteId} />;
|
||||
},
|
||||
"/notes/:noteId/unlock": ({ noteId }) => {
|
||||
closeOpenedDialog();
|
||||
return (
|
||||
<RouteContainer
|
||||
type="unlock"
|
||||
buttons={{
|
||||
back: isMobile()
|
||||
? {
|
||||
title: "Go back",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", {
|
||||
addNonce: true,
|
||||
replace: true
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
}}
|
||||
>
|
||||
<Unlock noteId={noteId} />
|
||||
</RouteContainer>
|
||||
);
|
||||
},
|
||||
"/notes/:noteId/conflict": ({ noteId }) => {
|
||||
closeOpenedDialog();
|
||||
return <DiffViewer noteId={noteId} />;
|
||||
},
|
||||
// "/notes/create": () => {
|
||||
// closeOpenedDialog();
|
||||
// editorStore.get().newSession();
|
||||
// // hashNavigate("/notes/create", { addNonce: true, replace: true });
|
||||
// },
|
||||
// "/notes/create/:nonce": ({ nonce }) => {
|
||||
// closeOpenedDialog();
|
||||
// // return <Editor nonce={nonce} />;
|
||||
// },
|
||||
// "/notes/:noteId/edit": ({ noteId }) => {
|
||||
// closeOpenedDialog();
|
||||
// editorStore.get().openSession(noteId);
|
||||
// // return <Editor noteId={noteId} />;
|
||||
// },
|
||||
// "/notes/:noteId/unlock": ({ noteId }) => {
|
||||
// closeOpenedDialog();
|
||||
// editorStore.get().openSession(noteId);
|
||||
// // return (
|
||||
// // <RouteContainer
|
||||
// // type="unlock"
|
||||
// // buttons={{
|
||||
// // back: isMobile()
|
||||
// // ? {
|
||||
// // title: "Go back",
|
||||
// // onClick: () =>
|
||||
// // hashNavigate("/notes/create", {
|
||||
// // addNonce: true,
|
||||
// // replace: true
|
||||
// // })
|
||||
// // }
|
||||
// // : undefined
|
||||
// // }}
|
||||
// // >
|
||||
// // <Unlock noteId={noteId} />
|
||||
// // </RouteContainer>
|
||||
// // );
|
||||
// },
|
||||
// "/notes/:noteId/conflict": ({ noteId }) => {
|
||||
// closeOpenedDialog();
|
||||
// return <DiffViewer noteId={noteId} />;
|
||||
// },
|
||||
"/buy": () => {
|
||||
showBuyDialog().then(afterAction);
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@ import { store as noteStore } from "./note-store";
|
||||
import { store as notebookStore } from "./notebook-store";
|
||||
import { store as trashStore } from "./trash-store";
|
||||
import { store as tagStore } from "./tag-store";
|
||||
import { store as editorstore } from "./editor-store";
|
||||
import { useEditorStore } from "./editor-store";
|
||||
import { store as attachmentStore } from "./attachment-store";
|
||||
import { store as monographStore } from "./monograph-store";
|
||||
import { store as reminderStore } from "./reminder-store";
|
||||
@@ -157,7 +157,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
await attachmentStore.refresh();
|
||||
await monographStore.refresh();
|
||||
await settingStore.refresh();
|
||||
await editorstore.refresh();
|
||||
await useEditorStore.getState().refresh();
|
||||
|
||||
await this.refreshNavItems();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import createStore from "../common/store";
|
||||
import { db } from "../common/db";
|
||||
import BaseStore from "./index";
|
||||
import { store as editorStore } from "./editor-store";
|
||||
import { useEditorStore } from "./editor-store";
|
||||
import { checkAttachment } from "../common/attachments";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { AttachmentStream } from "../utils/streams/attachment-stream";
|
||||
@@ -121,14 +121,14 @@ class AttachmentStore extends BaseStore<AttachmentStore> {
|
||||
if (await db.attachments.remove(attachment.hash, false)) {
|
||||
await this.get().refresh();
|
||||
|
||||
const sessionId = editorStore.get().session.id;
|
||||
const sessionId = useEditorStore.getState().session.id;
|
||||
if (
|
||||
sessionId &&
|
||||
(await db.relations
|
||||
.to({ id: attachment.id, type: "attachment" }, "note")
|
||||
.has(sessionId))
|
||||
) {
|
||||
await editorStore.clearSession();
|
||||
await useEditorStore.getState().clearSession();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -17,224 +17,470 @@ 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 createStore from "../common/store";
|
||||
import { store as noteStore } from "./note-store";
|
||||
import { store as attachmentStore } from "./attachment-store";
|
||||
import { createPersistedStore } from "../common/store";
|
||||
import { useStore as useNoteStore } from "./note-store";
|
||||
import { store as appStore } from "./app-store";
|
||||
import { store as settingStore } from "./setting-store";
|
||||
import { db } from "../common/db";
|
||||
import BaseStore from ".";
|
||||
import { EV, EVENTS } from "@notesnook/core/dist/common";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import { logger } from "../utils/logger";
|
||||
import Config from "../utils/config";
|
||||
import { setDocumentTitle } from "../utils/dom";
|
||||
import { Note, Tag } from "@notesnook/core";
|
||||
import { BaseTrashItem, ContentItem, Note, Tag } from "@notesnook/core";
|
||||
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
|
||||
import { Context } from "../components/list-container/types";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { getId } from "@notesnook/core/dist/utils/id";
|
||||
import { createJSONStorage } from "zustand/middleware";
|
||||
import { getFormattedHistorySessionDate } from "@notesnook/common";
|
||||
import { isCipher } from "@notesnook/core/dist/database/crypto";
|
||||
|
||||
enum SaveState {
|
||||
export enum SaveState {
|
||||
NotSaved = -1,
|
||||
Saving = 0,
|
||||
Saved = 1
|
||||
}
|
||||
enum SESSION_STATES {
|
||||
stale = "stale",
|
||||
new = "new",
|
||||
locked = "locked",
|
||||
unlocked = "unlocked",
|
||||
opening = "opening"
|
||||
new,
|
||||
old,
|
||||
locked,
|
||||
unlocked,
|
||||
conflicted
|
||||
}
|
||||
|
||||
export type EditorSession = {
|
||||
sessionType: "default" | "locked" | "preview";
|
||||
content?: NoteContent<false>;
|
||||
isDeleted: boolean;
|
||||
attachmentsLength: number;
|
||||
saveState: SaveState;
|
||||
sessionId: string;
|
||||
state: SESSION_STATES;
|
||||
context?: Context;
|
||||
nonce?: string;
|
||||
} & Partial<Note>;
|
||||
// type ConflictedContentItem = Omit<ContentItem, "conflicted"> & {
|
||||
// conflicted: ContentItem;
|
||||
// };
|
||||
|
||||
export const getDefaultSession = (sessionId?: string): EditorSession => {
|
||||
return {
|
||||
sessionType: "default",
|
||||
state: SESSION_STATES.new,
|
||||
saveState: SaveState.Saved, // -1 = not saved, 0 = saving, 1 = saved
|
||||
sessionId: sessionId || Date.now().toString(),
|
||||
attachmentsLength: 0,
|
||||
isDeleted: false
|
||||
};
|
||||
export type BaseEditorSession = {
|
||||
id: string;
|
||||
needsHydration?: boolean;
|
||||
pinned?: boolean;
|
||||
preview?: boolean;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
class EditorStore extends BaseStore<EditorStore> {
|
||||
session = getDefaultSession();
|
||||
export type LockedEditorSession = BaseEditorSession & {
|
||||
type: "locked";
|
||||
note: Note;
|
||||
};
|
||||
|
||||
export type ReadonlyEditorSession = BaseEditorSession & {
|
||||
type: "readonly";
|
||||
note: Note;
|
||||
content?: NoteContent<false>;
|
||||
color?: string;
|
||||
tags: Tag[] = [];
|
||||
tags?: Tag[];
|
||||
};
|
||||
|
||||
export type DeletedEditorSession = BaseEditorSession & {
|
||||
type: "deleted";
|
||||
note: BaseTrashItem<Note>;
|
||||
content?: NoteContent<false>;
|
||||
};
|
||||
|
||||
export type DefaultEditorSession = BaseEditorSession & {
|
||||
type: "default";
|
||||
note: Note;
|
||||
content?: NoteContent<false>;
|
||||
sessionId: string;
|
||||
color?: string;
|
||||
tags?: Tag[];
|
||||
attachmentsLength?: number;
|
||||
saveState: SaveState;
|
||||
};
|
||||
|
||||
export type NewEditorSession = BaseEditorSession & {
|
||||
type: "new";
|
||||
context?: Context;
|
||||
saveState: SaveState;
|
||||
};
|
||||
|
||||
export type ConflictedEditorSession = BaseEditorSession & {
|
||||
type: "conflicted" | "diff";
|
||||
note: Note;
|
||||
content: ContentItem;
|
||||
};
|
||||
|
||||
export type EditorSession =
|
||||
| DefaultEditorSession
|
||||
| LockedEditorSession
|
||||
| NewEditorSession
|
||||
| ConflictedEditorSession
|
||||
| ReadonlyEditorSession
|
||||
| DeletedEditorSession;
|
||||
|
||||
export type SessionType = keyof SessionTypeMap;
|
||||
type SessionTypeMap = {
|
||||
unlocked: DefaultEditorSession;
|
||||
default: DefaultEditorSession;
|
||||
locked: LockedEditorSession;
|
||||
new: NewEditorSession;
|
||||
conflicted: ConflictedEditorSession;
|
||||
diff: ConflictedEditorSession;
|
||||
readonly: ReadonlyEditorSession;
|
||||
deleted: DeletedEditorSession;
|
||||
};
|
||||
|
||||
export function isLockedSession(session: EditorSession): session is
|
||||
| LockedEditorSession
|
||||
// TODO: | DefaultEditorSession
|
||||
| DeletedEditorSession
|
||||
| ConflictedEditorSession {
|
||||
return (
|
||||
session.type === "locked" ||
|
||||
("content" in session &&
|
||||
!!session.content &&
|
||||
"locked" in session.content &&
|
||||
session.content.locked)
|
||||
);
|
||||
}
|
||||
|
||||
class EditorStore extends BaseStore<EditorStore> {
|
||||
sessions: EditorSession[] = [];
|
||||
activeSessionId?: string;
|
||||
|
||||
arePropertiesVisible = false;
|
||||
editorMargins = Config.get("editor:margins", true);
|
||||
history: string[] = [];
|
||||
|
||||
getSession = <T extends SessionType[]>(id: string, types?: T) => {
|
||||
return this.get().sessions.find(
|
||||
(s): s is SessionTypeMap[T[number]] =>
|
||||
s.id === id && (!types || types.includes(s.type))
|
||||
);
|
||||
};
|
||||
|
||||
getActiveSession = <T extends SessionType[]>(types?: T) => {
|
||||
const { activeSessionId, sessions } = this.get();
|
||||
return sessions.find(
|
||||
(s): s is SessionTypeMap[T[number]] =>
|
||||
s.id === activeSessionId && (!types || types.includes(s.type))
|
||||
);
|
||||
};
|
||||
|
||||
init = () => {
|
||||
EV.subscribe(EVENTS.userLoggedOut, () => {
|
||||
hashNavigate("/notes/create", { replace: true, addNonce: true });
|
||||
const { closeSessions, sessions } = this.get();
|
||||
closeSessions(...sessions.map((s) => s.id));
|
||||
});
|
||||
|
||||
EV.subscribe(EVENTS.vaultLocked, async () => {
|
||||
const { id, locked } = this.get().session;
|
||||
if (id && locked) hashNavigate(`/notes/${id}/unlock`, { replace: true });
|
||||
EV.subscribe(EVENTS.vaultLocked, () => {
|
||||
this.set((state) => {
|
||||
state.sessions = state.sessions.map((session) => {
|
||||
console.log("REFRESHIGN", session);
|
||||
if (isLockedSession(session)) {
|
||||
if (session.type === "diff" || session.type === "deleted")
|
||||
return session;
|
||||
|
||||
return {
|
||||
type: "locked",
|
||||
id: session.id,
|
||||
note: session.note,
|
||||
pinned: session.pinned,
|
||||
preview: session.preview
|
||||
};
|
||||
}
|
||||
return session;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
openSession,
|
||||
openDiffSession,
|
||||
activateSession,
|
||||
activeSessionId,
|
||||
getSession
|
||||
} = this.get();
|
||||
if (activeSessionId) {
|
||||
const session = getSession(activeSessionId);
|
||||
if (!session) return;
|
||||
|
||||
console.log("OPENING", session);
|
||||
if (session.type === "diff") openDiffSession(session.note.id, session.id);
|
||||
else if (session.type === "new") activateSession(session.id);
|
||||
else openSession(activeSessionId);
|
||||
}
|
||||
};
|
||||
|
||||
refreshTags = async () => {
|
||||
const { session } = this.get();
|
||||
if (!session.id) return;
|
||||
this.set({
|
||||
tags: await db.relations
|
||||
.to({ id: session.id, type: "note" }, "tag")
|
||||
.selector.items(undefined, {
|
||||
sortBy: "dateCreated",
|
||||
sortDirection: "asc"
|
||||
})
|
||||
});
|
||||
// const { session } = this.get();
|
||||
// if (!session.id) return;
|
||||
// this.set({
|
||||
// tags: await db.relations
|
||||
// .to({ id: session.id, type: "note" }, "tag")
|
||||
// .selector.items(undefined, {
|
||||
// sortBy: "dateCreated",
|
||||
// sortDirection: "asc"
|
||||
// })
|
||||
// });
|
||||
};
|
||||
|
||||
async refresh() {
|
||||
const sessionId = this.get().session.id;
|
||||
if (sessionId && !(await db.notes.exists(sessionId)))
|
||||
await this.clearSession();
|
||||
// const sessionId = this.get().session.id;
|
||||
// if (sessionId && !(await db.notes.exists(sessionId)))
|
||||
// await this.clearSession();
|
||||
}
|
||||
|
||||
updateSession = async (item: Note) => {
|
||||
updateSession = <T extends SessionType[]>(
|
||||
id: string,
|
||||
types: T,
|
||||
partial:
|
||||
| Partial<SessionTypeMap[T[number]]>
|
||||
| ((session: SessionTypeMap[T[number]]) => void)
|
||||
) => {
|
||||
this.set((state) => {
|
||||
state.session.title = item.title;
|
||||
state.session.pinned = item.pinned;
|
||||
state.session.favorite = item.favorite;
|
||||
state.session.readonly = item.readonly;
|
||||
state.session.dateEdited = item.dateEdited;
|
||||
state.session.dateCreated = item.dateCreated;
|
||||
state.session.locked = item.locked;
|
||||
});
|
||||
};
|
||||
|
||||
openLockedSession = async (note: Note) => {
|
||||
this.set((state) => {
|
||||
state.session = {
|
||||
...getDefaultSession(note.dateEdited.toString()),
|
||||
...note,
|
||||
locked: true,
|
||||
sessionType: "locked",
|
||||
id: undefined, // NOTE: we give a session id only after the note is opened.
|
||||
state: SESSION_STATES.unlocked
|
||||
};
|
||||
});
|
||||
appStore.setIsEditorOpen(true);
|
||||
hashNavigate(`/notes/${note.id}/edit`, { replace: true });
|
||||
};
|
||||
|
||||
openSession = async (noteId: string, force = false) => {
|
||||
const session = this.get().session;
|
||||
|
||||
if (session.id) await db.fs().cancel(session.id);
|
||||
if (session.id === noteId && !force) {
|
||||
noteStore.get().setSelectedNote(noteId);
|
||||
setDocumentTitle(
|
||||
settingStore.get().hideNoteTitle ? undefined : session.title
|
||||
const index = state.sessions.findIndex(
|
||||
(s) => s.id === id && types.includes(s.type)
|
||||
);
|
||||
return;
|
||||
if (index === -1) return;
|
||||
const session = state.sessions[index] as SessionTypeMap[T[number]];
|
||||
if (typeof partial === "function") partial(session);
|
||||
else state.sessions[index] = { ...session, ...partial };
|
||||
});
|
||||
};
|
||||
|
||||
activateSession = (id?: string) => {
|
||||
const session = this.get().sessions.find((s) => s.id === id);
|
||||
if (!session) id = undefined;
|
||||
|
||||
if (
|
||||
id &&
|
||||
!settingStore.get().hideNoteTitle &&
|
||||
session &&
|
||||
"note" in session
|
||||
) {
|
||||
setDocumentTitle(session.note.title);
|
||||
} else setDocumentTitle();
|
||||
|
||||
this.set({ activeSessionId: id });
|
||||
appStore.setIsEditorOpen(!!id);
|
||||
this.toggleProperties(false);
|
||||
|
||||
if (id) {
|
||||
const { history } = this.get();
|
||||
if (history.includes(id)) history.splice(history.indexOf(id), 1);
|
||||
history.push(id);
|
||||
}
|
||||
};
|
||||
|
||||
if (session.state === SESSION_STATES.unlocked) {
|
||||
this.set((state) => {
|
||||
state.session.id = noteId;
|
||||
state.session.state = SESSION_STATES.new;
|
||||
});
|
||||
return;
|
||||
}
|
||||
openDiffSession = async (noteId: string, sessionId: string) => {
|
||||
const session = await db.noteHistory.session(sessionId);
|
||||
const note = await db.notes.note(noteId);
|
||||
if (!session || !note || !note.contentId) return;
|
||||
|
||||
const note =
|
||||
(await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
|
||||
if (!note) return;
|
||||
const currentContent = await db.content.get(note.contentId);
|
||||
const oldContent = await db.noteHistory.content(session.id);
|
||||
|
||||
noteStore.get().setSelectedNote(note.id);
|
||||
setDocumentTitle(settingStore.get().hideNoteTitle ? undefined : note.title);
|
||||
if (!oldContent || !currentContent) return;
|
||||
|
||||
if (await db.vaults.itemExists(note))
|
||||
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
|
||||
if (note.conflicted)
|
||||
return hashNavigate(`/notes/${noteId}/conflict`, { replace: true });
|
||||
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
if (content && content.locked)
|
||||
return hashNavigate(`/notes/${noteId}/unlock`, { replace: true });
|
||||
|
||||
this.set((state) => {
|
||||
const defaultSession = getDefaultSession(note.dateEdited.toString());
|
||||
state.session = {
|
||||
...defaultSession,
|
||||
...note,
|
||||
content,
|
||||
state: SESSION_STATES.new,
|
||||
attachmentsLength: 0 // TODO: db.attachments.ofNote(note.id, "all")?.length || 0
|
||||
};
|
||||
|
||||
const isDeleted = note.type === "trash";
|
||||
if (isDeleted) {
|
||||
state.session.isDeleted = true;
|
||||
state.session.readonly = true;
|
||||
const label = getFormattedHistorySessionDate(session);
|
||||
this.get().addSession({
|
||||
type: "diff",
|
||||
id: session.id,
|
||||
note,
|
||||
title: label,
|
||||
content: {
|
||||
type: oldContent.type,
|
||||
dateCreated: session.dateCreated,
|
||||
dateEdited: session.dateModified,
|
||||
dateModified: session.dateModified,
|
||||
id: session.id,
|
||||
localOnly: false,
|
||||
noteId,
|
||||
conflicted: currentContent,
|
||||
...(isCipher(oldContent.data)
|
||||
? { locked: true, data: oldContent.data }
|
||||
: { locked: false, data: oldContent.data })
|
||||
}
|
||||
});
|
||||
appStore.setIsEditorOpen(true);
|
||||
this.toggleProperties(false);
|
||||
};
|
||||
|
||||
openSession = async (
|
||||
noteOrId: string | Note | BaseTrashItem<Note>,
|
||||
force = false
|
||||
): Promise<void> => {
|
||||
const { getSession } = this.get();
|
||||
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
|
||||
const session = getSession(noteId);
|
||||
|
||||
if (session && !force && !session.needsHydration) {
|
||||
return this.activateSession(noteId);
|
||||
}
|
||||
|
||||
if (session && session.id) await db.fs().cancel(session.id);
|
||||
|
||||
const note =
|
||||
typeof noteOrId === "object"
|
||||
? noteOrId
|
||||
: (await db.notes.note(noteId)) || (await db.notes.trashed(noteId));
|
||||
if (!note) return;
|
||||
const isPreview = session ? session.preview : true;
|
||||
|
||||
if (note.locked && note.type !== "trash") {
|
||||
this.addSession({
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
note,
|
||||
preview: isPreview
|
||||
});
|
||||
} else if (note.conflicted) {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
!content ||
|
||||
content.locked ||
|
||||
!content.conflicted ||
|
||||
note.type === "trash"
|
||||
) {
|
||||
note.conflicted = false;
|
||||
await db.notes.add({ id: note.id, conflicted: false });
|
||||
if (content?.locked) {
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
dateResolved: Date.now()
|
||||
});
|
||||
}
|
||||
return this.openSession(note, true);
|
||||
}
|
||||
|
||||
this.addSession({
|
||||
type: "conflicted",
|
||||
content: content,
|
||||
id: note.id,
|
||||
note,
|
||||
preview: isPreview
|
||||
});
|
||||
} else {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
|
||||
if (content?.locked) {
|
||||
note.locked = true;
|
||||
await db.notes.add({ id: note.id, locked: true });
|
||||
return this.openSession(note, true);
|
||||
}
|
||||
|
||||
if (note.type === "trash") {
|
||||
this.addSession({
|
||||
type: "deleted",
|
||||
note,
|
||||
id: note.id,
|
||||
content
|
||||
});
|
||||
} else if (note.readonly) {
|
||||
this.addSession({
|
||||
type: "readonly",
|
||||
note,
|
||||
id: note.id,
|
||||
content
|
||||
});
|
||||
} else {
|
||||
const attachmentsLength = await db.attachments
|
||||
.ofNote(note.id, "all")
|
||||
.count();
|
||||
|
||||
this.addSession({
|
||||
type: "default",
|
||||
id: note.id,
|
||||
note,
|
||||
saveState: SaveState.Saved,
|
||||
sessionId: `${Date.now()}`,
|
||||
attachmentsLength,
|
||||
content,
|
||||
preview: isPreview
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addSession = (session: EditorSession, activate = true) => {
|
||||
let oldSessionId: string | null = null;
|
||||
|
||||
this.set((state) => {
|
||||
const { activeSessionIndex, duplicateSessionIndex, previewSessionIndex } =
|
||||
findSessionIndices(state.sessions, session, state.activeSessionId);
|
||||
|
||||
if (duplicateSessionIndex > -1) {
|
||||
oldSessionId = state.sessions[duplicateSessionIndex].id;
|
||||
state.sessions[duplicateSessionIndex] = session;
|
||||
} else if (previewSessionIndex > -1) {
|
||||
oldSessionId = state.sessions[previewSessionIndex].id;
|
||||
state.sessions[previewSessionIndex] = session;
|
||||
} else if (activeSessionIndex > -1)
|
||||
state.sessions.splice(activeSessionIndex + 1, 0, session);
|
||||
else state.sessions.push(session);
|
||||
});
|
||||
|
||||
const { history } = this.get();
|
||||
if (
|
||||
oldSessionId &&
|
||||
oldSessionId !== session.id &&
|
||||
history.includes(oldSessionId)
|
||||
)
|
||||
history.splice(history.indexOf(oldSessionId), 1);
|
||||
|
||||
if (activate) this.activateSession(session.id);
|
||||
};
|
||||
|
||||
saveSession = async (
|
||||
sessionId: string | undefined,
|
||||
session: Partial<EditorSession>
|
||||
) => {
|
||||
if (!session) {
|
||||
logger.warn("Session cannot be undefined", { sessionId, session });
|
||||
return;
|
||||
id: string,
|
||||
partial: Partial<Omit<DefaultEditorSession, "note">> & {
|
||||
note?: Partial<Note>;
|
||||
ignoreEdit?: boolean;
|
||||
}
|
||||
) => {
|
||||
const currentSession = this.getSession(id, ["new", "default"]);
|
||||
if (!currentSession) return;
|
||||
|
||||
const currentSession = this.get().session;
|
||||
if (currentSession.readonly && session.readonly !== false) return; // do not allow saving of readonly session
|
||||
if (currentSession.saveState === 0 || currentSession.id !== sessionId)
|
||||
// do not allow saving of readonly session
|
||||
if (partial.note?.readonly) return;
|
||||
if (
|
||||
currentSession.saveState === SaveState.Saving ||
|
||||
currentSession.id !== id
|
||||
)
|
||||
return;
|
||||
|
||||
this.setSaveState(0);
|
||||
this.setSaveState(id, 0);
|
||||
try {
|
||||
if (session.content) this.get().session.content = session.content;
|
||||
|
||||
const id =
|
||||
currentSession.locked && sessionId && session.content
|
||||
? await db.vault.save({
|
||||
content: session.content,
|
||||
sessionId: session.sessionId,
|
||||
id: sessionId
|
||||
})
|
||||
: await db.notes.add({ ...session, id: sessionId });
|
||||
|
||||
if (currentSession && currentSession.id !== sessionId) {
|
||||
noteStore.refresh();
|
||||
throw new Error("Aborting save operation: old session.");
|
||||
// if (partial.content) currentSession.content = partial.content;
|
||||
if (isLockedSession(currentSession)) {
|
||||
await db.vault.save({
|
||||
content: partial.content,
|
||||
sessionId: partial.sessionId,
|
||||
id
|
||||
});
|
||||
} else {
|
||||
await db.notes.add({
|
||||
...partial.note,
|
||||
dateEdited:
|
||||
partial.ignoreEdit && currentSession.type === "default"
|
||||
? currentSession.note.dateEdited
|
||||
: undefined,
|
||||
content: partial.content,
|
||||
sessionId: partial.sessionId,
|
||||
id
|
||||
});
|
||||
}
|
||||
if (!id) throw new Error("Note not saved.");
|
||||
|
||||
// if (currentSession && currentSession.id !== id) {
|
||||
// noteStore.refresh();
|
||||
// throw new Error("Aborting save operation: old session.");
|
||||
// }
|
||||
// if (!id) throw new Error("Note not saved.");
|
||||
|
||||
// let note = await db.notes.note(id);
|
||||
// if (!note) throw new Error("Note not saved.");
|
||||
|
||||
if (!sessionId) {
|
||||
noteStore.setSelectedNote(id);
|
||||
hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
|
||||
}
|
||||
// hashNavigate(`/notes/${id}/edit`, { replace: true, notify: false });
|
||||
|
||||
const defaultNotebook = db.settings.getDefaultNotebook();
|
||||
if (currentSession.context) {
|
||||
if (currentSession.type === "new" && currentSession.context) {
|
||||
const { type } = currentSession.context;
|
||||
if (type === "notebook")
|
||||
await db.notes.addToNotebook(currentSession.context.id, id);
|
||||
@@ -243,130 +489,118 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
{ type, id: currentSession.context.id },
|
||||
{ id, type: "note" }
|
||||
);
|
||||
} else if (!sessionId && defaultNotebook) {
|
||||
} else if (!id && defaultNotebook) {
|
||||
await db.notes.addToNotebook(defaultNotebook, id);
|
||||
}
|
||||
|
||||
const note = await db.notes.note(id);
|
||||
if (!note) throw new Error("Note not saved.");
|
||||
|
||||
const shouldRefreshNotes =
|
||||
currentSession.context ||
|
||||
!sessionId ||
|
||||
note.title !== currentSession.title ||
|
||||
note.headline !== currentSession.headline;
|
||||
if (shouldRefreshNotes) noteStore.refresh();
|
||||
|
||||
const attachmentsLength = await db.attachments.ofNote(id, "all").count();
|
||||
if (attachmentsLength !== currentSession.attachmentsLength) {
|
||||
attachmentStore.refresh();
|
||||
const shouldRefreshNotes =
|
||||
currentSession.type === "new" ||
|
||||
!id ||
|
||||
note.title !== currentSession.note?.title ||
|
||||
note.headline !== currentSession.note?.headline ||
|
||||
attachmentsLength !== currentSession.attachmentsLength;
|
||||
if (shouldRefreshNotes) useNoteStore.getState().refresh();
|
||||
|
||||
if (currentSession.type === "new") {
|
||||
this.addSession({
|
||||
type: "default",
|
||||
id,
|
||||
note,
|
||||
saveState: SaveState.Saved,
|
||||
sessionId: partial.sessionId || `${Date.now()}`,
|
||||
attachmentsLength,
|
||||
pinned: currentSession.pinned,
|
||||
content: partial.content
|
||||
});
|
||||
} else {
|
||||
this.updateSession(id, ["unlocked", "default"], {
|
||||
preview: false,
|
||||
attachmentsLength: attachmentsLength,
|
||||
note
|
||||
});
|
||||
}
|
||||
|
||||
this.set((state) => {
|
||||
if (!!state.session.id && state.session.id !== note.id) return;
|
||||
|
||||
for (const key in session) {
|
||||
if (key === "content") continue;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
state.session[key] = session[key as keyof EditorSession];
|
||||
}
|
||||
|
||||
state.session.context = undefined;
|
||||
state.session.id = note.id;
|
||||
state.session.title = note.title;
|
||||
state.session.dateEdited = note.dateEdited;
|
||||
state.session.attachmentsLength = attachmentsLength;
|
||||
console.log("NOTE", note.dateEdited);
|
||||
});
|
||||
setDocumentTitle(
|
||||
settingStore.get().hideNoteTitle ? undefined : note.title
|
||||
);
|
||||
|
||||
this.setSaveState(1);
|
||||
} catch (err) {
|
||||
showToast(
|
||||
"error",
|
||||
err instanceof Error ? err.stack || err.message : JSON.stringify(err)
|
||||
);
|
||||
this.setSaveState(-1);
|
||||
this.setSaveState(id, -1);
|
||||
console.error(err);
|
||||
if (err instanceof Error) logger.error(err);
|
||||
if (currentSession.locked && currentSession.id) {
|
||||
await this.get().openSession(currentSession.id, true);
|
||||
if (isLockedSession(currentSession)) {
|
||||
// TODO:
|
||||
// hashNavigate(`/notes/${id}/unlock`, { replace: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
newSession = async (nonce?: string) => {
|
||||
const context = noteStore.get().context;
|
||||
const session = this.get().session;
|
||||
if (session.id) await db.fs().cancel(session.id);
|
||||
|
||||
this.set((state) => {
|
||||
state.session = {
|
||||
...getDefaultSession(),
|
||||
context,
|
||||
nonce,
|
||||
state: SESSION_STATES.new
|
||||
};
|
||||
newSession = () => {
|
||||
this.addSession({
|
||||
type: "new",
|
||||
id: getId(),
|
||||
context: useNoteStore.getState().context,
|
||||
saveState: SaveState.NotSaved
|
||||
});
|
||||
noteStore.setSelectedNote();
|
||||
appStore.setIsEditorOpen(true);
|
||||
setDocumentTitle();
|
||||
};
|
||||
|
||||
clearSession = async (shouldNavigate = true) => {
|
||||
const session = this.get().session;
|
||||
if (session.id) await db.fs().cancel(session.id);
|
||||
|
||||
closeSessions = (...ids: string[]) => {
|
||||
this.set((state) => {
|
||||
state.session = {
|
||||
...getDefaultSession(),
|
||||
state: SESSION_STATES.new
|
||||
};
|
||||
const sessions: EditorSession[] = [];
|
||||
for (let i = 0; i < state.sessions.length; ++i) {
|
||||
const session = state.sessions[i];
|
||||
if (!ids.includes(session.id)) {
|
||||
sessions.push(session);
|
||||
continue;
|
||||
}
|
||||
|
||||
db.fs().cancel(session.id).catch(console.error);
|
||||
if (state.history.includes(session.id))
|
||||
state.history.splice(state.history.indexOf(session.id), 1);
|
||||
}
|
||||
state.sessions = sessions;
|
||||
});
|
||||
noteStore.setSelectedNote();
|
||||
this.toggleProperties(false);
|
||||
setTimeout(() => {
|
||||
if (shouldNavigate)
|
||||
hashNavigate(`/notes/create`, { replace: true, addNonce: true });
|
||||
appStore.setIsEditorOpen(false);
|
||||
setDocumentTitle();
|
||||
}, 100);
|
||||
|
||||
const { history } = this.get();
|
||||
this.activateSession(history.pop());
|
||||
};
|
||||
|
||||
setTitle = (noteId: string | undefined, title: string) => {
|
||||
return this.saveSession(noteId, { title });
|
||||
setTitle = (id: string, title: string) => {
|
||||
return this.saveSession(id, { note: { title } });
|
||||
};
|
||||
|
||||
toggle = (
|
||||
noteId: string,
|
||||
id: string,
|
||||
name: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
|
||||
value: boolean | string
|
||||
) => {
|
||||
if (name === "color" && typeof value === "string")
|
||||
return this.set({ color: value });
|
||||
return this.saveSession(noteId, { [name]: value });
|
||||
return this.updateSession(id, ["readonly", "default"], { color: value });
|
||||
return this.saveSession(id, { note: { [name]: value } });
|
||||
};
|
||||
|
||||
saveSessionContent = (
|
||||
noteId: string | undefined,
|
||||
id: string,
|
||||
sessionId: string,
|
||||
ignoreEdit: boolean,
|
||||
content: NoteContent<false>
|
||||
) => {
|
||||
const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
|
||||
return this.saveSession(noteId, { sessionId, content, dateEdited });
|
||||
// const dateEdited = ignoreEdit ? this.get().session.dateEdited : undefined;
|
||||
return this.saveSession(id, { sessionId, content, ignoreEdit });
|
||||
};
|
||||
|
||||
setSaveState = (saveState: SaveState) => {
|
||||
this.set((state) => {
|
||||
state.session.saveState = saveState;
|
||||
});
|
||||
setSaveState = (id: string, saveState: SaveState) => {
|
||||
this.updateSession(id, ["default", "new"], { saveState: saveState });
|
||||
};
|
||||
|
||||
toggleProperties = (toggleState: boolean) => {
|
||||
toggleProperties = (toggleState?: boolean) => {
|
||||
this.set(
|
||||
(state) =>
|
||||
(state.arePropertiesVisible =
|
||||
@@ -374,22 +608,62 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
);
|
||||
};
|
||||
|
||||
toggleEditorMargins = (toggleState: boolean) => {
|
||||
toggleEditorMargins = (toggleState?: boolean) => {
|
||||
this.set((state) => {
|
||||
state.editorMargins =
|
||||
toggleState !== undefined ? toggleState : !state.editorMargins;
|
||||
Config.set("editor:margins", state.editorMargins);
|
||||
});
|
||||
};
|
||||
|
||||
// _getSaveFn = () => {
|
||||
// return this.get().session.locked
|
||||
// ? db.vault.save.bind(db.vault)
|
||||
// : db.notes.add.bind(db.notes);
|
||||
// };
|
||||
}
|
||||
|
||||
const [useStore, store] = createStore<EditorStore>(
|
||||
(set, get) => new EditorStore(set, get)
|
||||
);
|
||||
export { useStore, store, SESSION_STATES };
|
||||
const [useEditorStore] = createPersistedStore(EditorStore, {
|
||||
name: "editor-sessions",
|
||||
partialize: (state) => ({
|
||||
history: state.history,
|
||||
activeSessionId: state.activeSessionId,
|
||||
arePropertiesVisible: state.arePropertiesVisible,
|
||||
editorMargins: state.editorMargins,
|
||||
sessions: state.sessions.reduce((sessions, session) => {
|
||||
sessions.push({
|
||||
id: session.id,
|
||||
type: isLockedSession(session) ? "locked" : session.type,
|
||||
needsHydration: true,
|
||||
preview: session.preview,
|
||||
pinned: session.pinned,
|
||||
note:
|
||||
"note" in session
|
||||
? {
|
||||
title: session.note.title
|
||||
}
|
||||
: undefined
|
||||
} as EditorSession);
|
||||
|
||||
return sessions;
|
||||
}, [] as EditorSession[])
|
||||
}),
|
||||
storage: createJSONStorage(() => localStorage)
|
||||
});
|
||||
export { useEditorStore, SESSION_STATES };
|
||||
|
||||
function findSessionIndices(
|
||||
sessions: EditorSession[],
|
||||
session: EditorSession,
|
||||
activeSessionId?: string
|
||||
) {
|
||||
let activeSessionIndex = -1;
|
||||
let previewSessionIndex = -1;
|
||||
let duplicateSessionIndex = -1;
|
||||
for (let i = 0; i < sessions.length; ++i) {
|
||||
const { id, preview } = sessions[i];
|
||||
if (id === session.id) duplicateSessionIndex = i;
|
||||
else if (preview && session.preview) previewSessionIndex = i;
|
||||
else if (id === activeSessionId) activeSessionIndex = i;
|
||||
}
|
||||
|
||||
return {
|
||||
activeSessionIndex,
|
||||
previewSessionIndex,
|
||||
duplicateSessionIndex
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { db } from "../common/db";
|
||||
import createStore from "../common/store";
|
||||
import { store as editorStore } from "./editor-store";
|
||||
import { useEditorStore } from "./editor-store";
|
||||
import { store as appStore } from "./app-store";
|
||||
import { store as selectionStore } from "./selection-store";
|
||||
import Vault from "../common/vault";
|
||||
@@ -33,7 +33,6 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
notes: VirtualizedGrouping<Note> | undefined = undefined;
|
||||
contextNotes: VirtualizedGrouping<Note> | undefined = undefined;
|
||||
context: Context | undefined = undefined;
|
||||
selectedNote?: string;
|
||||
// nonce = 0;
|
||||
viewMode: ViewMode = Config.get("notes:viewMode", "detailed");
|
||||
|
||||
@@ -42,11 +41,6 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
Config.set("notes:viewMode", viewMode);
|
||||
};
|
||||
|
||||
setSelectedNote = (id?: string) => {
|
||||
if (!id) selectionStore.get().toggleSelectionMode(false);
|
||||
this.set({ selectedNote: id });
|
||||
};
|
||||
|
||||
refresh = async () => {
|
||||
const grouping = await db.notes.all.grouped(
|
||||
db.settings.getGroupOptions("home")
|
||||
@@ -77,7 +71,7 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
};
|
||||
|
||||
delete = async (...ids: string[]) => {
|
||||
const { session, clearSession } = editorStore.get();
|
||||
const { session, clearSession } = useEditorStore.getState();
|
||||
if (session.id && ids.indexOf(session.id) > -1) await clearSession();
|
||||
await db.notes.moveToTrash(...ids);
|
||||
await this.refresh();
|
||||
@@ -97,8 +91,8 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
|
||||
unlock = async (id: string) => {
|
||||
return await Vault.unlockNote(id).then(async (res) => {
|
||||
if (editorStore.get().session.id === id)
|
||||
await editorStore.clearSession(true);
|
||||
if (useEditorStore.getState().session.id === id)
|
||||
await useEditorStore.getState().openSession(id);
|
||||
await this.refresh();
|
||||
return res;
|
||||
});
|
||||
@@ -107,9 +101,8 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
lock = async (id: string) => {
|
||||
if (!(await Vault.lockNote(id))) return false;
|
||||
await this.refresh();
|
||||
if (editorStore.get().session.id === id)
|
||||
await editorStore.openSession(id, true);
|
||||
return true;
|
||||
if (useEditorStore.getState().session.id === id)
|
||||
await useEditorStore.getState().openSession(id, true);
|
||||
};
|
||||
|
||||
readonly = async (state: boolean, ...ids: string[]) => {
|
||||
@@ -149,7 +142,7 @@ class NoteStore extends BaseStore<NoteStore> {
|
||||
action: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
|
||||
value: boolean | string
|
||||
) => {
|
||||
const { session, toggle } = editorStore.get();
|
||||
const { session, toggle } = useEditorStore.getState();
|
||||
if (!session.id || !noteIds.includes(session.id)) return false;
|
||||
toggle(session.id, action, value);
|
||||
return true;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { desktop } from "../common/desktop-bridge";
|
||||
import createStore from "../common/store";
|
||||
import Config from "../utils/config";
|
||||
import BaseStore from "./index";
|
||||
import { store as editorStore } from "./editor-store";
|
||||
import { useEditorStore } from "./editor-store";
|
||||
import { isTelemetryEnabled, setTelemetry } from "../utils/telemetry";
|
||||
import { setDocumentTitle } from "../utils/dom";
|
||||
import { TimeFormat } from "@notesnook/core/dist/utils/date";
|
||||
@@ -181,7 +181,7 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
this.set({ hideNoteTitle: !hideNoteTitle });
|
||||
Config.set("hideNoteTitle", !hideNoteTitle);
|
||||
setDocumentTitle(
|
||||
!hideNoteTitle ? undefined : editorStore.get().session.title
|
||||
!hideNoteTitle ? undefined : useEditorStore.getState().session.title
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { tryParse } from "./parse";
|
||||
|
||||
function set<T>(key: string, value: T) {
|
||||
window.localStorage.setItem(key, JSON.stringify(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
function get<T>(key: string, def?: T): T {
|
||||
|
||||
@@ -25,6 +25,7 @@ import useNavigate from "../hooks/use-navigate";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
|
||||
function Home() {
|
||||
const notes = useStore((store) => store.notes);
|
||||
@@ -68,8 +69,7 @@ function Home() {
|
||||
items={filteredItems || notes}
|
||||
placeholder={<Placeholder context="notes" />}
|
||||
button={{
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { replace: true, addNonce: true })
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -51,7 +51,6 @@ import { FlexScrollContainer } from "../components/scroll-container";
|
||||
import { Menu } from "../hooks/use-menu";
|
||||
import Config from "../utils/config";
|
||||
import Notes from "./notes";
|
||||
// import { showSortMenu } from "../components/group-header";
|
||||
|
||||
type NotebookProps = {
|
||||
rootId: string;
|
||||
|
||||
@@ -28,6 +28,7 @@ import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { handleDrop } from "../common/drop-handler";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
|
||||
type NotesProps = { header?: JSX.Element };
|
||||
function Notes(props: NotesProps) {
|
||||
@@ -74,8 +75,7 @@ function Notes(props: NotesProps) {
|
||||
/>
|
||||
}
|
||||
button={{
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
}}
|
||||
header={header}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user