mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f751db673e | ||
|
|
6d9af85e2a | ||
|
|
1d58cfea3c | ||
|
|
c1fa4d3400 | ||
|
|
df179056d6 | ||
|
|
26f9e08c23 | ||
|
|
f719bebe5a | ||
|
|
9693c8ea54 | ||
|
|
040a677895 | ||
|
|
df2708147e | ||
|
|
faba5f6385 | ||
|
|
eab5bccb99 | ||
|
|
ed399c0d0a | ||
|
|
3c7e962fe0 | ||
|
|
56614ef19d | ||
|
|
169318c549 | ||
|
|
bd6a1b9bff | ||
|
|
443d6625f9 | ||
|
|
b6f95ff7cd | ||
|
|
1b13da54a1 | ||
|
|
adf0f132fd | ||
|
|
73ec04b42d | ||
|
|
54a68e4406 | ||
|
|
bd73ae9df4 | ||
|
|
fe41f7b687 | ||
|
|
c9adaa2442 | ||
|
|
10be5771c6 | ||
|
|
43f4669360 | ||
|
|
3b04925e54 | ||
|
|
ba3959af9e | ||
|
|
552d86d9d2 | ||
|
|
096602150e | ||
|
|
e5795bcbb2 | ||
|
|
9be52a69ad | ||
|
|
5809384f62 | ||
|
|
fff70dce16 | ||
|
|
258140b1e0 | ||
|
|
0a00844e64 | ||
|
|
03cea85efc | ||
|
|
8a16ef9b85 | ||
|
|
c82c30b530 | ||
|
|
46599f66da | ||
|
|
d1b532a4ae | ||
|
|
0baf7afc25 | ||
|
|
a0595a7d23 | ||
|
|
1e6f6ab477 | ||
|
|
62d677f9ad | ||
|
|
8de98d98ab | ||
|
|
5b57614bfe | ||
|
|
ae7c538ad7 | ||
|
|
c6394c9e37 | ||
|
|
acf3740a68 | ||
|
|
8423543904 | ||
|
|
b90cb28637 | ||
|
|
780e1e9db0 | ||
|
|
ca6f8e5e4b | ||
|
|
4c4cda1c39 | ||
|
|
6fcbde772c | ||
|
|
eac1750b1c | ||
|
|
5eea179a58 | ||
|
|
fe1ea6dc27 | ||
|
|
cf1b774255 | ||
|
|
198fa33fc0 | ||
|
|
e722866d62 | ||
|
|
58845e5508 | ||
|
|
543c6f17ed | ||
|
|
533f95fcff | ||
|
|
c02f1cde16 | ||
|
|
043f55ead0 | ||
|
|
fcece06a1c | ||
|
|
da7d79edc6 | ||
|
|
a2e7190a4a | ||
|
|
503f2b5876 |
364
apps/desktop/package-lock.json
generated
364
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -29,6 +29,7 @@
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -112,7 +113,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.2.tgz",
|
||||
"integrity": "sha512-eFZVFoRXb3GFGd7Ak7W4+6jBl9wBtiZ4AaYOse97ej6mKj5tkyO0dUnUChs1IhJZtx1BENo4/p4WUTXpi6vT+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"env-paths": "^2.2.0",
|
||||
@@ -133,7 +133,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
|
||||
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -147,7 +146,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -156,7 +154,6 @@
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -165,7 +162,6 @@
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
@@ -821,7 +817,6 @@
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -833,7 +828,6 @@
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
"integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.0"
|
||||
},
|
||||
@@ -878,7 +872,6 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
|
||||
"integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "*",
|
||||
"@types/keyv": "^3.1.4",
|
||||
@@ -907,14 +900,12 @@
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||
},
|
||||
"node_modules/@types/keyv": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
|
||||
"integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -928,8 +919,7 @@
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.16.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.1.tgz",
|
||||
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA=="
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
"version": "3.0.2",
|
||||
@@ -945,7 +935,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||
"integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -975,7 +964,6 @@
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -1120,6 +1108,80 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz",
|
||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
"buffer-crc32": "^0.2.1",
|
||||
"readable-stream": "^3.6.0",
|
||||
"readdir-glob": "^1.1.2",
|
||||
"tar-stream": "^2.2.0",
|
||||
"zip-stream": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
|
||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -1259,7 +1321,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
@@ -1310,7 +1371,6 @@
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
@@ -1386,7 +1446,6 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
"integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10.6.0"
|
||||
}
|
||||
@@ -1395,7 +1454,6 @@
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
|
||||
"integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"clone-response": "^1.0.2",
|
||||
"get-stream": "^5.1.0",
|
||||
@@ -1515,7 +1573,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz",
|
||||
"integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^1.0.0"
|
||||
},
|
||||
@@ -1569,6 +1626,22 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
|
||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1635,7 +1708,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"optional": true
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/crc": {
|
||||
"version": "3.8.0",
|
||||
@@ -1646,6 +1719,33 @@
|
||||
"buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/crc32-stream": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
|
||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -1738,7 +1838,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -1747,7 +1846,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
|
||||
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"has-property-descriptors": "^1.0.0",
|
||||
@@ -1781,7 +1879,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/dir-compare": {
|
||||
@@ -1911,7 +2008,6 @@
|
||||
"version": "29.3.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.1.tgz",
|
||||
"integrity": "sha512-auge1/6RVqgUd6TgIq88wKdUCJi2cjESi3jy7d+6X4JzvBGprKBqMJ8JSSFpu/Px1YJrFUKAxfy6SC+TQf1uLw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
@@ -1951,6 +2047,19 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder-squirrel-windows": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-24.13.3.tgz",
|
||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"archiver": "^5.3.1",
|
||||
"builder-util": "24.13.1",
|
||||
"fs-extra": "^10.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder/node_modules/builder-util-runtime": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
|
||||
@@ -2024,7 +2133,6 @@
|
||||
"version": "20.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@@ -2046,7 +2154,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -2061,7 +2168,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
@@ -2114,7 +2220,6 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -2135,7 +2240,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
@@ -2176,7 +2280,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
@@ -2319,7 +2422,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
@@ -2334,7 +2436,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
|
||||
"integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
@@ -2350,7 +2451,6 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
@@ -2424,7 +2524,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
@@ -2442,7 +2541,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
|
||||
"integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"define-properties": "^1.1.3"
|
||||
@@ -2458,7 +2556,6 @@
|
||||
"version": "11.8.6",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
|
||||
"integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^4.0.0",
|
||||
"@szmarczak/http-timer": "^4.0.5",
|
||||
@@ -2488,7 +2585,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
@@ -2510,7 +2606,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
|
||||
"integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.1.1"
|
||||
@@ -2523,7 +2618,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2536,7 +2630,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -2560,8 +2653,7 @@
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
@@ -2581,7 +2673,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
"integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.0.0"
|
||||
@@ -2748,6 +2839,13 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/isbinaryfile": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.2.tgz",
|
||||
@@ -2843,8 +2941,7 @@
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
@@ -2856,7 +2953,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
@@ -2886,37 +2982,125 @@
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
|
||||
"integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kysely": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz",
|
||||
"integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy-val": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="
|
||||
},
|
||||
"node_modules/lazystream": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2936,7 +3120,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
@@ -2982,7 +3165,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||
"integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -3108,7 +3290,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
|
||||
"integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3132,7 +3313,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -3150,7 +3330,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3213,8 +3392,7 @@
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -3291,11 +3469,17 @@
|
||||
"prebuildify": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
@@ -3335,7 +3519,6 @@
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -3402,6 +3585,16 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/readdir-glob": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
|
||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -3425,14 +3618,12 @@
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
|
||||
"integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^2.0.0"
|
||||
},
|
||||
@@ -3453,7 +3644,6 @@
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
@@ -3533,14 +3723,12 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.13.1"
|
||||
@@ -3556,7 +3744,6 @@
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -3709,7 +3896,6 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/stat-mode": {
|
||||
@@ -3809,7 +3995,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
"integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0"
|
||||
},
|
||||
@@ -4011,8 +4196,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.0",
|
||||
@@ -4161,12 +4345,48 @@
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
|
||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zip-stream/node_modules/archiver-utils": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
|
||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
"lazystream": "^1.0.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.union": "^4.6.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.21.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.6",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -30,6 +30,7 @@
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
|
||||
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
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 type { Database, Statement } from "better-sqlite3-multiple-ciphers";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
| string
|
||||
| Uint8Array
|
||||
| Array<number>
|
||||
| bigint
|
||||
| null;
|
||||
|
||||
export class SQLite {
|
||||
sqlite?: Database;
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
|
||||
async open(filePath: string) {
|
||||
if (this.sqlite) {
|
||||
console.error("Database is already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<Statement | undefined> {
|
||||
if (!this.sqlite) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const prepared = this.sqlite.prepare(sql);
|
||||
if (!prepared) return;
|
||||
|
||||
this.preparedStatements.set(sql, prepared);
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return prepared;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec<R>(
|
||||
sql: string,
|
||||
parameters: SQLiteCompatibleType[] = []
|
||||
): Promise<QueryResult<R>> {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return { rows: [] };
|
||||
try {
|
||||
if (prepared.reader) {
|
||||
return {
|
||||
rows: prepared.all(parameters) as R[]
|
||||
};
|
||||
} else {
|
||||
const { changes, lastInsertRowid } = prepared.run(parameters);
|
||||
const numAffectedRows =
|
||||
changes !== undefined && changes !== null && !isNaN(changes)
|
||||
? BigInt(changes)
|
||||
: undefined;
|
||||
return {
|
||||
numAffectedRows,
|
||||
insertId:
|
||||
lastInsertRowid !== undefined && lastInsertRowid !== null
|
||||
? typeof lastInsertRowid === "bigint"
|
||||
? lastInsertRowid
|
||||
: BigInt(lastInsertRowid)
|
||||
: undefined,
|
||||
rows: [] as R[]
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!this.sqlite) throw new Error("No database is not opened.");
|
||||
return await this.exec(sql, parameters);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.sqlite) return;
|
||||
|
||||
this.preparedStatements.clear();
|
||||
this.sqlite.close();
|
||||
this.sqlite = undefined;
|
||||
}
|
||||
|
||||
async delete(filePath: string) {
|
||||
await this.close();
|
||||
await require("fs/promises").rm(filePath, {
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,3 +21,4 @@ export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
export { SQLite } from "./api/sqlite-kysely.js";
|
||||
|
||||
@@ -100,6 +100,7 @@ async function createWindow() {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
|
||||
@@ -23,13 +23,11 @@ import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var createSQLite3Database: (filename: string) => Database;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -43,5 +41,4 @@ process.once("loaded", async () => {
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -16,15 +16,16 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "react-native-gesture-handler";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { initializeLogger } from "./common/database/logger";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
@@ -44,23 +45,28 @@ I18nManager.swapLeftAndRightInRTL(false);
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
@@ -43,9 +44,8 @@ const IOS_KEYCHAIN_ACCESS_GROUP = "group.org.streetwriters.notesnook";
|
||||
const IOS_KEYCHAIN_SERVICE_NAME = "org.streetwriters.notesnook";
|
||||
const KEYCHAIN_SERVER_DBKEY = "notesnook:db";
|
||||
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
@@ -117,9 +117,7 @@ export async function setAppLockVerificationCipher(appLockPassword) {
|
||||
NOTESNOOK_APPLOCK_KEY_SALT
|
||||
);
|
||||
const encrypted = await encrypt(appLockCredentials, generatePassword());
|
||||
|
||||
CipherStorage.setMap(APPLOCK_CIPHER, encrypted);
|
||||
|
||||
DatabaseLogger.info("setAppLockVerificationCipher");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
@@ -135,10 +133,8 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
@@ -214,7 +210,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -227,6 +223,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
@@ -239,7 +236,7 @@ export async function deriveCryptoKey(data) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
@@ -264,13 +261,12 @@ export async function getCryptoKey(_name) {
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey()
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
|
||||
@@ -85,4 +85,10 @@ export async function setupDatabase(password) {
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
|
||||
const setLogger = () => {
|
||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
};
|
||||
|
||||
export { DatabaseLogger, setLogger };
|
||||
|
||||
@@ -16,14 +16,27 @@ 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 { MMKVLoader } from "react-native-mmkv-storage";
|
||||
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { KV } from "./storage";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
const initializeLogger = async () => {
|
||||
await initialize({
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
return new RNSqliteDriver({ async: true, dbName: name });
|
||||
},
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}),
|
||||
tempStore: "memory",
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
|
||||
});
|
||||
setLogger();
|
||||
};
|
||||
|
||||
initialize(new KV(LoggerStorage));
|
||||
|
||||
export { LoggerStorage };
|
||||
export { initializeLogger };
|
||||
|
||||
@@ -34,11 +34,13 @@ import {
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { NotesnookModule } from "../../utils/notesnook-module";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
@@ -148,6 +150,12 @@ const AppLockedOverlay = () => {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -188,11 +196,7 @@ const AppLockedOverlay = () => {
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
SettingsService.getProperty("appLockEnabled")
|
||||
) {
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
@@ -209,6 +213,7 @@ const AppLockedOverlay = () => {
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -112,6 +112,10 @@ export const SessionExpired = () => {
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
|
||||
const key = await db.user.getEncryptionKey();
|
||||
if (!key) throw new Error("No encryption key found.");
|
||||
|
||||
Sync.run("global", false, "full", async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
@@ -127,7 +131,6 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
|
||||
@@ -44,6 +44,7 @@ type DialogInfo = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -174,6 +174,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
clearAppLockVerificationCipher,
|
||||
setAppLockVerificationCipher,
|
||||
@@ -44,6 +45,7 @@ import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
@@ -66,6 +68,7 @@ export const AppLockPassword = () => {
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -73,6 +76,7 @@ export const AppLockPassword = () => {
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
@@ -126,7 +130,9 @@ export const AppLockPassword = () => {
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
@@ -169,33 +175,39 @@ export const AppLockPassword = () => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
accountPass ? null : (
|
||||
<IconButton
|
||||
name={keyboardType === "password" ? "numeric" : "keyboard"}
|
||||
onPress={() => {
|
||||
setKeyboardType(
|
||||
keyboardType === "password" ? "pin" : "password"
|
||||
);
|
||||
setSecureTextEntry(false);
|
||||
setImmediate(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 5
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
mode === "change"
|
||||
accountPass
|
||||
? "Account password"
|
||||
: mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
}
|
||||
@@ -222,6 +234,36 @@ export const AppLockPassword = () => {
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{mode === "remove" ? (
|
||||
<>
|
||||
<Button
|
||||
icon={
|
||||
accountPass ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
onPress={() => {
|
||||
setSecureTextEntry(false);
|
||||
setAccountPass(!accountPass);
|
||||
setTimeout(() => {
|
||||
setSecureTextEntry(true);
|
||||
});
|
||||
}}
|
||||
iconSize={SIZE.lg}
|
||||
type="plain"
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title="Use account password instead"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
height: 30
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
@@ -249,7 +291,8 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
@@ -277,7 +320,6 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
@@ -293,8 +335,10 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
await setAppLockVerificationCipher(password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
@@ -305,14 +349,18 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -342,7 +390,9 @@ export const AppLockPassword = () => {
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle="Save"
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
}
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
|
||||
@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
ToastManager.show({
|
||||
heading: "Note copied",
|
||||
type: "success",
|
||||
@@ -602,7 +602,7 @@ export class VaultDialog extends Component {
|
||||
await Share.open({
|
||||
heading: "Share note",
|
||||
failOnCancel: false,
|
||||
message: await convertNoteToText(note)
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -21,10 +21,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
@@ -34,7 +34,7 @@ import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import { diff } from "diffblazer";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,12 +121,21 @@ export default function NotePreview({ session, content, note }) {
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
try {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
const currentContent = await db.content.get(_note.contentId);
|
||||
loadContent({
|
||||
data: diff(currentContent.data, content.data),
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e,
|
||||
"Failed to load history preview",
|
||||
"local"
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -86,16 +86,16 @@ const Sort = ({ type, screen }) => {
|
||||
<Button
|
||||
title={
|
||||
groupOptions?.sortDirection === "asc"
|
||||
? groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
? groupOptions?.groupBy === "abc" ||
|
||||
groupOptions?.sortBy === "title"
|
||||
? "A - Z"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
: groupOptions?.sortBy === "dueDate"
|
||||
? "Earliest first"
|
||||
: "Old - New"
|
||||
: groupOptions.groupBy === "abc" ||
|
||||
groupOptions.sortBy === "title"
|
||||
: groupOptions?.groupBy === "abc" ||
|
||||
groupOptions?.sortBy === "title"
|
||||
? "Z - A"
|
||||
: groupOptions.sortBy === "dueDate"
|
||||
: groupOptions?.sortBy === "dueDate"
|
||||
? "Latest first"
|
||||
: "New - Old"
|
||||
}
|
||||
@@ -134,7 +134,7 @@ const Sort = ({ type, screen }) => {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{groupOptions.groupBy === "abc" ? (
|
||||
{groupOptions?.groupBy === "abc" ? (
|
||||
<Button
|
||||
type="secondary"
|
||||
title="Title"
|
||||
@@ -154,18 +154,18 @@ const Sort = ({ type, screen }) => {
|
||||
item === "dateEdited") ? null : (
|
||||
<Button
|
||||
key={item}
|
||||
type={groupOptions.sortBy === item ? "selected" : "plain"}
|
||||
type={groupOptions?.sortBy === item ? "selected" : "plain"}
|
||||
title={SORT[item]}
|
||||
height={40}
|
||||
iconPosition="left"
|
||||
icon={groupOptions.sortBy === item ? "check" : null}
|
||||
icon={groupOptions?.sortBy === item ? "check" : null}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.sortBy === item
|
||||
groupOptions?.sortBy === item
|
||||
? colors.primary.accent
|
||||
: colors.secondary.paragraph
|
||||
}}
|
||||
@@ -213,11 +213,11 @@ const Sort = ({ type, screen }) => {
|
||||
key={item}
|
||||
testID={"btn-" + item}
|
||||
type={
|
||||
groupOptions.groupBy === GROUP[item] ? "selected" : "plain"
|
||||
groupOptions?.groupBy === GROUP[item] ? "selected" : "plain"
|
||||
}
|
||||
buttonType={{
|
||||
text:
|
||||
groupOptions.groupBy === GROUP[item]
|
||||
groupOptions?.groupBy === GROUP[item]
|
||||
? colors.primary.accent
|
||||
: colors.secondary.paragraph
|
||||
}}
|
||||
@@ -235,7 +235,7 @@ const Sort = ({ type, screen }) => {
|
||||
updateGroupOptions(_groupOptions);
|
||||
}}
|
||||
height={40}
|
||||
icon={groupOptions.groupBy === GROUP[item] ? "check" : null}
|
||||
icon={groupOptions?.groupBy === GROUP[item] ? "check" : null}
|
||||
title={
|
||||
item.slice(0, 1).toUpperCase() + item.slice(1, item.length)
|
||||
}
|
||||
|
||||
@@ -600,20 +600,11 @@ export const useActions = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const text = await convertNoteToText(item as Note, false);
|
||||
if (!text) {
|
||||
ToastManager.error(
|
||||
new Error(Errors.export("text")),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const html = text.replace(/\n/g, "<br />");
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
const html = (text || "").replace(/\n/g, "<br />");
|
||||
await Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: (item as Note).headline || text,
|
||||
message: (item as Note).headline || text || "",
|
||||
subtitle: "",
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
@@ -684,15 +675,11 @@ export const useActions = ({
|
||||
} else {
|
||||
processingId.current = "shareNote";
|
||||
const convertedText = await convertNoteToText(item);
|
||||
if (!convertedText) {
|
||||
ToastManager.error(new Error(Errors.export("text")));
|
||||
return;
|
||||
}
|
||||
processingId.current = undefined;
|
||||
Share.open({
|
||||
title: "Share note to",
|
||||
failOnCancel: false,
|
||||
message: convertedText
|
||||
message: convertedText || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -772,11 +759,7 @@ export const useActions = ({
|
||||
} else {
|
||||
processingId.current = "copyContent";
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
if (!text) {
|
||||
ToastManager.error(new Error(Errors.export("text")));
|
||||
return;
|
||||
}
|
||||
Clipboard.setString(text);
|
||||
Clipboard.setString(text || "");
|
||||
processingId.current = undefined;
|
||||
ToastManager.show({
|
||||
heading: "Note copied to clipboard",
|
||||
|
||||
@@ -22,20 +22,14 @@ import { Platform } from "react-native";
|
||||
import { Subscription } from "react-native-iap";
|
||||
import PremiumService from "../services/premium";
|
||||
import { db } from "../common/database";
|
||||
import { Product } from "@notesnook/core/dist/api/pricing";
|
||||
|
||||
type PurchaseInfo = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku: string;
|
||||
discount: number;
|
||||
};
|
||||
|
||||
const skuInfos: { [name: string]: PurchaseInfo | undefined } = {};
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
|
||||
export const usePricing = (period: "monthly" | "yearly") => {
|
||||
const [current, setCurrent] = useState<{
|
||||
period: string;
|
||||
info?: PurchaseInfo;
|
||||
info?: Product;
|
||||
product?: Subscription;
|
||||
}>();
|
||||
|
||||
@@ -54,6 +48,7 @@ export const usePricing = (period: "monthly" | "yearly") => {
|
||||
period
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await PremiumService.getProducts()) as Subscription[];
|
||||
let product = products.find((p) => p.productId === skuInfo?.sku);
|
||||
if (!product)
|
||||
|
||||
@@ -75,14 +75,16 @@ export function ReadonlyEditor(props: {
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
const attachment = (editorMessage.value as any).attachment as Attachment;
|
||||
|
||||
@@ -185,7 +185,8 @@ const camera = async (options: PickerOptions) => {
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
maxFiles: 10,
|
||||
writeTempFile: true
|
||||
writeTempFile: true,
|
||||
compressImageQuality: 1
|
||||
})
|
||||
.then((response) => {
|
||||
handleImageResponse(
|
||||
@@ -215,7 +216,8 @@ const gallery = async (options: PickerOptions) => {
|
||||
mediaType: "photo",
|
||||
maxFiles: 10,
|
||||
cropping: false,
|
||||
multiple: true
|
||||
multiple: true,
|
||||
compressImageQuality: 1
|
||||
})
|
||||
.then((response) =>
|
||||
handleImageResponse(
|
||||
@@ -266,7 +268,6 @@ const handleImageResponse = async (
|
||||
response: Image[],
|
||||
options: PickerOptions
|
||||
) => {
|
||||
console.log(response, "result-file-picker");
|
||||
const result = await AttachImage.present(response, options.context);
|
||||
if (!result) return;
|
||||
const compress = result.compress;
|
||||
@@ -302,10 +303,15 @@ const handleImageResponse = async (
|
||||
type: "url"
|
||||
});
|
||||
|
||||
const fileName = image.sourceURL
|
||||
let fileName = image.sourceURL
|
||||
? basename(image.sourceURL)
|
||||
: image.filename || "image";
|
||||
|
||||
fileName =
|
||||
image.mime === "image/jpeg"
|
||||
? fileName.replace(/HEIC|HEIF/, "jpeg")
|
||||
: fileName;
|
||||
|
||||
console.log("attaching image...", fileName);
|
||||
|
||||
console.log("attaching file...");
|
||||
|
||||
@@ -351,9 +351,18 @@ export const useEditor = (
|
||||
if (!unlocked)
|
||||
throw new Error("Could not save note, vault is locked");
|
||||
}
|
||||
noteData.contentId = note?.contentId;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await db.vault?.save(noteData as any);
|
||||
if (typeof noteData.title === "string") {
|
||||
await db.notes.add({
|
||||
title: noteData.title,
|
||||
id: noteData.id
|
||||
});
|
||||
}
|
||||
|
||||
noteData.contentId = note?.contentId;
|
||||
if (data) {
|
||||
await db.vault?.save(noteData as any);
|
||||
}
|
||||
clearTimeout(saveTimer);
|
||||
}
|
||||
|
||||
@@ -696,14 +705,13 @@ export const useEditor = (
|
||||
);
|
||||
}
|
||||
|
||||
console.log("readonly state changed...", note.readonly);
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
readonly: note.readonly
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "tiptap" && note && !isLocal) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateEdited) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -728,7 +736,7 @@ export const useEditor = (
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContents.current?.data) {
|
||||
if (_nextContent === currentContents.current[note.id]?.data) {
|
||||
return;
|
||||
}
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
|
||||
@@ -122,7 +122,8 @@ export default function DebugLogs() {
|
||||
colors.primary.paragraph,
|
||||
colors.error.paragraph,
|
||||
colors.static.black,
|
||||
colors.static.orange
|
||||
colors.static.orange,
|
||||
colors.primary.border
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -90,13 +90,26 @@ export async function verifyUserWithApplock() {
|
||||
positiveText: "Verify",
|
||||
secureTextEntry: true,
|
||||
negativeText: "Cancel",
|
||||
keyboardType: keyboardType,
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
const verified = await validateAppLockPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(verified);
|
||||
} catch (e) {
|
||||
resolve(false);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -871,6 +871,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "switch",
|
||||
property: "appLockEnabled",
|
||||
onChange: () => {
|
||||
SettingsService.set({
|
||||
privacyScreen: true
|
||||
});
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
},
|
||||
onVerify: async () => {
|
||||
@@ -973,7 +976,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
SettingsService.getProperty("applockKeyboardType") === "numeric"
|
||||
? "pin"
|
||||
: "password"
|
||||
}, app lock will fallback to using account password to unlock the app`,
|
||||
}, app lock will be disabled if no other security method is enabled.`,
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
|
||||
@@ -59,14 +59,14 @@ const recoveryKeyMessage = {
|
||||
() => {
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
},
|
||||
true,
|
||||
false,
|
||||
async () => {
|
||||
SettingsService.set({
|
||||
recoveryKeySaved: true
|
||||
});
|
||||
clearMessage();
|
||||
},
|
||||
"I have saved my key already"
|
||||
"Cancel"
|
||||
);
|
||||
},
|
||||
data: {},
|
||||
|
||||
@@ -952,13 +952,13 @@ async function pinNote(id: string) {
|
||||
const note = await db.notes.note(id as string);
|
||||
if (!note) return;
|
||||
|
||||
let text = await convertNoteToText(note, false);
|
||||
let text = await convertNoteToText(note, true);
|
||||
if (!text) text = "";
|
||||
const html = text.replace(/\n/g, "<br />");
|
||||
Notifications.displayNotification({
|
||||
title: note.title,
|
||||
message: note.headline || text,
|
||||
subtitle: note.headline || text,
|
||||
subtitle: "",
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
actions: ["UNPIN"],
|
||||
|
||||
@@ -76,6 +76,17 @@ function migrateAppLock() {
|
||||
DatabaseLogger.debug("App lock Migrated");
|
||||
}
|
||||
|
||||
function migrateSettings(settings: SettingStore["settings"]) {
|
||||
const version = settings.settingsVersion;
|
||||
if (!version) {
|
||||
settings.settingsVersion = 1;
|
||||
settings.privacyScreen = settings.appLockEnabled
|
||||
? true
|
||||
: settings.privacyScreen;
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
scale.fontScale = 1;
|
||||
const settingsJson = MMKV.getString("appSettings");
|
||||
@@ -83,14 +94,17 @@ function init() {
|
||||
if (!settingsJson) {
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
} else {
|
||||
const settingsParsed = JSON.parse(settingsJson);
|
||||
migrateSettings(settingsParsed);
|
||||
settings = {
|
||||
...settings,
|
||||
...JSON.parse(settingsJson)
|
||||
...settingsParsed
|
||||
};
|
||||
}
|
||||
if (settings.fontScale) {
|
||||
scale.fontScale = settings.fontScale;
|
||||
}
|
||||
|
||||
setTimeout(() => setPrivacyScreen(settings), 1);
|
||||
updateSize();
|
||||
useSettingStore.getState().setSettings({ ...settings });
|
||||
@@ -98,7 +112,7 @@ function init() {
|
||||
}
|
||||
|
||||
function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
if (settings.privacyScreen || settings.appLockEnabled) {
|
||||
if (settings.privacyScreen) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
|
||||
@@ -81,6 +81,7 @@ export type Settings = {
|
||||
biometricsAuthEnabled?: boolean;
|
||||
backgroundSync?: boolean;
|
||||
applockKeyboardType: "numeric" | "default";
|
||||
settingsVersion?: number;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -169,7 +170,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
markdownShortcuts: true,
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false,
|
||||
backgroundSync: true
|
||||
backgroundSync: true,
|
||||
settingsVersion: 0
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IOS_APPGROUPID = "group.org.streetwriters.notesnook";
|
||||
export const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
export const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
|
||||
export const BETA = true;
|
||||
export const BETA = false;
|
||||
|
||||
export const STORE_LINK =
|
||||
Platform.OS === "ios"
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3011
|
||||
versionCode 3016
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -37,4 +37,10 @@ allprojects {
|
||||
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
- Added push/pull changes to troubleshoot sync issues in settings
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1120,7 +1120,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1365,7 +1365,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1396,7 +1396,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1439,7 +1439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1544,7 +1544,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2100;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.3;
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -959,4 +959,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
4
apps/mobile/package-lock.json
generated
4
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.2",
|
||||
"version": "3.0.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.8",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -10,3 +10,19 @@ index 5de0845..1b158d8 100644
|
||||
|
||||
if (includeBase64) {
|
||||
image.putString("data", getBase64StringFromFile(compressedImagePath));
|
||||
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
index 9f20973..5e14da8 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
|
||||
@@ -595,8 +595,10 @@ - (void)qb_imagePickerController:
|
||||
NSString *mimeType = [self determineMimeTypeFromImageData:imageData];
|
||||
Boolean isKnownMimeType = [mimeType length] > 0;
|
||||
|
||||
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
|
||||
+
|
||||
ImageResult *imageResult = [[ImageResult alloc] init];
|
||||
- if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg) {
|
||||
+ if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg && !isHeicOrHeif) {
|
||||
// Use original, unmodified image
|
||||
imageResult.data = imageData;
|
||||
imageResult.width = @(imgT.size.width);
|
||||
|
||||
@@ -311,8 +311,7 @@ const ShareView = () => {
|
||||
type: "tiptap"
|
||||
},
|
||||
id: note.id,
|
||||
sessionId: Date.now(),
|
||||
title: noteTitle.current
|
||||
sessionId: Date.now()
|
||||
};
|
||||
} else {
|
||||
noteData = { ...note };
|
||||
@@ -487,25 +486,42 @@ const ShareView = () => {
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current.focus();
|
||||
}}
|
||||
/>
|
||||
{appendNoteId ? (
|
||||
<Heading
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
>
|
||||
Save note
|
||||
</Heading>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontSize: SIZE.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
defaultValue={noteTitle.current}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
title="Done"
|
||||
type="accent"
|
||||
|
||||
33
apps/theme-builder/package-lock.json
generated
33
apps/theme-builder/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -18,17 +18,18 @@
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.38.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "^0.16.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/react": "^18.2.17",
|
||||
"@types/react": "^18.2.39",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -978,7 +979,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.6",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -2026,6 +2027,15 @@
|
||||
"@trpc/server": "10.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.38.3",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.38.3.tgz",
|
||||
"integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.1",
|
||||
"dev": true,
|
||||
@@ -2098,25 +2108,30 @@
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.5",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
|
||||
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A=="
|
||||
},
|
||||
"node_modules/@types/styled-system": {
|
||||
"version": "5.1.16",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/theme-builder",
|
||||
"description": "Your private note taking space",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -17,18 +17,18 @@
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
"@trpc/client": "^10.38.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"clipboard-polyfill": "^4.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "^0.16.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/react": "^18.2.39",
|
||||
"@types/react": "^18.2.17",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
@@ -2,19 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAHUUExURUxpcf////////////////////////////////////////////////////////////////////////////////////39/f39/f////////////////////////////////z8/O3s7P7+/vTz8wAAAPz7++/v7/Py8u/u7vDv7+7t7dfX1/79/ezr66Kiovv6+vf29vX09OTj4w0NDfv7++Ti4vr6+vT09Ovq6vn5+f39/fb19dnX1/b29tvb29/f3xMTE/j4+Pn4+O7s7MnJyerq6uno6M/Pz8jIyN3d3dnZ2d7e3kZGRvj39/38/Pf39+zs7PDw8OPi4ubl5cvLy+rp6czMzPLy8vPz887Ozujo6MrKyuDf3+fn59PT0+bm5uXl5c3NzfX19evr6wsLC+/t7ePj4/Ty8u3r6/Lx8eXk5OPh4dDQ0Nza2tjX19vZ2fHw8Nva2tLR0djW1tbW1tra2tLS0uHh4evp6eLi4tXV1djY2L+/vysrK7q6utHR0aGhoa6urrm5uQICAigoKCEhIZCQkFxcXBgYGFRUVBISEm1tbQ4ODklJSbu7uyAgID09PZqamkVFRXh4eLGxsTg4OOTk5FFRUQEBAQoKCnx8fBcXF7/+rP8AAAAfdFJOUwBOqO2h6KT9Mtyr5+w3Str+6TTZOjXb31M24O8BOd0PapSsAAABxElEQVQ4y2NgYOBgYWWXxwLYBdmEGICAjx9Z1NJJVQfB42QE6hdG0RaoaaCqhKSCl4EF1VxNVAXyTAzcaArUUBXwMLBjKDBBViDAII+uwBZFgTy6AjU1awIKNKz1aa0g1VBfB58CDS08CuIb40AKzLAqsJKXN2tXacChwKS+rTtB3tJHRU9ewxmbFZ6tKiqTrSAKdIO1LDEUaPeoqMzsUwAp0NIt8Uj0ijJXQFVgnDJVZcK0XrACXcXY0sjoHM8AfyUkBbOn9KvMmgRSYFEW4hUVqJagHeaShqTAwX7ijJQusAkW5VXRHu6eruFqaigKkqarqAAVKFbYxWiYq1XGedijuMHBWz6xo7m2Rl7RwtvdJaw6NjzCFl2BVVNdshJQgbqBsqtdpIu7nasjXIFXS2cyRL2CW6i6JTCgFCLitUPs4QrM7cDmKZi7FRYVm6o5QeLbDGICLNEqJAVk59rk2djY5Me4FThawQOKFSzrrxmaZWpqBAamQUGmwRaakNTNzMAm7+ekrKmlqOjsrIgAGcAA1ci0NVPiYhAV8zVURgbp6nBg7isuwsDAKKfkp4AEVOHASklKApS9JZmY5bECaS5ZGQYA3vqpy6NoYh0AAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="
|
||||
/>
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<!-- <link rel="manifest" href="/site.webmanifest" /> -->
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#01c352" />
|
||||
<meta name="msapplication-TileColor" content="#01c352" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<meta name="theme-color" content="#01c352" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
@@ -49,13 +40,26 @@
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<title>Notesnook Theme Builder</title>
|
||||
<style id="theme-colors">
|
||||
#splash {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
import { themeToCSS } from "@notesnook/theme";
|
||||
|
||||
<script nonce="7WIq8hRwApoXhctoGZZthMLYQLRNiprTwcPi6Azdf">
|
||||
const colorScheme = JSON.parse(
|
||||
window.localStorage.getItem("colorScheme") || '"light"'
|
||||
);
|
||||
const root = document.querySelector("html");
|
||||
if (root) root.setAttribute("data-theme", colorScheme);
|
||||
|
||||
const theme = window.localStorage.getItem(`theme:${colorScheme}`);
|
||||
if (theme) {
|
||||
const css = themeToCSS(JSON.parse(theme));
|
||||
const stylesheet = document.getElementById("theme-colors");
|
||||
if (stylesheet) stylesheet.innerHTML = css;
|
||||
}
|
||||
</script>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
<style>
|
||||
@@ -63,21 +67,6 @@
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
--bg: #0f0f0f;
|
||||
--fg: #fff;
|
||||
--three-bars-bg: #494949;
|
||||
--gradient-stop-color: #111111;
|
||||
--n-letter-color: #e1e1e1;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg: #fff;
|
||||
--three-bars-bg: #bebebe;
|
||||
--gradient-stop-color: #fff9f9;
|
||||
--n-letter-color: #000;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .react-loading-skeleton {
|
||||
--base-color: var(--background-secondary);
|
||||
--highlight-color: #var(--background-secondary);
|
||||
@@ -88,6 +77,19 @@
|
||||
--highlight-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
#splash {
|
||||
background-color: var(--background);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#splash svg {
|
||||
transform: scale(1);
|
||||
animation: pulse 2s infinite;
|
||||
@@ -95,7 +97,7 @@
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
70% {
|
||||
@@ -103,7 +105,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,20 +130,9 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
0% {
|
||||
transform: translateY(500px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateY(0px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* svg {
|
||||
@@ -171,85 +162,40 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="theme-scope-base">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/highlightjs@9.16.2/highlight.pack.min.js"></script>
|
||||
-->
|
||||
<div id="root"></div>
|
||||
<div
|
||||
id="splash"
|
||||
style="
|
||||
background-color: var(--bg);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
"
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 0; width: 0; z-index: -1; position: absolute"
|
||||
>
|
||||
<svg viewBox="0 0 339 339" style="width: 150px">
|
||||
<defs />
|
||||
<defs>
|
||||
<linearGradient
|
||||
xlink:href="#a"
|
||||
id="b"
|
||||
x1="188.61227"
|
||||
x2="193.54405"
|
||||
y1="165.2058"
|
||||
y2="216.81519"
|
||||
gradientTransform="rotate(5 4448 -4204) scale(2.93671)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
/>
|
||||
|
||||
<linearGradient id="a">
|
||||
<stop offset="0" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="var(--gradient-stop-color)"
|
||||
stop-opacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1="167.8"
|
||||
x2="270.6"
|
||||
y1="76.9"
|
||||
y2="64.2"
|
||||
gradientTransform="rotate(5 465 -2050) scale(1.50082)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
xlink:href="#a"
|
||||
/>
|
||||
</defs>
|
||||
<g transform="translate(0 42)">
|
||||
<path fill="url(#b)" d="M160 205l154 42-141 44-155-42z" />
|
||||
<path fill="url(#c)" d="M160-35v240l154 42 1-253z" />
|
||||
<symbol id="full-logo" viewBox="0 0 1000 1000">
|
||||
<rect
|
||||
width="1000"
|
||||
height="1000"
|
||||
rx="30%"
|
||||
ry="30%"
|
||||
style="fill: var(--paragraph)"
|
||||
/>
|
||||
<g transform="translate(275,176.95254)">
|
||||
<path
|
||||
fill="none"
|
||||
stroke-width="1.2"
|
||||
d="M160 205V-35m0 240L18 249m142-44l154 41"
|
||||
/>
|
||||
<path
|
||||
fill="var(--n-letter-color)"
|
||||
d="M84 109l35 54V98l21-7v91l-27 9-35-54v65l-21 6v-91z"
|
||||
/>
|
||||
<rect
|
||||
width="86.1"
|
||||
height="12.6"
|
||||
x="185"
|
||||
y="97"
|
||||
fill="var(--three-bars-bg)"
|
||||
ry="2.3"
|
||||
transform="skewY(15) scale(.9669 1)"
|
||||
/>
|
||||
<path
|
||||
fill="var(--three-bars-bg)"
|
||||
d="M181 169l99 26 2 3v8c0 1-1 2-2 1l-99-26-2-3v-7c0-2 1-2 2-2zm0-47l99 27 2 2v8l-2 2-99-27c-1 0-2-1-2-3v-7l2-2z"
|
||||
style="fill: var(--background)"
|
||||
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
|
||||
/>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="themed-logo" viewBox="0 0 450 646.09491">
|
||||
<path
|
||||
style="fill: var(--paragraph)"
|
||||
d="M437.985 493.919c-17.255 50.411-51.835 93.065-97.588 120.372-45.752 27.307-99.71 37.496-152.269 28.753-52.56-8.743-100.313999-35.85-134.763-76.498C18.916 525.898.006 474.347 0 421.064v-110.25l79.121001 33.053v77.152c-.007 20.779 4.434 41.318 13.024 60.237C100.735 500.176 113.276 517.037 128.925 530.707c2.97 2.587 6.053 5.107 9.236 7.459 23.357 17.388 51.402 27.352 80.494 28.597.99 0 1.946.079 2.925.101.979.023 2.25 0 3.375 0h3.375c1.125 0 1.935 0 2.925-.101 29.081-1.248 57.116-11.203 80.471-28.575 3.173-2.351 6.255-4.86 9.237-7.447 20.556-17.98 35.656-41.366 43.582-67.5zM450 225v196.065c0 2.531 0 5.074-.158 7.605l-78.963-33.019V225c-.013-37.684-14.607-73.901-40.725-101.066-26.119-27.164003-61.735-43.168003-99.389-44.660003-37.655-1.492-74.426 11.644-102.611 36.657003-28.185999 25.013-45.599999 59.962-48.593999 97.526-.281 3.803-.439 7.662-.439 11.543v48.712L0 240.637V0h225c59.674 0 116.903 23.705 159.099 65.901C426.295 108.097 450 165.326 450 225Z"
|
||||
/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<div id="splash" class="hidden">
|
||||
<svg style="height: 120px">
|
||||
<use href="#themed-logo" />
|
||||
</svg>
|
||||
</div>
|
||||
<div id="dialogContainer"></div>
|
||||
|
||||
@@ -18,31 +18,59 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { init } from "@notesnook/web/src/bootstrap";
|
||||
import { render } from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
ErrorBoundary,
|
||||
ErrorComponent
|
||||
} from "@notesnook/web/src/components/error-boundary";
|
||||
import { BaseThemeProvider } from "@notesnook/web/src/components/theme-provider";
|
||||
import { App } from "./app";
|
||||
|
||||
renderApp();
|
||||
|
||||
async function renderApp() {
|
||||
const { component, props } = await init();
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
try {
|
||||
const { component, props } = await init();
|
||||
|
||||
const { default: Component } = await component();
|
||||
render(
|
||||
<BaseThemeProvider
|
||||
sx={{
|
||||
display: "flex",
|
||||
"#app": { flex: 1, height: "unset" },
|
||||
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Component route={props?.route || "login:email"} />
|
||||
<App />
|
||||
</BaseThemeProvider>,
|
||||
document.getElementById("root"),
|
||||
() => {
|
||||
document.getElementById("splash")?.remove();
|
||||
}
|
||||
);
|
||||
const { useKeyStore } = await import(
|
||||
"@notesnook/web/src/interfaces/key-store"
|
||||
);
|
||||
await useKeyStore.getState().init();
|
||||
|
||||
const { default: Component } = await component();
|
||||
const { default: AppLock } = await import(
|
||||
"@notesnook/web/src/views/app-lock"
|
||||
);
|
||||
|
||||
root.render(
|
||||
<ErrorBoundary>
|
||||
<BaseThemeProvider
|
||||
onRender={() => document.getElementById("splash")?.remove()}
|
||||
sx={{
|
||||
display: "flex",
|
||||
"#app": { flex: 1, height: "unset" },
|
||||
"& > :first-child:not(#menu-wrapper)": { flex: 1 },
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<AppLock>
|
||||
<Component route={props?.route || "login:email"} />
|
||||
</AppLock>
|
||||
<App />
|
||||
</BaseThemeProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} catch (e) {
|
||||
root.render(
|
||||
<>
|
||||
<ErrorComponent
|
||||
error={e}
|
||||
resetErrorBoundary={() => window.location.reload()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -30,4 +30,5 @@ dist
|
||||
public/workbox
|
||||
scripts/secrets
|
||||
test-results
|
||||
.swc
|
||||
.swc
|
||||
public/models
|
||||
@@ -259,14 +259,15 @@ test("unlock a note for editing", async ({ page }) => {
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForTimeout(500);
|
||||
await notes.waitForList();
|
||||
|
||||
const newContent = `${NOTE.content}${content}`;
|
||||
const editedNote = await notes.findNote({
|
||||
title: NOTE.title,
|
||||
content: newContent
|
||||
});
|
||||
await editedNote?.openLockedNote(PASSWORD);
|
||||
if (!editedNote) throw new Error("Could not find note.");
|
||||
await editedNote.openLockedNote(PASSWORD);
|
||||
await notes.editor.waitForLoading();
|
||||
expect(await notes.editor.getContent("text")).toContain(newContent);
|
||||
});
|
||||
|
||||
128
apps/web/__tests__/__benches__/ai.bench.ts
Normal file
128
apps/web/__tests__/__benches__/ai.bench.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
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 { describe, bench } from "vitest";
|
||||
import { pipeline } from "@xenova/transformers";
|
||||
import path from "path";
|
||||
|
||||
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
|
||||
Migrating to SQLite
|
||||
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
|
||||
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
|
||||
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
|
||||
The clients should perform predictably regardless of how much data you have.
|
||||
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
|
||||
With v3 we have acheived both of these goals.
|
||||
Fixing the Sync
|
||||
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
|
||||
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
|
||||
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
|
||||
Building everything on top of timestamps meant resumability during sync could never be stable.
|
||||
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
|
||||
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
|
||||
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
|
||||
Features, Features
|
||||
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
|
||||
Note linking
|
||||
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
|
||||
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
|
||||
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
|
||||
Tabs
|
||||
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
|
||||
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
|
||||
Nested notebooks
|
||||
|
||||
At rest encryption
|
||||
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
|
||||
App lock
|
||||
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
|
||||
At Notesnook, we take your security & privacy very seriously.
|
||||
Export attachments with notes
|
||||
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
|
||||
User profile
|
||||
Custom colors for note organization
|
||||
Fixed colors were nice but not nicer than custom colors.
|
||||
Customizable side bar
|
||||
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
|
||||
Callouts
|
||||
|
||||
Beta release channels
|
||||
|
||||
|
||||
Nested notebooks/topics
|
||||
Note linking
|
||||
Tabs
|
||||
Table of Contents
|
||||
Customizable/sortable sidebar
|
||||
User profile
|
||||
Custom colors
|
||||
Yearly reminders
|
||||
Callouts in editor
|
||||
Migration to SQLite
|
||||
New sync system
|
||||
At rest encryption
|
||||
app lock
|
||||
export attachments with notes
|
||||
.ccccccccccxccccccc`;
|
||||
|
||||
describe("benchmarking embeddings", async () => {
|
||||
const models = [
|
||||
["Xenova/jina-embeddings-v2-small-en", 87],
|
||||
["Snowflake/snowflake-arctic-embed-s", 41],
|
||||
["Snowflake/snowflake-arctic-embed-m", 20],
|
||||
["Snowflake/snowflake-arctic-embed-xs", 62],
|
||||
// "Alibaba-NLP/gte-base-en-v1.5",
|
||||
// "andersonbcdefg/bge-small-4096"
|
||||
["Xenova/GIST-small-Embedding-v0", 57],
|
||||
["Xenova/GIST-all-MiniLM-L6-v2", 88],
|
||||
// "Xenova/all-MiniLM-L12-v2" // 96
|
||||
["Xenova/NoInstruct-small-Embedding-v0", 40],
|
||||
["TaylorAI/bge-micro-v2", 100],
|
||||
// "Snowflake/snowflake-arctic-embed-m-long" // 19
|
||||
// "Xenova/e5-small-v2" // 67
|
||||
// "nomic-ai/nomic-embed-text-v1.5",
|
||||
// "Xenova/bert-base-uncased",
|
||||
["TaylorAI/gte-tiny", 88],
|
||||
["Xenova/gte-small", 63]
|
||||
];
|
||||
for (const provider of ["cpu"]) {
|
||||
for (const [model, rank] of models) {
|
||||
const extractor = await pipeline("feature-extraction", model, {
|
||||
// progress_callback: console.log,
|
||||
device: provider,
|
||||
dtype: "q8",
|
||||
cache_dir: path.join(__dirname, "..", "..", "public", "models")
|
||||
});
|
||||
console.log(model, extractor.tokenizer.model_max_length);
|
||||
extractor.tokenizer.model_max_length = 512;
|
||||
bench(`${rank}. ${model} (${provider})`, async () => {
|
||||
const output = await extractor(text, {
|
||||
pooling: extractor.tokenizer._tokenizer_config.cls_token
|
||||
? "cls"
|
||||
: "mean"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// console.time("extraction");
|
||||
// const output = await extractor(text);
|
||||
// console.timeEnd("extraction");
|
||||
// console.log(output.data.length);
|
||||
2314
apps/web/package-lock.json
generated
2314
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.6",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -38,6 +38,7 @@
|
||||
"@theme-ui/core": "^0.16.1",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/react-query": "10.38.3",
|
||||
"@xenova/transformers": "github:xenova/transformers.js#v3",
|
||||
"@zip.js/zip.js": "^2.7.32",
|
||||
"async-mutex": "^0.4.0",
|
||||
"axios": "^1.3.4",
|
||||
@@ -60,6 +61,7 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"mac-scrollbar": "^0.13.5",
|
||||
"marked": "^4.1.0",
|
||||
"onnxruntime-node": "^1.18.0",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
@@ -130,6 +132,9 @@
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"onnxruntime-node": "^1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env PLATFORM=web vite",
|
||||
"start:desktop": "cross-env PLATFORM=desktop vite",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createDialect } from "./sqlite";
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { generatePassword } from "../utils/password-generator";
|
||||
import { deriveKey } from "../interfaces/key-store";
|
||||
import { logManager } from "@notesnook/core/dist/logger";
|
||||
|
||||
const db = database;
|
||||
async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
@@ -58,7 +59,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
database.setup({
|
||||
sqliteOptions: {
|
||||
dialect: createDialect,
|
||||
dialect: (name, init) => createDialect(name, true, init),
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
@@ -99,8 +100,11 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
await db.init();
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (IS_DESKTOP_APP) db.sql().destroy();
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await db.sql().destroy();
|
||||
await logManager?.close();
|
||||
}
|
||||
});
|
||||
|
||||
logger.measure("Database initialization");
|
||||
|
||||
@@ -44,11 +44,272 @@ import { User } from "@notesnook/core";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { formatDate } from "@notesnook/core/dist/utils/date";
|
||||
import {
|
||||
pipeline,
|
||||
AutoModel,
|
||||
env,
|
||||
Tensor,
|
||||
mean_pooling,
|
||||
FeatureExtractionPipeline
|
||||
} from "@xenova/transformers";
|
||||
import { toChunks } from "@notesnook/core/dist/utils/array";
|
||||
|
||||
env.allowLocalModels = true;
|
||||
console.log(env);
|
||||
const text = `I am generally not in favor of huge feature packed releases instead preferring small releases without one or two features at most. v3 is a special case in that the sheer amount of changes required to migrate from our old key value database to SQLite forced us to release all of these updates together.
|
||||
Migrating to SQLite
|
||||
Initially, as all plans go, our only goal was to migrate to SQLite as quickly and with as few changes as possible. Unfortunately, that plan died in its infancy. Our old legacy database was not designed to handle tens of thousands of notes or gigabytes of data — something we quickly realized after some users with over 100K notes approached us with significant performance issues. While it "worked", it was neither efficient nor particularly safe.
|
||||
In August 2023, we began slowly migrating the Notesnook Core to SQLite. While migrating we quickly realized we'd need TypeScript if this was ever going to work because Kysely (the underlying query builder we used) heavily relied on types being there. So from the very first day we were doing 2 things: migrating the database to SQLite, and migrating the codebase to TypeScript.
|
||||
I won't bore you with too many technical details but we had a few primary goals behind all this upheaval:
|
||||
The clients should perform predictably regardless of how much data you have.
|
||||
The clients should use as less RAM as possible to work in extremely constrained enviroments (e.g. iOS limits all Share Extensions to around 50 MB before crashing).
|
||||
With v3 we have acheived both of these goals.
|
||||
Fixing the Sync
|
||||
Sync has notoriously been one of the least stable parts of Notesnook. From the beginning, we opted for a decentralized syncing system where each client could independently sync items without depending on a central server. This had a few drawbacks:
|
||||
Since each client was independent, it was a nightmare to debug why something wasn't syncing.
|
||||
In order to be efficient, we detected changed items based on their modified timestamp, but this forced us to juggle with the various inconsistencies of time on different devices.
|
||||
Building everything on top of timestamps meant resumability during sync could never be stable.
|
||||
In short, our old sync logic heavily depended on time to be the exact same (down to the last millisecond) for things to work. As you can imagine, this didn't work out that well.
|
||||
While migrating everything to SQLite, we greatly simplified the sync logic opting for a centralized system this time. This allowed us to completely get rid of all the time juggling resolving all the edge cases in our previous logic.
|
||||
As a result, in v3 you'll notice a bug free sync that "just works" regardless of where you are, how many devices you are on, the time difference between them etc.
|
||||
Features, Features
|
||||
In every new release, it is always a struggle to decide which features to add and which features to leave behind for later. v3 was no different because ultimately each feature you add pushes the deadline further and further away. Even then, v3 is one of our biggest releases ever.
|
||||
Note linking
|
||||
The crown jewel of v3! Yes, finally, you can link 2 notes together and even link directly to a specific block inside a note.
|
||||
We have also added controls to quickly see which notes you have linked to, and which notes link to a particular note.
|
||||
To keep things simple, a note link is no different than a normal hyperlink except that clicking on it will take you to the note instead of an external page. This also means that when you export your notes, your note links are automatically converted into hyperlinks that directly open the linked Markdown or HTML files of the notes. This is something no other app does instead keeping the links in their proprietary format making them essentially useless.
|
||||
Tabs
|
||||
The only reason we added tabs was to allow quick navigation to/from a note on clicking a note link. You can, of course, use tabs without ever linking a note.
|
||||
Notesnook is one of the few note taking apps (besides Obsidian & OneNote) that has a full fledged tab experience. With v3, we are slowly upgrading Notesnook to become a power user's tool while keeping things just as simple as before.
|
||||
Nested notebooks
|
||||
|
||||
At rest encryption
|
||||
Thanks to SQLite, at rest encryption is now a thing on all platforms. The encryption key is randomly generated on database creation without requiring any user intervention. In other words, it "just works" keeping your notes secure even if your device is compromised. Which brings me to...
|
||||
App lock
|
||||
App lock is an enhancement on at rest encryption. When you enable app lock, it further encrypts your database encryption key with your app lock pin (or security key) so it's not just an "overlay". If you forget your app lock pin, the only way into the app is to reset & clear the database and start over.
|
||||
At Notesnook, we take your security & privacy very seriously.
|
||||
Export attachments with notes
|
||||
Exports got a lot of love in v3 with things like automatic downloading & linking of attachments, organization of note files based on your notebook structure, and resolution of internal note links to actual HTML/MD files.
|
||||
User profile
|
||||
Custom colors for note organization
|
||||
Fixed colors were nice but not nicer than custom colors.
|
||||
Customizable side bar
|
||||
You can now drag/drop and reorganize how things in your side bar looks including hiding things that you don't use. It can be as simple or as complex as you want.
|
||||
Callouts
|
||||
|
||||
Beta release channels
|
||||
|
||||
|
||||
Nested notebooks/topics
|
||||
Note linking
|
||||
Tabs
|
||||
Table of Contents
|
||||
Customizable/sortable sidebar
|
||||
User profile
|
||||
Custom colors
|
||||
Yearly reminders
|
||||
Callouts in editor
|
||||
Migration to SQLite
|
||||
New sync system
|
||||
At rest encryption
|
||||
app lock
|
||||
export attachments with notes.`;
|
||||
|
||||
const average = (array: number[]) =>
|
||||
array.reduce((a, b) => a + b) / array.length;
|
||||
|
||||
function createTensorWithBatchSize(opt: [number, number]) {
|
||||
const size = opt.reduce((n, i) => n * i, 1);
|
||||
return new Tensor("int64", new BigInt64Array(size).fill(BigInt(1)), opt);
|
||||
}
|
||||
|
||||
function createBatch(batchSize: number, sequenceLength: number) {
|
||||
const tensor = createTensorWithBatchSize([batchSize, sequenceLength]);
|
||||
return {
|
||||
input_ids: tensor,
|
||||
attention_mask: tensor
|
||||
};
|
||||
}
|
||||
|
||||
function chunkify(tokens: string[], chunkSize: number, overlap: number) {
|
||||
const chunks = [];
|
||||
const totalParts = Math.ceil(tokens.length / chunkSize);
|
||||
for (let i = 0; i < totalParts; ++i) {
|
||||
const start = i === 0 ? 0 : i * chunkSize - i * overlap;
|
||||
const end = start + chunkSize;
|
||||
chunks.push(tokens.slice(start, end));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Helper function for padding values of an object, which are each arrays.
|
||||
* NOTE: No additional checks are made here for validity of arguments.
|
||||
* @param {Record<string, any[]>} item The input object.
|
||||
* @param {number} length The length to pad to.
|
||||
* @param {(key: string) => any} value_fn Determine the value to fill the array, based on its key.
|
||||
* @param {string} side Which side to pad the array.
|
||||
* @private
|
||||
*/
|
||||
function padHelper(
|
||||
item: Record<string, any[]>,
|
||||
length: number,
|
||||
value_fn: (key: string) => any,
|
||||
side: string
|
||||
) {
|
||||
for (const key of Object.keys(item)) {
|
||||
const diff = length - item[key].length;
|
||||
const value = value_fn(key);
|
||||
|
||||
const padData = new Array(diff).fill(value);
|
||||
item[key] =
|
||||
side === "right"
|
||||
? [...item[key], ...padData]
|
||||
: [...padData, ...item[key]];
|
||||
}
|
||||
}
|
||||
|
||||
function tokenizeInput(
|
||||
pipeline: FeatureExtractionPipeline,
|
||||
options: { max_length: number }
|
||||
) {
|
||||
const { tokenizer } = pipeline;
|
||||
const { max_length } = options;
|
||||
|
||||
const encoded = tokenizer._encode_text(text) ?? [];
|
||||
const encodedTokens = chunkify(encoded, max_length - 12, 20).map((chunk) => {
|
||||
const { tokens, token_type_ids } = tokenizer.post_processor._call(
|
||||
chunk,
|
||||
[],
|
||||
{ add_special_tokens: true }
|
||||
);
|
||||
const input_ids = tokenizer.model.convert_tokens_to_ids(tokens);
|
||||
|
||||
return {
|
||||
input_ids,
|
||||
attention_mask: new Array(input_ids.length).fill(1),
|
||||
token_type_ids: token_type_ids || []
|
||||
};
|
||||
});
|
||||
|
||||
for (let i = 0; i < encodedTokens.length; ++i) {
|
||||
if (encodedTokens[i].input_ids.length === max_length) {
|
||||
continue;
|
||||
} else if (encodedTokens[i].input_ids.length > max_length) {
|
||||
throw new Error("Truncation is not allowed.");
|
||||
} else {
|
||||
// t.length < max_length
|
||||
// possibly pad
|
||||
padHelper(
|
||||
encodedTokens[i],
|
||||
max_length,
|
||||
(key) => (key === "input_ids" ? tokenizer.pad_token_id : 0),
|
||||
tokenizer.padding_side
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const dims = [encodedTokens.length, encodedTokens[0].input_ids.length];
|
||||
const input: Record<string, Tensor> = {};
|
||||
for (const key of Object.keys(encodedTokens[0])) {
|
||||
input[key] = new Tensor(
|
||||
"int64",
|
||||
BigInt64Array.from(encodedTokens.flatMap((x) => x[key]).map(BigInt)),
|
||||
dims
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
async function generateEmbeddings(
|
||||
pipeline: FeatureExtractionPipeline,
|
||||
input: Record<string, Tensor>
|
||||
) {
|
||||
const { tokenizer, model } = pipeline;
|
||||
|
||||
const outputs = await model._call(input);
|
||||
|
||||
let result =
|
||||
outputs.last_hidden_state ?? outputs.logits ?? outputs.token_embeddings;
|
||||
const pooling: "mean" | "cls" = tokenizer._tokenizer_config.cls_token
|
||||
? "cls"
|
||||
: "mean";
|
||||
if (pooling === "mean") {
|
||||
result = mean_pooling(result, input.attention_mask);
|
||||
} else if (pooling === "cls") {
|
||||
result = result.slice(null, 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
onClick: async () => {
|
||||
const models = [
|
||||
// ["Xenova/jina-embeddings-v2-small-en", 87]
|
||||
["Snowflake/snowflake-arctic-embed-s", 41],
|
||||
// ["Snowflake/snowflake-arctic-embed-m", 20],
|
||||
["Snowflake/snowflake-arctic-embed-xs", 62],
|
||||
// ["Xenova/GIST-small-Embedding-v0", 57],
|
||||
["Xenova/GIST-all-MiniLM-L6-v2", 88],
|
||||
// ["Xenova/NoInstruct-small-Embedding-v0", 40],
|
||||
["TaylorAI/bge-micro-v2", 100]
|
||||
// ["TaylorAI/gte-tiny", 88],
|
||||
// ["Xenova/gte-small", 63]
|
||||
] as const;
|
||||
|
||||
// env.backends.onnx.wasm.proxy = false;
|
||||
// env.backends.onnx.webgpu.profiling = {
|
||||
// mode: "default"
|
||||
// };
|
||||
// env.webgpu.profiling = {
|
||||
// mode: "default"
|
||||
// };
|
||||
console.log(env);
|
||||
for (const provider of ["webgpu"] as const) {
|
||||
if (provider === "webgpu") env.backends.onnx.wasm.proxy = false;
|
||||
else env.backends.onnx.wasm.proxy = true;
|
||||
|
||||
for (const [modelId, rank] of models) {
|
||||
console.log("loading model", modelId);
|
||||
|
||||
const extractor = await pipeline("feature-extraction", modelId, {
|
||||
// progress_callback: console.log,
|
||||
device: provider,
|
||||
dtype: "fp32",
|
||||
cache_dir: "/models",
|
||||
local_files_only: true
|
||||
// session_options: {
|
||||
// // enableGraphCapture: true
|
||||
// }
|
||||
});
|
||||
|
||||
const input = tokenizeInput(extractor, { max_length: 512 });
|
||||
|
||||
// console.time(`[warming up] ${rank}. ${modelId} (${provider})`);
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
console.time(`${rank}. ${modelId} (${provider})`);
|
||||
await generateEmbeddings(extractor, input);
|
||||
console.timeEnd(`${rank}. ${modelId} (${provider})`);
|
||||
}
|
||||
// console.timeEnd(`[warming up] ${rank}. ${modelId} (${provider})`);
|
||||
|
||||
// const timings: number[] = [];
|
||||
// for (let i = 0; i < 10; ++i) {
|
||||
// const now = performance.now();
|
||||
// await generateEmbeddings(extractor, input);
|
||||
// timings.push(performance.now() - now);
|
||||
// }
|
||||
// console.log(
|
||||
// `[bench] ${rank}. ${modelId} (${provider})`,
|
||||
// `avg: ${average(timings)}ms`,
|
||||
// `min: ${Math.min(...timings)}ms`,
|
||||
// `max: ${Math.max(...timings)}ms`
|
||||
// );
|
||||
}
|
||||
}
|
||||
} // useEditorStore.getState().newSession()
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
@@ -77,12 +338,19 @@ export async function introduceFeatures() {
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup(rescueMode = false) {
|
||||
export async function createBackup(
|
||||
options: {
|
||||
rescueMode?: boolean;
|
||||
noVerify?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { rescueMode, noVerify } = options;
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
|
||||
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
|
||||
|
||||
const verified = rescueMode || encryptBackups || (await verifyAccount());
|
||||
const verified =
|
||||
rescueMode || encryptBackups || noVerify || (await verifyAccount());
|
||||
if (!verified) {
|
||||
showToast("error", "Could not create a backup: user verification failed.");
|
||||
return false;
|
||||
|
||||
@@ -18,9 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hotkeys from "hotkeys-js";
|
||||
import { navigate } from "../navigation";
|
||||
// import { store as themestore } from "../stores/theme-store";
|
||||
import { GlobalKeyboard } from "../utils/keyboard";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { useStore as useSearchStore } from "../stores/search-store";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
|
||||
const KEYMAP = [
|
||||
// {
|
||||
@@ -57,11 +58,24 @@ const KEYMAP = [
|
||||
keys: ["command+f", "ctrl+f"],
|
||||
description: "Search all notes",
|
||||
global: false,
|
||||
action: (e) => {
|
||||
if (e.target?.classList.contains("ProseMirror")) return;
|
||||
action: (e: KeyboardEvent) => {
|
||||
const isInEditor =
|
||||
e.target instanceof HTMLElement &&
|
||||
!!e.target?.closest(".editor-container");
|
||||
if (isInEditor) {
|
||||
const activeSession = useEditorStore.getState().getActiveSession();
|
||||
if (activeSession?.type === "readonly") {
|
||||
e.preventDefault();
|
||||
const editor = useEditorManager
|
||||
.getState()
|
||||
.getEditor(activeSession.id);
|
||||
editor?.editor?.startSearch();
|
||||
}
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
navigate("/search/notes");
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
}
|
||||
}
|
||||
// {
|
||||
@@ -137,7 +151,11 @@ export function registerKeyMap() {
|
||||
KEYMAP.forEach((key) => {
|
||||
hotkeys(
|
||||
key.keys.join(","),
|
||||
{ element: key.global ? GlobalKeyboard : window },
|
||||
{
|
||||
element: key.global
|
||||
? (GlobalKeyboard as unknown as HTMLElement)
|
||||
: document.body
|
||||
},
|
||||
key.action
|
||||
);
|
||||
});
|
||||
@@ -177,7 +177,7 @@ function isIgnored(key: keyof typeof NoticesData) {
|
||||
let openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup() {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await createBackup();
|
||||
await createBackup({ noVerify: true });
|
||||
} else if (isUserPremium() && !IS_TESTING) {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
|
||||
@@ -18,39 +18,93 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
SqliteDriver as KSqliteDriver,
|
||||
SqliteDialectConfig,
|
||||
Dialect,
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
SqliteQueryCompiler,
|
||||
DatabaseConnection,
|
||||
QueryResult,
|
||||
CompiledQuery,
|
||||
Driver
|
||||
} from "kysely";
|
||||
import { desktop } from "../desktop-bridge";
|
||||
import Worker from "./sqlite.worker.desktop.ts?worker";
|
||||
import type { SQLiteWorker } from "./sqlite.worker.desktop";
|
||||
import { wrap, Remote } from "comlink";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
class SqliteDriver extends KSqliteDriver {
|
||||
constructor(private readonly config: SqliteDialectConfig & { name: string }) {
|
||||
super(config);
|
||||
class SqliteDriver implements Driver {
|
||||
connection?: DatabaseConnection;
|
||||
private connectionMutex = new Mutex();
|
||||
worker: Remote<SQLiteWorker> = wrap<SQLiteWorker>(new Worker());
|
||||
constructor(private readonly config: { name: string }) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const path = await desktop!.integration.resolvePath.query({
|
||||
filePath: `userData/${this.config.name}.sql`
|
||||
});
|
||||
await this.worker.open(path);
|
||||
this.connection = new SqliteWorkerConnection(this.worker);
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
if (!this.connection) throw new Error("Driver not initialized.");
|
||||
|
||||
// SQLite only has one single connection. We use a mutex here to wait
|
||||
// until the single connection has been released.
|
||||
await this.connectionMutex.waitForUnlock();
|
||||
await this.connectionMutex.acquire();
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
}
|
||||
|
||||
async releaseConnection(): Promise<void> {
|
||||
this.connectionMutex.release();
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
await this.worker.close();
|
||||
}
|
||||
|
||||
async delete() {
|
||||
const path = await desktop!.integration.resolvePath.query({
|
||||
filePath: `userData/${this.config.name}.sql`
|
||||
});
|
||||
await desktop?.integration.deleteFile.query(path);
|
||||
await this.destroy();
|
||||
await this.worker.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
export const createDialect = (name: string): Dialect => {
|
||||
class SqliteWorkerConnection implements DatabaseConnection {
|
||||
constructor(private readonly worker: Remote<SQLiteWorker>) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
}
|
||||
|
||||
async executeQuery<R>(
|
||||
compiledQuery: CompiledQuery<unknown>
|
||||
): Promise<QueryResult<R>> {
|
||||
const { parameters, sql } = compiledQuery;
|
||||
return this.worker.run(sql, parameters as any) as unknown as QueryResult<R>;
|
||||
}
|
||||
}
|
||||
|
||||
export const createDialect = (name: string, _encrypted: boolean): Dialect => {
|
||||
return {
|
||||
createDriver: () =>
|
||||
new SqliteDriver({
|
||||
name,
|
||||
database: async () => {
|
||||
const path = await desktop!.integration.resolvePath.query({
|
||||
filePath: `userData/${name}.sql`
|
||||
});
|
||||
return window.createSQLite3Database(path).unsafeMode(true);
|
||||
}
|
||||
name
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
|
||||
@@ -34,6 +34,7 @@ declare module "kysely" {
|
||||
|
||||
export const createDialect = (
|
||||
name: string,
|
||||
encrypted: boolean,
|
||||
init?: () => Promise<void>
|
||||
): Dialect => {
|
||||
return {
|
||||
@@ -41,6 +42,7 @@ export const createDialect = (
|
||||
new WaSqliteWorkerDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted,
|
||||
init
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
|
||||
@@ -83,7 +83,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
}
|
||||
|
||||
activate(
|
||||
portProviderFunc: () => MessagePort | Promise<MessagePort>,
|
||||
portProviderFunc: () => Promise<{ port: MessagePort; onclose: () => void }>,
|
||||
onClientConnected: () => Promise<void>
|
||||
) {
|
||||
if (this.#onDeactivate) return;
|
||||
@@ -97,7 +97,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
navigator.locks
|
||||
.request(LOCK_NAME, { signal: this.#onDeactivate.signal }, async () => {
|
||||
// Get the port to request client ports.
|
||||
const port = await portProviderFunc();
|
||||
const { port, onclose } = await portProviderFunc();
|
||||
port.start();
|
||||
|
||||
// Listen for client requests. A separate BroadcastChannel
|
||||
@@ -159,6 +159,7 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
// Release the lock only on user abort or context destruction.
|
||||
return new Promise((_, reject) => {
|
||||
this.#onDeactivate?.signal.addEventListener("abort", () => {
|
||||
onclose();
|
||||
broadcastChannel.close();
|
||||
reject(this.#onDeactivate?.signal.reason);
|
||||
});
|
||||
|
||||
25
apps/web/src/common/sqlite/sqlite.worker.desktop.ts
Normal file
25
apps/web/src/common/sqlite/sqlite.worker.desktop.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
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 { SQLite } from "@notesnook/desktop";
|
||||
import { expose } from "comlink";
|
||||
|
||||
export type SQLiteWorker = typeof SQLite.prototype;
|
||||
const db = new SQLite();
|
||||
expose(db);
|
||||
@@ -32,219 +32,230 @@ type PreparedStatement = {
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
let sqlite: SQLiteAPI;
|
||||
let db: number | undefined = undefined;
|
||||
let vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
|
||||
let initialized = false;
|
||||
const preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||
const retryCounter: Record<string, number> = {};
|
||||
console.log("new sqlite worker");
|
||||
|
||||
async function open(dbName: string, async: boolean, url?: string) {
|
||||
if (db) {
|
||||
console.error("Database is already initialized", db);
|
||||
return;
|
||||
}
|
||||
|
||||
const option = url ? { locateFile: () => url } : {};
|
||||
const sqliteModule = async
|
||||
? await import("./wa-sqlite-async").then(
|
||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||
)
|
||||
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
|
||||
SQLiteSyncESMFactory(option)
|
||||
);
|
||||
sqlite = Factory(sqliteModule);
|
||||
vfs = await getVFS(dbName, async);
|
||||
|
||||
sqlite.vfs_register(vfs, false);
|
||||
db = await sqlite.open_v2(dbName, undefined, `multipleciphers-${vfs.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async function prepare(sql: string) {
|
||||
if (!db) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const str = sqlite.str_new(db, sql);
|
||||
const prepared = await sqlite.prepare_v2(db, sqlite.str_value(str));
|
||||
if (!prepared) return;
|
||||
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
preparedStatements.set(sql, statement);
|
||||
|
||||
sqlite.str_finish(str);
|
||||
|
||||
// reset retry count on success
|
||||
retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (retryCounter[sql] < 5) {
|
||||
retryCounter[sql] = (retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return prepare(sql);
|
||||
} else retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error || ex instanceof SQLiteError)
|
||||
ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async function run(
|
||||
sql: string,
|
||||
mode: RunMode,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
) {
|
||||
const prepared = await prepare(sql);
|
||||
if (!prepared) return [];
|
||||
try {
|
||||
if (parameters) sqlite.bind_collection(prepared.stmt, parameters);
|
||||
|
||||
// fast path for exec statements
|
||||
if (mode === "exec") {
|
||||
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW);
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: Record<string, SQLiteCompatibleType>[] = [];
|
||||
while ((await sqlite.step(prepared.stmt)) === SQLITE_ROW) {
|
||||
const row = sqlite.row(prepared.stmt);
|
||||
const acc: Record<string, SQLiteCompatibleType> = {};
|
||||
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
|
||||
rows.push(acc);
|
||||
}
|
||||
|
||||
return rows;
|
||||
} catch (e) {
|
||||
if (e instanceof Error || e instanceof SQLiteError)
|
||||
e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
await sqlite
|
||||
.reset(prepared.stmt)
|
||||
// we must clear/destruct the prepared statement if it can't be reset
|
||||
.catch(() =>
|
||||
sqlite
|
||||
.finalize(prepared.stmt)
|
||||
// ignore error (we will just prepare a new statement)
|
||||
.catch(console.error)
|
||||
.finally(() => preparedStatements.delete(sql))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function exec<R>(
|
||||
mode: RunMode,
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!sql.startsWith("PRAGMA key")) {
|
||||
await waitForDatabase();
|
||||
}
|
||||
if (!db) throw new Error("No database is not opened.");
|
||||
|
||||
const rows = (await run(sql, mode, parameters)) as R[];
|
||||
if (mode === "query") return { rows };
|
||||
|
||||
// initialize the database after it has been successfully decrypted.
|
||||
// all queries prior to that must wait otherwise we get the
|
||||
// "file is not a database" error
|
||||
if (sql.startsWith("PRAGMA key")) await initialize();
|
||||
|
||||
return {
|
||||
insertId: BigInt(sqlite.last_insert_rowid(db)),
|
||||
numAffectedRows: BigInt(sqlite.changes(db)),
|
||||
rows: mode === "raw" ? rows : []
|
||||
};
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (!db) return;
|
||||
|
||||
for (const [_, prepared] of preparedStatements) {
|
||||
await sqlite.finalize(prepared.stmt);
|
||||
}
|
||||
preparedStatements.clear();
|
||||
await sqlite.close(db);
|
||||
await vfs?.close();
|
||||
|
||||
db = undefined;
|
||||
class _SQLiteWorker {
|
||||
sqlite!: SQLiteAPI;
|
||||
db: number | undefined = undefined;
|
||||
vfs: IDBBatchAtomicVFS | AccessHandlePoolVFS | null = null;
|
||||
initialized = false;
|
||||
}
|
||||
preparedStatements: Map<string, PreparedStatement> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor(
|
||||
private readonly dbName: string,
|
||||
private readonly encrypted: boolean
|
||||
) {
|
||||
console.log("new sqlite worker", dbName, encrypted);
|
||||
}
|
||||
|
||||
async function exportDatabase(dbName: string, async: boolean) {
|
||||
const vfs = await getVFS(dbName, async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
||||
return transfer(stream, [stream]);
|
||||
}
|
||||
async open(async: boolean, url?: string) {
|
||||
if (this.db) {
|
||||
console.error("Database is already initialized", this.db);
|
||||
return;
|
||||
}
|
||||
|
||||
async function deleteDatabase(dbName: string, async: boolean) {
|
||||
await close();
|
||||
if (vfs) await vfs.delete();
|
||||
else await (await getVFS(dbName, async)).delete();
|
||||
}
|
||||
const option = url ? { locateFile: () => url } : {};
|
||||
const sqliteModule = async
|
||||
? await import("./wa-sqlite-async").then(
|
||||
({ default: SQLiteAsyncESMFactory }) => SQLiteAsyncESMFactory(option)
|
||||
)
|
||||
: await import("./wa-sqlite").then(({ default: SQLiteSyncESMFactory }) =>
|
||||
SQLiteSyncESMFactory(option)
|
||||
);
|
||||
this.sqlite = Factory(sqliteModule);
|
||||
this.vfs = await this.getVFS(this.dbName, async);
|
||||
|
||||
async function getVFS(dbName: string, async: boolean) {
|
||||
const vfs = async
|
||||
? await import("./IDBBatchAtomicVFS").then(
|
||||
({ IDBBatchAtomicVFS }) =>
|
||||
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
|
||||
)
|
||||
: await import("./AccessHandlePoolVFS").then(
|
||||
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
|
||||
this.sqlite.vfs_register(this.vfs, false);
|
||||
this.db = await this.sqlite.open_v2(
|
||||
this.dbName,
|
||||
undefined,
|
||||
`multipleciphers-${this.vfs.name}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<PreparedStatement | undefined> {
|
||||
if (!this.db) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const str = this.sqlite.str_new(this.db, sql);
|
||||
const prepared = await this.sqlite.prepare_v2(
|
||||
this.db,
|
||||
this.sqlite.str_value(str)
|
||||
);
|
||||
if ("isReady" in vfs) await vfs.isReady;
|
||||
return vfs;
|
||||
if (!prepared) return;
|
||||
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: this.sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
this.preparedStatements.set(sql, statement);
|
||||
|
||||
this.sqlite.str_finish(str);
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error || ex instanceof SQLiteError)
|
||||
ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec(sql: string, mode: RunMode, parameters?: SQLiteCompatibleType[]) {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return [];
|
||||
try {
|
||||
if (parameters) this.sqlite.bind_collection(prepared.stmt, parameters);
|
||||
|
||||
// fast path for exec statements
|
||||
if (mode === "exec") {
|
||||
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW);
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows: Record<string, SQLiteCompatibleType>[] = [];
|
||||
while ((await this.sqlite.step(prepared.stmt)) === SQLITE_ROW) {
|
||||
const row = this.sqlite.row(prepared.stmt);
|
||||
const acc: Record<string, SQLiteCompatibleType> = {};
|
||||
row.forEach((v, i) => (acc[prepared.columns[i]] = v));
|
||||
rows.push(acc);
|
||||
}
|
||||
|
||||
return rows;
|
||||
} catch (e) {
|
||||
if (e instanceof Error || e instanceof SQLiteError)
|
||||
e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
} finally {
|
||||
await this.sqlite
|
||||
.reset(prepared.stmt)
|
||||
// we must clear/destruct the prepared statement if it can't be reset
|
||||
.catch(() =>
|
||||
this.sqlite
|
||||
.finalize(prepared.stmt)
|
||||
// ignore error (we will just prepare a new statement)
|
||||
.catch(console.error)
|
||||
.finally(() => this.preparedStatements.delete(sql))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
mode: RunMode,
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (this.encrypted && !sql.startsWith("PRAGMA key")) {
|
||||
await this.waitForDatabase();
|
||||
}
|
||||
if (!this.db) throw new Error("No database is not opened.");
|
||||
|
||||
const rows = (await this.exec(sql, mode, parameters)) as R[];
|
||||
if (mode === "query") return { rows };
|
||||
|
||||
// initialize the database after it has been successfully decrypted.
|
||||
// all queries prior to that must wait otherwise we get the
|
||||
// "file is not a database" error
|
||||
if (this.encrypted && sql.startsWith("PRAGMA key")) await this.initialize();
|
||||
|
||||
return {
|
||||
insertId: BigInt(this.sqlite.last_insert_rowid(this.db)),
|
||||
numAffectedRows: BigInt(this.sqlite.changes(this.db)),
|
||||
rows: mode === "raw" ? rows : []
|
||||
};
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.db) return;
|
||||
|
||||
for (const [_, prepared] of this.preparedStatements) {
|
||||
await this.sqlite.finalize(prepared.stmt);
|
||||
}
|
||||
this.preparedStatements.clear();
|
||||
await this.sqlite.close(this.db);
|
||||
await this.vfs?.close();
|
||||
|
||||
this.db = undefined;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
async export(dbName: string, async: boolean) {
|
||||
const vfs = await this.getVFS(dbName, async);
|
||||
const stream = new ReadableStream(new DatabaseSource(vfs, dbName));
|
||||
return transfer(stream, [stream]);
|
||||
}
|
||||
|
||||
async delete(dbName: string, async: boolean) {
|
||||
await this.close();
|
||||
if (this.vfs) await this.vfs.delete();
|
||||
else await (await this.getVFS(dbName, async)).delete();
|
||||
}
|
||||
|
||||
async getVFS(dbName: string, async: boolean) {
|
||||
const vfs = async
|
||||
? await import("./IDBBatchAtomicVFS").then(
|
||||
({ IDBBatchAtomicVFS }) =>
|
||||
new IDBBatchAtomicVFS(dbName, { durability: "strict" })
|
||||
)
|
||||
: await import("./AccessHandlePoolVFS").then(
|
||||
({ AccessHandlePoolVFS }) => new AccessHandlePoolVFS(dbName)
|
||||
);
|
||||
if ("isReady" in vfs) await vfs.isReady;
|
||||
return vfs;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", {
|
||||
data: { type: "databaseInitialized", dbName: this.dbName }
|
||||
})
|
||||
);
|
||||
console.log("Database initialized", this.db);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
async waitForDatabase() {
|
||||
// if the database hasn't yet been initialized.
|
||||
if (!this.initialized) {
|
||||
console.log("Waiting for database to be initialized...", this.db);
|
||||
return await new Promise<boolean>((resolve) =>
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (
|
||||
ev.data.type === "databaseInitialized" &&
|
||||
ev.data.dbName === this.dbName
|
||||
)
|
||||
resolve(true);
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
self.dispatchEvent(
|
||||
new MessageEvent("message", { data: { type: "databaseInitialized" } })
|
||||
);
|
||||
console.log("Database initialized", db);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
const worker = {
|
||||
close,
|
||||
open,
|
||||
run: exec,
|
||||
export: exportDatabase,
|
||||
delete: deleteDatabase
|
||||
};
|
||||
|
||||
export type SQLiteWorker = typeof worker;
|
||||
export type SQLiteWorker = typeof _SQLiteWorker.prototype;
|
||||
|
||||
addEventListener("message", async (event) => {
|
||||
if (!event.data.type) {
|
||||
await worker.open(event.data.dbName, event.data.async, event.data.uri);
|
||||
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
|
||||
await worker.open(event.data.async, event.data.uri);
|
||||
const providerPort = createSharedServicePort(worker);
|
||||
postMessage(null, [providerPort]);
|
||||
|
||||
self.addEventListener("beforeunload", () => worker.close());
|
||||
}
|
||||
});
|
||||
|
||||
async function waitForDatabase() {
|
||||
// if the database hasn't yet been initialized.
|
||||
if (!initialized) {
|
||||
console.log("Waiting for database to be initialized...", db);
|
||||
return await new Promise<boolean>((resolve) =>
|
||||
self.addEventListener("message", (ev) => {
|
||||
if (ev.data.type === "databaseInitialized") resolve(true);
|
||||
})
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,12 @@ import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { SharedService } from "./shared-service";
|
||||
|
||||
type Config = { dbName: string; async: boolean; init?: () => Promise<void> };
|
||||
type Config = {
|
||||
dbName: string;
|
||||
async: boolean;
|
||||
encrypted: boolean;
|
||||
init?: () => Promise<void>;
|
||||
};
|
||||
|
||||
const servicePool = new Map<
|
||||
string,
|
||||
@@ -55,7 +60,6 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
if (closed) {
|
||||
console.log("Already activated. Reinitializing...");
|
||||
await service.proxy.open(
|
||||
this.config.dbName,
|
||||
this.config.async,
|
||||
this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
);
|
||||
@@ -72,19 +76,24 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
|
||||
service.activate(
|
||||
() =>
|
||||
new Promise<MessagePort>((resolve) => {
|
||||
new Promise<{ port: MessagePort; onclose: () => void }>((resolve) => {
|
||||
console.log("initializing worker");
|
||||
this.needsInitialization = true;
|
||||
|
||||
const worker = new Worker();
|
||||
worker.addEventListener(
|
||||
"message",
|
||||
(event) => resolve(event.ports[0]),
|
||||
(event) =>
|
||||
resolve({
|
||||
port: event.ports[0],
|
||||
onclose: () => worker.terminate()
|
||||
}),
|
||||
{ once: true }
|
||||
);
|
||||
worker.postMessage({
|
||||
dbName: this.config.dbName,
|
||||
async: this.config.async,
|
||||
encrypted: this.config.encrypted,
|
||||
uri: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -307,7 +307,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
break;
|
||||
}
|
||||
case "backup": {
|
||||
await createBackup(true);
|
||||
await createBackup();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -26,7 +26,7 @@ type ContentToggle = {
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
dateEdited: number;
|
||||
resolveConflict: (options: { saveCopy: boolean }) => void;
|
||||
resolveConflict: (saveCopy: boolean) => void;
|
||||
readonly: boolean;
|
||||
sx: FlexProps["sx"];
|
||||
};
|
||||
@@ -50,7 +50,7 @@ function ContentToggle(props: ContentToggle) {
|
||||
<Button
|
||||
variant="accent"
|
||||
mr={2}
|
||||
onClick={() => resolveConflict({ saveCopy: true })}
|
||||
onClick={() => resolveConflict(true)}
|
||||
p={1}
|
||||
px={2}
|
||||
>
|
||||
@@ -61,7 +61,7 @@ function ContentToggle(props: ContentToggle) {
|
||||
variant={isOtherSelected ? "error" : "accent"}
|
||||
onClick={() => {
|
||||
if (isOtherSelected) {
|
||||
resolveConflict({ saveCopy: false });
|
||||
resolveConflict(false);
|
||||
} else {
|
||||
onToggle();
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ 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 { useLayoutEffect, useRef, useState } from "react";
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Copy, Restore } from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
@@ -45,6 +45,24 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
);
|
||||
const root = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onResolveContent = useCallback(
|
||||
(saveCopy: boolean) => {
|
||||
const toKeep =
|
||||
selectedContent === 1 ? session.content?.conflicted : session.content;
|
||||
const toCopy =
|
||||
selectedContent === 1 ? session.content : session.content?.conflicted;
|
||||
if (!toKeep) return;
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep,
|
||||
toCopy: saveCopy ? toCopy : undefined,
|
||||
toKeepDateEdited: toKeep.dateEdited,
|
||||
dateResolved: conflictedContent!.dateModified
|
||||
});
|
||||
},
|
||||
[conflictedContent, selectedContent, session.content, session.note]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setConflictedContent((c) => {
|
||||
if (c?.dateEdited === session.content?.conflicted?.dateEdited) return c;
|
||||
@@ -185,15 +203,7 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
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
|
||||
});
|
||||
}}
|
||||
resolveConflict={onResolveContent}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
@@ -263,15 +273,7 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
<>
|
||||
<ContentToggle
|
||||
readonly={session.type === "diff"}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep: conflictedContent.data,
|
||||
toCopy: saveCopy ? content : undefined,
|
||||
toKeepDateEdited: conflictedContent.dateEdited,
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
}}
|
||||
resolveConflict={onResolveContent}
|
||||
label={
|
||||
session.type === "diff"
|
||||
? "Current version"
|
||||
@@ -326,31 +328,31 @@ async function resolveConflict({
|
||||
dateResolved
|
||||
}: {
|
||||
note: Note;
|
||||
toKeep: string;
|
||||
toKeep: ContentItem;
|
||||
toCopy?: ContentItem;
|
||||
toKeepDateEdited: number;
|
||||
dateResolved?: number;
|
||||
}) {
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
dateResolved,
|
||||
sessionId: `${Date.now()}`,
|
||||
...(toKeep.locked
|
||||
? { data: toKeep.data, locked: true }
|
||||
: { data: toKeep.data, locked: false })
|
||||
});
|
||||
|
||||
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, { force: true });
|
||||
}
|
||||
|
||||
async function createCopy(note: Note, content: ContentItem) {
|
||||
|
||||
@@ -152,7 +152,6 @@ export function EditorActionBar() {
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "readonly" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
onClick: editor?.startSearch
|
||||
|
||||
@@ -271,7 +271,7 @@ function EditorView({
|
||||
const isContent =
|
||||
item.type === "tiptap" && item.noteId === session.note.id;
|
||||
const isNote = item.type === "note" && item.id === session.note.id;
|
||||
if (isContent && lastChangedTime.current < item.dateEdited) {
|
||||
if (isContent && lastChangedTime.current < item.dateModified) {
|
||||
if (!item.locked) return editor.updateContent(item.data);
|
||||
|
||||
const result = await db.vault
|
||||
|
||||
@@ -344,7 +344,6 @@ function TipTap(props: TipTapProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (readonly) return null;
|
||||
return (
|
||||
<>
|
||||
<ScopedThemeProvider
|
||||
@@ -418,7 +417,7 @@ function TiptapWrapper(
|
||||
editorContainer={() => {
|
||||
if (editorContainerRef.current) return editorContainerRef.current;
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.classList.add("selectable", "editor-container");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.color =
|
||||
@@ -427,6 +426,7 @@ function TiptapWrapper(
|
||||
editorContainer.style.fontSize = `${editorConfig.fontSize}px`;
|
||||
editorContainer.style.fontFamily =
|
||||
getFontById(editorConfig.fontFamily)?.font || "sans-serif";
|
||||
editorContainer.tabIndex = -1;
|
||||
editorContainerRef.current = editorContainer;
|
||||
return editorContainer;
|
||||
}}
|
||||
|
||||
@@ -162,9 +162,10 @@ function getErrorHelp(props: FallbackProps) {
|
||||
if (
|
||||
errorText.includes("file is not a database") ||
|
||||
errorText.includes("unsupported file format") ||
|
||||
errorText.includes("database disk image is malformed") ||
|
||||
errorText.includes("null function or function signature mismatch") ||
|
||||
errorText.includes("malformed database schema")
|
||||
errorText.includes("malformed database schema") ||
|
||||
/table ".+?" already exists/.test(errorText) ||
|
||||
errorText.includes("corrupted migrations:")
|
||||
) {
|
||||
return {
|
||||
explanation: `This error usually means the database file is either corrupt or it could not be decrypted.`,
|
||||
@@ -195,6 +196,17 @@ function getErrorHelp(props: FallbackProps) {
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
} else if (errorText.includes("database disk image is malformed")) {
|
||||
return {
|
||||
explanation: `This error usually means the search index is corrupted.`,
|
||||
action:
|
||||
"This error can be fixed by rebuilding the search index. This action won't result in any kind of data loss.",
|
||||
fix: async () => {
|
||||
const { db } = await import("../../common/db");
|
||||
await db.lookup.rebuild();
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,11 @@ function NavigationItem(
|
||||
) : !isTablet && count !== undefined ? (
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{ mr: 1, bg: "hover", px: "3px", borderRadius: "default" }}
|
||||
sx={{
|
||||
mr: 1,
|
||||
px: "3px",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
{count > 100 ? "100+" : count}
|
||||
</Text>
|
||||
|
||||
@@ -524,8 +524,6 @@ const menuItems: (
|
||||
type: "button",
|
||||
key: "duplicate",
|
||||
title: "Duplicate",
|
||||
//!isSynced ||
|
||||
isDisabled: context?.locked,
|
||||
icon: Duplicate.path,
|
||||
onClick: () => store.get().duplicate(...ids),
|
||||
multiSelect: true
|
||||
|
||||
@@ -89,32 +89,7 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
)
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Bi-directional note linking",
|
||||
subtitle: "You can now link any 2 notes for easier referencing."
|
||||
},
|
||||
{
|
||||
title: "Tabs",
|
||||
subtitle:
|
||||
"Open multiple notes side by side for faster multi-tasking."
|
||||
},
|
||||
{
|
||||
title: "Nested notebooks",
|
||||
subtitle:
|
||||
"Create subnotebooks inside notebooks upto unlimited depth."
|
||||
},
|
||||
{
|
||||
title: "At rest encryption",
|
||||
subtitle:
|
||||
"All your data will now be stored encrypted on your device to keep you safe even if your device gets compromised."
|
||||
},
|
||||
{
|
||||
title: "App lock",
|
||||
subtitle:
|
||||
"App lock is now here on desktop/web apps! Put a lock on the app, and keep it safe from intruders."
|
||||
}
|
||||
],
|
||||
: [],
|
||||
cta: {
|
||||
title: "Got it",
|
||||
icon: Checkmark,
|
||||
|
||||
@@ -244,98 +244,102 @@ function SettingsSideBar(props: SettingsSideBarProps) {
|
||||
}}
|
||||
data-test-id="settings-navigation-menu"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
display: "flex",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
id="search"
|
||||
name="search"
|
||||
placeholder="Search"
|
||||
data-test-id="settings-search"
|
||||
<ScopedThemeProvider scope="navigationMenu" injectCssVars={false}>
|
||||
<Flex
|
||||
sx={{
|
||||
m: 2,
|
||||
mb: 2,
|
||||
width: "auto",
|
||||
bg: "background",
|
||||
py: "7px"
|
||||
flexDirection: "column",
|
||||
display: "flex",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
if (!query)
|
||||
return onNavigate(
|
||||
SettingsGroups.filter((g) => g.section === route)
|
||||
);
|
||||
>
|
||||
<Input
|
||||
id="search"
|
||||
name="search"
|
||||
placeholder="Search"
|
||||
data-test-id="settings-search"
|
||||
sx={{
|
||||
m: 2,
|
||||
mb: 2,
|
||||
width: "auto",
|
||||
bg: "background",
|
||||
py: "7px"
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
if (!query)
|
||||
return onNavigate(
|
||||
SettingsGroups.filter((g) => g.section === route)
|
||||
);
|
||||
|
||||
const groups: SettingsGroup[] = [];
|
||||
for (const group of SettingsGroups) {
|
||||
const isTitleMatch =
|
||||
typeof group.header === "string" &&
|
||||
group.header.toLowerCase().includes(query);
|
||||
const isSectionMatch = group.section.includes(query);
|
||||
const groups: SettingsGroup[] = [];
|
||||
for (const group of SettingsGroups) {
|
||||
const isTitleMatch =
|
||||
typeof group.header === "string" &&
|
||||
group.header.toLowerCase().includes(query);
|
||||
const isSectionMatch = group.section.includes(query);
|
||||
|
||||
if (isTitleMatch || isSectionMatch) {
|
||||
groups.push(group);
|
||||
continue;
|
||||
if (isTitleMatch || isSectionMatch) {
|
||||
groups.push(group);
|
||||
continue;
|
||||
}
|
||||
|
||||
const settings = group.settings.filter((setting) => {
|
||||
const description =
|
||||
typeof setting.description === "function"
|
||||
? setting.description()
|
||||
: setting.description;
|
||||
|
||||
return [
|
||||
description || "",
|
||||
setting.keywords?.join(" ") || "",
|
||||
setting.title
|
||||
]
|
||||
.join(" ")
|
||||
?.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
if (!settings.length) continue;
|
||||
groups.push({ ...group, settings });
|
||||
}
|
||||
|
||||
const settings = group.settings.filter((setting) => {
|
||||
const description =
|
||||
typeof setting.description === "function"
|
||||
? setting.description()
|
||||
: setting.description;
|
||||
|
||||
return [
|
||||
description || "",
|
||||
setting.keywords?.join(" ") || "",
|
||||
setting.title
|
||||
]
|
||||
.join(" ")
|
||||
?.toLowerCase()
|
||||
.includes(query);
|
||||
});
|
||||
if (!settings.length) continue;
|
||||
groups.push({ ...group, settings });
|
||||
}
|
||||
onNavigate(groups);
|
||||
}}
|
||||
/>
|
||||
{sectionGroups.map((group) => (
|
||||
<Flex key={group.key} sx={{ flexDirection: "column", mb: 2 }}>
|
||||
<Text
|
||||
variant={"subBody"}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: "paragraph",
|
||||
mx: 3,
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</Text>
|
||||
{group.sections.map(
|
||||
(section) =>
|
||||
(!section.isHidden || !section.isHidden()) && (
|
||||
<NavigationItem
|
||||
key={section.key}
|
||||
icon={section.icon}
|
||||
title={section.title}
|
||||
selected={section.key === route}
|
||||
onClick={() => {
|
||||
onNavigate(
|
||||
SettingsGroups.filter((g) => g.section === section.key)
|
||||
);
|
||||
setRoute(section.key);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
onNavigate(groups);
|
||||
}}
|
||||
/>
|
||||
{sectionGroups.map((group) => (
|
||||
<Flex key={group.key} sx={{ flexDirection: "column", mb: 2 }}>
|
||||
<Text
|
||||
variant={"subBody"}
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
color: "paragraph",
|
||||
mx: 3,
|
||||
mb: 1
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</Text>
|
||||
{group.sections.map(
|
||||
(section) =>
|
||||
(!section.isHidden || !section.isHidden()) && (
|
||||
<NavigationItem
|
||||
key={section.key}
|
||||
icon={section.icon}
|
||||
title={section.title}
|
||||
selected={section.key === route}
|
||||
onClick={() => {
|
||||
onNavigate(
|
||||
SettingsGroups.filter(
|
||||
(g) => g.section === section.key
|
||||
)
|
||||
);
|
||||
setRoute(section.key);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</ScopedThemeProvider>
|
||||
</FlexScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
});
|
||||
|
||||
onPageVisibilityChanged(async (_, documentHidden) => {
|
||||
if (!documentHidden) return;
|
||||
if (documentHidden) return;
|
||||
|
||||
logger.info("Page visibility changed. Reconnecting SSE...");
|
||||
await db.connectSSE({ force: false }).catch(logger.error);
|
||||
|
||||
@@ -233,6 +233,15 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (noteId && session.id !== noteId && session.note.id !== noteId)
|
||||
continue;
|
||||
if (isDeleted(item) || isTrashItem(item)) clearIds.push(session.id);
|
||||
// if a note becomes conflicted, reopen the session
|
||||
else if (
|
||||
session.type !== "conflicted" &&
|
||||
item.type === "tiptap" &&
|
||||
item.conflicted
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
// if a note is locked, reopen the session
|
||||
else if (
|
||||
session.type === "default" &&
|
||||
@@ -257,9 +266,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
}
|
||||
// if a deleted note is restored, reopen the session
|
||||
else if (session.type === "deleted" && item.type === "note") {
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
}
|
||||
// if a readonly note is made editable, reopen the session
|
||||
else if (
|
||||
@@ -267,9 +274,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
item.type === "note" &&
|
||||
!item.readonly
|
||||
)
|
||||
waitForSync().then(() =>
|
||||
openSession(session.note.id, { force: true, silent: true })
|
||||
);
|
||||
openSession(session.note.id, { force: true, silent: true });
|
||||
// update the note in all sessions
|
||||
else if (item.type === "note") {
|
||||
updateSession(
|
||||
@@ -344,6 +349,10 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
session.type !== "new" && session.note.contentId;
|
||||
if (!contentId || !event.ids.includes(contentId)) continue;
|
||||
if (
|
||||
// if note's conflict is resolved
|
||||
(session.type === "conflicted" && !event.item.conflicted) ||
|
||||
// if note becomes conflicted
|
||||
(session.type !== "conflicted" && event.item.conflicted) ||
|
||||
// if note is locked
|
||||
(session.type === "default" &&
|
||||
!session.locked &&
|
||||
@@ -548,12 +557,14 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const noteId = typeof noteOrId === "string" ? noteOrId : noteOrId.id;
|
||||
const session = getSession(noteId);
|
||||
|
||||
if (session && !options.force && !session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
if (session && !options.force) {
|
||||
if (!session.needsHydration) {
|
||||
return this.activateSession(noteId, options.activeBlockId);
|
||||
}
|
||||
|
||||
if (session && (session.type === "diff" || session.type === "conflicted")) {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
if (session.type === "diff" || session.type === "conflicted") {
|
||||
return openDiffSession(session.note.id, session.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (session && session.id) await db.fs().cancel(session.id);
|
||||
@@ -566,19 +577,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
const isPreview = session ? session.preview : !options?.newSession;
|
||||
const isLocked = await db.vaults.itemExists(note);
|
||||
|
||||
if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (note.conflicted) {
|
||||
if (note.conflicted) {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
@@ -612,6 +611,18 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else if (isLocked && note.type !== "trash") {
|
||||
this.addSession(
|
||||
{
|
||||
type: "locked",
|
||||
id: note.id,
|
||||
pinned: session?.pinned,
|
||||
note,
|
||||
preview: isPreview,
|
||||
activeBlockId: options.activeBlockId
|
||||
},
|
||||
!options.silent
|
||||
);
|
||||
} else {
|
||||
const content = note.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
|
||||
@@ -22,15 +22,32 @@ import {
|
||||
logger as _logger,
|
||||
logManager
|
||||
} from "@notesnook/core/dist/logger";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
|
||||
import { LogMessage, NoopLogger, format } from "@notesnook/logger";
|
||||
import { ZipFile, createZipStream } from "./streams/zip-stream";
|
||||
import { createWriteStream } from "./stream-saver";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { createDialect } from "../common/sqlite";
|
||||
import { isFeatureSupported } from "./feature-check";
|
||||
|
||||
let logger: typeof _logger;
|
||||
async function initializeLogger(persistence: DatabasePersistence = "db") {
|
||||
initialize(new NNStorage("Logs", () => null, persistence), false);
|
||||
let logger: typeof _logger = new NoopLogger();
|
||||
async function initializeLogger() {
|
||||
await initialize(
|
||||
{
|
||||
dialect: (name, init) => createDialect(name, false, init),
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
journalMode: "MEMORY",
|
||||
lockingMode: "exclusive"
|
||||
}),
|
||||
tempStore: "memory",
|
||||
synchronous: "normal",
|
||||
pageSize: 8192,
|
||||
cacheSize: -32000,
|
||||
skipInitialization: !IS_DESKTOP_APP
|
||||
},
|
||||
false
|
||||
);
|
||||
logger = _logger.scope("notesnook-web");
|
||||
}
|
||||
|
||||
@@ -47,11 +64,9 @@ async function downloadLogs() {
|
||||
return;
|
||||
}
|
||||
controller.enqueue({
|
||||
path: sanitizeFilename(log.key, { replacement: "-" }),
|
||||
path: sanitizeFilename(log.key, { replacement: "-" }) + ".log",
|
||||
data: textEncoder.encode(
|
||||
(log.logs as LogMessage[])
|
||||
.map((line) => JSON.stringify(line))
|
||||
.join("\n")
|
||||
(log.logs as LogMessage[]).map((line) => format(line)).join("\n")
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,24 +66,28 @@ export function onPageVisibilityChanged(
|
||||
if (isEventIgnored()) return;
|
||||
|
||||
if (!window.document.hasFocus()) return;
|
||||
handler("focus", isDocumentHidden());
|
||||
handler("focus", false);
|
||||
}, 1000);
|
||||
|
||||
const onBlur = debounce((_) => {
|
||||
if (isEventIgnored()) return;
|
||||
|
||||
if (window.document.hasFocus()) return;
|
||||
handler("blur", isDocumentHidden());
|
||||
handler("blur", true);
|
||||
}, 1000);
|
||||
|
||||
document.addEventListener(visibilityChange(), onVisibilityChanged);
|
||||
window.addEventListener("focus", onFocus);
|
||||
window.addEventListener("blur", onBlur);
|
||||
window.addEventListener("pageshow", onFocus);
|
||||
window.addEventListener("pagehide", onBlur);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(visibilityChange(), onVisibilityChanged);
|
||||
window.removeEventListener("focus", onFocus);
|
||||
window.removeEventListener("blur", onBlur);
|
||||
window.removeEventListener("pageshow", onFocus);
|
||||
window.removeEventListener("pagehide", onBlur);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ function BackupData(props: BaseRecoveryComponentProps<"backup">) {
|
||||
"Please wait while we create a backup file for you to download."
|
||||
}}
|
||||
onSubmit={async () => {
|
||||
await createBackup(true);
|
||||
await createBackup({ rescueMode: true });
|
||||
navigate("new");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -56,6 +56,7 @@ export default defineConfig({
|
||||
minify: "esbuild",
|
||||
cssMinify: true,
|
||||
emptyOutDir: true,
|
||||
sourcemap: !isDesktop,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
plugins: [emitEditorStyles()],
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15064.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15064.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15069.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15069.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15074.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15074.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15079.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15079.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
3
fastlane/metadata/android/en-US/changelogs/15084.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15084.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -63,6 +63,15 @@ exports[`get web pricing tier > get monthly web tier > monthly-web-pricing 1`] =
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`get web pricing tier > get monthly web tier > yearly-ios-pricing 1`] = `
|
||||
{
|
||||
"country": Any<String>,
|
||||
"countryCode": Any<String>,
|
||||
"discount": Any<Number>,
|
||||
"sku": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`get web pricing tier > get yearly web tier > yearly-web-pricing 1`] = `
|
||||
{
|
||||
"country": Any<String>,
|
||||
|
||||
@@ -28,7 +28,7 @@ import { login } from "./utils";
|
||||
import { SqliteDialect } from "kysely";
|
||||
import BetterSQLite3 from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
const TEST_TIMEOUT = 30 * 1000;
|
||||
const TEST_TIMEOUT = 60 * 1000;
|
||||
|
||||
test(
|
||||
"case 1: device A & B should only download the changes from device C (no uploading)",
|
||||
@@ -156,6 +156,111 @@ test(
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: syncing should not affect the items' dateModified",
|
||||
async (t) => {
|
||||
const [deviceA] = await Promise.all([initializeDevice("deviceA")]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
const noteDateBefore = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateBefore = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
await deviceA.sync({ type: "full" });
|
||||
const noteDateAfter = (await deviceA.notes.note(noteId)).dateModified;
|
||||
const contentDateAfter = (await deviceA.content.findByNoteId(noteId))
|
||||
.dateModified;
|
||||
expect(noteDateBefore).toBe(noteDateAfter);
|
||||
expect(contentDateBefore).toBe(contentDateAfter);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"case 4: local content changed after remote content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
expect(await deviceA.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
test(
|
||||
"case 5: remote content changed after local content should create a conflict",
|
||||
async (t) => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
t.onTestFinished(async (r) => {
|
||||
console.log(`${t.task.name} log out`);
|
||||
await cleanup(deviceA, deviceB);
|
||||
});
|
||||
|
||||
const noteId = await deviceA.notes.add({
|
||||
title: "Test note from device A",
|
||||
content: { data: "<p>Hello</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
await deviceA.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device B)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceA.sync({ type: "full" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
await deviceB.notes.add({
|
||||
id: noteId,
|
||||
content: { data: "<p>Hello (I am from device A)</p>", type: "tiptap" }
|
||||
});
|
||||
await deviceB.sync({ type: "full" });
|
||||
|
||||
expect(await deviceB.notes.conflicted.count()).toBeGreaterThan(0);
|
||||
},
|
||||
TEST_TIMEOUT * 10
|
||||
);
|
||||
|
||||
// test(
|
||||
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
|
||||
// async () => {
|
||||
@@ -378,6 +483,8 @@ test(
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
async function initializeDevice(id, capabilities = []) {
|
||||
// initialize(new NodeStorageInterface(), false);
|
||||
|
||||
console.time(`Init ${id}`);
|
||||
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
|
||||
return {
|
||||
@@ -429,7 +536,6 @@ async function cleanup(...devices) {
|
||||
await device.syncer.stop();
|
||||
await device.user.logout();
|
||||
device.eventManager.unsubscribeAll();
|
||||
await device.reset();
|
||||
}
|
||||
EV.unsubscribeAll();
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ import { Settings } from "../collections/settings";
|
||||
import {
|
||||
DatabaseAccessor,
|
||||
DatabaseSchema,
|
||||
RawDatabaseSchema,
|
||||
SQLiteOptions,
|
||||
changeDatabasePassword,
|
||||
createDatabase,
|
||||
@@ -74,7 +75,8 @@ import { Vaults } from "../collections/vaults";
|
||||
import { KVStorage } from "../database/kv";
|
||||
import { QueueValue } from "../utils/queue-value";
|
||||
import { Sanitizer } from "../database/sanitizer";
|
||||
import { dropTriggers } from "../database/triggers";
|
||||
import { createTriggers, dropTriggers } from "../database/triggers";
|
||||
import { NNMigrationProvider } from "../database/migrations";
|
||||
|
||||
type EventSourceConstructor = new (
|
||||
uri: string,
|
||||
@@ -179,7 +181,7 @@ class Database {
|
||||
subscriptions = new Subscriptions(this.tokenManager);
|
||||
offers = new Offers();
|
||||
debug = new Debug();
|
||||
pricing = new Pricing();
|
||||
pricing = Pricing;
|
||||
|
||||
user = new UserManager(this);
|
||||
syncer = new Sync(this);
|
||||
@@ -244,7 +246,11 @@ class Database {
|
||||
await sql.raw(statement).execute(this.sql());
|
||||
}
|
||||
|
||||
await initializeDatabase(this.sql().withTables());
|
||||
await initializeDatabase(
|
||||
this.sql().withTables(),
|
||||
new NNMigrationProvider()
|
||||
);
|
||||
await this.onInit(this.sql() as Kysely<RawDatabaseSchema>);
|
||||
await this.initCollections();
|
||||
return true;
|
||||
}
|
||||
@@ -274,10 +280,11 @@ class Database {
|
||||
this.disconnectSSE();
|
||||
});
|
||||
|
||||
this._sql = (await createDatabase(
|
||||
"notesnook",
|
||||
this.options.sqliteOptions
|
||||
)) as unknown as Kysely<DatabaseSchema>;
|
||||
this._sql = (await createDatabase<RawDatabaseSchema>("notesnook", {
|
||||
...this.options.sqliteOptions,
|
||||
migrationProvider: new NNMigrationProvider(),
|
||||
onInit: (db) => this.onInit(db)
|
||||
})) as unknown as Kysely<DatabaseSchema>;
|
||||
|
||||
await this.sanitizer.init();
|
||||
|
||||
@@ -290,6 +297,10 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
private async onInit(db: Kysely<RawDatabaseSchema>) {
|
||||
await createTriggers(db);
|
||||
}
|
||||
|
||||
async initCollections() {
|
||||
await this.legacySettings.init();
|
||||
// collections
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import http from "../utils/http";
|
||||
|
||||
type Product = {
|
||||
export type Product = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku?: string;
|
||||
|
||||
@@ -123,7 +123,7 @@ describe.concurrent("merge content", (test) => {
|
||||
await loginFakeUser(db);
|
||||
const merger = new Merger(db);
|
||||
|
||||
const merged = await merger.mergeContent(
|
||||
const merged = merger.mergeContent(
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Remote",
|
||||
@@ -132,6 +132,7 @@ describe.concurrent("merge content", (test) => {
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Local",
|
||||
synced: true,
|
||||
dateEdited: Date.now() - 1000
|
||||
}
|
||||
);
|
||||
@@ -140,13 +141,13 @@ describe.concurrent("merge content", (test) => {
|
||||
expect(merged.data).toBe("Remote");
|
||||
}));
|
||||
|
||||
test("trigger conflict if local item dateEdited is newer", () =>
|
||||
test("trigger conflict if local item is unsynced", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
await loginFakeUser(db);
|
||||
const merger = new Merger(db);
|
||||
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
const merged = await merger.mergeContent(
|
||||
const merged = merger.mergeContent(
|
||||
{
|
||||
type: "tiptap",
|
||||
data: "Remote",
|
||||
@@ -157,7 +158,7 @@ describe.concurrent("merge content", (test) => {
|
||||
type: "tiptap",
|
||||
data: "Local",
|
||||
noteId,
|
||||
|
||||
synced: false,
|
||||
dateEdited: Date.now()
|
||||
}
|
||||
);
|
||||
@@ -166,7 +167,6 @@ describe.concurrent("merge content", (test) => {
|
||||
expect(merged.data).toBe("Local");
|
||||
expect(merged.conflicted).toBeDefined();
|
||||
expect(merged.conflicted.data).toBe("Remote");
|
||||
expect(await db.notes.conflicted.has(noteId)).toBe(true);
|
||||
}));
|
||||
|
||||
test("merge conflicts if local item is already conflicted", () =>
|
||||
|
||||
@@ -57,37 +57,34 @@ class Collector {
|
||||
const collection = this.db[collectionKey].collection;
|
||||
let pushTimestamp = Date.now();
|
||||
for await (const chunk of collection.unsynced(chunkSize, isForceSync)) {
|
||||
const items = await this.prepareChunk(chunk, key);
|
||||
const { ids, items: syncableItems } = filterSyncableItems(chunk);
|
||||
if (!ids.length) continue;
|
||||
const ciphers = await this.db
|
||||
.storage()
|
||||
.encryptMulti(key, syncableItems);
|
||||
const items = toPushItem(ids, ciphers);
|
||||
if (!items) continue;
|
||||
yield { items, type: itemType };
|
||||
|
||||
await collection.update(
|
||||
chunk.map((i) => i.id),
|
||||
{ synced: true },
|
||||
{
|
||||
sendEvent: false,
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
condition: (eb) => eb("dateModified", "<=", pushTimestamp)
|
||||
}
|
||||
);
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable(collection.type)
|
||||
.where("id", "in", ids)
|
||||
// EDGE CASE:
|
||||
// Sometimes an item can get updated while it's being pushed.
|
||||
// The result is that its `synced` property becomes true even
|
||||
// though it's modification wasn't yet synced.
|
||||
// In order to prevent that, we only set the `synced` property
|
||||
// to true for items that haven't been modified since we last ran
|
||||
// the push. Everything else will be collected again in the next
|
||||
// push.
|
||||
.where("dateModified", "<=", pushTimestamp)
|
||||
.set({ synced: true })
|
||||
.execute();
|
||||
pushTimestamp = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async prepareChunk(chunk: MaybeDeletedItem<Item>[], key: SerializedKey) {
|
||||
const { ids, items } = filterSyncableItems(chunk);
|
||||
if (!ids.length) return;
|
||||
const ciphers = await this.db.storage().encryptMulti(key, items);
|
||||
return toPushItem(ids, ciphers);
|
||||
}
|
||||
}
|
||||
export default Collector;
|
||||
|
||||
@@ -113,9 +110,6 @@ function filterSyncableItems(items: MaybeDeletedItem<Item>[]): {
|
||||
const ids = [];
|
||||
const syncableItems = [];
|
||||
for (const item of items) {
|
||||
// do not sync conflicted note or content
|
||||
if ("conflicted" in item && item.conflicted) continue;
|
||||
|
||||
delete item.synced;
|
||||
|
||||
ids.push(item.id);
|
||||
|
||||
@@ -242,6 +242,20 @@ class Sync {
|
||||
|
||||
this.connection?.off("SendItems");
|
||||
this.connection?.off("SendVaultKey");
|
||||
|
||||
await this.db
|
||||
.sql()
|
||||
.updateTable("notes")
|
||||
.where("id", "in", (eb) =>
|
||||
eb
|
||||
.selectFrom("content")
|
||||
.select("noteId as id")
|
||||
.where("conflicted", "is not", null)
|
||||
.where("conflicted", "!=", false)
|
||||
.$castTo<string | null>()
|
||||
)
|
||||
.set({ conflicted: true })
|
||||
.execute();
|
||||
}
|
||||
|
||||
async send(deviceId: string, isForceSync?: boolean) {
|
||||
@@ -334,10 +348,8 @@ class Sync {
|
||||
const localItems = await collection.records(chunk.items.map((i) => i.id));
|
||||
let items: (MaybeDeletedItem<Item> | undefined)[] = [];
|
||||
if (itemType === "content") {
|
||||
items = await Promise.all(
|
||||
deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
)
|
||||
items = deserialized.map((item) =>
|
||||
this.merger.mergeContent(item, localItems[item.id])
|
||||
);
|
||||
} else {
|
||||
items =
|
||||
|
||||
@@ -46,7 +46,7 @@ class Merger {
|
||||
}
|
||||
}
|
||||
|
||||
async mergeContent(
|
||||
mergeContent(
|
||||
remoteItem: MaybeDeletedItem<Item>,
|
||||
localItem: MaybeDeletedItem<Item> | undefined
|
||||
) {
|
||||
@@ -75,10 +75,7 @@ class Merger {
|
||||
else if (!conflicted) return;
|
||||
|
||||
// otherwise we trigger the conflicts
|
||||
await this.db.notes.add({
|
||||
id: localItem.noteId,
|
||||
conflicted: true
|
||||
});
|
||||
this.logger.info("conflict marked", { id: localItem.noteId });
|
||||
localItem.conflicted = remoteItem;
|
||||
return localItem;
|
||||
}
|
||||
@@ -124,9 +121,8 @@ export function isContentConflicted(
|
||||
remoteItem.dateModified &&
|
||||
localItem.dateResolved === remoteItem.dateModified;
|
||||
const isEdited =
|
||||
// the local item is edited if it was changed/edited after the remote
|
||||
// note and it also wasn't synced yet.
|
||||
localItem.dateEdited > remoteItem.dateEdited && !localItem.synced;
|
||||
// the local item is edited if it wasn't synced yet.
|
||||
!localItem.synced;
|
||||
if (isEdited && !isResolved) {
|
||||
// If time difference between local item's edits & remote item's edits
|
||||
// is less than threshold, we shouldn't trigger a merge conflict; instead
|
||||
|
||||
@@ -94,7 +94,7 @@ class TokenManager {
|
||||
) {
|
||||
return await getSafeToken(async () => {
|
||||
const token = await this.getToken(true, forceRenew);
|
||||
if (!token) return;
|
||||
if (!token || !token.scope) return;
|
||||
if (!scopes.some((s) => token.scope.includes(s))) return;
|
||||
return token.access_token;
|
||||
}, "Error getting access token:");
|
||||
|
||||
@@ -100,9 +100,6 @@ export class Attachments implements ICollection {
|
||||
|
||||
async init() {
|
||||
await this.collection.init();
|
||||
logger.debug("attachments initialized", {
|
||||
total: await this.collection.count()
|
||||
});
|
||||
}
|
||||
|
||||
async add(
|
||||
|
||||
@@ -175,10 +175,7 @@ export class Content implements ICollection {
|
||||
const content = await this.collection.get(id);
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
}
|
||||
return content;
|
||||
}
|
||||
@@ -246,10 +243,7 @@ export class Content implements ICollection {
|
||||
.executeTakeFirst()) as ContentItem;
|
||||
if (!content || isDeleted(content)) return;
|
||||
if (!content.locked && this.preProcess(content)) {
|
||||
await this.db.content.add({
|
||||
...content,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
await this.collection.update([content.id], content, { modify: false });
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -318,17 +318,9 @@ export class Notes implements ICollection {
|
||||
const content = note.contentId
|
||||
? await this.db.content.get(note.contentId)
|
||||
: undefined;
|
||||
if (content && (isDeleted(content) || content.locked))
|
||||
throw new Error("Cannot duplicate a locked or deleted note.");
|
||||
const duplicateId = await this.db.notes.add({
|
||||
...clone(note),
|
||||
id: undefined,
|
||||
content: content
|
||||
? {
|
||||
type: content.type,
|
||||
data: content.data
|
||||
}
|
||||
: undefined,
|
||||
readonly: false,
|
||||
favorite: false,
|
||||
pinned: false,
|
||||
@@ -338,19 +330,38 @@ export class Notes implements ICollection {
|
||||
dateCreated: undefined,
|
||||
dateModified: undefined
|
||||
});
|
||||
if (!duplicateId) continue;
|
||||
|
||||
for (const relation of await this.db.relations
|
||||
.to(note, "notebook")
|
||||
.get()) {
|
||||
const contentId = await this.db.content.add({
|
||||
...clone(content),
|
||||
id: undefined,
|
||||
noteId: duplicateId,
|
||||
dateResolved: undefined,
|
||||
dateEdited: undefined,
|
||||
dateCreated: undefined,
|
||||
dateModified: undefined
|
||||
});
|
||||
|
||||
await this.db.notes.add({ id: duplicateId, contentId });
|
||||
|
||||
for (const relation of await this.db.relations.to(note).get()) {
|
||||
await this.db.relations.add(
|
||||
{ type: "notebook", id: relation.fromId },
|
||||
{ type: relation.fromType, id: relation.fromId },
|
||||
{
|
||||
id: duplicateId,
|
||||
type: "note"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const relation of await this.db.relations.from(note).get()) {
|
||||
await this.db.relations.add(
|
||||
{
|
||||
id: duplicateId,
|
||||
type: "note"
|
||||
},
|
||||
{ type: relation.toType, id: relation.toId }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ export class Relations implements ICollection {
|
||||
});
|
||||
}
|
||||
|
||||
from(
|
||||
reference: ItemReference | ItemReferences
|
||||
): RelationsArray<keyof RelatableTable>;
|
||||
from(
|
||||
reference: ItemReference | ItemReferences,
|
||||
types: (keyof RelatableTable)[]
|
||||
@@ -73,16 +76,19 @@ export class Relations implements ICollection {
|
||||
): RelationsArray<TType>;
|
||||
from<TType extends keyof RelatableTable = keyof RelatableTable>(
|
||||
reference: ItemReference | ItemReferences,
|
||||
type: TType | keyof RelatableTable[]
|
||||
type?: TType | keyof RelatableTable[]
|
||||
) {
|
||||
return new RelationsArray(
|
||||
this.db,
|
||||
reference,
|
||||
Array.isArray(type) ? type : [type],
|
||||
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
|
||||
"from"
|
||||
);
|
||||
}
|
||||
|
||||
to(
|
||||
reference: ItemReference | ItemReferences
|
||||
): RelationsArray<keyof RelatableTable>;
|
||||
to(
|
||||
reference: ItemReference | ItemReferences,
|
||||
types: (keyof RelatableTable)[]
|
||||
@@ -93,12 +99,12 @@ export class Relations implements ICollection {
|
||||
): RelationsArray<TType>;
|
||||
to<TType extends keyof RelatableTable = keyof RelatableTable>(
|
||||
reference: ItemReference | ItemReferences,
|
||||
type: TType | keyof RelatableTable[]
|
||||
type?: TType | keyof RelatableTable[]
|
||||
) {
|
||||
return new RelationsArray(
|
||||
this.db,
|
||||
reference,
|
||||
Array.isArray(type) ? type : [type],
|
||||
type ? (Array.isArray(type) ? type : ([type] as TType[])) : undefined,
|
||||
"to"
|
||||
);
|
||||
}
|
||||
@@ -224,21 +230,26 @@ const TABLE_MAP = {
|
||||
type RelatableTable = typeof TABLE_MAP;
|
||||
|
||||
class RelationsArray<TType extends keyof RelatableTable> {
|
||||
private table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
|
||||
|
||||
constructor(
|
||||
private readonly db: Database,
|
||||
private readonly reference: ItemReference | ItemReferences,
|
||||
private readonly types: TType[],
|
||||
private readonly types: TType[] | undefined,
|
||||
private readonly direction: "from" | "to"
|
||||
) {}
|
||||
|
||||
get selector() {
|
||||
if (!this.types)
|
||||
throw new Error("Cannot use selector when no tables are specified.");
|
||||
if (this.types.length > 1)
|
||||
throw new Error(
|
||||
"Cannot use selector when more than 1 tables are specified."
|
||||
);
|
||||
const table: ValueOf<RelatableTable> = TABLE_MAP[this.types[0]];
|
||||
return new FilteredSelector<ItemMap[TType]>(
|
||||
this.table,
|
||||
table,
|
||||
this.db
|
||||
.sql()
|
||||
.selectFrom<keyof DatabaseSchema>(this.table)
|
||||
.selectFrom<keyof DatabaseSchema>(table)
|
||||
.where("id", "in", (b) =>
|
||||
b
|
||||
.selectFrom("relations")
|
||||
@@ -356,10 +367,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
) => {
|
||||
if (this.direction === "to") {
|
||||
return builder
|
||||
.where(
|
||||
"fromType",
|
||||
this.types.length > 1 ? "in" : "==",
|
||||
this.types.length > 1 ? this.types : this.types[0]
|
||||
.$if(!!this.types, (eb) =>
|
||||
eb.where(
|
||||
"fromType",
|
||||
this.types!.length > 1 ? "in" : "==",
|
||||
this.types!.length > 1 ? this.types : this.types![0]
|
||||
)
|
||||
)
|
||||
.where("toType", "==", this.reference.type)
|
||||
.where(
|
||||
@@ -370,12 +383,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
: this.reference.id
|
||||
)
|
||||
.$if(
|
||||
this.types.includes("note" as TType) &&
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
this.types.includes("notebook" as TType) &&
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
(b) => b.where("fromId", "not in", this.db.trash.cache.notebooks)
|
||||
)
|
||||
@@ -383,10 +396,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
.$narrowType<{ id: string }>();
|
||||
} else {
|
||||
return builder
|
||||
.where(
|
||||
"toType",
|
||||
this.types.length > 1 ? "in" : "==",
|
||||
this.types.length > 1 ? this.types : this.types[0]
|
||||
.$if(!!this.types, (eb) =>
|
||||
eb.where(
|
||||
"toType",
|
||||
this.types!.length > 1 ? "in" : "==",
|
||||
this.types!.length > 1 ? this.types : this.types![0]
|
||||
)
|
||||
)
|
||||
.where("fromType", "==", this.reference.type)
|
||||
.where(
|
||||
@@ -397,12 +412,12 @@ class RelationsArray<TType extends keyof RelatableTable> {
|
||||
: this.reference.id
|
||||
)
|
||||
.$if(
|
||||
this.types.includes("note" as TType) &&
|
||||
!!this.types?.includes("note" as TType) &&
|
||||
this.db.trash.cache.notes.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.trash.cache.notes)
|
||||
)
|
||||
.$if(
|
||||
this.types.includes("notebook" as TType) &&
|
||||
!!this.types?.includes("notebook" as TType) &&
|
||||
this.db.trash.cache.notebooks.length > 0,
|
||||
(b) => b.where("toId", "not in", this.db.trash.cache.notebooks)
|
||||
)
|
||||
|
||||
@@ -184,7 +184,7 @@ export class Tiptap {
|
||||
this.data = new HTMLRewriter({
|
||||
ontag: (name, attr) => {
|
||||
const href = attr[ATTRIBUTES.href];
|
||||
if (name === "a" && href.startsWith("nn://")) {
|
||||
if (name === "a" && href && href.startsWith("nn://")) {
|
||||
const link = resolve(href);
|
||||
if (!link) return;
|
||||
attr[ATTRIBUTES.href] = link;
|
||||
@@ -244,7 +244,10 @@ export class Tiptap {
|
||||
if (types.includes("internalLinks")) {
|
||||
result.internalLinks.push(
|
||||
...findAll(
|
||||
(e) => e.tagName === "a" && e.attribs.href.startsWith("nn://"),
|
||||
(e) =>
|
||||
e.tagName === "a" &&
|
||||
!!e.attribs.href &&
|
||||
e.attribs.href.startsWith("nn://"),
|
||||
document.childNodes
|
||||
)
|
||||
.map((e) => parseInternalLink(e.attribs.href))
|
||||
|
||||
@@ -34,7 +34,8 @@ import {
|
||||
ColumnType,
|
||||
ExpressionBuilder,
|
||||
ReferenceExpression,
|
||||
Dialect
|
||||
Dialect,
|
||||
MigrationProvider
|
||||
} from "kysely";
|
||||
import {
|
||||
Attachment,
|
||||
@@ -58,8 +59,6 @@ import {
|
||||
Vault,
|
||||
isDeleted
|
||||
} from "../types";
|
||||
import { NNMigrationProvider } from "./migrations";
|
||||
import { createTriggers } from "./triggers";
|
||||
import { logger } from "../logger";
|
||||
|
||||
// type FilteredKeys<T, U> = {
|
||||
@@ -160,6 +159,7 @@ type AsyncOrSyncResult<Async extends boolean, Response> = Async extends true
|
||||
: Response;
|
||||
|
||||
export interface DatabaseCollection<T, IsAsync extends boolean> {
|
||||
type: keyof DatabaseSchema;
|
||||
clear(): Promise<void>;
|
||||
init(): Promise<void>;
|
||||
upsert(item: T): Promise<void>;
|
||||
@@ -257,8 +257,8 @@ const DataMappers: Partial<Record<ItemType, (row: any) => void>> = {
|
||||
}
|
||||
};
|
||||
|
||||
async function setupDatabase(
|
||||
db: Kysely<RawDatabaseSchema>,
|
||||
async function setupDatabase<Schema>(
|
||||
db: Kysely<Schema>,
|
||||
options: SQLiteOptions
|
||||
) {
|
||||
if (options.password)
|
||||
@@ -294,11 +294,14 @@ async function setupDatabase(
|
||||
);
|
||||
}
|
||||
|
||||
export async function initializeDatabase(db: Kysely<RawDatabaseSchema>) {
|
||||
export async function initializeDatabase<Schema>(
|
||||
db: Kysely<Schema>,
|
||||
migrationProvider: MigrationProvider
|
||||
) {
|
||||
try {
|
||||
const migrator = new Migrator({
|
||||
db,
|
||||
provider: new NNMigrationProvider()
|
||||
provider: migrationProvider
|
||||
});
|
||||
const { error, results } = await migrator.migrateToLatest();
|
||||
|
||||
@@ -313,8 +316,6 @@ export async function initializeDatabase(db: Kysely<RawDatabaseSchema>) {
|
||||
.join(", ")}`
|
||||
);
|
||||
|
||||
await createTriggers(db);
|
||||
|
||||
return db;
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to initialized database.");
|
||||
@@ -335,8 +336,14 @@ export type SQLiteOptions = {
|
||||
|
||||
skipInitialization?: boolean;
|
||||
};
|
||||
export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
const db = new Kysely<RawDatabaseSchema>({
|
||||
export async function createDatabase<Schema>(
|
||||
name: string,
|
||||
options: SQLiteOptions & {
|
||||
migrationProvider: MigrationProvider;
|
||||
onInit?: (db: Kysely<Schema>) => Promise<void>;
|
||||
}
|
||||
) {
|
||||
const db = new Kysely<Schema>({
|
||||
// log: (event) => {
|
||||
// if (event.queryDurationMillis > 5)
|
||||
// console.warn(event.query.sql, event.queryDurationMillis);
|
||||
@@ -344,7 +351,8 @@ export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
dialect: options.dialect(name, async () => {
|
||||
await db.connection().execute(async (db) => {
|
||||
await setupDatabase(db, options);
|
||||
await initializeDatabase(db);
|
||||
await initializeDatabase(db, options.migrationProvider);
|
||||
if (options.onInit) await options.onInit(db);
|
||||
});
|
||||
}),
|
||||
plugins: [new SqliteBooleanPlugin()]
|
||||
@@ -352,7 +360,8 @@ export async function createDatabase(name: string, options: SQLiteOptions) {
|
||||
if (!options.skipInitialization)
|
||||
await db.connection().execute(async (db) => {
|
||||
await setupDatabase(db, options);
|
||||
await initializeDatabase(db);
|
||||
await initializeDatabase(db, options.migrationProvider);
|
||||
if (options.onInit) await options.onInit(db);
|
||||
});
|
||||
|
||||
return db;
|
||||
|
||||
@@ -289,6 +289,26 @@ export class NNMigrationProvider implements MigrationProvider {
|
||||
async up(db) {
|
||||
await rebuildSearchIndex(db);
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
async up(db) {
|
||||
await db
|
||||
.updateTable("notes")
|
||||
.where("id", "in", (eb) =>
|
||||
eb
|
||||
.selectFrom("content")
|
||||
.select("noteId as id")
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("conflicted", "is", null),
|
||||
eb("conflicted", "==", false)
|
||||
])
|
||||
)
|
||||
.$castTo<string | null>()
|
||||
)
|
||||
.set({ conflicted: false })
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class SQLCachedCollection<
|
||||
startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
type: TCollectionType,
|
||||
public type: TCollectionType,
|
||||
eventManager: EventManager,
|
||||
sanitizer: Sanitizer
|
||||
) {
|
||||
|
||||
@@ -75,7 +75,7 @@ export class SQLCollection<
|
||||
_startTransaction: (
|
||||
executor: (tr: Kysely<DatabaseSchema>) => Promise<void>
|
||||
) => Promise<void>,
|
||||
private readonly type: TCollectionType,
|
||||
public readonly type: TCollectionType,
|
||||
private readonly eventManager: EventManager,
|
||||
private readonly sanitizer: Sanitizer
|
||||
) {}
|
||||
@@ -227,14 +227,16 @@ export class SQLCollection<
|
||||
ids: string[],
|
||||
partial: Partial<SQLiteItem<T>>,
|
||||
options: {
|
||||
sendEvent: boolean;
|
||||
sendEvent?: boolean;
|
||||
modify?: boolean;
|
||||
condition?: ExpressionOrFactory<
|
||||
DatabaseSchema,
|
||||
keyof DatabaseSchema,
|
||||
SqlBool
|
||||
>;
|
||||
} = { sendEvent: true }
|
||||
} = {}
|
||||
) {
|
||||
const { sendEvent = true, modify = true, condition } = options;
|
||||
if (!this.sanitizer.sanitize(this.type, partial)) return;
|
||||
|
||||
await this.db()
|
||||
@@ -244,16 +246,16 @@ export class SQLCollection<
|
||||
await tx
|
||||
.updateTable<keyof DatabaseSchema>(this.type)
|
||||
.where("id", "in", chunk)
|
||||
.$if(!!options.condition, (eb) => eb.where(options.condition!))
|
||||
.$if(!!condition, (eb) => eb.where(condition!))
|
||||
.set({
|
||||
...partial,
|
||||
dateModified: Date.now(),
|
||||
dateModified: modify ? Date.now() : undefined,
|
||||
synced: partial.synced || false
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
if (options.sendEvent) {
|
||||
if (sendEvent) {
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, <UpdateEvent>{
|
||||
type: "update",
|
||||
collection: this.type,
|
||||
@@ -284,6 +286,12 @@ export class SQLCollection<
|
||||
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||
.select((a) => a.fn.count<number>("id").as("count"))
|
||||
.where(isFalse("synced"))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
@@ -304,6 +312,12 @@ export class SQLCollection<
|
||||
.selectAll()
|
||||
.$if(lastRowId != null, (qb) => qb.where("id", ">", lastRowId!))
|
||||
.$if(!forceSync, (eb) => eb.where(isFalse("synced")))
|
||||
.$if(this.type === "content", (eb) =>
|
||||
eb.where("conflicted", "is", null)
|
||||
)
|
||||
.$if(this.type === "notes", (eb) =>
|
||||
eb.where("conflicted", "is not", true)
|
||||
)
|
||||
.$if(this.type === "attachments", (eb) =>
|
||||
eb.where((eb) =>
|
||||
eb.or([eb("dateUploaded", ">", 0), eb("deleted", "==", true)])
|
||||
|
||||
@@ -28,7 +28,9 @@ import {
|
||||
format,
|
||||
ILogger
|
||||
} from "@notesnook/logger";
|
||||
import { IStorage } from "./interfaces";
|
||||
import { Kysely, Migration, MigrationProvider } from "kysely";
|
||||
import { SQLiteOptions, createDatabase } from "./database";
|
||||
import { toChunks } from "./utils/array";
|
||||
|
||||
const WEEK = 86400000 * 7;
|
||||
|
||||
@@ -38,10 +40,46 @@ const WEEK = 86400000 * 7;
|
||||
// 3. Keep 7 days of logs
|
||||
// 4. Implement functions for log retrieval & filtering
|
||||
|
||||
type SQLiteItem<T> = {
|
||||
[P in keyof T]?: T[P] | null;
|
||||
};
|
||||
|
||||
type LogMessageWithDate = LogMessage & { date: string };
|
||||
export type LogDatabaseSchema = {
|
||||
logs: SQLiteItem<LogMessageWithDate>;
|
||||
};
|
||||
|
||||
class NNLogsMigrationProvider implements MigrationProvider {
|
||||
async getMigrations(): Promise<Record<string, Migration>> {
|
||||
return {
|
||||
"1": {
|
||||
async up(db) {
|
||||
await db.schema
|
||||
.createTable("logs")
|
||||
.addColumn("timestamp", "integer", (c) => c.notNull())
|
||||
.addColumn("message", "text", (c) => c.notNull())
|
||||
.addColumn("level", "integer", (c) => c.notNull())
|
||||
.addColumn("date", "text")
|
||||
.addColumn("scope", "text")
|
||||
.addColumn("extras", "text")
|
||||
.addColumn("elapsed", "integer")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("log_timestamp_index")
|
||||
.on("logs")
|
||||
.column("timestamp")
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseLogReporter {
|
||||
writer: DatabaseLogWriter;
|
||||
constructor(storage: IStorage) {
|
||||
this.writer = new DatabaseLogWriter(storage);
|
||||
constructor(db: Kysely<LogDatabaseSchema>) {
|
||||
this.writer = new DatabaseLogWriter(db);
|
||||
}
|
||||
|
||||
write(log: LogMessage) {
|
||||
@@ -50,10 +88,10 @@ class DatabaseLogReporter {
|
||||
}
|
||||
|
||||
class DatabaseLogWriter {
|
||||
private queue: Map<string, LogMessage> = new Map();
|
||||
private queue: LogMessageWithDate[] = [];
|
||||
private hasCleared = false;
|
||||
|
||||
constructor(private readonly storage: IStorage) {
|
||||
constructor(private readonly db: Kysely<LogDatabaseSchema>) {
|
||||
setInterval(() => {
|
||||
setTimeout(() => {
|
||||
if (!this.hasCleared) {
|
||||
@@ -66,86 +104,92 @@ class DatabaseLogWriter {
|
||||
}
|
||||
|
||||
push(message: LogMessage) {
|
||||
const key = new Date(message.timestamp).toLocaleDateString();
|
||||
this.queue.set(`${key}:${message.timestamp}`, message);
|
||||
const date = new Date(message.timestamp);
|
||||
(message as LogMessageWithDate).date = `${date.getFullYear()}-${
|
||||
date.getMonth() + 1
|
||||
}-${date.getDate()}`;
|
||||
this.queue.push(message as LogMessageWithDate);
|
||||
}
|
||||
|
||||
async flush() {
|
||||
if (this.queue.size === 0) return;
|
||||
const queueCopy = Array.from(this.queue.entries());
|
||||
this.queue = new Map();
|
||||
|
||||
await this.storage.writeMulti(queueCopy);
|
||||
if (this.queue.length === 0) return;
|
||||
const queueCopy = this.queue.slice();
|
||||
this.queue = [];
|
||||
for (const chunk of toChunks(queueCopy, 1000)) {
|
||||
await this.db.insertInto("logs").values(chunk).execute();
|
||||
}
|
||||
}
|
||||
|
||||
async rotate() {
|
||||
const logKeys = (await this.storage.getAllKeys()).sort();
|
||||
const keysToRemove = [];
|
||||
for (const key of logKeys) {
|
||||
const keyParts = key.split(":");
|
||||
if (keyParts.length === 1 || parseInt(keyParts[1]) < Date.now() - WEEK) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
|
||||
const range = Date.now() - WEEK;
|
||||
await this.db.deleteFrom("logs").where("timestamp", "<", range).execute();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseLogManager {
|
||||
constructor(private readonly storage: IStorage) {}
|
||||
constructor(private readonly db: Kysely<LogDatabaseSchema>) {}
|
||||
|
||||
async get() {
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
const logEntries = await this.storage.readMulti<LogMessage>(logKeys);
|
||||
const logs: Record<string, LogMessage[]> = {};
|
||||
const logs = await this.db
|
||||
.selectFrom("logs")
|
||||
.select([
|
||||
"timestamp",
|
||||
"message",
|
||||
"level",
|
||||
"scope",
|
||||
"extras",
|
||||
"elapsed",
|
||||
"date"
|
||||
])
|
||||
.execute();
|
||||
const groupedLogs: Record<string, LogMessage[]> = {};
|
||||
|
||||
for (const [logKey, log] of logEntries) {
|
||||
const keyParts = logKey.split(":");
|
||||
if (keyParts.length === 1) continue;
|
||||
|
||||
const key = keyParts[0];
|
||||
if (!logs[key]) logs[key] = [];
|
||||
logs[key].push(log);
|
||||
for (const log of logs) {
|
||||
const key = log.date!;
|
||||
if (!groupedLogs[key]) groupedLogs[key] = [];
|
||||
groupedLogs[key].push(log as LogMessage);
|
||||
}
|
||||
|
||||
return Object.keys(logs)
|
||||
return Object.keys(groupedLogs)
|
||||
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
|
||||
.map((key) => ({
|
||||
key,
|
||||
logs: logs[key]?.sort((a, b) => a.timestamp - b.timestamp)
|
||||
logs: groupedLogs[key]?.sort((a, b) => a.timestamp - b.timestamp)
|
||||
}));
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
await this.storage.removeMulti(logKeys);
|
||||
await this.db.deleteFrom("logs").execute();
|
||||
}
|
||||
|
||||
async delete(key: string) {
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
const keysToRemove = [];
|
||||
for (const logKey of logKeys) {
|
||||
const keyParts = logKey.split(":");
|
||||
if (keyParts.length === 1) continue;
|
||||
const currKey = keyParts[0];
|
||||
if (currKey === key) keysToRemove.push(logKey);
|
||||
}
|
||||
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
|
||||
await this.db.deleteFrom("logs").where("date", "==", key).execute();
|
||||
}
|
||||
|
||||
close() {
|
||||
return this.db.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
function initialize(storage: IStorage, disableConsoleLogs?: boolean) {
|
||||
if (storage) {
|
||||
const reporters: ILogReporter[] = [new DatabaseLogReporter(storage)];
|
||||
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
|
||||
reporters.push(consoleReporter);
|
||||
logger = new Logger({
|
||||
reporter: combineReporters(reporters),
|
||||
lastTime: Date.now()
|
||||
});
|
||||
logManager = new DatabaseLogManager(storage);
|
||||
}
|
||||
async function initialize(
|
||||
options: SQLiteOptions,
|
||||
disableConsoleLogs?: boolean
|
||||
) {
|
||||
const db = await createDatabase<LogDatabaseSchema>("notesnook-logs", {
|
||||
...options,
|
||||
migrationProvider: new NNLogsMigrationProvider()
|
||||
});
|
||||
|
||||
const reporters: ILogReporter[] = [new DatabaseLogReporter(db)];
|
||||
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
|
||||
reporters.push(consoleReporter);
|
||||
const instance = new Logger({
|
||||
reporter: combineReporters(reporters),
|
||||
lastTime: Date.now()
|
||||
});
|
||||
if (logger instanceof NoopLogger) logger.replaceWith(instance);
|
||||
logger = instance;
|
||||
logManager = new DatabaseLogManager(db);
|
||||
}
|
||||
|
||||
let logger: ILogger = new NoopLogger();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user