mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
1 Commits
ai
...
fix-notebo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a9483aae |
@@ -148,7 +148,7 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
apps/mobile/native/android/app/build/outputs/apk/release/notesnook-x86_64.apk
|
||||
|
||||
- name: Upload sourcemaps
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: sourcemaps
|
||||
path: |
|
||||
|
||||
2
.github/workflows/ios.publish.yml
vendored
2
.github/workflows/ios.publish.yml
vendored
@@ -98,7 +98,7 @@ jobs:
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Notesnook.zip
|
||||
path: |
|
||||
|
||||
6
.github/workflows/web.tests.yml
vendored
6
.github/workflows/web.tests.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
run: npm run build:test:web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-${{ matrix.shard }}
|
||||
|
||||
364
apps/desktop/package-lock.json
generated
364
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -29,7 +29,6 @@
|
||||
"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",
|
||||
@@ -113,6 +112,7 @@
|
||||
"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,6 +133,7 @@
|
||||
"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",
|
||||
@@ -146,6 +147,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -154,6 +156,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -162,6 +165,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -817,6 +821,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -828,6 +833,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -872,6 +878,7 @@
|
||||
"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",
|
||||
@@ -900,12 +907,14 @@
|
||||
"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=="
|
||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"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": "*"
|
||||
}
|
||||
@@ -919,7 +928,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-DZxSZWXxFfOlx7k7Rv4LAyiMroaxa3Ly/7OOzZO8cBNho0YzAi4qlbrx8W27JGqG57IgR/6J7r+nOJWw6kcvZA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@types/plist": {
|
||||
"version": "3.0.2",
|
||||
@@ -935,6 +945,7 @@
|
||||
"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": "*"
|
||||
}
|
||||
@@ -964,6 +975,7 @@
|
||||
"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": "*"
|
||||
@@ -1108,80 +1120,6 @@
|
||||
"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",
|
||||
@@ -1321,6 +1259,7 @@
|
||||
"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": {
|
||||
@@ -1371,6 +1310,7 @@
|
||||
"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": "*"
|
||||
}
|
||||
@@ -1446,6 +1386,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -1454,6 +1395,7 @@
|
||||
"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",
|
||||
@@ -1573,6 +1515,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -1626,22 +1569,6 @@
|
||||
"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",
|
||||
@@ -1708,7 +1635,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==",
|
||||
"devOptional": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/crc": {
|
||||
"version": "3.8.0",
|
||||
@@ -1719,33 +1646,6 @@
|
||||
"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",
|
||||
@@ -1838,6 +1738,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -1846,6 +1747,7 @@
|
||||
"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",
|
||||
@@ -1879,6 +1781,7 @@
|
||||
"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": {
|
||||
@@ -2008,6 +1911,7 @@
|
||||
"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",
|
||||
@@ -2047,19 +1951,6 @@
|
||||
"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",
|
||||
@@ -2133,6 +2024,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2154,6 +2046,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2168,6 +2061,7 @@
|
||||
"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": {
|
||||
@@ -2220,6 +2114,7 @@
|
||||
"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"
|
||||
@@ -2240,6 +2135,7 @@
|
||||
"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",
|
||||
@@ -2280,6 +2176,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2422,6 +2319,7 @@
|
||||
"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": {
|
||||
@@ -2436,6 +2334,7 @@
|
||||
"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",
|
||||
@@ -2451,6 +2350,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -2524,6 +2424,7 @@
|
||||
"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",
|
||||
@@ -2541,6 +2442,7 @@
|
||||
"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"
|
||||
@@ -2556,6 +2458,7 @@
|
||||
"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",
|
||||
@@ -2585,6 +2488,7 @@
|
||||
"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"
|
||||
@@ -2606,6 +2510,7 @@
|
||||
"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"
|
||||
@@ -2618,6 +2523,7 @@
|
||||
"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"
|
||||
@@ -2630,6 +2536,7 @@
|
||||
"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"
|
||||
@@ -2653,7 +2560,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "5.0.0",
|
||||
@@ -2673,6 +2581,7 @@
|
||||
"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"
|
||||
@@ -2839,13 +2748,6 @@
|
||||
"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",
|
||||
@@ -2941,7 +2843,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
@@ -2953,6 +2856,7 @@
|
||||
"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": {
|
||||
@@ -2982,125 +2886,37 @@
|
||||
"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"
|
||||
}
|
||||
@@ -3120,6 +2936,7 @@
|
||||
"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"
|
||||
@@ -3165,6 +2982,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -3290,6 +3108,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -3313,6 +3132,7 @@
|
||||
"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"
|
||||
@@ -3330,6 +3150,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -3392,7 +3213,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -3469,17 +3291,11 @@
|
||||
"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"
|
||||
}
|
||||
@@ -3519,6 +3335,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -3585,16 +3402,6 @@
|
||||
"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",
|
||||
@@ -3618,12 +3425,14 @@
|
||||
"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=="
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"dev": true
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -3644,6 +3453,7 @@
|
||||
"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",
|
||||
@@ -3723,12 +3533,14 @@
|
||||
"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"
|
||||
@@ -3744,6 +3556,7 @@
|
||||
"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"
|
||||
@@ -3896,6 +3709,7 @@
|
||||
"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": {
|
||||
@@ -3995,6 +3809,7 @@
|
||||
"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"
|
||||
},
|
||||
@@ -4196,7 +4011,8 @@
|
||||
"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=="
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.0",
|
||||
@@ -4345,48 +4161,12 @@
|
||||
"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.6",
|
||||
"version": "3.0.2",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -30,7 +30,6 @@
|
||||
"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",
|
||||
|
||||
@@ -38,7 +38,7 @@ await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
await exec(
|
||||
"yarn nx build:desktop @notesnook/web",
|
||||
"npx nx build:desktop @notesnook/web",
|
||||
path.join(__dirname, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
@@ -58,15 +58,15 @@ await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
});
|
||||
|
||||
if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle:mas`);
|
||||
await exec(`npm run bundle:mas`);
|
||||
} else {
|
||||
await exec(`yarn run bundle`);
|
||||
await exec(`npm run bundle`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
await exec(`npx tsc`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
await exec(`npx electron-builder --dir --x64`);
|
||||
if (process.platform === "win32") {
|
||||
await exec(`.\\output\\win-unpacked\\Notesnook.exe`);
|
||||
} else if (process.platform === "darwin") {
|
||||
|
||||
@@ -53,11 +53,11 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
await exec("npx electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`npm run bundle`);
|
||||
execAsync(`npx`, [`tsc`]);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
@@ -66,7 +66,7 @@ async function onChange(first) {
|
||||
|
||||
if (first) {
|
||||
await spawnAndWaitUntil(
|
||||
["yarn", "nx", "start:desktop", "@notesnook/web"],
|
||||
["npx", "nx", "start:desktop", "@notesnook/web"],
|
||||
path.join(__dirname, "..", "..", ".."),
|
||||
(data) => data.includes("Network: use --host to expose")
|
||||
);
|
||||
@@ -78,7 +78,7 @@ async function onChange(first) {
|
||||
}
|
||||
|
||||
execAsync(
|
||||
"yarn",
|
||||
"npx",
|
||||
["electron", path.join("build", "electron.js")],
|
||||
true,
|
||||
cleanup
|
||||
|
||||
@@ -58,11 +58,11 @@ export const osIntegrationRouter = t.router({
|
||||
|
||||
customDns: t.procedure.query(() => config.customDns),
|
||||
setCustomDns: t.procedure
|
||||
.input(z.boolean().optional())
|
||||
.input(z.boolean())
|
||||
.mutation(({ input: customDns }) => {
|
||||
if (customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
config.customDns = !!customDns;
|
||||
config.customDns = customDns;
|
||||
}),
|
||||
|
||||
proxyRules: t.procedure.query(() => config.proxyRules),
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 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,4 +21,3 @@ 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,7 +100,6 @@ async function createWindow() {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ 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 () => {
|
||||
@@ -41,4 +43,5 @@ process.once("loaded", async () => {
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -16,16 +16,15 @@ 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";
|
||||
@@ -45,28 +44,23 @@ I18nManager.swapLeftAndRightInRTL(false);
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
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);
|
||||
|
||||
//@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,7 +25,6 @@ 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
|
||||
@@ -44,8 +43,9 @@ 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 = "kBwr1Kre86ebOZ8ThLu2OA";
|
||||
const NOTESNOOK_DB_KEY_SALT = "SNuzOcEK3amoqL0WvPeKqw";
|
||||
const NOTESNOOK_APPLOCK_KEY_SALT = "3dqclWbOYllfk9kk";
|
||||
const NOTESNOOK_DB_KEY_SALT = "2rcgSprDmRvZ1AAa";
|
||||
const NOTESNOOK_USER_KEY_SALT = "7qO4qeoM6PbsAJ0Q";
|
||||
|
||||
const DB_KEY_CIPHER = "databaseKeyCipher";
|
||||
const USER_KEY_CIPHER = "userKeyCipher";
|
||||
@@ -117,7 +117,9 @@ 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);
|
||||
@@ -133,8 +135,10 @@ export async function validateAppLockPassword(appLockPassword) {
|
||||
try {
|
||||
const appLockCipher = CipherStorage.getMap(APPLOCK_CIPHER);
|
||||
if (!appLockCipher) return true;
|
||||
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
|
||||
const decrypted = await decrypt(key, appLockCipher);
|
||||
const decrypted = await decrypt(
|
||||
await Sodium.deriveKey(appLockPassword, NOTESNOOK_APPLOCK_KEY_SALT),
|
||||
appLockCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info(
|
||||
`validateAppLockPassword: ${typeof decrypted === "string"}`
|
||||
@@ -210,7 +214,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -223,7 +227,6 @@ 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;
|
||||
@@ -236,7 +239,7 @@ export async function deriveCryptoKey(data) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
salt: NOTESNOOK_USER_KEY_SALT
|
||||
},
|
||||
credentials.key
|
||||
);
|
||||
@@ -261,12 +264,13 @@ export async function getCryptoKey(_name) {
|
||||
|
||||
const key = await decrypt(
|
||||
{
|
||||
key: await getDatabaseKey(),
|
||||
salt: keyCipher.salt
|
||||
key: await getDatabaseKey()
|
||||
},
|
||||
keyCipher
|
||||
);
|
||||
|
||||
DatabaseLogger.info("User key decrypted: ", !!key);
|
||||
|
||||
return key;
|
||||
} catch (e) {
|
||||
console.log("getCryptoKey", e);
|
||||
|
||||
@@ -85,10 +85,4 @@ export async function setupDatabase(password) {
|
||||
}
|
||||
|
||||
export const db = database;
|
||||
let DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
|
||||
const setLogger = () => {
|
||||
DatabaseLogger = dbLogger.scope(Platform.OS);
|
||||
};
|
||||
|
||||
export { DatabaseLogger, setLogger };
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -16,27 +16,14 @@ 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 { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { KV } from "./storage";
|
||||
|
||||
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();
|
||||
};
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
export { initializeLogger };
|
||||
initialize(new KV(LoggerStorage));
|
||||
|
||||
export { LoggerStorage };
|
||||
|
||||
@@ -20,13 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React from "react";
|
||||
import { Linking, View } from "react-native";
|
||||
//import SettingsBackupAndRestore from '../../screens/settings/backup-restore';
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eCloseAnnouncementDialog } from "../../utils/events";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAnnouncementDialog, eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { PricingPlans } from "../premium/pricing-plans";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Progress } from "../sheets/progress";
|
||||
import { Button } from "../ui/button";
|
||||
import { allowedOnPlatform, getStyle } from "./functions";
|
||||
|
||||
@@ -54,6 +56,13 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
/>
|
||||
)
|
||||
});
|
||||
} else if (item.type === "force-sync") {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, true, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -34,13 +34,11 @@ 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";
|
||||
@@ -150,12 +148,6 @@ 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);
|
||||
@@ -196,7 +188,11 @@ const AppLockedOverlay = () => {
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
|
||||
if (
|
||||
SettingsService.get().privacyScreen ||
|
||||
SettingsService.getProperty("appLockEnabled")
|
||||
) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +209,6 @@ const AppLockedOverlay = () => {
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
||||
@@ -67,7 +67,7 @@ export const Login = ({ changeMode }) => {
|
||||
Progress.present();
|
||||
setTimeout(() => {
|
||||
if (!useUserStore.getState().syncing) {
|
||||
Sync.run("global", false, "full");
|
||||
Sync.run("global", false, true);
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
@@ -112,11 +112,7 @@ 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) => {
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
if (!complete) {
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
@@ -131,6 +127,7 @@ export const SessionExpired = () => {
|
||||
setVisible(false);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
let user = await db.user.getUser();
|
||||
if (!user) return;
|
||||
email.current = user.email;
|
||||
|
||||
@@ -44,7 +44,6 @@ 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,7 +174,6 @@ export const Dialog = ({ context = "global" }) => {
|
||||
onSubmit={onPressPositive}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
keyboardType={dialogInfo.keyboardType || "default"}
|
||||
placeholder={dialogInfo.inputPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -19,7 +19,6 @@ 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,
|
||||
@@ -45,7 +44,6 @@ 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";
|
||||
@@ -68,7 +66,6 @@ export const AppLockPassword = () => {
|
||||
confirmPassword?: string;
|
||||
}>({});
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
const [accountPass, setAccountPass] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const subs = [
|
||||
@@ -76,7 +73,6 @@ export const AppLockPassword = () => {
|
||||
eOpenAppLockPasswordDialog,
|
||||
(mode: "create" | "change" | "remove") => {
|
||||
setMode(mode);
|
||||
setAccountPass(false);
|
||||
setVisible(true);
|
||||
}
|
||||
),
|
||||
@@ -130,9 +126,7 @@ export const AppLockPassword = () => {
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
? `Remove app lock ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
icon="shield"
|
||||
@@ -175,39 +169,33 @@ export const AppLockPassword = () => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
defaultValue={values.current.password}
|
||||
keyboardType={
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
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}
|
||||
/>
|
||||
)
|
||||
<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={
|
||||
accountPass
|
||||
? "Account password"
|
||||
: mode === "change"
|
||||
mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
}
|
||||
@@ -234,36 +222,6 @@ 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
|
||||
@@ -291,8 +249,7 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const password = values.current.password;
|
||||
setAppLockVerificationCipher(password);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
} else if (mode === "change") {
|
||||
if (
|
||||
@@ -320,6 +277,7 @@ export const AppLockPassword = () => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.currentPassword
|
||||
);
|
||||
@@ -335,10 +293,8 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = values.current.password;
|
||||
await clearAppLockVerificationCipher();
|
||||
SettingsService.setProperty("appLockHasPasswordSecurity", true);
|
||||
await setAppLockVerificationCipher(password);
|
||||
await setAppLockVerificationCipher(values.current.password);
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
@@ -349,18 +305,14 @@ export const AppLockPassword = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = accountPass
|
||||
? await db.user.verifyPassword(values.current.password)
|
||||
: await validateAppLockPassword(values.current.password);
|
||||
const isCurrentPasswordCorrect = await validateAppLockPassword(
|
||||
values.current.password
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -390,9 +342,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
close();
|
||||
}}
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
}
|
||||
positiveTitle="Save"
|
||||
negativeTitle="Cancel"
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
|
||||
@@ -17,13 +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 React, { useState } from "react";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Image, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { Image as ImageType } from "react-native-image-crop-picker";
|
||||
import { useThemeColors } from "../../../../../../packages/theme/dist";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
@@ -67,14 +61,11 @@ export default function AttachImage({
|
||||
Attaching {response?.length} image(s):
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
{response?.map?.((item) => (
|
||||
{response?.map((item) => (
|
||||
<TouchableOpacity key={item.filename} activeOpacity={0.9}>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
Platform.OS === "ios"
|
||||
? item.sourceURL || item.path
|
||||
: item.path
|
||||
uri: item.sourceURL || item.path
|
||||
}}
|
||||
style={{
|
||||
width: 100,
|
||||
|
||||
@@ -586,7 +586,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
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);
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function List(props: ListProps) {
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, "full", () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
props.onRefresh?.();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
import Editor from "../../screens/editor";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } 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 { diff } from "diffblazer";
|
||||
import { ReadonlyEditor } from "../../screens/editor/readonly-editor";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -121,21 +121,12 @@ export default function NotePreview({ session, content, note }) {
|
||||
<ReadonlyEditor
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
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"
|
||||
);
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
loadContent({
|
||||
data: content.data,
|
||||
id: _note.id
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -19,13 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { Fragment, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View
|
||||
} from "react-native";
|
||||
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
import Share from "react-native-share";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
@@ -35,12 +29,14 @@ import { requestInAppReview } from "../../../services/app-review";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import Exporter from "../../../services/exporter";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Dialog } from "../../dialog";
|
||||
@@ -69,7 +65,6 @@ const ExportNotesSheet = ({
|
||||
filePath: string;
|
||||
name: string;
|
||||
type: string;
|
||||
fileDir: string;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
@@ -300,9 +295,7 @@ const ExportNotesSheet = ({
|
||||
successfully as {result?.fileName}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={
|
||||
Platform.OS === "android" ? "Open file location" : "Open"
|
||||
}
|
||||
title="Open"
|
||||
type="accent"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
@@ -312,23 +305,20 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result?.filePath) return;
|
||||
if (Platform.OS === "android") {
|
||||
Linking.openURL(result.fileDir).catch((e) => {
|
||||
ToastManager.error(e as Error);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(500);
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
FileViewer.open(result?.filePath, {
|
||||
showOpenWithDialog: true,
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
@@ -341,10 +331,10 @@ const ExportNotesSheet = ({
|
||||
borderRadius: 100
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!result) return;
|
||||
if (!result?.filePath) return;
|
||||
if (Platform.OS === "ios") {
|
||||
Share.open({
|
||||
url: result?.fileDir + result.fileName
|
||||
url: result.filePath
|
||||
}).catch(console.log);
|
||||
} else {
|
||||
FileViewer.open(result.filePath, {
|
||||
@@ -357,7 +347,7 @@ const ExportNotesSheet = ({
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
type="inverted"
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
|
||||
@@ -35,10 +35,7 @@ import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import {
|
||||
EditorEvents,
|
||||
editorController
|
||||
} from "../../../screens/editor/tiptap/utils";
|
||||
import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
|
||||
const ListNoteItem = ({
|
||||
id,
|
||||
@@ -193,13 +190,13 @@ export default function LinkNote(props: {
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
editorController.current?.postMessage(EditorEvents.resolve, {
|
||||
data: {
|
||||
editorController.current.commands.createInternalLink(
|
||||
{
|
||||
href: link,
|
||||
title: selectedNote.title
|
||||
},
|
||||
resolverId: props.resolverId
|
||||
});
|
||||
props.resolverId
|
||||
);
|
||||
};
|
||||
|
||||
const onSelectNote = async (note: Note) => {
|
||||
|
||||
@@ -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,11 +600,20 @@ export const useActions = ({
|
||||
});
|
||||
return;
|
||||
}
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
const html = (text || "").replace(/\n/g, "<br />");
|
||||
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 />");
|
||||
await Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: (item as Note).headline || text || "",
|
||||
message: (item as Note).headline || text,
|
||||
subtitle: "",
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
@@ -675,11 +684,15 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -759,7 +772,11 @@ export const useActions = ({
|
||||
} else {
|
||||
processingId.current = "copyContent";
|
||||
const text = await convertNoteToText(item as Note, true);
|
||||
Clipboard.setString(text || "");
|
||||
if (!text) {
|
||||
ToastManager.error(new Error(Errors.export("text")));
|
||||
return;
|
||||
}
|
||||
Clipboard.setString(text);
|
||||
processingId.current = undefined;
|
||||
ToastManager.show({
|
||||
heading: "Note copied to clipboard",
|
||||
|
||||
@@ -86,7 +86,6 @@ import { updateStatusBarColor } from "../utils/colors";
|
||||
import { BETA } from "../utils/constants";
|
||||
import {
|
||||
eCloseSheet,
|
||||
eEditorReset,
|
||||
eLoginSessionExpired,
|
||||
eOnLoadNote,
|
||||
eOpenAnnouncementDialog,
|
||||
@@ -201,13 +200,11 @@ const onRequestPartialSync = async (
|
||||
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
|
||||
);
|
||||
|
||||
await Sync.run(
|
||||
"global",
|
||||
force,
|
||||
full ? "full" : "send",
|
||||
undefined,
|
||||
lastSyncTime
|
||||
);
|
||||
if (full || force) {
|
||||
await Sync.run("global", force, full, undefined, lastSyncTime);
|
||||
} else {
|
||||
await Sync.run("global", false, false, undefined, lastSyncTime);
|
||||
}
|
||||
};
|
||||
|
||||
const onLogout = async (reason: string) => {
|
||||
@@ -236,7 +233,7 @@ async function checkForShareExtensionLaunchedInBackground() {
|
||||
if (notesAddedFromIntent || shareExtensionOpened) {
|
||||
const id = useTabStore.getState().getCurrentNoteId();
|
||||
const note = id && (await db.notes.note(id));
|
||||
eSendEvent(eEditorReset);
|
||||
eSendEvent("webview_reset");
|
||||
if (note) setTimeout(() => eSendEvent("loadingNote", note), 1);
|
||||
MMKV.removeItem("shareExtensionOpened");
|
||||
}
|
||||
@@ -533,14 +530,6 @@ export const useAppEvents = () => {
|
||||
}
|
||||
//@ts-ignore
|
||||
globalThis["IS_SHARE_EXTENSION"] = false;
|
||||
|
||||
if (
|
||||
SettingsService.getBackgroundEnterTime() + 60 * 1000 * 10 <
|
||||
Date.now()
|
||||
) {
|
||||
// Reset the editor if the app has been in background for more than 10 minutes.
|
||||
eSendEvent(eEditorReset);
|
||||
}
|
||||
} else {
|
||||
await saveEditorState();
|
||||
if (
|
||||
|
||||
@@ -22,14 +22,20 @@ 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";
|
||||
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
type PurchaseInfo = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku: string;
|
||||
discount: number;
|
||||
};
|
||||
|
||||
const skuInfos: { [name: string]: PurchaseInfo | undefined } = {};
|
||||
|
||||
export const usePricing = (period: "monthly" | "yearly") => {
|
||||
const [current, setCurrent] = useState<{
|
||||
period: string;
|
||||
info?: Product;
|
||||
info?: PurchaseInfo;
|
||||
product?: Subscription;
|
||||
}>();
|
||||
|
||||
@@ -48,7 +54,6 @@ 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)
|
||||
|
||||
@@ -39,7 +39,6 @@ import {
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import {
|
||||
eEditorReset,
|
||||
eOnLoadNote,
|
||||
eUnlockNote,
|
||||
eUnlockWithBiometrics,
|
||||
@@ -55,10 +54,8 @@ import { syncTabs, useTabStore } from "./tiptap/use-tab-store";
|
||||
import {
|
||||
editorController,
|
||||
editorState,
|
||||
openInternalLink,
|
||||
randId
|
||||
openInternalLink
|
||||
} from "./tiptap/utils";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
|
||||
const style: ViewStyle = {
|
||||
height: "100%",
|
||||
@@ -105,20 +102,24 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
const renderKey = useRef(randId("editor-id") + editorId);
|
||||
const renderKey = useRef(`editor-0` + editorId);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
useLockedNoteHandler();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current = randId("editor-id") + editorId;
|
||||
renderKey.current =
|
||||
renderKey.current === `editor-0`
|
||||
? `editor-1` + editorId
|
||||
: `editor-0` + editorId;
|
||||
|
||||
editor.state.current.ready = false;
|
||||
editor.setLoading(true);
|
||||
}, [editor, editorId]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [eSubscribeEvent(eEditorReset, onError)];
|
||||
const sub = [eSubscribeEvent("webview_reset", onError)];
|
||||
return () => {
|
||||
sub.forEach((s) => s?.unsubscribe());
|
||||
};
|
||||
@@ -145,7 +146,6 @@ const Editor = React.memo(
|
||||
nestedScrollEnabled
|
||||
onError={onError}
|
||||
injectedJavaScriptBeforeContentLoaded={`
|
||||
globalThis.__DEV__ = ${__DEV__}
|
||||
globalThis.readonly=${readonly};
|
||||
globalThis.noToolbar=${noToolbar};
|
||||
globalThis.noHeader=${noHeader};
|
||||
@@ -339,7 +339,7 @@ const useLockedNoteHandler = () => {
|
||||
}),
|
||||
eSubscribeEvent(eUnlockWithPassword, onSubmit)
|
||||
];
|
||||
if (tabRef.current?.locked && tabBarRef.current?.page() === 2) {
|
||||
if (tabRef.current?.locked) {
|
||||
unlock();
|
||||
}
|
||||
return () => {
|
||||
|
||||
@@ -75,16 +75,14 @@ export function ReadonlyEditor(props: {
|
||||
if (editorMessage.type === EventTypes.readonlyEditorLoaded) {
|
||||
console.log("Readonly editor loaded.");
|
||||
props.onLoad?.((content: { data: string; id: string }) => {
|
||||
setTimeout(() => {
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
}, 300);
|
||||
noteId.current = content.id;
|
||||
editorRef.current?.postMessage(
|
||||
JSON.stringify({
|
||||
type: "native:html",
|
||||
value: content.data
|
||||
})
|
||||
);
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (editorMessage.type === EventTypes.getAttachmentData) {
|
||||
const attachment = (editorMessage.value as any).attachment as Attachment;
|
||||
|
||||
@@ -48,6 +48,5 @@ export const EventTypes = {
|
||||
unlockWithBiometrics: "editor-events:unlock-biometrics",
|
||||
disableReadonlyMode: "editor-events:disable-readonly-mode",
|
||||
readonlyEditorLoaded: "readonlyEditorLoaded",
|
||||
error: "editorError",
|
||||
dbLogger: "editor-events:dbLogger"
|
||||
error: "editorError"
|
||||
};
|
||||
|
||||
@@ -41,7 +41,6 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { basename } from "pathe";
|
||||
|
||||
const showEncryptionSheet = (file: DocumentPickerResponse) => {
|
||||
presentSheet({
|
||||
@@ -109,6 +108,7 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("file uri: ", uri);
|
||||
uri = Platform.OS === "ios" ? santizeUri(uri) : uri;
|
||||
showEncryptionSheet(file);
|
||||
const hash = await Sodium.hashFile({
|
||||
@@ -164,14 +164,13 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastManager.show({
|
||||
heading: (e as Error).message,
|
||||
message: "You need internet access to attach a file",
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
DatabaseLogger.error(e);
|
||||
console.log("attachment error: ", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -185,15 +184,9 @@ const camera = async (options: PickerOptions) => {
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
maxFiles: 10,
|
||||
writeTempFile: true,
|
||||
compressImageQuality: 1
|
||||
writeTempFile: true
|
||||
})
|
||||
.then((response) => {
|
||||
handleImageResponse(
|
||||
Array.isArray(response) ? response : [response],
|
||||
options
|
||||
);
|
||||
})
|
||||
.then((response) => handleImageResponse(response, options))
|
||||
.catch((e) => {
|
||||
console.log("camera error: ", e);
|
||||
});
|
||||
@@ -216,15 +209,9 @@ const gallery = async (options: PickerOptions) => {
|
||||
mediaType: "photo",
|
||||
maxFiles: 10,
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
compressImageQuality: 1
|
||||
multiple: true
|
||||
})
|
||||
.then((response) =>
|
||||
handleImageResponse(
|
||||
Array.isArray(response) ? response : [response],
|
||||
options
|
||||
)
|
||||
)
|
||||
.then((response) => handleImageResponse(response, options))
|
||||
.catch((e) => {
|
||||
console.log("gallery error: ", e);
|
||||
});
|
||||
@@ -303,17 +290,7 @@ const handleImageResponse = async (
|
||||
type: "url"
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const fileName = image.filename || "image";
|
||||
console.log("attaching file...");
|
||||
if (!(await attachFile(uri, hash, image.mime, fileName, options))) return;
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ export type EditorState = {
|
||||
isAwaitingResult: boolean;
|
||||
scrollPosition: number;
|
||||
overlay?: boolean;
|
||||
initialLoadCalled?: boolean;
|
||||
};
|
||||
|
||||
export type Settings = {
|
||||
@@ -75,8 +74,6 @@ export type EditorMessage<T> = {
|
||||
type: string;
|
||||
noteId: string;
|
||||
tabId: number;
|
||||
resolverId?: string;
|
||||
hasTimeout?: boolean;
|
||||
};
|
||||
|
||||
export type SavePayload = {
|
||||
@@ -87,7 +84,6 @@ export type SavePayload = {
|
||||
sessionHistoryId?: number;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
pendingChanges?: boolean;
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
|
||||
@@ -77,6 +77,7 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { EditorEvents, editorState, openInternalLink } from "./utils";
|
||||
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user) {
|
||||
@@ -353,15 +354,8 @@ export const useEditorEvents = (
|
||||
const data = event.nativeEvent.data;
|
||||
const editorMessage = JSON.parse(data) as EditorMessage<any>;
|
||||
|
||||
if (editorMessage.hasTimeout && editorMessage.resolverId) {
|
||||
editor.postMessage(EditorEvents.resolve, {
|
||||
data: true,
|
||||
resolverId: editorMessage.resolverId
|
||||
});
|
||||
}
|
||||
|
||||
if (editorMessage.type === EventTypes.load) {
|
||||
DatabaseLogger.log("Editor is ready");
|
||||
console.log("Editor loaded");
|
||||
editor.onLoad();
|
||||
return;
|
||||
}
|
||||
@@ -389,36 +383,21 @@ export const useEditorEvents = (
|
||||
content: editorMessage.value.html as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
|
||||
pendingChanges: editorMessage.value?.pendingChanges
|
||||
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit
|
||||
});
|
||||
break;
|
||||
case EventTypes.title:
|
||||
DatabaseLogger.log("EventTypes.title");
|
||||
editor.saveContent({
|
||||
type: editorMessage.type,
|
||||
title: editorMessage.value?.title as string,
|
||||
title: editorMessage.value as string,
|
||||
noteId: noteId,
|
||||
tabId: editorMessage.tabId,
|
||||
ignoreEdit: false,
|
||||
pendingChanges: editorMessage.value?.pendingChanges
|
||||
ignoreEdit: false
|
||||
});
|
||||
break;
|
||||
case EventTypes.logger:
|
||||
logger.info("[EDITOR LOG]", editorMessage.value);
|
||||
break;
|
||||
case EventTypes.dbLogger:
|
||||
if (editorMessage.value.error) {
|
||||
DatabaseLogger.error(
|
||||
editorMessage.value.error,
|
||||
editorMessage.value.error,
|
||||
{
|
||||
message: "[EDITOR_ERROR]" + editorMessage.value.message
|
||||
}
|
||||
);
|
||||
} else {
|
||||
DatabaseLogger.info("[EDITOR_LOG]" + editorMessage.value.message);
|
||||
}
|
||||
logger.info("[WEBVIEW LOG]", editorMessage.value);
|
||||
break;
|
||||
case EventTypes.contentchange:
|
||||
editor.onContentChanged(editorMessage.noteId);
|
||||
@@ -506,17 +485,17 @@ export const useEditorEvents = (
|
||||
console.log(
|
||||
"Got attachment data:",
|
||||
!!data,
|
||||
editorMessage.resolverId
|
||||
(editorMessage.value as any).resolverId
|
||||
);
|
||||
editor.postMessage(EditorEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
DatabaseLogger.error(e);
|
||||
editor.postMessage(EditorEvents.resolve, {
|
||||
resolverId: editorMessage.resolverId,
|
||||
editor.postMessage(EditorEvents.attachmentData, {
|
||||
resolverId: (editorMessage.value as any).resolverId,
|
||||
data: undefined
|
||||
});
|
||||
});
|
||||
@@ -643,7 +622,7 @@ export const useEditorEvents = (
|
||||
case EventTypes.createInternalLink: {
|
||||
LinkNote.present(
|
||||
editorMessage.value.attributes,
|
||||
editorMessage.resolverId as string
|
||||
editorMessage.value.resolverId
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ import { DatabaseLogger, db } from "../../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -47,20 +46,16 @@ import {
|
||||
import Navigation from "../../../services/navigation";
|
||||
import Notifications from "../../../services/notifications";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { useTagStore } from "../../../stores/use-tag-store";
|
||||
import {
|
||||
eEditorReset,
|
||||
eEditorTabFocused,
|
||||
eOnLoadNote,
|
||||
eShowMergeDialog,
|
||||
eUpdateNoteInEditor
|
||||
} from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { unlockVault } from "../../../utils/unlock-vault";
|
||||
import { onNoteCreated } from "../../notes/common";
|
||||
import Commands from "./commands";
|
||||
import { EventTypes } from "./editor-events";
|
||||
import { SessionHistory } from "./session-history";
|
||||
import { EditorState, SavePayload } from "./types";
|
||||
import { syncTabs, useTabStore } from "./use-tab-store";
|
||||
@@ -73,7 +68,6 @@ import {
|
||||
isEditorLoaded,
|
||||
post
|
||||
} from "./utils";
|
||||
import { sleep } from "../../../utils/time";
|
||||
|
||||
type NoteWithContent = Note & {
|
||||
content?: NoteContent<false>;
|
||||
@@ -151,9 +145,8 @@ export const useEditor = (
|
||||
|
||||
useEffect(() => {
|
||||
const event = eSubscribeEvent(eEditorTabFocused, (tabId) => {
|
||||
console.log("Editot tab focus changed", lastTabFocused.current, tabId);
|
||||
if (lastTabFocused.current !== tabId) lock.current = false;
|
||||
lastTabFocused.current = tabId as number;
|
||||
console.log(tabId);
|
||||
});
|
||||
return () => {
|
||||
event?.unsubscribe();
|
||||
@@ -172,11 +165,10 @@ export const useEditor = (
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
overlay(true);
|
||||
state.current.ready = false;
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, overlay]);
|
||||
}, [loading]);
|
||||
|
||||
const withTimer = useCallback(
|
||||
(id: string, fn: () => void, duration: number) => {
|
||||
@@ -224,8 +216,7 @@ export const useEditor = (
|
||||
type,
|
||||
ignoreEdit,
|
||||
sessionHistoryId: currentSessionHistoryId,
|
||||
tabId,
|
||||
pendingChanges
|
||||
tabId
|
||||
}: SavePayload) => {
|
||||
if (currentNotes.current[id as string]?.readonly || readonly) return;
|
||||
try {
|
||||
@@ -286,26 +277,12 @@ export const useEditor = (
|
||||
);
|
||||
}, 50);
|
||||
|
||||
const saveTimer = setTimeout(() => {
|
||||
DatabaseLogger.log(`Note save timeout: ${id}...`);
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
"Copy your changes and restart the app to avoid data loss. If the issue persists, please report to us at support@streetwriters.co."
|
||||
),
|
||||
"Saving note is taking too long",
|
||||
"global",
|
||||
15000
|
||||
);
|
||||
}, 30 * 1000);
|
||||
|
||||
if (!locked) {
|
||||
DatabaseLogger.log(`Saving note: ${id}...`);
|
||||
id = await db.notes?.add({ ...noteData });
|
||||
saved = true;
|
||||
DatabaseLogger.log(`Note saved: ${id}...`);
|
||||
|
||||
clearTimeout(saveTimer);
|
||||
|
||||
if (!note && id) {
|
||||
editorSessionHistory.newSession(id);
|
||||
if (id) {
|
||||
@@ -339,31 +316,9 @@ export const useEditor = (
|
||||
Notifications.pinNote(id as string);
|
||||
}
|
||||
} else {
|
||||
if (!db.vault.unlocked) {
|
||||
if (pendingChanges) await sleep(3000);
|
||||
const unlocked = await unlockVault({
|
||||
title: "Unlock vault to save note",
|
||||
paragraph: `This note is locked, unlock to save ${
|
||||
pendingChanges ? "some pending" : ""
|
||||
} changes`,
|
||||
context: "global"
|
||||
});
|
||||
if (!unlocked)
|
||||
throw new Error("Could not save note, vault is locked");
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await db.vault?.save(noteData as any);
|
||||
}
|
||||
|
||||
if (id && useTabStore.getState().getTabForNote(id) === tabId) {
|
||||
@@ -386,16 +341,6 @@ export const useEditor = (
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
id &&
|
||||
id === useTabStore.getState().getCurrentNoteId() &&
|
||||
pendingChanges
|
||||
) {
|
||||
postMessage(EditorEvents.title, title || note?.title, tabId);
|
||||
postMessage(EditorEvents.html, data, tabId);
|
||||
currentNotes.current[id] = note;
|
||||
}
|
||||
|
||||
saveCount.current++;
|
||||
return id;
|
||||
} catch (e) {
|
||||
@@ -470,11 +415,7 @@ export const useEditor = (
|
||||
commands.focus(tabId);
|
||||
});
|
||||
} else {
|
||||
if (!event.item) {
|
||||
overlay(false);
|
||||
return;
|
||||
}
|
||||
console.log("LOADING NOTE", event.item.id);
|
||||
if (!event.item) return;
|
||||
const item = event.item;
|
||||
|
||||
const currentTab = useTabStore
|
||||
@@ -502,6 +443,7 @@ export const useEditor = (
|
||||
useTabStore.getState().focusTab(tabId);
|
||||
setTimeout(() => {
|
||||
if (blockIdRef.current) {
|
||||
console.log("scrolling to block", blockIdRef.current);
|
||||
commands.scrollIntoViewById(blockIdRef.current);
|
||||
blockIdRef.current = undefined;
|
||||
}
|
||||
@@ -560,7 +502,6 @@ export const useEditor = (
|
||||
loadingState.current === currentContents.current[item.id]?.data
|
||||
) {
|
||||
// If note is already loading, return.
|
||||
console.log("Note is already loading...");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -594,8 +535,11 @@ export const useEditor = (
|
||||
10000
|
||||
);
|
||||
|
||||
console.log("blockId", blockIdRef.current);
|
||||
|
||||
setTimeout(() => {
|
||||
if (blockIdRef.current) {
|
||||
console.log("scrolling to block", blockIdRef.current);
|
||||
commands.scrollIntoViewById(blockIdRef.current);
|
||||
blockIdRef.current = undefined;
|
||||
}
|
||||
@@ -629,130 +573,124 @@ export const useEditor = (
|
||||
data: Note | ContentItem | TrashItem | DeletedItem,
|
||||
isLocal?: boolean
|
||||
) => {
|
||||
try {
|
||||
await (async () => {
|
||||
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
|
||||
if (!data) return;
|
||||
if (SettingsService.get().disableRealtimeSync && !isLocal) return;
|
||||
if (!data) return;
|
||||
|
||||
if (isDeleted(data) || isTrashItem(data)) {
|
||||
const tabId = useTabStore.getState().getTabForNote(data.id);
|
||||
if (tabId !== undefined) {
|
||||
console.log("Removing tab");
|
||||
await commands.clearContent(tabId);
|
||||
useTabStore.getState().removeTab(tabId);
|
||||
if (isDeleted(data) || isTrashItem(data)) {
|
||||
const tabId = useTabStore.getState().getTabForNote(data.id);
|
||||
if (tabId !== undefined) {
|
||||
console.log("Removing tab");
|
||||
await commands.clearContent(tabId);
|
||||
useTabStore.getState().removeTab(tabId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId =
|
||||
(data as ContentItem).type === "tiptap"
|
||||
? (data as ContentItem).noteId
|
||||
: data.id;
|
||||
|
||||
if (!useTabStore.getState().hasTabForNote(noteId)) return;
|
||||
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
|
||||
|
||||
const tab = useTabStore.getState().getTab(tabId);
|
||||
|
||||
const note = data.type === "note" ? data : await db.notes?.note(noteId);
|
||||
|
||||
lock.current = true;
|
||||
|
||||
// Handle this case where note was locked on another device and synced.
|
||||
const locked = await db.vaults.itemExists(
|
||||
currentNotes.current[noteId] as ItemReference
|
||||
);
|
||||
|
||||
if (note) {
|
||||
if (!locked && tab?.noteLocked) {
|
||||
// Note lock removed.
|
||||
if (tab.locked) {
|
||||
if (useTabStore.getState().currentTab === tabId) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: false,
|
||||
noteLocked: false
|
||||
});
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
}
|
||||
} else if (!tab?.noteLocked && locked) {
|
||||
// Note lock added.
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: true,
|
||||
noteLocked: true
|
||||
});
|
||||
if (useTabStore.getState().currentTab !== tabId) {
|
||||
commands.clearContent(tabId);
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNotes.current[noteId]?.title !== note.title) {
|
||||
postMessage(EditorEvents.title, note.title, tabId);
|
||||
}
|
||||
commands.setTags(note);
|
||||
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
|
||||
commands.setStatus(
|
||||
getFormattedDate(note.dateEdited, "date-time"),
|
||||
"Saved",
|
||||
tabId as number
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
lock.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (locked && isEncryptedContent(data)) {
|
||||
const decryptedContent = await db.vault?.decryptContent(data, noteId);
|
||||
if (!decryptedContent) {
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: true,
|
||||
noteLocked: true
|
||||
});
|
||||
if (useTabStore.getState().currentTab !== tabId) {
|
||||
commands.clearContent(tabId);
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
} else {
|
||||
await postMessage(
|
||||
EditorEvents.updatehtml,
|
||||
decryptedContent.data,
|
||||
tabId
|
||||
);
|
||||
currentContents.current[note.id] = decryptedContent;
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContents.current?.data) {
|
||||
lock.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const noteId =
|
||||
(data as ContentItem).type === "tiptap"
|
||||
? (data as ContentItem).noteId
|
||||
: data.id;
|
||||
|
||||
if (!useTabStore.getState().hasTabForNote(noteId)) return;
|
||||
const tabId = useTabStore.getState().getTabForNote(noteId) as number;
|
||||
|
||||
const tab = useTabStore.getState().getTab(tabId);
|
||||
|
||||
const note =
|
||||
data.type === "note" ? data : await db.notes?.note(noteId);
|
||||
|
||||
lock.current = true;
|
||||
|
||||
// Handle this case where note was locked on another device and synced.
|
||||
const locked = await db.vaults.itemExists(
|
||||
currentNotes.current[noteId] as ItemReference
|
||||
);
|
||||
|
||||
if (note) {
|
||||
if (!locked && tab?.noteLocked) {
|
||||
// Note lock removed.
|
||||
if (tab.locked) {
|
||||
if (useTabStore.getState().currentTab === tabId) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
} else {
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: false,
|
||||
noteLocked: false
|
||||
});
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
}
|
||||
} else if (!tab?.noteLocked && locked) {
|
||||
// Note lock added.
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: true,
|
||||
noteLocked: true
|
||||
});
|
||||
if (useTabStore.getState().currentTab !== tabId) {
|
||||
commands.clearContent(tabId);
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNotes.current[noteId]?.title !== note.title) {
|
||||
postMessage(EditorEvents.title, note.title, tabId);
|
||||
}
|
||||
commands.setTags(note);
|
||||
if (currentNotes.current[noteId]?.dateEdited !== note.dateEdited) {
|
||||
commands.setStatus(
|
||||
getFormattedDate(note.dateEdited, "date-time"),
|
||||
"Saved",
|
||||
tabId as number
|
||||
);
|
||||
}
|
||||
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
readonly: note.readonly
|
||||
});
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
|
||||
if (!isEncryptedContent(data)) {
|
||||
currentContents.current[note.id] = data as UnencryptedContentItem;
|
||||
}
|
||||
|
||||
if (data.type === "tiptap" && note && !isLocal) {
|
||||
if (lastContentChangeTime.current[noteId] >= data.dateModified) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (locked && isEncryptedContent(data)) {
|
||||
const decryptedContent = await db.vault?.decryptContent(data);
|
||||
if (!decryptedContent) {
|
||||
useTabStore.getState().updateTab(tabId, {
|
||||
locked: true,
|
||||
noteLocked: true
|
||||
});
|
||||
if (useTabStore.getState().currentTab !== tabId) {
|
||||
commands.clearContent(tabId);
|
||||
commands.setLoading(true, tabId);
|
||||
}
|
||||
} else {
|
||||
await postMessage(
|
||||
EditorEvents.updatehtml,
|
||||
decryptedContent.data,
|
||||
tabId
|
||||
);
|
||||
currentContents.current[note.id] = decryptedContent;
|
||||
}
|
||||
} else {
|
||||
const _nextContent = data.data;
|
||||
if (_nextContent === currentContents.current[note.id]?.data) {
|
||||
return;
|
||||
}
|
||||
lastContentChangeTime.current[note.id] = note.dateEdited;
|
||||
await postMessage(EditorEvents.updatehtml, _nextContent, tabId);
|
||||
if (!isEncryptedContent(data)) {
|
||||
currentContents.current[note.id] =
|
||||
data as UnencryptedContentItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error, "Error when applying sync changes");
|
||||
} finally {
|
||||
lock.current = false;
|
||||
}
|
||||
}
|
||||
lock.current = false;
|
||||
},
|
||||
[postMessage, commands]
|
||||
);
|
||||
@@ -775,8 +713,7 @@ export const useEditor = (
|
||||
type,
|
||||
ignoreEdit,
|
||||
noteId,
|
||||
tabId,
|
||||
pendingChanges
|
||||
tabId
|
||||
}: {
|
||||
noteId?: string;
|
||||
title?: string;
|
||||
@@ -784,29 +721,18 @@ export const useEditor = (
|
||||
type: string;
|
||||
ignoreEdit: boolean;
|
||||
tabId: number;
|
||||
pendingChanges?: boolean;
|
||||
}) => {
|
||||
DatabaseLogger.log(
|
||||
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
|
||||
);
|
||||
DatabaseLogger.log(`Saving content...`);
|
||||
if (
|
||||
lock.current ||
|
||||
(currentLoadingNoteId.current &&
|
||||
currentLoadingNoteId.current === noteId)
|
||||
) {
|
||||
DatabaseLogger.log(`Skipped saving content:
|
||||
DatabaseLogger.log(`Skipped saving conent:
|
||||
|
||||
lock.current: ${lock.current}
|
||||
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
|
||||
`);
|
||||
if (lock.current) {
|
||||
setTimeout(() => {
|
||||
if (lock.current) {
|
||||
DatabaseLogger.warn("Editor force removed lock after 5 seconds");
|
||||
lock.current = false;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -814,7 +740,7 @@ export const useEditor = (
|
||||
lastContentChangeTime.current[noteId] = Date.now();
|
||||
}
|
||||
|
||||
if (type === EventTypes.content && noteId) {
|
||||
if (type === EditorEvents.content && noteId) {
|
||||
currentContents.current[noteId as string] = {
|
||||
data: content,
|
||||
type: "tiptap",
|
||||
@@ -829,8 +755,7 @@ export const useEditor = (
|
||||
id: noteId,
|
||||
ignoreEdit,
|
||||
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
|
||||
tabId: tabId,
|
||||
pendingChanges
|
||||
tabId: tabId
|
||||
};
|
||||
withTimer(
|
||||
noteId || "newnote",
|
||||
@@ -842,16 +767,7 @@ export const useEditor = (
|
||||
onChange(params.data);
|
||||
return;
|
||||
}
|
||||
if (useSettingStore.getState().isAppLoading) {
|
||||
const sub = useSettingStore.subscribe((state) => {
|
||||
if (!state.isAppLoading) {
|
||||
saveNote(params);
|
||||
sub();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
saveNote(params);
|
||||
}
|
||||
saveNote(params);
|
||||
},
|
||||
ignoreEdit ? 0 : 150
|
||||
);
|
||||
@@ -894,7 +810,7 @@ export const useEditor = (
|
||||
useTabStore.getState().currentTab
|
||||
))
|
||||
) {
|
||||
eSendEvent(eEditorReset, "onReady");
|
||||
eSendEvent("webview_reset", "onReady");
|
||||
return false;
|
||||
} else {
|
||||
syncTabs();
|
||||
@@ -904,6 +820,7 @@ export const useEditor = (
|
||||
}, [isDefaultEditor, restoreEditorState]);
|
||||
|
||||
const onLoad = useCallback(async () => {
|
||||
if (currentNotes.current) overlay(true);
|
||||
setTimeout(() => {
|
||||
postMessage(EditorEvents.theme, theme);
|
||||
});
|
||||
@@ -918,22 +835,12 @@ export const useEditor = (
|
||||
|
||||
const noteId = useTabStore.getState().getCurrentNoteId();
|
||||
if (!noteId) {
|
||||
overlay(false);
|
||||
loadNote({ newNote: true });
|
||||
if (tabBarRef.current?.page() === 1) {
|
||||
state.current.currentlyEditing = false;
|
||||
}
|
||||
} else if (state.current?.initialLoadCalled) {
|
||||
const note = currentNotes.current[noteId];
|
||||
if (note) {
|
||||
loadNote({
|
||||
item: note
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!state.current?.initialLoadCalled) {
|
||||
state.current.initialLoadCalled = true;
|
||||
}
|
||||
overlay(false);
|
||||
}, [
|
||||
postMessage,
|
||||
theme,
|
||||
|
||||
@@ -46,7 +46,7 @@ export function editorState() {
|
||||
return editorController.current?.state.current || defaultState;
|
||||
}
|
||||
|
||||
export const EditorEvents = {
|
||||
export const EditorEvents: { [name: string]: string } = {
|
||||
html: "native:html",
|
||||
updatehtml: "native:updatehtml",
|
||||
title: "native:title",
|
||||
@@ -55,8 +55,7 @@ export const EditorEvents = {
|
||||
logger: "native:logger",
|
||||
status: "native:status",
|
||||
keyboardShown: "native:keyboardShown",
|
||||
attachmentData: "native:attachment-data",
|
||||
resolve: "native:resolve"
|
||||
attachmentData: "native:attachment-data"
|
||||
};
|
||||
|
||||
export function randId(prefix: string) {
|
||||
|
||||
@@ -122,8 +122,7 @@ export default function DebugLogs() {
|
||||
colors.primary.paragraph,
|
||||
colors.error.paragraph,
|
||||
colors.static.black,
|
||||
colors.static.orange,
|
||||
colors.primary.border
|
||||
colors.static.orange
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -90,26 +90,13 @@ 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 {
|
||||
|
||||
@@ -458,7 +458,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
id: "background-sync",
|
||||
name: "Background sync (experimental)",
|
||||
description:
|
||||
"Periodically wake up the app in background to run sync.",
|
||||
"Periodically wake up the app in background to sync your notes from other devices.",
|
||||
type: "switch",
|
||||
property: "backgroundSync",
|
||||
onChange: (value) => {
|
||||
@@ -470,43 +470,22 @@ export const settingsGroups: SettingSection[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pull-sync",
|
||||
name: "Force pull changes",
|
||||
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.\n\nThese must only be used for troubleshooting. Using them regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
|
||||
modifer: () => {
|
||||
id: "sync-issues-fix",
|
||||
name: "Having problems with sync",
|
||||
description: "Try force sync to resolve issues with syncing",
|
||||
icon: "sync-alert",
|
||||
modifer: async () => {
|
||||
presentDialog({
|
||||
title: "Force Pull changes",
|
||||
title: "Force sync",
|
||||
paragraph:
|
||||
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
|
||||
"If your data on two devices is out of sync even after trying to sync normally. You can run force sync to solve such problems. Usually you should never need to run this otherwise. Force sync means that all your data on this device is reuploaded to the server.",
|
||||
negativeText: "Cancel",
|
||||
positiveText: "Start",
|
||||
positivePress: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, "fetch", () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "push-sync",
|
||||
name: "Force push changes",
|
||||
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.`,
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: "Force Push changes",
|
||||
paragraph:
|
||||
"This must only be used for troubleshooting. Using this regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.",
|
||||
negativeText: "Cancel",
|
||||
positiveText: "Start",
|
||||
positivePress: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Progress.present();
|
||||
Sync.run("global", true, "send", () => {
|
||||
Sync.run("global", true, true, () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
});
|
||||
}
|
||||
@@ -871,9 +850,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "switch",
|
||||
property: "appLockEnabled",
|
||||
onChange: () => {
|
||||
SettingsService.set({
|
||||
privacyScreen: true
|
||||
});
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
},
|
||||
onVerify: async () => {
|
||||
@@ -976,7 +952,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
SettingsService.getProperty("applockKeyboardType") === "numeric"
|
||||
? "pin"
|
||||
: "password"
|
||||
}, app lock will be disabled if no other security method is enabled.`,
|
||||
}, app lock will fallback to using account password to unlock the app`,
|
||||
hidden: () => {
|
||||
return !SettingsService.getProperty("appLockHasPasswordSecurity");
|
||||
},
|
||||
|
||||
@@ -162,7 +162,7 @@ export const ToastManager = {
|
||||
});
|
||||
},
|
||||
hide: () => eSendEvent(eHideToast),
|
||||
error: (e: Error, title?: string, context?: any, duration = 5000) => {
|
||||
error: (e: Error, title?: string, context?: any) => {
|
||||
ToastManager.show({
|
||||
heading: title,
|
||||
message: e?.message || "",
|
||||
@@ -176,7 +176,7 @@ export const ToastManager = {
|
||||
heading: "Logs copied!",
|
||||
type: "success",
|
||||
context: "global",
|
||||
duration: duration
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,26 +17,40 @@ 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 { decode, EntityLevel } from "entities";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import RNHTMLtoPDF from "react-native-html-to-pdf-lite";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
import { DatabaseLogger } from "../common/database/index";
|
||||
import { DatabaseLogger, db } from "../common/database/index";
|
||||
import Storage from "../common/database/storage";
|
||||
|
||||
import {
|
||||
exportNote as _exportNote,
|
||||
ExportableAttachment,
|
||||
ExportableNote,
|
||||
exportNotes
|
||||
exportNotes,
|
||||
sanitizeFilename
|
||||
} from "@notesnook/common";
|
||||
import { Note } from "@notesnook/core";
|
||||
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { basename, dirname, join } from "pathe";
|
||||
import downloadAttachment from "../common/filesystem/download-attachment";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import BiometricService from "./biometrics";
|
||||
import { ToastManager } from "./event-manager";
|
||||
import { cacheDir } from "../common/filesystem/utils";
|
||||
import { unlockVault } from "../utils/unlock-vault";
|
||||
|
||||
const MIMETypes = {
|
||||
txt: "text/plain",
|
||||
pdf: "application/pdf",
|
||||
md: "text/markdown",
|
||||
"md-frontmatter": "text/markdown",
|
||||
html: "text/html"
|
||||
};
|
||||
|
||||
const FolderNames: { [name: string]: string } = {
|
||||
txt: "Text",
|
||||
@@ -45,6 +59,16 @@ const FolderNames: { [name: string]: string } = {
|
||||
html: "Html"
|
||||
};
|
||||
|
||||
async function releasePermissions(path: string) {
|
||||
if (Platform.OS === "ios") return;
|
||||
const uris = await ScopedStorage.getPersistedUriPermissions();
|
||||
for (const uri of uris) {
|
||||
if (path.startsWith(uri)) {
|
||||
await ScopedStorage.releasePersistableUriPermission(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getPath(type: string) {
|
||||
let path =
|
||||
Platform.OS === "ios" &&
|
||||
@@ -58,11 +82,153 @@ async function getPath(type: string) {
|
||||
return path;
|
||||
}
|
||||
|
||||
async function unlockVaultForNoteExport() {
|
||||
return await unlockVault({
|
||||
title: "Unlock vault",
|
||||
paragraph: "Some exported notes are locked, Unlock to export them",
|
||||
context: "export-notes"
|
||||
async function save(
|
||||
path: string,
|
||||
data: string,
|
||||
fileName: string,
|
||||
extension: "txt" | "pdf" | "md" | "html" | "md-frontmatter"
|
||||
) {
|
||||
let uri;
|
||||
if (Platform.OS === "android") {
|
||||
uri = await ScopedStorage.writeFile(
|
||||
path,
|
||||
data,
|
||||
`${fileName}.${extension}`,
|
||||
MIMETypes[extension],
|
||||
extension === "pdf" ? "base64" : "utf8",
|
||||
false
|
||||
);
|
||||
await releasePermissions(path);
|
||||
} else {
|
||||
path = path + fileName + `.${extension}`;
|
||||
await RNFetchBlob.fs.writeFile(path, data, "utf8");
|
||||
}
|
||||
return uri || path;
|
||||
}
|
||||
|
||||
async function makeHtml(note: Note, content?: NoteContent<false>) {
|
||||
let html = await db.notes.export(note.id, {
|
||||
format: "html",
|
||||
contentItem: content
|
||||
});
|
||||
if (!html) return "";
|
||||
|
||||
html = decode(html, {
|
||||
level: EntityLevel.HTML
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
async function exportAs(
|
||||
type: string,
|
||||
note: Note,
|
||||
bulk?: boolean,
|
||||
content?: NoteContent<false>
|
||||
) {
|
||||
let data;
|
||||
switch (type) {
|
||||
case "html":
|
||||
{
|
||||
data = await makeHtml(note, content);
|
||||
}
|
||||
break;
|
||||
case "md":
|
||||
data = await db.notes.export(note.id, {
|
||||
format: "md",
|
||||
contentItem: content
|
||||
});
|
||||
break;
|
||||
case "md-frontmatter":
|
||||
data = await db.notes.export(note.id, {
|
||||
format: "md-frontmatter",
|
||||
contentItem: content
|
||||
});
|
||||
break;
|
||||
case "pdf":
|
||||
{
|
||||
const html = await makeHtml(note, content);
|
||||
const fileName = sanitizeFilename(note.title + Date.now(), {
|
||||
replacement: "_"
|
||||
});
|
||||
|
||||
const options = {
|
||||
html: html,
|
||||
fileName:
|
||||
Platform.OS === "ios" ? "/exported/PDF/" + fileName : fileName,
|
||||
width: 595,
|
||||
height: 852,
|
||||
bgColor: "#FFFFFF",
|
||||
padding: 30,
|
||||
base64: bulk || Platform.OS === "android"
|
||||
} as { [name: string]: any };
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
options.directory = "Documents";
|
||||
}
|
||||
const res = await RNHTMLtoPDF.convert(options);
|
||||
data = !bulk && Platform.OS === "ios" ? res.filePath : res.base64;
|
||||
if (bulk && res.filePath) {
|
||||
RNFetchBlob.fs.unlink(res.filePath);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "txt":
|
||||
{
|
||||
data = await db.notes.export(note.id, {
|
||||
format: "txt",
|
||||
contentItem: content
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function unlockVault() {
|
||||
const biometry = await BiometricService.isBiometryAvailable();
|
||||
const fingerprint = await BiometricService.hasInternetCredentials();
|
||||
if (biometry && fingerprint) {
|
||||
const credentials = await BiometricService.getCredentials(
|
||||
"Unlock vault",
|
||||
"Unlock vault to export locked notes"
|
||||
);
|
||||
if (credentials) {
|
||||
return db.vault.unlock(credentials.password);
|
||||
}
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
presentDialog({
|
||||
context: "export-notes",
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
positiveText: "Unlock",
|
||||
title: "Unlock vault",
|
||||
paragraph: "Some exported notes are locked, Unlock to export them",
|
||||
inputPlaceholder: "Enter password",
|
||||
positivePress: async (value) => {
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastManager.show({
|
||||
heading: "Invalid password",
|
||||
message: "Please enter a valid password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(unlocked);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
return true;
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,7 +285,6 @@ async function createZip(
|
||||
callback: (progress?: string) => void
|
||||
) {
|
||||
const fileName = `nn-export-${totalNotes}-${type}-${Date.now()}.zip`;
|
||||
const dir = path;
|
||||
try {
|
||||
callback("Creating zip");
|
||||
const zipOutputPath =
|
||||
@@ -127,7 +292,6 @@ async function createZip(
|
||||
? join(path, fileName)
|
||||
: join(RNFetchBlob.fs.dirs.CacheDir, fileName);
|
||||
await zip(cacheFolder, zipOutputPath);
|
||||
|
||||
callback("Saving zip file");
|
||||
if (Platform.OS === "android") {
|
||||
const file = await ScopedStorage.createFile(
|
||||
@@ -139,8 +303,6 @@ async function createZip(
|
||||
await copyFileAsync("file://" + zipOutputPath, path);
|
||||
await RNFetchBlob.fs.unlink(zipOutputPath);
|
||||
callback();
|
||||
} else {
|
||||
path = zipOutputPath;
|
||||
}
|
||||
RNFetchBlob.fs.unlink(cacheFolder);
|
||||
} catch (e) {
|
||||
@@ -149,7 +311,6 @@ async function createZip(
|
||||
|
||||
return {
|
||||
filePath: path,
|
||||
fileDir: dir,
|
||||
type: "application/zip",
|
||||
name: "zip",
|
||||
fileName: fileName,
|
||||
@@ -227,7 +388,7 @@ async function bulkExport(
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of exportNotes(notes, {
|
||||
format: type,
|
||||
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
@@ -271,7 +432,7 @@ async function exportNote(
|
||||
let currentAttachmentProgress = 0;
|
||||
for await (const item of _exportNote(note, {
|
||||
format: type,
|
||||
unlockVault: unlockVaultForNoteExport as () => Promise<boolean>
|
||||
unlockVault: unlockVault as () => Promise<boolean>
|
||||
})) {
|
||||
if (item instanceof Error) {
|
||||
DatabaseLogger.error(item);
|
||||
|
||||
@@ -59,14 +59,14 @@ const recoveryKeyMessage = {
|
||||
() => {
|
||||
eSendEvent(eOpenRecoveryKeyDialog);
|
||||
},
|
||||
false,
|
||||
true,
|
||||
async () => {
|
||||
SettingsService.set({
|
||||
recoveryKeySaved: true
|
||||
});
|
||||
clearMessage();
|
||||
},
|
||||
"Cancel"
|
||||
"I have saved my key already"
|
||||
);
|
||||
},
|
||||
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, true);
|
||||
let text = await convertNoteToText(note, false);
|
||||
if (!text) text = "";
|
||||
const html = text.replace(/\n/g, "<br />");
|
||||
Notifications.displayNotification({
|
||||
title: note.title,
|
||||
message: note.headline || text,
|
||||
subtitle: "",
|
||||
subtitle: note.headline || text,
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
actions: ["UNPIN"],
|
||||
|
||||
@@ -76,17 +76,6 @@ 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");
|
||||
@@ -94,17 +83,14 @@ function init() {
|
||||
if (!settingsJson) {
|
||||
MMKV.setString("appSettings", JSON.stringify(settings));
|
||||
} else {
|
||||
const settingsParsed = JSON.parse(settingsJson);
|
||||
migrateSettings(settingsParsed);
|
||||
settings = {
|
||||
...settings,
|
||||
...settingsParsed
|
||||
...JSON.parse(settingsJson)
|
||||
};
|
||||
}
|
||||
if (settings.fontScale) {
|
||||
scale.fontScale = settings.fontScale;
|
||||
}
|
||||
|
||||
setTimeout(() => setPrivacyScreen(settings), 1);
|
||||
updateSize();
|
||||
useSettingStore.getState().setSettings({ ...settings });
|
||||
@@ -112,7 +98,7 @@ function init() {
|
||||
}
|
||||
|
||||
function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
if (settings.privacyScreen) {
|
||||
if (settings.privacyScreen || settings.appLockEnabled) {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
@@ -205,11 +191,11 @@ function canLockAppInBackground() {
|
||||
}
|
||||
let backgroundEnterTime = 0;
|
||||
function appEnteredBackground() {
|
||||
backgroundEnterTime = Date.now();
|
||||
if (canLockAppInBackground()) {
|
||||
backgroundEnterTime = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
const getBackgroundEnterTime = () => backgroundEnterTime;
|
||||
|
||||
function shouldLockAppOnEnterForeground() {
|
||||
if (
|
||||
useUserStore.getState().disableAppLockRequests ||
|
||||
@@ -241,8 +227,7 @@ export const SettingsService = {
|
||||
shouldLockAppOnEnterForeground,
|
||||
canLockAppInBackground,
|
||||
appEnteredBackground,
|
||||
setPrivacyScreen,
|
||||
getBackgroundEnterTime
|
||||
setPrivacyScreen
|
||||
};
|
||||
|
||||
init();
|
||||
|
||||
@@ -32,24 +32,19 @@ export const ignoredMessages = [
|
||||
"WebSocket failed to connect",
|
||||
"Failed to start the HttpConnection before"
|
||||
];
|
||||
let pendingSync: any = undefined;
|
||||
let syncTimer: NodeJS.Timeout;
|
||||
|
||||
let pendingSync = undefined;
|
||||
let syncTimer = 0;
|
||||
const run = async (
|
||||
context = "global",
|
||||
forced = false,
|
||||
type: "full" | "send" | "fetch" = "full",
|
||||
onCompleted?: (status?: number) => void,
|
||||
lastSyncTime?: number
|
||||
full = true,
|
||||
onCompleted,
|
||||
lastSyncTime
|
||||
) => {
|
||||
if (useUserStore.getState().syncing) {
|
||||
DatabaseLogger.info("Sync in progress");
|
||||
pendingSync = {
|
||||
forced,
|
||||
type: type,
|
||||
context: context,
|
||||
onCompleted,
|
||||
lastSyncTime
|
||||
full: full
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -75,7 +70,7 @@ const run = async (
|
||||
) {
|
||||
initAfterSync();
|
||||
pendingSync = undefined;
|
||||
return onCompleted?.(SyncStatus.Failed);
|
||||
return onCompleted?.(false);
|
||||
}
|
||||
userstore.setSyncing(true);
|
||||
|
||||
@@ -85,8 +80,9 @@ const run = async (
|
||||
await BackgroundSync.doInBackground(async () => {
|
||||
try {
|
||||
await db.sync({
|
||||
type: type,
|
||||
force: forced
|
||||
type: full ? "full" : "send",
|
||||
force: forced,
|
||||
lastSyncTime
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
@@ -99,16 +95,14 @@ const run = async (
|
||||
} catch (e) {
|
||||
error = e;
|
||||
if (
|
||||
!ignoredMessages.find((message) =>
|
||||
(e as Error).message?.includes(message)
|
||||
) &&
|
||||
!ignoredMessages.find((message) => e.message?.includes(message)) &&
|
||||
userstore.user &&
|
||||
status.isConnected &&
|
||||
status.isInternetReachable
|
||||
) {
|
||||
userstore.setSyncing(false, SyncStatus.Failed);
|
||||
if (status.isConnected && status.isInternetReachable) {
|
||||
ToastManager.error(e as Error, "Sync failed", context);
|
||||
ToastManager.error(e, "Sync failed", context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,14 +115,7 @@ const run = async (
|
||||
);
|
||||
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
|
||||
setImmediate(() => {
|
||||
if (pendingSync)
|
||||
Sync.run(
|
||||
pendingSync.context,
|
||||
pendingSync.forced,
|
||||
pendingSync.type,
|
||||
pendingSync.onCompleted,
|
||||
pendingSync.lastSyncTime
|
||||
);
|
||||
if (pendingSync) Sync.run("global", false, pendingSync.full);
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
@@ -81,7 +81,6 @@ export type Settings = {
|
||||
biometricsAuthEnabled?: boolean;
|
||||
backgroundSync?: boolean;
|
||||
applockKeyboardType: "numeric" | "default";
|
||||
settingsVersion?: number;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -170,8 +169,7 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
markdownShortcuts: true,
|
||||
biometricsAuthEnabled: false,
|
||||
appLockHasPasswordSecurity: false,
|
||||
backgroundSync: true,
|
||||
settingsVersion: 0
|
||||
backgroundSync: true
|
||||
};
|
||||
|
||||
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 = false;
|
||||
export const BETA = true;
|
||||
|
||||
export const STORE_LINK =
|
||||
Platform.OS === "ios"
|
||||
|
||||
@@ -174,4 +174,3 @@ export const eUnlockWithPassword = "619";
|
||||
export const eUpdateNoteInEditor = "620";
|
||||
export const eOnEnterEditor = "621";
|
||||
export const eOnExitEditor = "622";
|
||||
export const eEditorReset = "623";
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { db } from "../common/database";
|
||||
import { presentDialog } from "../components/dialog/functions";
|
||||
import BiometricService from "../services/biometrics";
|
||||
import { ToastManager } from "../services/event-manager";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
|
||||
let unlockPromise: Promise<any> | undefined = undefined;
|
||||
export async function unlockVault({
|
||||
context,
|
||||
title,
|
||||
paragraph
|
||||
}: {
|
||||
context?: string;
|
||||
title: string;
|
||||
paragraph: string;
|
||||
}) {
|
||||
if (unlockPromise) {
|
||||
console.log("Unlocking.... waiting for unlock promise");
|
||||
return unlockPromise;
|
||||
}
|
||||
unlockPromise = new Promise(async (resolve) => {
|
||||
const result = await (async () => {
|
||||
if (db.vault.unlocked) return true;
|
||||
const biometry = await BiometricService.isBiometryAvailable();
|
||||
const fingerprint = await BiometricService.hasInternetCredentials();
|
||||
if (biometry && fingerprint) {
|
||||
const credentials = await BiometricService.getCredentials(
|
||||
title,
|
||||
paragraph
|
||||
);
|
||||
if (credentials) {
|
||||
return db.vault.unlock(credentials.password);
|
||||
}
|
||||
}
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(() => {
|
||||
presentDialog({
|
||||
context: context,
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
positiveText: "Unlock",
|
||||
title: title,
|
||||
paragraph: paragraph,
|
||||
inputPlaceholder: "Enter password",
|
||||
positivePress: async (value) => {
|
||||
const unlocked = await db.vault.unlock(value);
|
||||
if (!unlocked) {
|
||||
ToastManager.show({
|
||||
heading: "Invalid password",
|
||||
message: "Please enter a valid password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
resolve(unlocked);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
return true;
|
||||
},
|
||||
onClose: () => {
|
||||
resolve(false);
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
unlockPromise = undefined;
|
||||
resolve(result);
|
||||
});
|
||||
return unlockPromise;
|
||||
}
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3016
|
||||
versionCode 3010
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:node="remove"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<queries>
|
||||
|
||||
@@ -37,10 +37,4 @@ allprojects {
|
||||
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
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 = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
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 = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
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.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
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 = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
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.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
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 = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
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.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
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 = 2105;
|
||||
CURRENT_PROJECT_VERSION = 2099;
|
||||
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.8;
|
||||
MARKETING_VERSION = 3.0.2;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -350,8 +350,7 @@ PODS:
|
||||
- react-native-theme-switch-animation (0.6.0):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- React-Core
|
||||
- react-native-webview (13.10.0):
|
||||
- RCT-Folly (= 2021.07.22.00)
|
||||
- react-native-webview (11.26.1):
|
||||
- React-Core
|
||||
- React-NativeModulesApple (0.72.0):
|
||||
- React-callinvoker
|
||||
@@ -908,7 +907,7 @@ SPEC CHECKSUMS:
|
||||
react-native-share-extension: faed334b1ddf165f1e576fcabd3dc1c9e748bfa9
|
||||
react-native-sodium: 955bb0dc3ea05f8ea06d5e96cb89d1be7b5d7681
|
||||
react-native-theme-switch-animation: 220f883f7be290e79f2ab022093ed1a7a5929e6d
|
||||
react-native-webview: 90153193e679163ec257011fa457d02765120210
|
||||
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
|
||||
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
|
||||
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
|
||||
React-RCTActionSheet: 00b0a4c382a13b834124fa3f541a7d8d1d56efb9
|
||||
@@ -959,4 +958,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.14.2
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"react-native-swiper-flatlist": "3.2.2",
|
||||
"react-native-tooltips": "^1.0.3",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-webview": "^13.10.0",
|
||||
"react-native-webview": "^11.14.1",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-native-quick-sqlite": "^8.0.6",
|
||||
"react-native-theme-switch-animation": "^0.6.0",
|
||||
|
||||
12
apps/mobile/package-lock.json
generated
12
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.4",
|
||||
"version": "3.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -3234,7 +3234,6 @@
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"buffer": "^6.0.3",
|
||||
"framer-motion": "^10.16.8",
|
||||
"localforage": "^1.10.0",
|
||||
"mdi-react": "9.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -28496,7 +28495,7 @@
|
||||
"react-native-tooltips": "^1.0.3",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-webview": "^13.10.0",
|
||||
"react-native-webview": "^11.14.1",
|
||||
"react-native-zip-archive": "6.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45373,9 +45372,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webview": {
|
||||
"version": "13.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.10.0.tgz",
|
||||
"integrity": "sha512-bntBbc3JHBve17NL5fqBWPiOYYDz1VmYUPA0UbsUPHgZj6t7TXUQd4yn2yL//XFKXPDlvMO+MPB9E1uAYpEp/g==",
|
||||
"version": "11.26.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "2.0.0",
|
||||
"invariant": "2.2.4"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.8",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
index 5de0845..1b158d8 100644
|
||||
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
|
||||
@@ -692,6 +692,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
|
||||
image.putString("mime", options.outMimeType);
|
||||
image.putInt("size", (int) new File(compressedImagePath).length());
|
||||
image.putString("modificationDate", String.valueOf(modificationDate));
|
||||
+ image.putString("sourceURL", path);
|
||||
|
||||
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);
|
||||
169
apps/mobile/patches/react-native-image-picker+4.1.2.patch
Normal file
169
apps/mobile/patches/react-native-image-picker+4.1.2.patch
Normal file
@@ -0,0 +1,169 @@
|
||||
diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
|
||||
index 8e59033..c87febe 100644
|
||||
--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
|
||||
+++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/Utils.java
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.imagepicker;
|
||||
|
||||
import android.Manifest;
|
||||
+import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ContentResolver;
|
||||
@@ -21,6 +22,7 @@ import android.provider.OpenableColumns;
|
||||
import android.util.Base64;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
+import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
@@ -36,6 +38,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
+import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -266,7 +269,9 @@ public class Utils {
|
||||
MediaMetadataRetriever m = new MediaMetadataRetriever();
|
||||
m.setDataSource(context, uri);
|
||||
int duration = Math.round(Float.parseFloat(m.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))) / 1000;
|
||||
- m.release();
|
||||
+ try {
|
||||
+ m.release();
|
||||
+ } catch(Exception e) {}
|
||||
return duration;
|
||||
}
|
||||
|
||||
@@ -383,7 +388,7 @@ public class Utils {
|
||||
return fileUris;
|
||||
}
|
||||
|
||||
- static ReadableMap getImageResponseMap(Uri uri, Options options, Context context) {
|
||||
+ static WritableMap getImageResponseMap(Uri uri, Options options, Context context) {
|
||||
String fileName = uri.getLastPathSegment();
|
||||
int[] dimensions = getImageDimensions(uri, context);
|
||||
|
||||
@@ -418,13 +423,16 @@ public class Utils {
|
||||
|
||||
for(int i = 0; i < fileUris.size(); ++i) {
|
||||
Uri uri = fileUris.get(i);
|
||||
+ String fileName = getNameFromURI(context,uri);
|
||||
|
||||
if (isImageType(uri, context)) {
|
||||
if (uri.getScheme().contains("content")) {
|
||||
uri = getAppSpecificStorageUri(uri, context);
|
||||
}
|
||||
uri = resizeImage(uri, context, options);
|
||||
- assets.pushMap(getImageResponseMap(uri, options, context));
|
||||
+ WritableMap resMap = getImageResponseMap(uri, options, context);
|
||||
+ resMap.putString("originalFileName",fileName);
|
||||
+ assets.pushMap(resMap);
|
||||
} else if (isVideoType(uri, context)) {
|
||||
assets.pushMap(getVideoResponseMap(uri, context));
|
||||
} else {
|
||||
@@ -438,6 +446,32 @@ public class Utils {
|
||||
return response;
|
||||
}
|
||||
|
||||
+ /**
|
||||
+ * Return file name from Uri given.
|
||||
+ * @param context the context, cannot be null.
|
||||
+ * @param uri uri request for file name, cannot be null
|
||||
+ * @return the corresponding display name for file defined in uri or null if error occurs.
|
||||
+ */
|
||||
+ @SuppressLint("Range")
|
||||
+ static String getNameFromURI(@NonNull Context context, @NonNull Uri uri) {
|
||||
+ String result = null;
|
||||
+ Cursor c = null;
|
||||
+ try {
|
||||
+ c = context.getContentResolver().query(uri, null, null, null, null);
|
||||
+ c.moveToFirst();
|
||||
+ result = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||
+ }
|
||||
+ catch (Exception e){
|
||||
+ // error occurs
|
||||
+ }
|
||||
+ finally {
|
||||
+ if(c != null){
|
||||
+ c.close();
|
||||
+ }
|
||||
+ }
|
||||
+ return result;
|
||||
+ }
|
||||
+
|
||||
static ReadableMap getErrorMap(String errCode, String errMsg) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("errorCode", errCode);
|
||||
diff --git a/node_modules/react-native-image-picker/ios/ImagePickerManager.m b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
index b634ffb..18b8950 100644
|
||||
--- a/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
+++ b/node_modules/react-native-image-picker/ios/ImagePickerManager.m
|
||||
@@ -96,6 +96,39 @@ - (void) showPickerViewController:(UIViewController *)picker
|
||||
});
|
||||
}
|
||||
|
||||
+-(NSString *)cachesDirectoryName
|
||||
+{
|
||||
+ static NSString *cachePath = nil;
|
||||
+ if(!cachePath) {
|
||||
+ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
+ cachePath = [paths objectAtIndex:0];
|
||||
+ }
|
||||
+
|
||||
+ return cachePath;
|
||||
+}
|
||||
+
|
||||
+///
|
||||
+/// Builds paths for identifiers in the cache directory
|
||||
+///
|
||||
+-(NSString *)pathForName:(NSString *)name
|
||||
+{
|
||||
+ NSString *cachePath = [self cachesDirectoryName];
|
||||
+ NSString *path = [cachePath stringByAppendingPathComponent:name];
|
||||
+ return path;
|
||||
+}
|
||||
+
|
||||
+#pragma mark - NSData Cache methods
|
||||
+
|
||||
+///
|
||||
+/// Saves the given data to the cache directory
|
||||
+///
|
||||
+-(NSString *)saveToCacheDirectory:(NSData *)data withName:(NSString *)name
|
||||
+{
|
||||
+ NSString *path = [self pathForName:name];
|
||||
+ [data writeToFile:path atomically:YES];
|
||||
+ return path;
|
||||
+}
|
||||
+
|
||||
#pragma mark - Helpers
|
||||
|
||||
-(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
|
||||
@@ -121,22 +154,14 @@ -(NSMutableDictionary *)mapImageToAsset:(UIImage *)image data:(NSData *)data {
|
||||
asset[@"type"] = [@"image/" stringByAppendingString:fileType];
|
||||
|
||||
NSString *fileName = [self getImageFileName:fileType];
|
||||
- NSString *path = [[NSTemporaryDirectory() stringByStandardizingPath] stringByAppendingPathComponent:fileName];
|
||||
- [data writeToFile:path atomically:YES];
|
||||
+ NSString *path = [self saveToCacheDirectory:data withName:fileName];
|
||||
|
||||
if ([self.options[@"includeBase64"] boolValue]) {
|
||||
asset[@"base64"] = [data base64EncodedStringWithOptions:0];
|
||||
}
|
||||
|
||||
- NSURL *fileURL = [NSURL fileURLWithPath:path];
|
||||
- asset[@"uri"] = [fileURL absoluteString];
|
||||
-
|
||||
- NSNumber *fileSizeValue = nil;
|
||||
- NSError *fileSizeError = nil;
|
||||
- [fileURL getResourceValue:&fileSizeValue forKey:NSURLFileSizeKey error:&fileSizeError];
|
||||
- if (fileSizeValue){
|
||||
- asset[@"fileSize"] = fileSizeValue;
|
||||
- }
|
||||
+ asset[@"uri"] = path;
|
||||
+ asset[@"fileSize"] = [NSNumber numberWithInteger:[data length]];
|
||||
|
||||
asset[@"fileName"] = fileName;
|
||||
asset[@"width"] = @(image.size.width);
|
||||
@@ -311,7 +311,8 @@ const ShareView = () => {
|
||||
type: "tiptap"
|
||||
},
|
||||
id: note.id,
|
||||
sessionId: Date.now()
|
||||
sessionId: Date.now(),
|
||||
title: noteTitle.current
|
||||
};
|
||||
} else {
|
||||
noteData = { ...note };
|
||||
@@ -486,42 +487,25 @@ const ShareView = () => {
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
{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();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<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();
|
||||
}}
|
||||
/>
|
||||
<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.2.0",
|
||||
"version": "1.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/theme-builder",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.1",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -18,18 +18,17 @@
|
||||
"@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",
|
||||
@@ -979,7 +978,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -2027,15 +2026,6 @@
|
||||
"@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,
|
||||
@@ -2108,30 +2098,25 @@
|
||||
},
|
||||
"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=="
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"dev": true
|
||||
},
|
||||
"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.2.0",
|
||||
"version": "1.1.1",
|
||||
"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,10 +2,19 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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" />
|
||||
<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" />
|
||||
<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
|
||||
@@ -40,26 +49,13 @@
|
||||
/>
|
||||
<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>
|
||||
@@ -67,6 +63,21 @@
|
||||
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);
|
||||
@@ -77,19 +88,6 @@
|
||||
--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;
|
||||
@@ -97,7 +95,7 @@
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
70% {
|
||||
@@ -105,7 +103,7 @@
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +128,20 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@keyframes fadeUp {
|
||||
0% {
|
||||
transform: translateY(500px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
80% {
|
||||
transform: translateY(0px);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* svg {
|
||||
@@ -162,40 +171,85 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="theme-scope-base">
|
||||
<body>
|
||||
<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>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="height: 0; width: 0; z-index: -1; position: absolute"
|
||||
<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;
|
||||
"
|
||||
>
|
||||
<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)">
|
||||
<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" />
|
||||
<path
|
||||
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"
|
||||
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"
|
||||
/>
|
||||
</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,59 +18,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { init } from "@notesnook/web/src/bootstrap";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
ErrorBoundary,
|
||||
ErrorComponent
|
||||
} from "@notesnook/web/src/components/error-boundary";
|
||||
import { render } from "react-dom";
|
||||
import { BaseThemeProvider } from "@notesnook/web/src/components/theme-provider";
|
||||
import { App } from "./app";
|
||||
|
||||
renderApp();
|
||||
|
||||
async function renderApp() {
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) return;
|
||||
const root = createRoot(rootElement);
|
||||
try {
|
||||
const { component, props } = await init();
|
||||
const { component, props } = await init();
|
||||
|
||||
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()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -30,5 +30,4 @@ dist
|
||||
public/workbox
|
||||
scripts/secrets
|
||||
test-results
|
||||
.swc
|
||||
public/models
|
||||
.swc
|
||||
@@ -1 +1 @@
|
||||
53ad8e4e40ebebd0f400498dTest note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)
|
||||
Test note 1 (9) Test note 1 (8) Test note 1 (7) Test note 1 (6) Test note 1 (5) Test note 1 (4) Test note 1 (3) Test note 1 (2) Test note 1 (1) Test note 1 (0) 53ad8e4e40ebebd0f400498d
|
||||
@@ -1 +1 @@
|
||||
f054d19e9a2f46eff7b9bb25Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)
|
||||
Test note 2 (9)Test note 2 (8)Test note 2 (7)Test note 2 (6)Test note 2 (5)Test note 2 (4)Test note 2 (3)Test note 2 (2)Test note 2 (1)Test note 2 (0)f054d19e9a2f46eff7b9bb25
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 94 KiB |
@@ -1 +1 @@
|
||||
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1An edit I made
|
||||
An edit I madeThis is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
|
||||
@@ -262,7 +262,7 @@ test("using Shift+Click when no notes are selected should not crash the app", as
|
||||
info.setTimeout(60 * 1000);
|
||||
|
||||
const { notes } = await populateList(page, 5);
|
||||
await notes.focus();
|
||||
await page.reload();
|
||||
|
||||
const note = await notes.findNote({ title: "Test note 3" });
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export class AppModel {
|
||||
);
|
||||
}
|
||||
|
||||
async waitForSync(state: "completed" | "synced" | "syncing" = "completed") {
|
||||
async waitForSync(state: "completed" | "synced" = "completed") {
|
||||
await this.page
|
||||
.locator(getTestId(`sync-status-${state}`))
|
||||
.waitFor({ state: "visible" });
|
||||
|
||||
@@ -259,15 +259,14 @@ test("unlock a note for editing", async ({ page }) => {
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await page.reload();
|
||||
await notes.waitForList();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const newContent = `${NOTE.content}${content}`;
|
||||
const editedNote = await notes.findNote({
|
||||
title: NOTE.title,
|
||||
content: newContent
|
||||
});
|
||||
if (!editedNote) throw new Error("Could not find note.");
|
||||
await editedNote.openLockedNote(PASSWORD);
|
||||
await editedNote?.openLockedNote(PASSWORD);
|
||||
await notes.editor.waitForLoading();
|
||||
expect(await notes.editor.getContent("text")).toContain(newContent);
|
||||
});
|
||||
@@ -285,9 +284,11 @@ test("change title of a locked note", async ({ page }) => {
|
||||
await page.waitForTimeout(150);
|
||||
|
||||
await page.reload();
|
||||
await notes.waitForList();
|
||||
const editedNote = await notes.findNote({ title });
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const editedNote = await notes.findNote({ title, content: NOTE.content });
|
||||
await editedNote?.openLockedNote(PASSWORD);
|
||||
await notes.editor.waitForLoading();
|
||||
expect(await note?.getTitle()).toContain(title);
|
||||
expect(await notes.editor.getTitle()).toContain(title);
|
||||
});
|
||||
|
||||
@@ -17,25 +17,12 @@
|
||||
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
|
||||
|
||||
<style>
|
||||
|
||||
.image-container {
|
||||
display: block;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.image-container.align-right {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.image-container.align-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.image-container.float {
|
||||
float: left;
|
||||
}
|
||||
.image-container.float.align-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: transparent !important;
|
||||
color: #202124;
|
||||
@@ -179,6 +166,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test 1</h1>
|
||||
<p data-block-id="xxx" data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
|
||||
<p data-spacing="double">This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { test, Browser, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { USER } from "./utils";
|
||||
import exp from "constants";
|
||||
|
||||
async function createDevice(browser: Browser) {
|
||||
// Create two isolated browser contexts
|
||||
@@ -39,9 +40,7 @@ async function actAndSync<T>(
|
||||
) {
|
||||
const results = await Promise.all([
|
||||
...actions.filter((a) => !!a),
|
||||
...devices.map((d) =>
|
||||
d.waitForSync("syncing").then(() => d.waitForSync("synced"))
|
||||
)
|
||||
...devices.map((d) => d.waitForSync("synced"))
|
||||
]);
|
||||
|
||||
await Promise.all(devices.map((d) => d.page.waitForTimeout(2000)));
|
||||
@@ -62,7 +61,6 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
|
||||
createDevice(browser),
|
||||
createDevice(browser)
|
||||
]);
|
||||
|
||||
const [notesA, notesB] = await Promise.all(
|
||||
[deviceA, deviceB].map((d) => d.goToNotes())
|
||||
);
|
||||
@@ -74,17 +72,14 @@ test(`edits in a note opened on 2 devices should sync in real-time`, async ({
|
||||
|
||||
if ((await notesB.editor.getContent("text")) !== "")
|
||||
await actAndSync([deviceA, deviceB], notesB.editor.clear());
|
||||
await expect(notesA.editor.content).toBeEmpty();
|
||||
await expect(notesB.editor.content).toBeEmpty();
|
||||
await expect(notesA.editor.content).toHaveText("");
|
||||
await expect(notesB.editor.content).toHaveText("");
|
||||
await actAndSync([deviceA, deviceB], notesB.editor.setContent(newContent));
|
||||
|
||||
expect(noteA).toBeDefined();
|
||||
expect(noteB).toBeDefined();
|
||||
await expect(notesA.editor.content).toHaveText(newContent);
|
||||
await expect(notesB.editor.content).toHaveText(newContent);
|
||||
|
||||
await (await deviceA.goToSettings())?.logout();
|
||||
await (await deviceB.goToSettings())?.logout();
|
||||
});
|
||||
|
||||
function makeid(length: number) {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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.6",
|
||||
"version": "3.0.2",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -38,7 +38,6 @@
|
||||
"@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",
|
||||
@@ -61,7 +60,6 @@
|
||||
"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",
|
||||
@@ -132,9 +130,6 @@
|
||||
"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,7 +26,6 @@ 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) {
|
||||
@@ -59,7 +58,7 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
database.setup({
|
||||
sqliteOptions: {
|
||||
dialect: (name, init) => createDialect(name, true, init),
|
||||
dialect: createDialect,
|
||||
...(IS_DESKTOP_APP || isFeatureSupported("opfs")
|
||||
? { journalMode: "WAL", lockingMode: "exclusive" }
|
||||
: {
|
||||
@@ -100,11 +99,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
|
||||
await db.init();
|
||||
|
||||
window.addEventListener("beforeunload", async () => {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await db.sql().destroy();
|
||||
await logManager?.close();
|
||||
}
|
||||
window.addEventListener("beforeunload", () => {
|
||||
if (IS_DESKTOP_APP) db.sql().destroy();
|
||||
});
|
||||
|
||||
logger.measure("Database initialization");
|
||||
|
||||
@@ -31,8 +31,6 @@ import {
|
||||
} from "@notesnook/common";
|
||||
import Vault from "./vault";
|
||||
import { ExportStream } from "../utils/streams/export-stream";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { confirm } from "./dialog-controller";
|
||||
|
||||
export async function exportToPDF(
|
||||
title: string,
|
||||
@@ -81,36 +79,20 @@ export async function exportNotes(
|
||||
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
|
||||
notes: FilteredSelector<Note>
|
||||
): Promise<boolean> {
|
||||
const result = await TaskManager.startTask({
|
||||
return await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: "Exporting notes",
|
||||
subtitle: "Please wait while your notes are exported.",
|
||||
action: async (report) => {
|
||||
const errors: Error[] = [];
|
||||
const exportStream = new ExportStream(report, (e) => errors.push(e));
|
||||
await fromAsyncIterator(
|
||||
_exportNotes(notes, { format, unlockVault: Vault.unlockVault })
|
||||
)
|
||||
.pipeThrough(exportStream)
|
||||
.pipeThrough(new ExportStream(report))
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(await createWriteStream("notes.zip"));
|
||||
return {
|
||||
errors,
|
||||
count: exportStream.progress
|
||||
};
|
||||
return true;
|
||||
}
|
||||
});
|
||||
confirm({
|
||||
title: `Exported ${result.count} notes`,
|
||||
message:
|
||||
result.errors.length > 0
|
||||
? `Export completed with ${result.errors.length} errors:
|
||||
|
||||
${result.errors.map((e, i) => `${i + 1}. ${e.message}`).join("\n")}`
|
||||
: "Export completed with 0 errors.",
|
||||
positiveButtonText: "Okay"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const FORMAT_TO_EXT = {
|
||||
@@ -148,9 +130,7 @@ export async function exportNote(
|
||||
unlockVault: Vault.unlockVault
|
||||
})
|
||||
)
|
||||
.pipeThrough(
|
||||
new ExportStream(report, (e) => showToast("error", e.message))
|
||||
)
|
||||
.pipeThrough(new ExportStream(report))
|
||||
.pipeThrough(createZipStream())
|
||||
.pipeTo(
|
||||
await createWriteStream(
|
||||
|
||||
@@ -44,272 +44,11 @@ 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: 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()
|
||||
onClick: () => useEditorStore.getState().newSession()
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
@@ -338,19 +77,12 @@ export async function introduceFeatures() {
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup(
|
||||
options: {
|
||||
rescueMode?: boolean;
|
||||
noVerify?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { rescueMode, noVerify } = options;
|
||||
export async function createBackup(rescueMode = false) {
|
||||
const { isLoggedIn } = useUserStore.getState();
|
||||
const { encryptBackups, toggleEncryptBackups } = useSettingStore.getState();
|
||||
if (!isLoggedIn && encryptBackups) toggleEncryptBackups();
|
||||
|
||||
const verified =
|
||||
rescueMode || encryptBackups || noVerify || (await verifyAccount());
|
||||
const verified = rescueMode || encryptBackups || (await verifyAccount());
|
||||
if (!verified) {
|
||||
showToast("error", "Could not create a backup: user verification failed.");
|
||||
return false;
|
||||
|
||||
@@ -18,10 +18,9 @@ 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 = [
|
||||
// {
|
||||
@@ -58,24 +57,11 @@ const KEYMAP = [
|
||||
keys: ["command+f", "ctrl+f"],
|
||||
description: "Search all notes",
|
||||
global: false,
|
||||
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;
|
||||
}
|
||||
action: (e) => {
|
||||
if (e.target?.classList.contains("ProseMirror")) return;
|
||||
e.preventDefault();
|
||||
|
||||
useSearchStore.setState({ isSearching: true, searchType: "notes" });
|
||||
navigate("/search/notes");
|
||||
}
|
||||
}
|
||||
// {
|
||||
@@ -151,11 +137,7 @@ export function registerKeyMap() {
|
||||
KEYMAP.forEach((key) => {
|
||||
hotkeys(
|
||||
key.keys.join(","),
|
||||
{
|
||||
element: key.global
|
||||
? (GlobalKeyboard as unknown as HTMLElement)
|
||||
: document.body
|
||||
},
|
||||
{ element: key.global ? GlobalKeyboard : window },
|
||||
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({ noVerify: true });
|
||||
await createBackup();
|
||||
} else if (isUserPremium() && !IS_TESTING) {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
|
||||
@@ -18,93 +18,39 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
SqliteDriver as KSqliteDriver,
|
||||
SqliteDialectConfig,
|
||||
Dialect,
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler,
|
||||
DatabaseConnection,
|
||||
QueryResult,
|
||||
CompiledQuery,
|
||||
Driver
|
||||
SqliteQueryCompiler
|
||||
} 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 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);
|
||||
class SqliteDriver extends KSqliteDriver {
|
||||
constructor(private readonly config: SqliteDialectConfig & { name: string }) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
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 this.worker.delete(path);
|
||||
await desktop?.integration.deleteFile.query(path);
|
||||
await this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
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 => {
|
||||
export const createDialect = (name: string): Dialect => {
|
||||
return {
|
||||
createDriver: () =>
|
||||
new SqliteDriver({
|
||||
name
|
||||
name,
|
||||
database: async () => {
|
||||
const path = await desktop!.integration.resolvePath.query({
|
||||
filePath: `userData/${name}.sql`
|
||||
});
|
||||
return window.createSQLite3Database(path).unsafeMode(true);
|
||||
}
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
|
||||
@@ -34,7 +34,6 @@ declare module "kysely" {
|
||||
|
||||
export const createDialect = (
|
||||
name: string,
|
||||
encrypted: boolean,
|
||||
init?: () => Promise<void>
|
||||
): Dialect => {
|
||||
return {
|
||||
@@ -42,7 +41,6 @@ export const createDialect = (
|
||||
new WaSqliteWorkerDriver({
|
||||
async: !isFeatureSupported("opfs"),
|
||||
dbName: name,
|
||||
encrypted,
|
||||
init
|
||||
}),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
|
||||
@@ -75,15 +75,11 @@ export class SharedService<T extends object> extends EventTarget {
|
||||
{ signal: this.#onClose.signal }
|
||||
);
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
this.proxy = this.#createProxy();
|
||||
}
|
||||
|
||||
activate(
|
||||
portProviderFunc: () => Promise<{ port: MessagePort; onclose: () => void }>,
|
||||
portProviderFunc: () => MessagePort | Promise<MessagePort>,
|
||||
onClientConnected: () => Promise<void>
|
||||
) {
|
||||
if (this.#onDeactivate) return;
|
||||
@@ -97,7 +93,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, onclose } = await portProviderFunc();
|
||||
const port = await portProviderFunc();
|
||||
port.start();
|
||||
|
||||
// Listen for client requests. A separate BroadcastChannel
|
||||
@@ -159,7 +155,6 @@ 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);
|
||||
});
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { SQLite } from "@notesnook/desktop";
|
||||
import { expose } from "comlink";
|
||||
|
||||
export type SQLiteWorker = typeof SQLite.prototype;
|
||||
const db = new SQLite();
|
||||
expose(db);
|
||||
@@ -32,230 +32,219 @@ type PreparedStatement = {
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
async open(async: boolean, url?: string) {
|
||||
if (this.db) {
|
||||
console.error("Database is already initialized", this.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)
|
||||
);
|
||||
this.sqlite = Factory(sqliteModule);
|
||||
this.vfs = await this.getVFS(this.dbName, async);
|
||||
|
||||
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)
|
||||
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)
|
||||
);
|
||||
if (!prepared) return;
|
||||
sqlite = Factory(sqliteModule);
|
||||
vfs = await getVFS(dbName, async);
|
||||
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: this.sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
this.preparedStatements.set(sql, statement);
|
||||
sqlite.vfs_register(vfs, false);
|
||||
db = await sqlite.open_v2(dbName, undefined, `multipleciphers-${vfs.name}`);
|
||||
}
|
||||
|
||||
this.sqlite.str_finish(str);
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
const str = sqlite.str_new(db, sql);
|
||||
const prepared = await sqlite.prepare_v2(db, sqlite.str_value(str));
|
||||
if (!prepared) return;
|
||||
|
||||
// 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 : []
|
||||
const statement: PreparedStatement = {
|
||||
stmt: prepared.stmt,
|
||||
columns: sqlite.column_names(prepared.stmt)
|
||||
};
|
||||
}
|
||||
preparedStatements.set(sql, statement);
|
||||
|
||||
async close() {
|
||||
if (!this.db) return;
|
||||
sqlite.str_finish(str);
|
||||
|
||||
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();
|
||||
// reset retry count on success
|
||||
retryCounter[sql] = 0;
|
||||
return statement;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
this.db = undefined;
|
||||
this.initialized = false;
|
||||
}
|
||||
// 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;
|
||||
|
||||
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;
|
||||
if (ex instanceof Error || ex instanceof SQLiteError)
|
||||
ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
export type SQLiteWorker = typeof _SQLiteWorker.prototype;
|
||||
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;
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
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 function deleteDatabase(dbName: string, async: boolean) {
|
||||
await close();
|
||||
if (vfs) await vfs.delete();
|
||||
else await (await getVFS(dbName, async)).delete();
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
if ("isReady" in vfs) await vfs.isReady;
|
||||
return vfs;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
addEventListener("message", async (event) => {
|
||||
if (!event.data.type) {
|
||||
const worker = new _SQLiteWorker(event.data.dbName, event.data.encrypted);
|
||||
await worker.open(event.data.async, event.data.uri);
|
||||
await worker.open(event.data.dbName, 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,12 +26,7 @@ import SQLiteAsyncURI from "./wa-sqlite-async.wasm?url";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { SharedService } from "./shared-service";
|
||||
|
||||
type Config = {
|
||||
dbName: string;
|
||||
async: boolean;
|
||||
encrypted: boolean;
|
||||
init?: () => Promise<void>;
|
||||
};
|
||||
type Config = { dbName: string; async: boolean; init?: () => Promise<void> };
|
||||
|
||||
const servicePool = new Map<
|
||||
string,
|
||||
@@ -60,6 +55,7 @@ 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
|
||||
);
|
||||
@@ -76,24 +72,19 @@ export class WaSqliteWorkerDriver implements Driver {
|
||||
|
||||
service.activate(
|
||||
() =>
|
||||
new Promise<{ port: MessagePort; onclose: () => void }>((resolve) => {
|
||||
new Promise<MessagePort>((resolve) => {
|
||||
console.log("initializing worker");
|
||||
this.needsInitialization = true;
|
||||
|
||||
const worker = new Worker();
|
||||
worker.addEventListener(
|
||||
"message",
|
||||
(event) =>
|
||||
resolve({
|
||||
port: event.ports[0],
|
||||
onclose: () => worker.terminate()
|
||||
}),
|
||||
(event) => resolve(event.ports[0]),
|
||||
{ once: true }
|
||||
);
|
||||
worker.postMessage({
|
||||
dbName: this.config.dbName,
|
||||
async: this.config.async,
|
||||
encrypted: this.config.encrypted,
|
||||
uri: this.config.async ? SQLiteAsyncURI : SQLiteSyncURI
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -303,11 +303,11 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
break;
|
||||
}
|
||||
case "force-sync": {
|
||||
await appStore.sync({ type: "full", force: true });
|
||||
await appStore.sync(true, true);
|
||||
break;
|
||||
}
|
||||
case "backup": {
|
||||
await createBackup();
|
||||
await createBackup(true);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -26,7 +26,7 @@ type ContentToggle = {
|
||||
onToggle: () => void;
|
||||
label: string;
|
||||
dateEdited: number;
|
||||
resolveConflict: (saveCopy: boolean) => void;
|
||||
resolveConflict: (options: { saveCopy: boolean }) => void;
|
||||
readonly: boolean;
|
||||
sx: FlexProps["sx"];
|
||||
};
|
||||
@@ -50,7 +50,7 @@ function ContentToggle(props: ContentToggle) {
|
||||
<Button
|
||||
variant="accent"
|
||||
mr={2}
|
||||
onClick={() => resolveConflict(true)}
|
||||
onClick={() => resolveConflict({ saveCopy: true })}
|
||||
p={1}
|
||||
px={2}
|
||||
>
|
||||
@@ -61,7 +61,7 @@ function ContentToggle(props: ContentToggle) {
|
||||
variant={isOtherSelected ? "error" : "accent"}
|
||||
onClick={() => {
|
||||
if (isOtherSelected) {
|
||||
resolveConflict(false);
|
||||
resolveConflict({ saveCopy: 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 { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Copy, Restore } from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
@@ -45,24 +45,6 @@ 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;
|
||||
@@ -203,7 +185,15 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
isSelected={selectedContent === 0}
|
||||
isOtherSelected={selectedContent === 1}
|
||||
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
|
||||
resolveConflict={onResolveContent}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep: content.data,
|
||||
toCopy: saveCopy ? conflictedContent : undefined,
|
||||
toKeepDateEdited: content.dateEdited,
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
@@ -273,7 +263,15 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
<>
|
||||
<ContentToggle
|
||||
readonly={session.type === "diff"}
|
||||
resolveConflict={onResolveContent}
|
||||
resolveConflict={({ saveCopy }) => {
|
||||
resolveConflict({
|
||||
note: session.note,
|
||||
toKeep: conflictedContent.data,
|
||||
toCopy: saveCopy ? content : undefined,
|
||||
toKeepDateEdited: conflictedContent.dateEdited,
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
}}
|
||||
label={
|
||||
session.type === "diff"
|
||||
? "Current version"
|
||||
@@ -328,31 +326,31 @@ async function resolveConflict({
|
||||
dateResolved
|
||||
}: {
|
||||
note: Note;
|
||||
toKeep: ContentItem;
|
||||
toKeep: string;
|
||||
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,6 +152,7 @@ export function EditorActionBar() {
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "readonly" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
onClick: editor?.startSearch
|
||||
|
||||
@@ -73,44 +73,15 @@ import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
async function saveContent(
|
||||
noteId: string,
|
||||
ignoreEdit: boolean,
|
||||
content: string
|
||||
) {
|
||||
function saveContent(noteId: string, ignoreEdit: boolean, content: string) {
|
||||
logger.debug("saving content", {
|
||||
noteId,
|
||||
ignoreEdit,
|
||||
length: content.length
|
||||
});
|
||||
await Promise.race([
|
||||
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
"Saving this note is taking too long. Copy your changes and restart the app to prevent data loss. If the problem persists, please report it to us at support@streetwriters.co."
|
||||
)
|
||||
),
|
||||
30 * 1000
|
||||
)
|
||||
)
|
||||
]).catch((e) => {
|
||||
const { hide } = showToast(
|
||||
"error",
|
||||
(e as Error).message,
|
||||
[
|
||||
{
|
||||
text: "Dismiss",
|
||||
onClick: () => hide()
|
||||
}
|
||||
],
|
||||
0
|
||||
);
|
||||
useEditorStore.getState().saveSessionContent(noteId, ignoreEdit, {
|
||||
type: "tiptap",
|
||||
data: content
|
||||
});
|
||||
}
|
||||
const deferredSave = debounceWithId(saveContent, 100);
|
||||
@@ -271,11 +242,11 @@ 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.dateModified) {
|
||||
if (isContent && lastChangedTime.current < item.dateEdited) {
|
||||
if (!item.locked) return editor.updateContent(item.data);
|
||||
|
||||
const result = await db.vault
|
||||
.decryptContent(item)
|
||||
.decryptContent(item, item.noteId)
|
||||
.catch(() => EV.publish(EVENTS.vaultLocked));
|
||||
if (!result) return;
|
||||
editor.updateContent(result.data);
|
||||
|
||||
@@ -344,6 +344,7 @@ function TipTap(props: TipTapProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (readonly) return null;
|
||||
return (
|
||||
<>
|
||||
<ScopedThemeProvider
|
||||
@@ -417,7 +418,7 @@ function TiptapWrapper(
|
||||
editorContainer={() => {
|
||||
if (editorContainerRef.current) return editorContainerRef.current;
|
||||
const editorContainer = document.createElement("div");
|
||||
editorContainer.classList.add("selectable", "editor-container");
|
||||
editorContainer.classList.add("selectable");
|
||||
editorContainer.style.flex = "1";
|
||||
editorContainer.style.cursor = "text";
|
||||
editorContainer.style.color =
|
||||
@@ -426,7 +427,6 @@ 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;
|
||||
}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user