mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
242 Commits
fix-codebl
...
v2.6.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d7a8b9ea0 | ||
|
|
812ecf93c8 | ||
|
|
74d99d97bc | ||
|
|
5bbc453268 | ||
|
|
d71b9edc7f | ||
|
|
134904b551 | ||
|
|
3eeb292d70 | ||
|
|
04125bbb98 | ||
|
|
cfc3a8341c | ||
|
|
cbb0a511a8 | ||
|
|
dff1c7ed32 | ||
|
|
7987ef81a6 | ||
|
|
8d8d4ee782 | ||
|
|
3a50c73cab | ||
|
|
4ee0071eca | ||
|
|
248192f404 | ||
|
|
e56b766c3a | ||
|
|
170b2b24ab | ||
|
|
50137e0994 | ||
|
|
8b5babd5f8 | ||
|
|
39e83200c8 | ||
|
|
ead0e0107e | ||
|
|
1c888bd4fd | ||
|
|
ff92530339 | ||
|
|
ee38032403 | ||
|
|
2ce245659a | ||
|
|
2c635eb4d7 | ||
|
|
d653223df0 | ||
|
|
290b0dfc78 | ||
|
|
06bb9e654b | ||
|
|
576fe78e33 | ||
|
|
0c16bfb5ff | ||
|
|
a101dbdf31 | ||
|
|
682609044f | ||
|
|
c0a7fb3a5c | ||
|
|
a153aa791c | ||
|
|
76b3dd1f96 | ||
|
|
8b9e2c8526 | ||
|
|
0dc2d14736 | ||
|
|
fc5dada701 | ||
|
|
f34060c4e0 | ||
|
|
006a239680 | ||
|
|
c6ed4bcab1 | ||
|
|
b0fe967087 | ||
|
|
3f32673b0a | ||
|
|
0525cecaee | ||
|
|
7d84a9582b | ||
|
|
289acd8ab6 | ||
|
|
bb6d9f604b | ||
|
|
c20a8232cd | ||
|
|
c886212e89 | ||
|
|
799ae111ae | ||
|
|
2a990979a0 | ||
|
|
2d1071117b | ||
|
|
aefce1ee67 | ||
|
|
64d10f5050 | ||
|
|
95b9edca62 | ||
|
|
06c8422cbe | ||
|
|
177d8b5112 | ||
|
|
39afa98fce | ||
|
|
5bc1ad4ba5 | ||
|
|
0b1223976c | ||
|
|
3873758fe5 | ||
|
|
3c876cb1d8 | ||
|
|
0a1a16c197 | ||
|
|
3c56325fa6 | ||
|
|
e5db3353ae | ||
|
|
3416ba4eaa | ||
|
|
cdb69bf553 | ||
|
|
1aff6d0d55 | ||
|
|
67b7bdcea3 | ||
|
|
9b6a6de415 | ||
|
|
8325246a56 | ||
|
|
3d11fb5e3f | ||
|
|
4627a58a52 | ||
|
|
e557047af1 | ||
|
|
8b4cea7339 | ||
|
|
ba5e35ec5c | ||
|
|
f53817e94d | ||
|
|
846efc5351 | ||
|
|
0149d00d64 | ||
|
|
275f9730f1 | ||
|
|
43bc954989 | ||
|
|
c88bc722e1 | ||
|
|
ec6fc6839e | ||
|
|
8383c93292 | ||
|
|
37d8d85935 | ||
|
|
95db7c6329 | ||
|
|
636783335f | ||
|
|
f88431ded4 | ||
|
|
91b0de0580 | ||
|
|
a5704ad75c | ||
|
|
ba8c09a7d9 | ||
|
|
dbe4015655 | ||
|
|
76237ad7dd | ||
|
|
dbfb1168fb | ||
|
|
830d28fbe5 | ||
|
|
c6def2a8fe | ||
|
|
ebc95fcea7 | ||
|
|
f78cfe59e8 | ||
|
|
a2cbc93d34 | ||
|
|
4728f14e8c | ||
|
|
e94b431d58 | ||
|
|
0ac5e2b995 | ||
|
|
365764e3e3 | ||
|
|
8d2b5aa3de | ||
|
|
9a031a4a43 | ||
|
|
d412679fd7 | ||
|
|
22c52d4432 | ||
|
|
9e5bdf9e33 | ||
|
|
7253eca5c0 | ||
|
|
b1f3dc29c5 | ||
|
|
8e3df8d41b | ||
|
|
efad22c00a | ||
|
|
b7690f13b1 | ||
|
|
6e1739da2e | ||
|
|
77ab423041 | ||
|
|
96a8e885d3 | ||
|
|
2b917c7eca | ||
|
|
942d2f7a4e | ||
|
|
75142ff1b2 | ||
|
|
74172dc3fa | ||
|
|
848fdda8d7 | ||
|
|
3895569a0c | ||
|
|
0aac29ca76 | ||
|
|
25f9eece8d | ||
|
|
0e22b8ab2c | ||
|
|
577c2ce6cd | ||
|
|
5b9cb8ca4b | ||
|
|
2eda93c0c2 | ||
|
|
d916adf2e3 | ||
|
|
1f065bcbb2 | ||
|
|
3efdd9505e | ||
|
|
ab3f5f6a6c | ||
|
|
9027be7bbb | ||
|
|
7b08b5c035 | ||
|
|
4a557c67b7 | ||
|
|
a6ebe092e8 | ||
|
|
9759e1b396 | ||
|
|
b6147ca5a1 | ||
|
|
83887db77a | ||
|
|
1facfbec0e | ||
|
|
d6608325b4 | ||
|
|
439b090470 | ||
|
|
ed98319a21 | ||
|
|
54f94f44c0 | ||
|
|
57cf0e23cf | ||
|
|
9e66269aca | ||
|
|
a4f50fa2c3 | ||
|
|
7a0bafb02c | ||
|
|
a24658ac0e | ||
|
|
bb77385b66 | ||
|
|
e8b1bb649c | ||
|
|
023347da3f | ||
|
|
f10146be03 | ||
|
|
7bac3c1326 | ||
|
|
528c0a10de | ||
|
|
c1e1a8d1a9 | ||
|
|
21720a510d | ||
|
|
a2d5e7f32f | ||
|
|
629fb7b9cd | ||
|
|
f6a4af8c00 | ||
|
|
837e60316a | ||
|
|
2592a90853 | ||
|
|
788429efb7 | ||
|
|
674d8672f0 | ||
|
|
8d371fce79 | ||
|
|
f0f391ec73 | ||
|
|
abc3b5569c | ||
|
|
c42dc0f12e | ||
|
|
7ec14f2eff | ||
|
|
31d4e88efd | ||
|
|
da06184d51 | ||
|
|
08296793f1 | ||
|
|
3b6fde2ea6 | ||
|
|
1f2948c9a5 | ||
|
|
e62267042e | ||
|
|
883fe48946 | ||
|
|
6ba18c5bd1 | ||
|
|
207347190d | ||
|
|
0cf76e1f11 | ||
|
|
9d76311f82 | ||
|
|
c327a292c7 | ||
|
|
5a965f6d52 | ||
|
|
a81c621d49 | ||
|
|
2e7b5d0d08 | ||
|
|
6d324fe8da | ||
|
|
6d2be3a083 | ||
|
|
d252c6fc5c | ||
|
|
30c5e87c67 | ||
|
|
2db819de2b | ||
|
|
bff5e64484 | ||
|
|
3492bef2c7 | ||
|
|
6d9aa56084 | ||
|
|
94c40b3444 | ||
|
|
278c8b1115 | ||
|
|
8e8581cce5 | ||
|
|
bd29f4e9e2 | ||
|
|
eb2b92d85c | ||
|
|
042ac0d338 | ||
|
|
a7011f0d6b | ||
|
|
22144ee18d | ||
|
|
b85484fceb | ||
|
|
b58956371f | ||
|
|
f487e9f7e2 | ||
|
|
f14efd3b1e | ||
|
|
3496a42c19 | ||
|
|
0882c3ecf5 | ||
|
|
1f6d76bac7 | ||
|
|
db7c3f4321 | ||
|
|
9ea4651fb0 | ||
|
|
ed5a75447f | ||
|
|
d08994a314 | ||
|
|
2bf6e7073c | ||
|
|
7990458cf3 | ||
|
|
f3b13735f3 | ||
|
|
ae680a32bc | ||
|
|
cbd0f18c6e | ||
|
|
fd62b1b631 | ||
|
|
2159f6720e | ||
|
|
67c3a36464 | ||
|
|
652efc8624 | ||
|
|
30da041ea3 | ||
|
|
e90c401503 | ||
|
|
83d919f06e | ||
|
|
74fef0d2d9 | ||
|
|
5906b24da1 | ||
|
|
8dd36f7adf | ||
|
|
f2dc26dbff | ||
|
|
87915ecd00 | ||
|
|
3cf4e55621 | ||
|
|
4c7cd840df | ||
|
|
bd238a6fe4 | ||
|
|
beaace9b54 | ||
|
|
c5352c2a73 | ||
|
|
dd6410c33e | ||
|
|
ac9dca8098 | ||
|
|
74ade6fecc | ||
|
|
e1cfbb155d | ||
|
|
030aba2ac3 | ||
|
|
9ab7e722e0 | ||
|
|
14d6714209 |
1
.github/workflows/desktop.publish.yml
vendored
1
.github/workflows/desktop.publish.yml
vendored
@@ -97,6 +97,7 @@ jobs:
|
||||
- name: Get App Store Version
|
||||
id: appstore
|
||||
uses: streetwriters/appstore-connect-app-version@develop
|
||||
if: inputs.publish-apple
|
||||
with:
|
||||
app-id: ${{ steps.app_metadata.outputs.apple_app_id }}
|
||||
key-id: ${{ secrets.api_key_id }}
|
||||
|
||||
3
apps/desktop/app-update.yml
Normal file
3
apps/desktop/app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
112
apps/desktop/package-lock.json
generated
112
apps/desktop/package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.12",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.8.0",
|
||||
"electron": "25.9.3",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
@@ -216,6 +216,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",
|
||||
@@ -236,6 +237,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",
|
||||
@@ -249,6 +251,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"
|
||||
}
|
||||
@@ -257,6 +260,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"
|
||||
}
|
||||
@@ -265,6 +269,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"
|
||||
}
|
||||
@@ -798,6 +803,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"
|
||||
},
|
||||
@@ -809,6 +815,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"
|
||||
},
|
||||
@@ -831,20 +838,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/client": {
|
||||
"version": "10.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.31.0.tgz",
|
||||
"integrity": "sha512-VCqbJEvFJb8C4hQFw7AD+dkQTjgEdV/QAzO4D+/cX5e93u5NpfNXI+PKS0QFXwG/zqgwQwVV6OkYc/D/MFwA6g==",
|
||||
"version": "10.38.3",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/client/-/client-10.38.3.tgz",
|
||||
"integrity": "sha512-hHPsElTL4sB+UGzuAZ4iwRVHjELrk/Nsl2owsYw9ITJz3EY7VWRsQ6vK48kJvlHPYPhP+efID5UOgyZMs4bFXA==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "10.31.0"
|
||||
"@trpc/server": "10.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.31.0.tgz",
|
||||
"integrity": "sha512-9EnRTSDE9nF11LZsvSOqNKqkRYzHqFX4ch5AJ6VIu8uta2vxVTN4FxxsNRSOluTzVYZDeaCISbwmOJ5iihCCIg==",
|
||||
"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"
|
||||
]
|
||||
@@ -853,6 +860,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",
|
||||
@@ -881,12 +889,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": "*"
|
||||
}
|
||||
@@ -900,7 +910,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/normalize-package-data": {
|
||||
"version": "2.4.1",
|
||||
@@ -922,6 +933,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": "*"
|
||||
}
|
||||
@@ -951,6 +963,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": "*"
|
||||
@@ -1191,6 +1204,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": {
|
||||
@@ -1242,6 +1256,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": "*"
|
||||
}
|
||||
@@ -1316,6 +1331,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"
|
||||
}
|
||||
@@ -1324,6 +1340,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",
|
||||
@@ -1443,6 +1460,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"
|
||||
},
|
||||
@@ -1586,6 +1604,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
@@ -1600,6 +1619,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -1611,6 +1631,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"
|
||||
}
|
||||
@@ -1619,6 +1640,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",
|
||||
@@ -1644,6 +1666,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": {
|
||||
@@ -1751,9 +1774,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz",
|
||||
"integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==",
|
||||
"version": "25.9.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.3.tgz",
|
||||
"integrity": "sha512-dacaHg/PuwVcFRgPDCM5j7UDzqGJWOsbBRdS5wPKLNS/ejPeccIjuNUT1cqcrpvCJKAFW8swHWg9kdizNSEDHQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
@@ -1920,6 +1944,7 @@
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
@@ -1928,6 +1953,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"
|
||||
}
|
||||
@@ -1951,6 +1977,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": {
|
||||
@@ -2002,6 +2029,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"
|
||||
@@ -2027,6 +2055,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",
|
||||
@@ -2067,6 +2096,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"
|
||||
}
|
||||
@@ -2183,7 +2213,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
@@ -2197,6 +2227,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",
|
||||
@@ -2212,6 +2243,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"
|
||||
},
|
||||
@@ -2280,6 +2312,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",
|
||||
@@ -2297,6 +2330,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"
|
||||
@@ -2312,6 +2346,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",
|
||||
@@ -2341,7 +2376,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@@ -2362,6 +2397,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"
|
||||
@@ -2374,6 +2410,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"
|
||||
@@ -2386,6 +2423,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"
|
||||
@@ -2409,7 +2447,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",
|
||||
@@ -2429,6 +2468,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"
|
||||
@@ -2691,7 +2731,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-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
@@ -2709,6 +2750,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": {
|
||||
@@ -2738,6 +2780,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2773,6 +2816,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -2792,6 +2836,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"
|
||||
@@ -2837,6 +2882,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"
|
||||
}
|
||||
@@ -2959,6 +3005,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"
|
||||
},
|
||||
@@ -2970,6 +3017,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"
|
||||
@@ -2979,6 +3027,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -2987,6 +3036,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"
|
||||
}
|
||||
@@ -3066,7 +3116,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",
|
||||
@@ -3105,6 +3156,7 @@
|
||||
"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"
|
||||
}
|
||||
@@ -3126,6 +3178,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
@@ -3144,6 +3197,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"
|
||||
},
|
||||
@@ -3338,12 +3392,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"
|
||||
},
|
||||
@@ -3379,6 +3435,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",
|
||||
@@ -3458,12 +3515,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"
|
||||
@@ -3479,6 +3538,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"
|
||||
@@ -3599,6 +3659,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/stat-mode": {
|
||||
@@ -3671,6 +3732,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"
|
||||
},
|
||||
@@ -3940,7 +4002,8 @@
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "15.1.1",
|
||||
@@ -3993,6 +4056,7 @@
|
||||
"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"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.3",
|
||||
"version": "2.6.12",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -12,8 +12,8 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "25.8.0",
|
||||
"electron": "25.9.3",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
@@ -191,6 +191,7 @@
|
||||
"allowNativeWayland": true
|
||||
},
|
||||
"extraResources": [
|
||||
"app-update.yml",
|
||||
"./assets/**"
|
||||
],
|
||||
"extraMetadata": {
|
||||
|
||||
@@ -29,6 +29,8 @@ import { dirname } from "path";
|
||||
import { resolvePath } from "../utils/resolve-path";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { AssetManager } from "../utils/asset-manager";
|
||||
import { isFlatpak } from "../utils";
|
||||
import { setupDesktopIntegration } from "../utils/desktop-integration";
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
@@ -44,6 +46,8 @@ const NotificationOptions = z.object({
|
||||
});
|
||||
|
||||
export const osIntegrationRouter = t.router({
|
||||
isFlatpak: t.procedure.query(() => isFlatpak()),
|
||||
|
||||
zoomFactor: t.procedure.query(() => config.zoomFactor),
|
||||
setZoomFactor: t.procedure.input(z.number()).mutation(({ input: factor }) => {
|
||||
globalThis.window?.webContents.setZoomFactor(factor);
|
||||
@@ -79,6 +83,7 @@ export const osIntegrationRouter = t.router({
|
||||
AutoLaunch.disable();
|
||||
}
|
||||
config.desktopSettings = settings;
|
||||
setupDesktopIntegration(settings);
|
||||
}),
|
||||
|
||||
selectDirectory: t.procedure
|
||||
@@ -117,6 +122,14 @@ export const osIntegrationRouter = t.router({
|
||||
writeFileSync(resolvedPath, data);
|
||||
}),
|
||||
|
||||
resolvePath: t.procedure
|
||||
.input(z.object({ filePath: z.string() }))
|
||||
.query(({ input }) => {
|
||||
const { filePath } = input;
|
||||
if (!filePath) return;
|
||||
return resolvePath(filePath);
|
||||
}),
|
||||
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
|
||||
@@ -27,19 +27,23 @@ export type CLIOptions = {
|
||||
hidden: boolean;
|
||||
};
|
||||
|
||||
export async function parseArguments(): Promise<CLIOptions> {
|
||||
export async function parseArguments(argv: string[]): Promise<CLIOptions> {
|
||||
const result: CLIOptions = {
|
||||
note: false,
|
||||
notebook: false,
|
||||
reminder: false,
|
||||
hidden: false
|
||||
};
|
||||
const { hidden } = await yargs(hideBin(process.argv))
|
||||
const { hidden } = await yargs(hideBin(argv))
|
||||
.boolean("hidden")
|
||||
// have to account for this flag added on Windows when launching
|
||||
// via Jumplist
|
||||
.boolean("allow-file-access-from-files")
|
||||
.command("new", "Create a new item", (yargs) => {
|
||||
return yargs
|
||||
.command("note", "Create a new note", {}, () => {
|
||||
result.note = true;
|
||||
console.log("HERE!");
|
||||
})
|
||||
.command("notebook", "Create a new notebook", {}, () => {
|
||||
result.notebook = true;
|
||||
|
||||
@@ -24,7 +24,6 @@ import { configureAutoUpdater } from "./utils/autoupdater";
|
||||
import { getBackgroundColor, getTheme, setTheme } from "./utils/theme";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { WindowState } from "./utils/window-state";
|
||||
import { AutoLaunch } from "./utils/autolaunch";
|
||||
import { setupJumplist } from "./utils/jumplist";
|
||||
import { setupTray } from "./utils/tray";
|
||||
import { CLIOptions, parseArguments } from "./cli";
|
||||
@@ -34,6 +33,8 @@ import { router, api } from "./api";
|
||||
import { config } from "./utils/config";
|
||||
import path from "path";
|
||||
import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
@@ -63,7 +64,7 @@ process.on("unhandledRejection", (reason) => {
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
async function createWindow() {
|
||||
const cliOptions = await parseArguments();
|
||||
const cliOptions = await parseArguments(process.argv);
|
||||
setTheme(getTheme());
|
||||
|
||||
const mainWindowState = new WindowState({});
|
||||
@@ -76,7 +77,7 @@ async function createWindow() {
|
||||
darkTheme: getTheme() === "dark",
|
||||
backgroundColor: getBackgroundColor(),
|
||||
opacity: 0,
|
||||
autoHideMenuBar: true,
|
||||
autoHideMenuBar: false,
|
||||
icon: AssetManager.appIcon({
|
||||
size: 512,
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
@@ -90,7 +91,9 @@ async function createWindow() {
|
||||
}
|
||||
});
|
||||
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
globalThis.window = mainWindow;
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
mainWindowState.manage(mainWindow);
|
||||
|
||||
if (cliOptions.hidden && !config.desktopSettings.minimizeToSystemTray)
|
||||
@@ -104,8 +107,7 @@ async function createWindow() {
|
||||
}
|
||||
|
||||
await AssetManager.loadIcons();
|
||||
setupDesktopIntegration();
|
||||
createIPCHandler({ router, windows: [mainWindow] });
|
||||
setupDesktopIntegration(config.desktopSettings);
|
||||
|
||||
mainWindow.webContents.session.setSpellCheckerDictionaryDownloadURL(
|
||||
"http://dictionaries.notesnook.com/"
|
||||
@@ -146,8 +148,12 @@ app.once("window-all-closed", () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on("second-instance", () => {
|
||||
app.on("second-instance", async (_ev, argv) => {
|
||||
if (!globalThis.window) return;
|
||||
const cliOptions = await parseArguments(argv);
|
||||
if (cliOptions.note) bridge.onCreateItem("note");
|
||||
if (cliOptions.notebook) bridge.onCreateItem("notebook");
|
||||
if (cliOptions.reminder) bridge.onCreateItem("reminder");
|
||||
bringToFront();
|
||||
});
|
||||
|
||||
@@ -171,49 +177,3 @@ function createURL(options: CLIOptions, path = "/") {
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function setupDesktopIntegration() {
|
||||
const desktopIntegration = config.desktopSettings;
|
||||
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
if (desktopIntegration.closeToSystemTray) {
|
||||
app.on("before-quit", () => app.exit(0));
|
||||
}
|
||||
|
||||
globalThis.window?.once("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ declare global {
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
console.log("HELLO!");
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
|
||||
@@ -35,6 +35,8 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
|
||||
"autostart"
|
||||
);
|
||||
|
||||
const STARTUP_ARGS = ["--hidden"];
|
||||
|
||||
export class AutoLaunch {
|
||||
static enable(hidden: boolean) {
|
||||
if (process.platform === "linux") {
|
||||
@@ -47,10 +49,14 @@ export class AutoLaunch {
|
||||
LINUX_DESKTOP_ENTRY(hidden)
|
||||
);
|
||||
} else {
|
||||
const loginItemSettings = app.getLoginItemSettings({
|
||||
args: STARTUP_ARGS
|
||||
});
|
||||
if (loginItemSettings.openAtLogin) return;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: hidden,
|
||||
args: ["--hidden"]
|
||||
args: STARTUP_ARGS
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
74
apps/desktop/src/utils/desktop-integration.ts
Normal file
74
apps/desktop/src/utils/desktop-integration.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
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 { app } from "electron";
|
||||
import { DesktopIntegration, config } from "./config";
|
||||
import { setupTray, destroyTray } from "./tray";
|
||||
import { AutoLaunch } from "./autolaunch";
|
||||
|
||||
export function setupDesktopIntegration(
|
||||
desktopIntegration: DesktopIntegration
|
||||
) {
|
||||
if (
|
||||
desktopIntegration.closeToSystemTray ||
|
||||
desktopIntegration.minimizeToSystemTray
|
||||
) {
|
||||
setupTray();
|
||||
} else {
|
||||
destroyTray();
|
||||
}
|
||||
|
||||
// when close to system tray is enabled, it becomes nigh impossible
|
||||
// to "quit" the app. This is necessary in order to fix that.
|
||||
app.on("before-quit", () =>
|
||||
desktopIntegration.closeToSystemTray ? app.exit(0) : null
|
||||
);
|
||||
|
||||
globalThis.window?.on("close", (e) => {
|
||||
if (config.desktopSettings.closeToSystemTray) {
|
||||
e.preventDefault();
|
||||
if (process.platform == "darwin") {
|
||||
// on macOS window cannot be minimized/hidden if it is already fullscreen
|
||||
// so we just close it.
|
||||
if (globalThis.window?.isFullScreen()) app.exit(0);
|
||||
else app.hide();
|
||||
} else {
|
||||
try {
|
||||
globalThis.window?.minimize();
|
||||
globalThis.window?.hide();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
globalThis.window?.on("minimize", () => {
|
||||
if (config.desktopSettings.minimizeToSystemTray) {
|
||||
if (process.platform == "darwin") {
|
||||
app.hide();
|
||||
} else {
|
||||
globalThis.window?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (desktopIntegration.autoStart) {
|
||||
AutoLaunch.enable(!!desktopIntegration.startMinimized);
|
||||
}
|
||||
}
|
||||
@@ -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 { protocol, net } from "electron";
|
||||
import { protocol } from "electron";
|
||||
import { isDevelopment } from "./index";
|
||||
import { createReadStream } from "fs";
|
||||
import { extname, normalize } from "path";
|
||||
@@ -62,8 +62,25 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
return net.fetch(request, {
|
||||
bypassCustomProtocolHandlers: true
|
||||
if (request.headers.has("X-Content-Length")) {
|
||||
request.headers.set(
|
||||
"Content-Length",
|
||||
request.headers.get("X-Content-Length") || "0"
|
||||
);
|
||||
request.headers.delete("X-Content-Length");
|
||||
}
|
||||
const headers = Object.fromEntries(request.headers.entries());
|
||||
|
||||
return await fetch(request.url, {
|
||||
signal: request.signal,
|
||||
mode: request.mode,
|
||||
headers,
|
||||
method: request.method,
|
||||
body: request.body,
|
||||
credentials: request.credentials,
|
||||
referrer: (request as any).referrer,
|
||||
duplex: "half",
|
||||
redirect: "manual"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@ 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 { initalize } from "@notesnook/core/dist/logger";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
@@ -26,4 +26,4 @@ const LoggerStorage = new MMKVLoader()
|
||||
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export {};
|
||||
export { LoggerStorage };
|
||||
|
||||
@@ -90,6 +90,10 @@ export class KV {
|
||||
return this.storage.removeItem(key);
|
||||
}
|
||||
|
||||
async removeMulti(keys) {
|
||||
return this.storage.removeItems(...keys);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return this.storage.clearStore();
|
||||
}
|
||||
@@ -145,6 +149,7 @@ export default {
|
||||
clear: () => DefaultStorage.clear(),
|
||||
getAllKeys: () => DefaultStorage.getAllKeys(),
|
||||
writeMulti: (items) => DefaultStorage.writeMulti(items),
|
||||
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
|
||||
encrypt,
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir } from "./io";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -62,6 +63,7 @@ export async function downloadAttachments(
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
|
||||
@@ -182,6 +184,8 @@ export default async function downloadAttachment(
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
|
||||
@@ -24,15 +24,18 @@ import { ToastEvent } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
|
||||
await createCacheDir();
|
||||
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
if (exists) {
|
||||
if (await exists(filename)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -60,44 +63,55 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
console.log("downloading: ", recieved, total);
|
||||
});
|
||||
|
||||
cancelToken.cancel = request.cancel;
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
if (e.message !== "canceled") {
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastEvent.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log("download file error: ", e, url, headers);
|
||||
console.log("Download file error:", e, url, headers);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.user.tokenManager.getAccessToken();
|
||||
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
const contentLength = parseInt(attachmentInfo.headers?.get("content-length"));
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
|
||||
@@ -20,32 +20,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Platform } from "react-native";
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { cacheDir, getRandomId } from "./utils";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { db } from "../database";
|
||||
import { compressToBase64 } from "./compress";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
console.log("Read encrypted file...");
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
try {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists =
|
||||
(await RNFetchBlob.fs.exists(path)) ||
|
||||
(Platform.OS === "ios" && (await RNFetchBlob.fs.exists(appGroupPath)));
|
||||
|
||||
if (!exists) {
|
||||
if (!(await exists(filename))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const isPng = /(png)/g.test(attachment?.metadata.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(attachment?.metadata.type);
|
||||
|
||||
console.log("decrypting....");
|
||||
let output = await Sodium.decryptFile(
|
||||
key,
|
||||
{
|
||||
@@ -59,6 +52,7 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
: "base64"
|
||||
: "text"
|
||||
);
|
||||
console.log("file decrypted...");
|
||||
if (cipherData.outputType === "base64" && (isPng || isJpeg)) {
|
||||
const dCachePath = `${cacheDir}/${output}`;
|
||||
output = await compressToBase64(
|
||||
@@ -70,7 +64,7 @@ export async function readEncrypted(filename, key, cipherData) {
|
||||
return output;
|
||||
} catch (e) {
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
console.log(e);
|
||||
console.log("readEncrypted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +82,7 @@ export async function hashBase64(data) {
|
||||
}
|
||||
|
||||
export async function writeEncryptedBase64({ data, key }) {
|
||||
await createCacheDir();
|
||||
let filepath = cacheDir + `/${getRandomId("imagecache_")}`;
|
||||
await RNFetchBlob.fs.writeFile(filepath, data, "base64");
|
||||
let output = await Sodium.encryptFile(key, {
|
||||
@@ -103,6 +98,7 @@ export async function writeEncryptedBase64({ data, key }) {
|
||||
}
|
||||
|
||||
export async function deleteFile(filename, data) {
|
||||
await createCacheDir();
|
||||
let delFilePath = cacheDir + `/${filename}`;
|
||||
if (!data) {
|
||||
if (!filename) return;
|
||||
@@ -128,19 +124,89 @@ export async function deleteFile(filename, data) {
|
||||
export async function clearFileStorage() {
|
||||
try {
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
|
||||
for (let file of files) {
|
||||
try {
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
await RNFetchBlob.fs.unlink(cacheDir + `/${file}`).catch(console.log);
|
||||
}
|
||||
for (let file of oldCache) {
|
||||
await RNFetchBlob.fs.unlink(cacheDirOld + `/${file}`).catch(console.log);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.log("clearFileStorage", e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
console.log("Cache directory created");
|
||||
}
|
||||
}
|
||||
|
||||
export async function migrateFilesFromCache() {
|
||||
try {
|
||||
await createCacheDir();
|
||||
const migratedFilesPath = cacheDir + "/.migrated_1";
|
||||
const migrated = await RNFetchBlob.fs.exists(migratedFilesPath);
|
||||
if (migrated) {
|
||||
console.log("Files migrated already");
|
||||
return;
|
||||
}
|
||||
|
||||
let files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
console.log("Files to migrate:", files.join(","));
|
||||
|
||||
let oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
|
||||
for (let file of oldCache) {
|
||||
if (file.startsWith("org.") || file.startsWith("com.")) continue;
|
||||
RNFetchBlob.fs
|
||||
.mv(cacheDirOld + `/${file}`, cacheDir + `/${file}`)
|
||||
.catch(console.log);
|
||||
console.log("Moved", file);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(migratedFilesPath, "1", "utf8");
|
||||
} catch (e) {
|
||||
console.log("migrateFilesFromCache", e);
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function exists(filename) {
|
||||
let exists = await RNFetchBlob.fs.exists(`${cacheDir}/${filename}`);
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
|
||||
let exists = await RNFetchBlob.fs.exists(path);
|
||||
|
||||
// Check if file is present in app group path.
|
||||
let existsInAppGroup = false;
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
existsInAppGroup = await RNFetchBlob.fs.exists(appGroupPath);
|
||||
}
|
||||
|
||||
if (exists || existsInAppGroup) {
|
||||
const attachment = db.attachments.attachment(filename);
|
||||
const totalChunks = Math.ceil(attachment.length / attachment.chunkSize);
|
||||
const totalAbytes = totalChunks * ABYTES;
|
||||
const expectedFileSize = attachment.length + totalAbytes;
|
||||
|
||||
const stat = await RNFetchBlob.fs.stat(
|
||||
existsInAppGroup ? appGroupPath : path
|
||||
);
|
||||
|
||||
if (stat.size !== expectedFileSize) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(existsInAppGroup ? appGroupPath : path)
|
||||
.catch(console.log);
|
||||
return false;
|
||||
}
|
||||
|
||||
exists = true;
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
@@ -72,7 +73,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
);
|
||||
});
|
||||
|
||||
cancelToken.cancel = request.cancel;
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
};
|
||||
let response = await request;
|
||||
|
||||
let status = response.info().status;
|
||||
|
||||
@@ -21,7 +21,12 @@ import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
export const cacheDir = RNFetchBlob.fs.dirs.CacheDir;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
Platform.OS == "ios"
|
||||
? RNFetchBlob.fs.dirs.LibraryDir + "/.cache"
|
||||
: RNFetchBlob.fs.dirs.DocumentDir + "/.cache";
|
||||
|
||||
export function getRandomId(prefix) {
|
||||
return Math.random()
|
||||
@@ -66,9 +71,12 @@ export function cancelable(operation) {
|
||||
}
|
||||
|
||||
export function copyFileAsync(source, dest) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ScopedStorage.copyFile(source, dest, (e, r) => {
|
||||
console.log(e, r);
|
||||
if (e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,10 +34,69 @@ import BaseDialog from "../dialog/base-dialog";
|
||||
import { allowedOnPlatform, renderItem } from "./functions";
|
||||
import { useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Test announcement
|
||||
* {
|
||||
id: "some-announcement",
|
||||
type: "dialog",
|
||||
body: [
|
||||
{
|
||||
type: "title",
|
||||
text: "This is a title",
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "description",
|
||||
text: "— May privacy reign.",
|
||||
style: {
|
||||
marginBottom: 1
|
||||
},
|
||||
platforms: ["all"]
|
||||
},
|
||||
{
|
||||
type: "callToActions",
|
||||
actions: [
|
||||
{
|
||||
type: "promo",
|
||||
title: "15% Off",
|
||||
platforms: ["android"],
|
||||
data: "com.streetwriters.notesnook.sub.yr.15"
|
||||
}
|
||||
],
|
||||
platforms: ["all"]
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
export const AnnouncementDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [info, setInfo] = useState(null);
|
||||
const [info, setInfo] = useState();
|
||||
const remove = useMessageStore((state) => state.remove);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,7 +110,9 @@ export const AnnouncementDialog = () => {
|
||||
|
||||
const open = (data) => {
|
||||
setInfo(data);
|
||||
setVisible(true);
|
||||
setImmediate(() => {
|
||||
setVisible(true);
|
||||
});
|
||||
};
|
||||
|
||||
const close = useCallback(() => {
|
||||
|
||||
@@ -132,7 +132,7 @@ export const AttachmentItem = ({
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
db.fs.cancel(attachment.metadata.hash, currentProgress.type);
|
||||
setCurrentProgress(null);
|
||||
}}
|
||||
style={{
|
||||
|
||||
@@ -85,11 +85,13 @@ const AuthModal = () => {
|
||||
background={colors.primary.background}
|
||||
transparent={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
|
||||
@@ -46,6 +46,7 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
@@ -68,7 +69,8 @@ export const SessionExpired = () => {
|
||||
eSendEvent(eUserLoggedIn, true);
|
||||
setVisible(false);
|
||||
setFocused(false);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const logout = async () => {
|
||||
@@ -130,15 +132,20 @@ export const SessionExpired = () => {
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<Modal
|
||||
<BaseDialog
|
||||
transparent={false}
|
||||
background={colors.primary.background}
|
||||
bounce={false}
|
||||
animated={false}
|
||||
centered={false}
|
||||
onShow={async () => {
|
||||
await sleep(300);
|
||||
passwordInputRef.current?.focus();
|
||||
setFocused(true);
|
||||
}}
|
||||
enableSheetKeyboardHandler={true}
|
||||
visible={true}
|
||||
>
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
style={{
|
||||
width: focused ? "100%" : "99.9%",
|
||||
@@ -231,7 +238,9 @@ export const SessionExpired = () => {
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
<Dialog context="session_expiry" />
|
||||
</Modal>
|
||||
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
</BaseDialog>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (onFinishLogin) => {
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
@@ -69,6 +69,7 @@ export const useLogin = (onFinishLogin) => {
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(async (mfa, callback) => {
|
||||
try {
|
||||
@@ -101,7 +102,12 @@ export const useLogin = (onFinishLogin) => {
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
await db.user.authenticatePassword(email.current, password.current);
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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 React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
@@ -31,6 +31,9 @@ import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { BouncingView } from "../ui/transitions/bouncing-view";
|
||||
import { ScopedThemeProvider } from "@notesnook/theme";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
|
||||
const BaseDialog = ({
|
||||
visible,
|
||||
@@ -48,9 +51,13 @@ const BaseDialog = ({
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false
|
||||
avoidKeyboardResize = false,
|
||||
enableSheetKeyboardHandler = false
|
||||
}) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
const [internalVisible, setIntervalVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -58,14 +65,33 @@ const BaseDialog = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (appState === "background") {
|
||||
setIntervalVisible(false);
|
||||
if (useUserStore.getState().appLocked) {
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
setIntervalVisible(true);
|
||||
unsub();
|
||||
setTimeout(() => {
|
||||
lockEvents.current = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
const Wrapper = useSafeArea ? SafeAreaView : View;
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="dialog">
|
||||
<Modal
|
||||
visible={visible}
|
||||
visible={visible && internalVisible}
|
||||
transparent={true}
|
||||
animated
|
||||
statusBarTranslucent={statusBarTranslucent}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
@@ -75,13 +101,17 @@ const BaseDialog = ({
|
||||
"landscape-right"
|
||||
]}
|
||||
onShow={() => {
|
||||
if (lockEvents.current) return;
|
||||
if (onShow) {
|
||||
onShow();
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
if (!enableSheetKeyboardHandler) {
|
||||
useSettingStore.getState().setSheetKeyboardHandler(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
animationType={animation}
|
||||
onRequestClose={() => {
|
||||
if (lockEvents.current) return;
|
||||
if (!closeOnTouch) return null;
|
||||
useSettingStore.getState().setSheetKeyboardHandler(true);
|
||||
onRequestClose && onRequestClose();
|
||||
@@ -107,6 +137,7 @@ const BaseDialog = ({
|
||||
style={[
|
||||
styles.backdrop,
|
||||
{
|
||||
alignItems: centered ? "center" : undefined,
|
||||
justifyContent: centered
|
||||
? "center"
|
||||
: bottom
|
||||
@@ -133,8 +164,7 @@ const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
justifyContent: "center"
|
||||
},
|
||||
overlayButton: {
|
||||
width: "100%",
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProFeatures = ({ count = 6 }) => {
|
||||
content: "Instantly sync to unlimited devices"
|
||||
},
|
||||
{
|
||||
content: "A private vault to keep everything imporant always locked"
|
||||
content: "A private vault to keep everything important always locked"
|
||||
},
|
||||
{
|
||||
content:
|
||||
|
||||
@@ -442,6 +442,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
|
||||
if (this.state.goToEditor) {
|
||||
this._openInEditor(note);
|
||||
} else if (this.state.share) {
|
||||
@@ -452,7 +453,6 @@ export class VaultDialog extends Component {
|
||||
await this._copyNote(note);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
};
|
||||
@@ -467,44 +467,40 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
async _enrollFingerprint(password) {
|
||||
try {
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastEvent.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _createVault() {
|
||||
@@ -543,7 +539,7 @@ export class VaultDialog extends Component {
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastEvent.show({
|
||||
heading: "Note permanantly unlocked.",
|
||||
heading: "Note permanently unlocked.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
|
||||
@@ -39,9 +39,9 @@ class ExceptionHandler extends React.Component<{
|
||||
error: Error | null;
|
||||
hasError: boolean;
|
||||
} = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: error };
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class ExceptionHandler extends React.Component<{
|
||||
this.props.component
|
||||
)}
|
||||
defaultTitle={this.state.error?.message}
|
||||
issueTitle="An exception occured"
|
||||
issueTitle="An exception occurred"
|
||||
/>
|
||||
<Dialog />
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -17,35 +17,25 @@ 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, { useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import React, { useRef } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { db } from "../../../common/database";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { eOpenJumpToDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Button } from "../../ui/button";
|
||||
import Sort from "../../sheets/sort";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const SectionHeader = React.memo(
|
||||
function SectionHeader({ item, index, type, color, screen }) {
|
||||
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions(type)
|
||||
);
|
||||
let groupBy = Object.keys(GROUP).find(
|
||||
(key) => GROUP[key] === groupOptions.groupBy
|
||||
);
|
||||
@@ -65,17 +55,6 @@ export const SectionHeader = React.memo(
|
||||
? "Default"
|
||||
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...db.settings?.getGroupOptions(type) });
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -77,7 +77,7 @@ export const openNotebookTopic = (item) => {
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -43,6 +43,9 @@ const SelectionWrapper = ({
|
||||
|
||||
const onLongPress = () => {
|
||||
if (!useSelectionStore.getState().selectionMode) {
|
||||
useSelectionStore.setState({
|
||||
selectedItemsList: []
|
||||
});
|
||||
useSelectionStore.getState().setSelectionMode(true);
|
||||
}
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useMessageStore } from "../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -32,6 +32,7 @@ export const Card = ({ color, warning }) => {
|
||||
color = color ? color : colors.primary.accent;
|
||||
const messageBoardState = useMessageStore((state) => state.message);
|
||||
const announcement = useMessageStore((state) => state.announcement);
|
||||
const fontScale = Dimensions.get("window").fontScale;
|
||||
|
||||
return !messageBoardState.visible || announcement || warning ? null : (
|
||||
<View
|
||||
@@ -44,7 +45,6 @@ export const Card = ({ color, warning }) => {
|
||||
type="gray"
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
width: "95%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
@@ -54,17 +54,18 @@ export const Card = ({ color, warning }) => {
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
width: 40 * fontScale,
|
||||
backgroundColor:
|
||||
messageBoardState.type === "error"
|
||||
? hexToRGBA(colors.static.red, 0.15)
|
||||
: hexToRGBA(color, 0.15),
|
||||
height: 40,
|
||||
height: 40 * fontScale,
|
||||
borderRadius: 100,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
@@ -75,6 +76,7 @@ export const Card = ({ color, warning }) => {
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
allowFontScaling
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
</View>
|
||||
@@ -91,7 +93,7 @@ export const Card = ({ color, warning }) => {
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
flexWrap: "nowrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
color={colors.primary.heading}
|
||||
@@ -101,22 +103,24 @@ export const Card = ({ color, warning }) => {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
{fontScale > 1 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-right"
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</PressableButton>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ import { Empty } from "./empty";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
|
||||
const renderItems = {
|
||||
note: NoteWrapper,
|
||||
@@ -99,9 +100,10 @@ const List = ({
|
||||
type === "notebooks" ||
|
||||
notebooksListMode === "compact";
|
||||
const groupType =
|
||||
screen === "Notes" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const groupOptions = db.settings?.getGroupOptions(groupType);
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
|
||||
@@ -113,18 +115,21 @@ const List = ({
|
||||
color={headerProps?.color}
|
||||
title={headerProps?.heading}
|
||||
dateBy={dateBy}
|
||||
type={
|
||||
screen === "Notes"
|
||||
? "home"
|
||||
: screen === "Favorites"
|
||||
? "favorites"
|
||||
: type
|
||||
}
|
||||
type={groupType}
|
||||
screen={screen}
|
||||
isSheet={isSheet}
|
||||
groupOptions={groupOptions}
|
||||
/>
|
||||
),
|
||||
[headerProps?.color, headerProps?.heading, screen, type, isSheet, dateBy]
|
||||
[
|
||||
headerProps?.color,
|
||||
headerProps?.heading,
|
||||
screen,
|
||||
isSheet,
|
||||
dateBy,
|
||||
groupType,
|
||||
groupOptions
|
||||
]
|
||||
);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
@@ -230,7 +235,7 @@ const List = ({
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
type={screen === "Home" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -245,13 +245,17 @@ const MergeConflicts = () => {
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<Modal
|
||||
<BaseDialog
|
||||
statusBarTranslucent
|
||||
transparent={false}
|
||||
animationType="slide"
|
||||
animated={false}
|
||||
bounce={false}
|
||||
onRequestClose={() => {
|
||||
close();
|
||||
}}
|
||||
centered={false}
|
||||
background={colors?.primary.background}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
"portrait-upside-down",
|
||||
@@ -363,7 +367,7 @@ const MergeConflicts = () => {
|
||||
</Animated.View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -56,7 +56,7 @@ export const CompactFeatures = ({
|
||||
icon: "shield"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & montly",
|
||||
highlight: "Daily, weekly & monthly",
|
||||
content: "recurring reminders",
|
||||
icon: "bell"
|
||||
},
|
||||
|
||||
@@ -165,7 +165,12 @@ export const Component = ({ close, promo }) => {
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
({pricing?.product?.localizedPrice} / mo)
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
|
||||
@@ -48,23 +48,23 @@ export const Expiring = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [status, setStatus] = useState({
|
||||
title: "Your trial is ending soon",
|
||||
offer: null,
|
||||
offer: "Get 30% off",
|
||||
extend: true
|
||||
});
|
||||
const pricing = usePricing("yearly");
|
||||
|
||||
const promo = status.offer
|
||||
? {
|
||||
promoCode:
|
||||
pricing?.info?.discount > 30
|
||||
? pricing.info.sku
|
||||
: "com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
text: `GET ${
|
||||
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}% OFF on yearly`,
|
||||
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}
|
||||
: null;
|
||||
const promo =
|
||||
status.offer && pricing?.info
|
||||
? {
|
||||
promoCode:
|
||||
pricing?.info?.discount > 30
|
||||
? pricing.info.sku
|
||||
: "com.streetwriters.notesnook.sub.yr.trialoffer",
|
||||
text: `GET ${
|
||||
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}% OFF on yearly`,
|
||||
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
|
||||
}
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenTrialEndingDialog, open);
|
||||
|
||||
@@ -98,7 +98,7 @@ export const features = [
|
||||
icon: "bell"
|
||||
},
|
||||
{
|
||||
highlight: "Daily, weekly & montly",
|
||||
highlight: "Daily, weekly & monthly",
|
||||
content: "reminders",
|
||||
icon: "refresh",
|
||||
pro: true
|
||||
@@ -194,7 +194,7 @@ export const features = [
|
||||
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
|
||||
features: [
|
||||
{
|
||||
highlight: "Basic formating",
|
||||
highlight: "Basic formatting",
|
||||
content: "and lists",
|
||||
icon: "format-bold"
|
||||
},
|
||||
|
||||
@@ -18,13 +18,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Platform, View } from "react-native";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import RNIap from "react-native-iap";
|
||||
|
||||
export const PricingItem = ({ product, onPress, compact }) => {
|
||||
export const PricingItem = ({
|
||||
product,
|
||||
onPress,
|
||||
compact,
|
||||
strikethrough
|
||||
}: {
|
||||
product: {
|
||||
type: "yearly" | "monthly";
|
||||
data?: RNIap.Subscription;
|
||||
info: string;
|
||||
offerType?: "yearly" | "monthly";
|
||||
};
|
||||
strikethrough?: boolean;
|
||||
onPress?: () => void;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<PressableButton
|
||||
onPress={onPress}
|
||||
@@ -36,8 +52,10 @@ export const PricingItem = ({ product, onPress, compact }) => {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: compact ? 15 : 10,
|
||||
width: compact ? null : "100%",
|
||||
minWidth: 150
|
||||
minWidth: 150,
|
||||
opacity: strikethrough ? 0.7 : 1
|
||||
}}
|
||||
disabled={strikethrough}
|
||||
>
|
||||
{!compact && (
|
||||
<View>
|
||||
@@ -53,8 +71,25 @@ export const PricingItem = ({ product, onPress, compact }) => {
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph size={SIZE.sm}>
|
||||
<Heading size={SIZE.lg - 2}>{product?.data?.localizedPrice}/</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textDecorationLine: strikethrough ? "line-through" : undefined
|
||||
}}
|
||||
size={SIZE.lg - 2}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice}
|
||||
</Heading>
|
||||
{product?.type === "yearly" || product?.offerType === "yearly"
|
||||
? "/year"
|
||||
: "/month"}
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { db } from "../../common/database";
|
||||
import { DatabaseLogger, db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import {
|
||||
eSendEvent,
|
||||
@@ -47,11 +47,29 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
const UUID_VARIANT = "a";
|
||||
|
||||
function toUUID(str: string) {
|
||||
return [
|
||||
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
|
||||
str.substring(2, 6), // # next 4 oid digits
|
||||
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
|
||||
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
|
||||
str.substring(12)
|
||||
].join("-");
|
||||
}
|
||||
|
||||
const promoCyclesMonthly = {
|
||||
1: "first month",
|
||||
2: "first 2 months",
|
||||
3: "first 3 months"
|
||||
3: "first 3 months",
|
||||
4: "first 4 months",
|
||||
5: "first 5 months",
|
||||
6: "first 3 months"
|
||||
};
|
||||
|
||||
const promoCyclesYearly = {
|
||||
@@ -65,10 +83,24 @@ export const PricingPlans = ({
|
||||
marginTop,
|
||||
heading = true,
|
||||
compact = false
|
||||
}: {
|
||||
promo?: {
|
||||
promoCode: string;
|
||||
};
|
||||
marginTop?: any;
|
||||
heading?: boolean;
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [product, setProduct] = useState(null);
|
||||
const [product, setProduct] = useState<{
|
||||
type: string;
|
||||
offerType: "monthly" | "yearly";
|
||||
data: RNIap.Subscription;
|
||||
cycleText: string;
|
||||
info: string;
|
||||
}>();
|
||||
|
||||
const [buying, setBuying] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const userCanRequestTrial =
|
||||
@@ -90,27 +122,40 @@ export const PricingPlans = ({
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
const getPromo = async (code) => {
|
||||
const getPromo = async (code: string) => {
|
||||
try {
|
||||
let productId;
|
||||
let skuId: string;
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
productId = code;
|
||||
skuId = code;
|
||||
} else {
|
||||
productId = await db.offers.getCode(code.split(":")[0], Platform.OS);
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
}
|
||||
|
||||
let products = await PremiumService.getProducts();
|
||||
let product = products.find((p) => p.productId === productId);
|
||||
const products = await PremiumService.getProducts();
|
||||
const product = products.find((p) => p.productId === skuId);
|
||||
if (!product) return false;
|
||||
let isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
let cycleText = isMonthly
|
||||
const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
product.introductoryPriceCyclesAndroid ||
|
||||
product.introductoryPriceNumberOfPeriodsIOS
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
product.introductoryPriceCyclesAndroid ||
|
||||
product.introductoryPriceNumberOfPeriodsIOS
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
];
|
||||
|
||||
setProduct({
|
||||
@@ -118,7 +163,7 @@ export const PricingPlans = ({
|
||||
offerType: isMonthly ? "monthly" : "yearly",
|
||||
data: product,
|
||||
cycleText: cycleText,
|
||||
info: "Pay monthly, cancel anytime"
|
||||
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -131,22 +176,41 @@ export const PricingPlans = ({
|
||||
getSkus();
|
||||
}, [getSkus]);
|
||||
|
||||
const buySubscription = async (product) => {
|
||||
if (buying) return;
|
||||
const buySubscription = async (product: RNIap.Subscription) => {
|
||||
if (buying || !product) return;
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
await RNIap.requestSubscription(
|
||||
product?.productId,
|
||||
false,
|
||||
null,
|
||||
-1,
|
||||
user.id,
|
||||
user.id
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
const androidOfferToken =
|
||||
Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
.offerToken
|
||||
: null;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setBuying(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
@@ -167,6 +231,35 @@ export const PricingPlans = ({
|
||||
}
|
||||
};
|
||||
|
||||
function getStandardPrice() {
|
||||
if (!product) return;
|
||||
const productType = product.offerType;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
|
||||
|
||||
if (!pricingPhaseListItem) {
|
||||
const product =
|
||||
productType === "monthly"
|
||||
? monthlyPlan?.product
|
||||
: yearlyPlan?.product;
|
||||
return (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
|
||||
?.formattedPrice;
|
||||
}
|
||||
|
||||
return pricingPhaseListItem?.formattedPrice;
|
||||
} else {
|
||||
const productDefault =
|
||||
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
return (
|
||||
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -198,7 +291,14 @@ export const PricingPlans = ({
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{PremiumService.getMontlySub().localizedPrice} / mo
|
||||
{(Platform.OS === "android"
|
||||
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice) ||
|
||||
(PremiumService.getMontlySub() as any)?.localizedPrice}
|
||||
/ mo
|
||||
</Paragraph>
|
||||
<Button
|
||||
onPress={() => {
|
||||
@@ -218,7 +318,7 @@ export const PricingPlans = ({
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user.activateTrial();
|
||||
await db.user?.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
@@ -239,33 +339,91 @@ export const PricingPlans = ({
|
||||
) : (
|
||||
<>
|
||||
{product?.type === "promo" ? (
|
||||
<Heading
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 15,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
alignItems: "center"
|
||||
}}
|
||||
size={SIZE.lg - 4}
|
||||
>
|
||||
{product.data.introductoryPrice}
|
||||
<Paragraph
|
||||
{product?.offerType === "monthly" ? (
|
||||
<PricingItem
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
textDecorationLine: "line-through",
|
||||
color: colors.secondary.paragraph
|
||||
paddingTop: 15,
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
({product.data.localizedPrice})
|
||||
</Paragraph>{" "}
|
||||
for {product.cycleText}
|
||||
</Heading>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice ||
|
||||
(product.data as RNIap.SubscriptionIOS)
|
||||
?.localizedPrice}{" "}
|
||||
{product?.cycleText
|
||||
? `for ${product.cycleText}`
|
||||
: product?.offerType}
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
then {getStandardPrice()} {product?.offerType}.
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{user && !product ? (
|
||||
<>
|
||||
{heading || monthlyPlan?.info?.discount > 0 ? (
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && monthlyPlan?.info?.discount > 0 ? (
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
@@ -273,7 +431,12 @@ export const PricingPlans = ({
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.accent}>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
@@ -300,12 +463,15 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
<PricingItem
|
||||
onPress={() => buySubscription(monthlyPlan?.product)}
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay monthly, cancel anytime."
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -319,12 +485,15 @@ export const PricingPlans = ({
|
||||
)}
|
||||
|
||||
<PricingItem
|
||||
onPress={() => buySubscription(yearlyPlan?.product)}
|
||||
onPress={() => {
|
||||
if (!yearlyPlan?.product) return;
|
||||
buySubscription(yearlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay yearly"
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -346,7 +515,7 @@ export const PricingPlans = ({
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value)))
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastEvent.show({
|
||||
heading: "Discount applied!",
|
||||
@@ -358,7 +527,7 @@ export const PricingPlans = ({
|
||||
setBuying(false);
|
||||
ToastEvent.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: e.message,
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -396,7 +565,7 @@ export const PricingPlans = ({
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 30,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
@@ -427,7 +596,10 @@ export const PricingPlans = ({
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => buySubscription(product.data)}
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
@@ -436,7 +608,7 @@ export const PricingPlans = ({
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(null);
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
@@ -500,7 +672,7 @@ export const PricingPlans = ({
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
By subscribing, your will be charged on your Google Account, and
|
||||
By subscribing, you will be charged on your Google Account, and
|
||||
your subscription will automatically renew until you cancel prior
|
||||
to the end of the then current period.
|
||||
</Paragraph>
|
||||
@@ -523,7 +695,7 @@ export const PricingPlans = ({
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors)
|
||||
openLinkInBrowser("https://notesnook.com/tos")
|
||||
.catch(() => {})
|
||||
.then(() => {});
|
||||
}}
|
||||
@@ -538,7 +710,7 @@ export const PricingPlans = ({
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors)
|
||||
openLinkInBrowser("https://notesnook.com/privacy")
|
||||
.catch(() => {})
|
||||
.then(() => {});
|
||||
}}
|
||||
@@ -16,8 +16,8 @@ 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 from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import React, { useRef } from "react";
|
||||
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -54,7 +54,6 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const alias = item.alias || item.title;
|
||||
const isColor = !!ColorValues[item.title];
|
||||
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
@@ -62,6 +61,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -135,6 +135,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Synced item={item} close={close} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import { Dimensions, FlatList, ScrollView, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useActions } from "../../hooks/use-actions";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
@@ -33,9 +33,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const actions = useActions({ item, close });
|
||||
const data = actions.filter((i) => buttons.indexOf(i.id) > -1 && !i.hidden);
|
||||
|
||||
let width = dimensions.width > 600 ? 600 : dimensions.width;
|
||||
let columnItemsCount = DDS.isLargeTablet() ? 7 : 5;
|
||||
const shouldShrink =
|
||||
Dimensions.get("window").fontScale > 1 &&
|
||||
Dimensions.get("window").width < 450;
|
||||
let columnItemsCount = DDS.isLargeTablet() ? 7 : shouldShrink ? 4 : 5;
|
||||
let columnItemWidth = DDS.isTab
|
||||
? (width - 12) / columnItemsCount
|
||||
: (width - 12) / columnItemsCount;
|
||||
@@ -66,6 +68,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.lg}
|
||||
color={
|
||||
@@ -78,7 +81,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph size={SIZE.xs} style={{ textAlign: "center" }}>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
textBreakStrategy="simple"
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
@@ -139,6 +146,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
allowFontScaling
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.md + 4}
|
||||
color={
|
||||
item.on
|
||||
@@ -150,7 +158,11 @@ export const Items = ({ item, buttons, close }) => {
|
||||
/>
|
||||
</PressableButton>
|
||||
|
||||
<Paragraph size={SIZE.xxs + 1} style={{ textAlign: "center" }}>
|
||||
<Paragraph
|
||||
textBreakStrategy="simple"
|
||||
size={SIZE.xxs + 1}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{item.title}
|
||||
</Paragraph>
|
||||
</PressableButton>
|
||||
@@ -163,9 +175,12 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"copy",
|
||||
"share",
|
||||
"export",
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
"lock-unlock"
|
||||
];
|
||||
if (!shouldShrink) {
|
||||
topBarItemsList.push("publish");
|
||||
}
|
||||
|
||||
const topBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
@@ -17,10 +17,10 @@ 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 from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
@@ -33,6 +33,8 @@ export const Synced = ({ item, close }) => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const lastSynced = useUserStore((state) => state.lastSynced);
|
||||
|
||||
const dimensions = useWindowDimensions();
|
||||
const shouldShrink = dimensions.fontScale > 1 && dimensions.width < 450;
|
||||
return user && lastSynced >= item.dateModified ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -49,7 +51,11 @@ export const Synced = ({ item, close }) => {
|
||||
borderTopColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
<Icon name="shield-key-outline" color={colors.primary.accent} size={SIZE.xxxl} />
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
color={colors.primary.accent}
|
||||
size={shouldShrink ? SIZE.xxl : SIZE.xxxl}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -67,15 +73,17 @@ export const Synced = ({ item, close }) => {
|
||||
>
|
||||
Encrypted and synced
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
No one can view this {item.itemType || item.type} except you.
|
||||
</Paragraph>
|
||||
{shouldShrink ? null : (
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
No one can view this {item.itemType || item.type} except you.
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
@@ -91,8 +99,8 @@ export const Synced = ({ item, close }) => {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
title="Learn more"
|
||||
fontSize={SIZE.xs}
|
||||
height={30}
|
||||
type="grayAccent"
|
||||
/>
|
||||
|
||||
@@ -207,7 +207,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
|
||||
{screen === "Trash" ||
|
||||
screen === "Notebooks" ||
|
||||
screen === "Notebook" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
@@ -224,6 +223,21 @@ export const SelectionHeader = React.memo(() => {
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
}}
|
||||
tooltipText="Export"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="export"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
@@ -239,20 +253,6 @@ export const SelectionHeader = React.memo(() => {
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
}}
|
||||
tooltipText="Export"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="export"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -317,23 +317,8 @@ export const SelectionHeader = React.memo(() => {
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selectedItemsList.length > 1 ? "items" : "item"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1 ? "these items?" : "this item?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: () => {
|
||||
deleteItems();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
|
||||
return;
|
||||
onPress={() => {
|
||||
deleteItems();
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
|
||||
@@ -198,7 +198,6 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
key={data.actionText}
|
||||
title={data.actionText}
|
||||
accentColor={data.iconColor}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={40}
|
||||
width={250}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { FlashList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
@@ -54,7 +54,7 @@ export const FilteredList = ({
|
||||
}, [data, onChangeText]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
<FlashList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
|
||||
@@ -161,9 +161,6 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
: "deselected";
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(notebook);
|
||||
} else {
|
||||
contextValue.deselect(notebook);
|
||||
}
|
||||
for (let topic of notebook.topics) {
|
||||
itemState[topic.id] = state
|
||||
@@ -176,9 +173,6 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(topic);
|
||||
} else {
|
||||
contextValue.deselect(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +183,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
},
|
||||
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
|
||||
[getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
|
||||
const getItemsForItem = (item) => {
|
||||
@@ -214,7 +208,7 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const itemState = { ...useItemSelectionStore.getState().itemState };
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
@@ -370,56 +364,93 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
</View>
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<FilteredList
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85
|
||||
maxHeight: dimensions.height * 0.85,
|
||||
height: 50 * (notebooks.length + 2)
|
||||
}}
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
>
|
||||
<FilteredList
|
||||
ListEmptyComponent={
|
||||
notebooks.length > 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.primary.icon}
|
||||
size={100}
|
||||
/>
|
||||
<Paragraph style={{ marginBottom: 10 }}>
|
||||
You do not have any notebooks.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={notebooks}
|
||||
hasHeaderSearch={true}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
hasNotes={getSelectedNotesCountInItem(item) > 0}
|
||||
sheetRef={actionSheetRef}
|
||||
infoText={
|
||||
<>
|
||||
{item.topics.length === 1
|
||||
? item.topics.length + " topic"
|
||||
: item.topics.length + " topics"}
|
||||
</>
|
||||
}
|
||||
getListItems={getItemsForItem}
|
||||
getSublistItemProps={(topic) => ({
|
||||
hasNotes: getSelectedNotesCountInItem(topic) > 0,
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
height: 40
|
||||
},
|
||||
onPress: (item) => {
|
||||
const itemState =
|
||||
useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
},
|
||||
key: item.id,
|
||||
type: "transparent"
|
||||
})}
|
||||
icon={(expanded) => ({
|
||||
name: expanded ? "chevron-up" : "chevron-down",
|
||||
color: expanded
|
||||
? colors.primary.accent
|
||||
: colors.primary.paragraph
|
||||
})}
|
||||
onScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="book-outline"
|
||||
color={colors.primary.icon}
|
||||
size={100}
|
||||
/>
|
||||
<Paragraph style={{ marginBottom: 10 }}>
|
||||
You do not have any notebooks.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
data={notebooks}
|
||||
hasHeaderSearch={true}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
key={item.id}
|
||||
index={index}
|
||||
hasNotes={getSelectedNotesCountInItem(item) > 0}
|
||||
sheetRef={actionSheetRef}
|
||||
infoText={
|
||||
<>
|
||||
{item.topics.length === 1
|
||||
? item.topics.length + " topic"
|
||||
: item.topics.length + " topics"}
|
||||
</>
|
||||
}
|
||||
getListItems={getItemsForItem}
|
||||
getSublistItemProps={(topic) => ({
|
||||
hasNotes: getSelectedNotesCountInItem(topic) > 0,
|
||||
style: {
|
||||
marginBottom: 0,
|
||||
height: 40
|
||||
},
|
||||
onPress: (item) => {
|
||||
hasSubList={true}
|
||||
hasHeaderSearch={false}
|
||||
type="grayBg"
|
||||
sublistItemType="topic"
|
||||
onAddItem={(title) => {
|
||||
return onAddTopic(title, item);
|
||||
}}
|
||||
onAddSublistItem={(item) => {
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
onPress={(item) => {
|
||||
const itemState =
|
||||
useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
@@ -429,47 +460,16 @@ const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
},
|
||||
key: item.id,
|
||||
type: "transparent"
|
||||
})}
|
||||
icon={(expanded) => ({
|
||||
name: expanded ? "chevron-up" : "chevron-down",
|
||||
color: expanded
|
||||
? colors.primary.accent
|
||||
: colors.primary.paragraph
|
||||
})}
|
||||
onScrollEnd={() => {
|
||||
actionSheetRef.current?.handleChildScrollEnd();
|
||||
}}
|
||||
hasSubList={true}
|
||||
hasHeaderSearch={false}
|
||||
type="grayBg"
|
||||
sublistItemType="topic"
|
||||
onAddItem={(title) => {
|
||||
return onAddTopic(title, item);
|
||||
}}
|
||||
onAddSublistItem={(item) => {
|
||||
openAddTopicDialog(item);
|
||||
}}
|
||||
onPress={(item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const currentState = itemState[item.id];
|
||||
if (currentState !== "selected") {
|
||||
resetItemState("deselected");
|
||||
contextValue.select(item);
|
||||
} else {
|
||||
contextValue.deselect(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
itemType="notebook"
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
ListFooterComponent={<View style={{ height: 20 }} />}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
itemType="notebook"
|
||||
onAddItem={async (title) => {
|
||||
return await onAddNotebook(title);
|
||||
}}
|
||||
ListFooterComponent={<View style={{ height: 20 }} />}
|
||||
/>
|
||||
</View>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
|
||||
@@ -273,6 +273,7 @@ export const ListItem = ({
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
}}
|
||||
estimatedItemSize={40}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
renderItem={({ item, index }) => (
|
||||
|
||||
@@ -97,6 +97,16 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
{
|
||||
title: "Markdown + Frontmatter",
|
||||
func: async () => {
|
||||
await save("md-frontmatter");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
{
|
||||
title: "Plain Text",
|
||||
func: async () => {
|
||||
@@ -178,7 +188,9 @@ const ExportNotesSheet = ({ notes, update }) => {
|
||||
>
|
||||
<Icon
|
||||
name={item.icon}
|
||||
color={item.pro ? colors.primary.accent : colors.primary.icon}
|
||||
color={
|
||||
item.pro ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
size={SIZE.xxxl + 10}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -111,7 +111,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
heading: "An error occured",
|
||||
heading: "An error occurred",
|
||||
message: e.message,
|
||||
type: "error"
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function Migrate() {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
An error occured while migrating your data. You can logout of your
|
||||
An error occurred while migrating your data. You can logout of your
|
||||
account and try to relogin. However this is not recommended as it
|
||||
may result in some data loss if your data was not synced.
|
||||
</Paragraph>
|
||||
|
||||
@@ -40,7 +40,9 @@ import { requestInAppReview } from "../../../services/app-review";
|
||||
const PublishNoteSheet = ({ note: item, update }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const actionSheetRef = useRef();
|
||||
const loading = useAttachmentStore((state) => state.loading);
|
||||
|
||||
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
|
||||
const downloading = attachmentDownloads[`monograph-${item.id}`];
|
||||
const [selfDestruct, setSelfDestruct] = useState(false);
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const [note, setNote] = useState(item);
|
||||
@@ -137,9 +139,9 @@ const PublishNoteSheet = ({ note: item, update }) => {
|
||||
}}
|
||||
>
|
||||
Please wait...
|
||||
{loading && loading.current && loading.total
|
||||
{downloading && downloading.current && downloading.total
|
||||
? `\nDownloading attachments (${
|
||||
loading?.current / loading?.total
|
||||
downloading?.current / downloading?.total
|
||||
})`
|
||||
: ""}
|
||||
</Paragraph>
|
||||
|
||||
@@ -253,6 +253,57 @@ export default function ReminderSheet({
|
||||
marginBottom: DDS.isTab ? 25 : undefined
|
||||
}}
|
||||
>
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<ScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
borderWidth: 1,
|
||||
marginTop: 12,
|
||||
borderRadius: 5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingLeft: 12,
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={mode}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
}}
|
||||
icon={
|
||||
mode === "Silent"
|
||||
? "minus-circle"
|
||||
: mode === "Vibrate"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
}
|
||||
height={35}
|
||||
type={
|
||||
reminderNotificationMode ===
|
||||
ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
]
|
||||
? "grayAccent"
|
||||
: "gray"
|
||||
}
|
||||
onPress={() => {
|
||||
const _mode = ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
] as Reminder["priority"];
|
||||
SettingsService.set({
|
||||
reminderNotificationMode: _mode
|
||||
});
|
||||
setReminderNotificatioMode(_mode);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<Input
|
||||
fwdRef={titleRef}
|
||||
defaultValue={reminder?.title || referencedItem?.title}
|
||||
@@ -280,15 +331,21 @@ export default function ReminderSheet({
|
||||
}}
|
||||
height={80}
|
||||
wrapperStyle={{
|
||||
marginBottom: 20
|
||||
marginBottom: 12
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
<ScrollView
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 12
|
||||
marginBottom: 12,
|
||||
height: 50,
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingLeft: 12
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{Object.keys(ReminderModes).map((mode) => (
|
||||
<Button
|
||||
@@ -327,7 +384,7 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{reminderMode === ReminderModes.Repeat ? (
|
||||
<View
|
||||
@@ -474,9 +531,7 @@ export default function ReminderSheet({
|
||||
androidVariant="nativeAndroid"
|
||||
is24hourSource="locale"
|
||||
locale={
|
||||
db.settings?.getTimeFormat() === "24-hour"
|
||||
? "en_GB.UTF8"
|
||||
: "en_US.UTF8"
|
||||
db.settings?.getTimeFormat() === "24-hour" ? "en_GB" : "en_US"
|
||||
}
|
||||
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
|
||||
/>
|
||||
@@ -498,53 +553,6 @@ export default function ReminderSheet({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{reminderMode === ReminderModes.Permanent ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
paddingTop: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
{Object.keys(ReminderNotificationModes).map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
title={mode}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
borderRadius: 100
|
||||
}}
|
||||
icon={
|
||||
mode === "Silent"
|
||||
? "minus-circle"
|
||||
: mode === "Vibrate"
|
||||
? "vibrate"
|
||||
: "volume-high"
|
||||
}
|
||||
height={35}
|
||||
type={
|
||||
reminderNotificationMode ===
|
||||
ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
]
|
||||
? "grayAccent"
|
||||
: "gray"
|
||||
}
|
||||
onPress={() => {
|
||||
const _mode = ReminderNotificationModes[
|
||||
mode as keyof typeof ReminderNotificationModes
|
||||
] as Reminder["priority"];
|
||||
SettingsService.set({
|
||||
reminderNotificationMode: _mode
|
||||
});
|
||||
setReminderNotificatioMode(_mode);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{reminderMode === ReminderModes.Once ||
|
||||
reminderMode === ReminderModes.Permanent ? null : (
|
||||
<View
|
||||
@@ -595,6 +603,11 @@ export default function ReminderSheet({
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
<View
|
||||
style={{
|
||||
height: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import DocumentPicker, {
|
||||
DocumentPickerResponse
|
||||
} from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
@@ -45,6 +47,8 @@ import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -129,75 +133,71 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 300);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setRestoring(true);
|
||||
let prefix = Platform.OS === "ios" ? "" : "file:/";
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(item.uri, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(prefix + item.path, "utf8");
|
||||
}
|
||||
backup = JSON.parse(backup);
|
||||
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
await restoreBackup(backup, value);
|
||||
close();
|
||||
setRestoring(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = (onsubmit, onclose = () => {}) => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file to restore it",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: onclose,
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password) => {
|
||||
try {
|
||||
return await onsubmit(password);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to backup data",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
return false;
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file to restore it",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password) => {
|
||||
resolve(password);
|
||||
resolved = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -217,21 +217,164 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password) => {
|
||||
await db.backup.import(backup, password);
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
throw new Error("Backup file is invalid");
|
||||
}
|
||||
|
||||
let password;
|
||||
|
||||
console.log(`Found ${backupFiles?.length} files to restore from backup`);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
password = await withPassword();
|
||||
if (!password) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const password = await withPassword();
|
||||
if (password) {
|
||||
try {
|
||||
await restoreBackup(backup, password);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
@@ -271,76 +414,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const restoreBackup = async (backup, password) => {
|
||||
await db.backup.import(backup, password);
|
||||
setRestoring(false);
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentPicker.pickSingle()
|
||||
.then((r) => {
|
||||
setRestoring(true);
|
||||
fetch(r.uri)
|
||||
.then(async (r) => {
|
||||
try {
|
||||
let backup = await r.json();
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
restoreBackup(backup, value).then(() => {
|
||||
close();
|
||||
setRestoring(false);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
|
||||
@@ -16,6 +16,7 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import qclone from "qclone";
|
||||
import React, {
|
||||
createContext,
|
||||
@@ -26,7 +27,7 @@ import React, {
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { RefreshControl, useWindowDimensions, View } from "react-native";
|
||||
import ActionSheet, {
|
||||
ActionSheetRef,
|
||||
FlatList
|
||||
@@ -45,7 +46,6 @@ import {
|
||||
import useNavigationStore, {
|
||||
NotebookScreenParams
|
||||
} from "../../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import {
|
||||
eOnNewTopicAdded,
|
||||
eOnTopicSheetUpdate,
|
||||
@@ -54,18 +54,17 @@ import {
|
||||
import { normalize, SIZE } from "../../../utils/size";
|
||||
import { GroupHeader, NotebookType, TopicType } from "../../../utils/types";
|
||||
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { groupArray } from "@notesnook/core/dist/utils/grouping";
|
||||
import Config from "react-native-config";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { openEditor } from "../../../screens/notes/common";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Properties } from "../../properties";
|
||||
import Sort from "../sort";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
|
||||
type ConfigItem = { id: string; type: string };
|
||||
class TopicSheetConfig {
|
||||
@@ -108,7 +107,8 @@ export const TopicsSheet = () => {
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
const currentItem = useRef<string>();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions("topics")
|
||||
);
|
||||
@@ -206,6 +206,11 @@ export const TopicsSheet = () => {
|
||||
if (canShow) {
|
||||
setTimeout(() => {
|
||||
const id = isTopic ? currentScreen?.notebookId : currentScreen?.id;
|
||||
if (currentItem.current !== id) {
|
||||
setSelection([]);
|
||||
setEnabled(false);
|
||||
}
|
||||
currentItem.current = id;
|
||||
const notebook = db.notebooks?.notebook(id as string)?.data;
|
||||
const snapPoint = isTopic
|
||||
? 0
|
||||
@@ -226,6 +231,8 @@ export const TopicsSheet = () => {
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
setSelection([]);
|
||||
setEnabled(false);
|
||||
ref.current?.hide();
|
||||
}
|
||||
}, [
|
||||
@@ -271,7 +278,7 @@ export const TopicsSheet = () => {
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
keyboardHandlerEnabled={false}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [25, 100]}
|
||||
snapPoints={Config.isTesting === "true" ? [100] : [20, 100]}
|
||||
initialSnapIndex={1}
|
||||
backgroundInteractionEnabled
|
||||
gestureEnabled
|
||||
@@ -305,8 +312,8 @@ export const TopicsSheet = () => {
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
height: 300,
|
||||
maxHeight: 450,
|
||||
height: 450,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
@@ -329,30 +336,19 @@ export const TopicsSheet = () => {
|
||||
{enabled ? (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
marginLeft: 10,
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
onPress={async () => {
|
||||
//@ts-ignore
|
||||
useSelectionStore.setState({
|
||||
selectedItemsList: selection
|
||||
});
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selection.length > 1 ? "topics" : "topics"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selection.length > 1 ? "these topics?" : "this topic?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
await deleteItems();
|
||||
useSelectionStore.getState().clearSelection();
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
await deleteItems();
|
||||
useSelectionStore.getState().clearSelection();
|
||||
setEnabled(false);
|
||||
setSelection([]);
|
||||
return;
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
@@ -378,8 +374,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
@@ -389,8 +385,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -408,8 +404,8 @@ export const TopicsSheet = () => {
|
||||
color={colors.primary.icon}
|
||||
size={22}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -483,6 +479,7 @@ const TopicItem = ({
|
||||
selection.selection.findIndex((selected) => selected.id === item.id) > -1;
|
||||
const isFocused = screen.id === item.id;
|
||||
const notesCount = getTotalNotes(item);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
@@ -519,6 +516,10 @@ const TopicItem = ({
|
||||
<IconButton
|
||||
size={SIZE.lg}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
name={
|
||||
isSelected
|
||||
? "check-circle-outline"
|
||||
@@ -538,8 +539,8 @@ const TopicItem = ({
|
||||
<IconButton
|
||||
name="dots-horizontal"
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import Navigation from "../../services/navigation";
|
||||
@@ -109,6 +109,7 @@ export const MenuItem = React.memo(
|
||||
textAlignVertical: "center",
|
||||
textAlign: "left"
|
||||
}}
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
color={
|
||||
item.icon === "crown"
|
||||
|
||||
@@ -209,13 +209,19 @@ export const PinItem = React.memo(
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Icon color={color} size={SIZE.lg - 2} name={icons[item.type]} />
|
||||
<Icon
|
||||
allowFontScaling
|
||||
color={color}
|
||||
size={SIZE.lg - 2}
|
||||
name={icons[item.type]}
|
||||
/>
|
||||
<Icon
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: -6,
|
||||
left: -6
|
||||
}}
|
||||
allowFontScaling
|
||||
color={color}
|
||||
size={SIZE.xs}
|
||||
name="arrow-top-right-thick"
|
||||
|
||||
@@ -113,6 +113,7 @@ export const UserStatus = () => {
|
||||
<Icon
|
||||
name="checkbox-blank-circle"
|
||||
size={11}
|
||||
allowFontScaling
|
||||
color={
|
||||
!user || lastSyncStatus === SyncStatus.Failed
|
||||
? colors.error.icon
|
||||
@@ -149,9 +150,15 @@ export const UserStatus = () => {
|
||||
color={colors.error.icon}
|
||||
name="sync-alert"
|
||||
size={SIZE.lg}
|
||||
allowFontScaling
|
||||
/>
|
||||
) : (
|
||||
<Icon color={colors.primary.accent} name="sync" size={SIZE.lg} />
|
||||
<Icon
|
||||
allowFontScaling
|
||||
color={colors.primary.accent}
|
||||
name="sync"
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</PressableButton>
|
||||
|
||||
@@ -134,6 +134,7 @@ export const Button = ({
|
||||
{icon && !loading && iconPosition === "left" ? (
|
||||
<Icon
|
||||
name={icon}
|
||||
allowFontScaling
|
||||
style={[{ marginRight: 0 }, iconStyle as any]}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
@@ -170,6 +171,7 @@ export const Button = ({
|
||||
{icon && !loading && iconPosition === "right" ? (
|
||||
<Icon
|
||||
name={icon}
|
||||
allowFontScaling
|
||||
style={[{ marginLeft: 0 }, iconStyle as any]}
|
||||
color={iconColor || buttonType?.text || textColor}
|
||||
size={iconSize}
|
||||
|
||||
@@ -40,7 +40,6 @@ interface IconButtonProps extends PressableButtonProps {
|
||||
iconStyle?: TextStyle;
|
||||
}
|
||||
|
||||
const AnimatedIcon = Animated.createAnimatedComponent(Icon);
|
||||
export const IconButton = ({
|
||||
onPress,
|
||||
name,
|
||||
@@ -88,10 +87,10 @@ export const IconButton = ({
|
||||
...customStyle
|
||||
}}
|
||||
>
|
||||
<AnimatedIcon
|
||||
layout={Layout}
|
||||
<Icon
|
||||
name={name}
|
||||
style={iconStyle as any}
|
||||
allowFontScaling
|
||||
color={
|
||||
restProps.disabled
|
||||
? RGB_Linear_Shade(-0.05, hexToRGBA(colors.secondary.background))
|
||||
|
||||
@@ -17,15 +17,17 @@ 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 from "react";
|
||||
import { ScopedThemeProvider, useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { ScopedThemeProvider, useThemeColors } from "@notesnook/theme";
|
||||
import { PremiumToast } from "../../premium/premium-toast";
|
||||
import { Toast } from "../../toast";
|
||||
import { BouncingView } from "../transitions/bouncing-view";
|
||||
import { useAppState } from "../../../hooks/use-app-state";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const SheetWrapper = ({
|
||||
children,
|
||||
@@ -41,6 +43,7 @@ const SheetWrapper = ({
|
||||
enableGesturesInScrollView = false,
|
||||
bottomPadding = true
|
||||
}) => {
|
||||
const localRef = useRef(null);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const sheetKeyboardHandler = useSettingStore(
|
||||
@@ -50,6 +53,8 @@ const SheetWrapper = ({
|
||||
const smallTablet = deviceMode === "smallTablet";
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
let width = dimensions.width > 600 ? 600 : 500;
|
||||
|
||||
const style = React.useMemo(() => {
|
||||
@@ -68,19 +73,40 @@ const SheetWrapper = ({
|
||||
}, [colors.primary.background, largeTablet, smallTablet, width]);
|
||||
|
||||
const _onOpen = () => {
|
||||
if (lockEvents.current) return;
|
||||
onOpen && onOpen();
|
||||
};
|
||||
|
||||
const _onClose = async () => {
|
||||
if (lockEvents.current) return;
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (SettingsService.get().appLockMode === "background") {
|
||||
if (appState === "background") {
|
||||
const ref = fwdRef || localRef;
|
||||
ref?.current?.hide();
|
||||
if (useUserStore.getState().appLocked) {
|
||||
lockEvents.current = true;
|
||||
const unsub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked) {
|
||||
ref?.current?.show();
|
||||
unsub();
|
||||
lockEvents.current = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [appState, fwdRef]);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider value="sheet">
|
||||
<ActionSheet
|
||||
ref={fwdRef}
|
||||
ref={fwdRef || localRef}
|
||||
testIDs={{
|
||||
backdrop: "sheet-backdrop"
|
||||
}}
|
||||
|
||||
@@ -59,7 +59,6 @@ const Heading = ({
|
||||
<Component
|
||||
layout={restProps.layout || Layout}
|
||||
allowFontScaling={true}
|
||||
maxFontSizeMultiplier={1}
|
||||
{...restProps}
|
||||
style={[
|
||||
{
|
||||
|
||||
@@ -45,8 +45,6 @@ const Paragraph = ({
|
||||
return (
|
||||
<Component
|
||||
layout={restProps.layout || Layout}
|
||||
allowFontScaling
|
||||
maxFontSizeMultiplier={1}
|
||||
{...restProps}
|
||||
style={[
|
||||
{
|
||||
|
||||
@@ -70,8 +70,8 @@ const NotebookWelcome = () => {
|
||||
count: 5
|
||||
},
|
||||
{
|
||||
title: "Recipies",
|
||||
description: "I love cooking and collecting recipies",
|
||||
title: "Recipes",
|
||||
description: "I love cooking and collecting recipes",
|
||||
count: 10
|
||||
}
|
||||
]);
|
||||
@@ -195,7 +195,7 @@ const notebooks: { id: string; steps: TStep[] } = {
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>
|
||||
<Icon color={colors.primary.icon} size={SIZE.sm} name="note" />{" "}
|
||||
Feburary 2022 Week 2
|
||||
February 2022 Week 2
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
@@ -211,7 +211,7 @@ const notebooks: { id: string; steps: TStep[] } = {
|
||||
>
|
||||
<Paragraph size={SIZE.xs}>
|
||||
<Icon color={colors.primary.icon} size={SIZE.sm} name="note" />{" "}
|
||||
Feburary 2022 Week 1
|
||||
February 2022 Week 1
|
||||
</Paragraph>
|
||||
</View>
|
||||
<View
|
||||
@@ -257,7 +257,7 @@ const notebooks: { id: string; steps: TStep[] } = {
|
||||
title: "Tasks",
|
||||
type: "topic"
|
||||
}}
|
||||
onPress={() => {}}
|
||||
onPress={() => { }}
|
||||
/>
|
||||
|
||||
<PinItem
|
||||
@@ -267,7 +267,7 @@ const notebooks: { id: string; steps: TStep[] } = {
|
||||
title: "Work and office",
|
||||
type: "notebook"
|
||||
}}
|
||||
onPress={() => {}}
|
||||
onPress={() => { }}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
|
||||
@@ -188,7 +188,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
Notifications.displayNotification({
|
||||
title: item.title,
|
||||
message: item.headline || text,
|
||||
subtitle: item.headline || text,
|
||||
subtitle: "",
|
||||
bigText: html,
|
||||
ongoing: true,
|
||||
actions: ["UNPIN"],
|
||||
@@ -520,7 +520,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Permanent delete",
|
||||
paragraph: `Are you sure you want to delete this ${item.itemType} permanantly from trash?`,
|
||||
paragraph: `Are you sure you want to delete this ${item.itemType} permanently from trash?`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
@@ -529,7 +529,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -649,7 +649,7 @@ export const useActions = ({ close = () => null, item }) => {
|
||||
},
|
||||
{
|
||||
id: "favorite",
|
||||
title: item.favorite ? "Unfavorite" : "Favorite",
|
||||
title: item.favorite ? "Unfav" : "Fav",
|
||||
icon: item.favorite ? "star-off" : "star-outline",
|
||||
func: addToFavorites,
|
||||
close: false,
|
||||
|
||||
@@ -83,7 +83,6 @@ import {
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
const onCheckSyncStatus = async (type) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -107,10 +106,20 @@ const onFileEncryptionProgress = ({ total, progress }) => {
|
||||
.setEncryptionProgress((progress / total).toFixed(2));
|
||||
};
|
||||
|
||||
const onLoadingAttachmentProgress = (data) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setLoading(data.total === data.current ? null : data);
|
||||
const onDownloadingAttachmentProgress = (data) => {
|
||||
useAttachmentStore.getState().setDownloading(data);
|
||||
};
|
||||
|
||||
const onUploadingAttachmentProgress = (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
};
|
||||
|
||||
const onDownloadedAttachmentProgress = (data) => {
|
||||
useAttachmentStore.getState().setDownloading(data);
|
||||
};
|
||||
|
||||
const onUploadedAttachmentProgress = (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
};
|
||||
|
||||
const onUserSessionExpired = async () => {
|
||||
@@ -220,12 +229,25 @@ async function saveEditorState() {
|
||||
MMKV.setString("appState", state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RNIap.Purchase} subscription
|
||||
*/
|
||||
const onSuccessfulSubscription = async (subscription) => {
|
||||
console.log(
|
||||
"Subscription success!",
|
||||
subscription.transactionId,
|
||||
subscription.obfuscatedAccountIdAndroid,
|
||||
subscription.obfuscatedProfileIdAndroid
|
||||
);
|
||||
await PremiumService.subscriptions.set(subscription);
|
||||
await PremiumService.subscriptions.verify(subscription);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RNIap.PurchaseError} error
|
||||
*/
|
||||
const onSubscriptionError = async (error) => {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to subscribe",
|
||||
@@ -301,7 +323,16 @@ export const useAppEvents = () => {
|
||||
EVENTS.userSubscriptionUpdated,
|
||||
onUserSubscriptionStatusChanged
|
||||
),
|
||||
EV.subscribe(EVENTS.attachmentsLoading, onLoadingAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileDownload, onDownloadingAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileUpload, onUploadingAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileDownloaded, onDownloadedAttachmentProgress),
|
||||
EV.subscribe(EVENTS.fileUploaded, onUploadedAttachmentProgress),
|
||||
EV.subscribe(EVENTS.downloadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setDownloading(data);
|
||||
}),
|
||||
EV.subscribe(EVENTS.uploadCanceled, (data) => {
|
||||
useAttachmentStore.getState().setUploading(data);
|
||||
}),
|
||||
eSubscribeEvent(eUserLoggedIn, onUserUpdated)
|
||||
];
|
||||
|
||||
@@ -429,7 +460,6 @@ export const useAppEvents = () => {
|
||||
}
|
||||
|
||||
clearMessage();
|
||||
subscribeToIAPListeners();
|
||||
if (!login) {
|
||||
user = await db.user.fetchUser();
|
||||
setUser(user);
|
||||
@@ -448,8 +478,12 @@ export const useAppEvents = () => {
|
||||
userEmailConfirmed: true
|
||||
});
|
||||
}
|
||||
|
||||
subscribeToIAPListeners();
|
||||
} catch (e) {
|
||||
ToastEvent.error(e, "An error occured", "global");
|
||||
DatabaseLogger.error(error);
|
||||
|
||||
ToastEvent.error(e, "An error occurred", "global");
|
||||
}
|
||||
|
||||
user = await db.user.getUser();
|
||||
@@ -472,14 +506,15 @@ export const useAppEvents = () => {
|
||||
);
|
||||
|
||||
const subscribeToIAPListeners = useCallback(async () => {
|
||||
await RNIap.initConnection()
|
||||
.catch(() => null)
|
||||
.then(async () => {
|
||||
refValues.current.subsriptionSuccessListener =
|
||||
RNIap.purchaseUpdatedListener(onSuccessfulSubscription);
|
||||
refValues.current.subsriptionErrorListener =
|
||||
RNIap.purchaseErrorListener(onSubscriptionError);
|
||||
});
|
||||
if (Platform.OS === "android") {
|
||||
try {
|
||||
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
|
||||
} catch (e) {}
|
||||
}
|
||||
refValues.current.subsriptionSuccessListener =
|
||||
RNIap.purchaseUpdatedListener(onSuccessfulSubscription);
|
||||
refValues.current.subsriptionErrorListener =
|
||||
RNIap.purchaseErrorListener(onSubscriptionError);
|
||||
}, []);
|
||||
|
||||
const unSubscribeFromIAPListeners = () => {
|
||||
|
||||
49
apps/mobile/app/hooks/use-group-options.ts
Normal file
49
apps/mobile/app/hooks/use-group-options.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { db } from "../common/database";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
|
||||
export function useGroupOptions(type: any) {
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions(type)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onUpdate = () => {
|
||||
const options = db.settings?.getGroupOptions(type) as any;
|
||||
if (
|
||||
groupOptions?.groupBy !== options.groupBy ||
|
||||
groupOptions?.sortBy !== options.sortBy ||
|
||||
groupOptions?.sortDirection !== groupOptions?.sortDirection
|
||||
) {
|
||||
setGroupOptions({ ...options });
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [type, groupOptions]);
|
||||
|
||||
return groupOptions;
|
||||
}
|
||||
@@ -384,7 +384,16 @@ const _TabsHolder = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
changeNavigationBarColor(colors.primary.background, isDark, true);
|
||||
function updateSystemBars() {
|
||||
changeNavigationBarColor(colors.primary.background, isDark, true);
|
||||
StatusBar.setBackgroundColor("transparent");
|
||||
StatusBar.setTranslucent(true);
|
||||
StatusBar.setBarStyle(isDark ? "light-content" : "dark-content");
|
||||
}
|
||||
updateSystemBars();
|
||||
setTimeout(() => {
|
||||
updateSystemBars();
|
||||
}, 1000);
|
||||
}, [colors.primary.background, isDark]);
|
||||
|
||||
return (
|
||||
@@ -406,11 +415,7 @@ const _TabsHolder = () => {
|
||||
: 0
|
||||
}}
|
||||
>
|
||||
<StatusBar
|
||||
barStyle={isDark ? "light-content" : "dark-content"}
|
||||
translucent={true}
|
||||
backgroundColor="transparent"
|
||||
/>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
|
||||
{!introCompleted ? (
|
||||
<NavigationStack />
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.23",
|
||||
"react-native-actions-sheet": "0.9.0-alpha.24",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
|
||||
@@ -115,7 +115,7 @@ const Editor = React.memo(
|
||||
} else {
|
||||
editor.commands.updateImage({
|
||||
hash: hash,
|
||||
src: src
|
||||
dataurl: src
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -167,7 +167,7 @@ const Editor = React.memo(
|
||||
globalThis.noToolbar=${noToolbar};
|
||||
globalThis.noHeader=${noHeader};
|
||||
`}
|
||||
injectedJavaScript={`globalThis.sessionId="${editor.sessionId}";`}
|
||||
useSharedProcessPool={false}
|
||||
javaScriptEnabled={true}
|
||||
focusable={true}
|
||||
onContentProcessDidTerminate={onError}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
export const ProgressBar = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const loading = useAttachmentStore((state) => state.loading);
|
||||
const [prog, setProg] = useState(0);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const timer = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [width, setWidth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
if (loading.current !== loading.total) {
|
||||
setVisible(true);
|
||||
setProg(loading.current / loading.total);
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const clear = () => {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
timer.current = setTimeout(() => {
|
||||
setProg(1);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 1000);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return visible ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
marginTop: insets.top + 45,
|
||||
width: "100%"
|
||||
}}
|
||||
onLayout={(event) => setWidth(event.nativeEvent.layout.width)}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
size={SIZE.xxl}
|
||||
progress={prog}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
height={1}
|
||||
width={width || 400}
|
||||
/>
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
136
apps/mobile/app/screens/editor/progress.tsx
Normal file
136
apps/mobile/app/screens/editor/progress.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
|
||||
export const ProgressBar = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const currentlyEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
);
|
||||
const downloading = useAttachmentStore((state) => state.downloading);
|
||||
|
||||
const loading = currentlyEditingNote
|
||||
? downloading?.[currentlyEditingNote]
|
||||
: undefined;
|
||||
|
||||
const attachmentProgress = useAttachmentStore((state) => state.progress);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const timer = useRef<NodeJS.Timeout>();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [width, setWidth] = useState(400);
|
||||
|
||||
const groupProgressInfo = useRef<{
|
||||
[name: string]: {
|
||||
total: number;
|
||||
current: number;
|
||||
};
|
||||
}>({});
|
||||
|
||||
const currentItemProgress = loading?.filename
|
||||
? attachmentProgress?.[loading?.filename]
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
console.log(loading);
|
||||
if (
|
||||
loading.current === loading.total &&
|
||||
typeof loading.success === "boolean"
|
||||
) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
setVisible(true);
|
||||
if (!loading.filename) return;
|
||||
|
||||
if (!groupProgressInfo.current[loading.filename]) {
|
||||
groupProgressInfo.current[loading.filename] = {
|
||||
total: 1,
|
||||
current: 0
|
||||
};
|
||||
}
|
||||
|
||||
const itemProgressCurrent = groupProgressInfo.current[loading.filename];
|
||||
|
||||
const itemTotalSize =
|
||||
currentItemProgress?.total || itemProgressCurrent.total;
|
||||
|
||||
const itemCurrentProgress =
|
||||
currentItemProgress?.recieved ||
|
||||
currentItemProgress?.sent ||
|
||||
itemProgressCurrent.current;
|
||||
|
||||
groupProgressInfo.current[loading.filename] = {
|
||||
current: itemCurrentProgress,
|
||||
total: itemTotalSize
|
||||
};
|
||||
|
||||
const itemProgressPercent = itemCurrentProgress / itemTotalSize;
|
||||
|
||||
setProgress(
|
||||
((loading.current || 0) + itemProgressPercent) / (loading.total || 1)
|
||||
);
|
||||
} else {
|
||||
clear();
|
||||
}
|
||||
}, [currentItemProgress, loading]);
|
||||
|
||||
const clear = () => {
|
||||
clearTimeout(timer.current);
|
||||
timer.current = undefined;
|
||||
timer.current = setTimeout(() => {
|
||||
setProgress(1);
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
setProgress(0);
|
||||
groupProgressInfo.current = {};
|
||||
}, 1000);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
position: "absolute",
|
||||
zIndex: visible ? 1 : -1,
|
||||
marginTop: insets.top + 45,
|
||||
width: "100%"
|
||||
}}
|
||||
onLayout={(event) => setWidth(event.nativeEvent.layout.width)}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
progress={progress}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
borderRadius={0}
|
||||
height={2}
|
||||
width={width || 400}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -113,7 +113,7 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
};
|
||||
|
||||
setSessionId = async (id: string | null) =>
|
||||
await this.doAsync(`globalThis.sessionId = "${id}"`);
|
||||
await this.doAsync(`globalThis.sessionId = "${id}";`);
|
||||
|
||||
setStatus = async (date: string | undefined, saved: string) =>
|
||||
await this.doAsync(
|
||||
@@ -211,9 +211,20 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
insertImage = async (image: ImageAttributes) => {
|
||||
insertImage = async (
|
||||
image: Omit<ImageAttributes, "bloburl"> & {
|
||||
dataurl: string;
|
||||
}
|
||||
) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.insertImage(${JSON.stringify(image)})`
|
||||
`const image = toBlobURL("${image.dataurl}", "${image.hash}");
|
||||
editor && editor.commands.insertImage({
|
||||
...${JSON.stringify({
|
||||
...image,
|
||||
dataurl: undefined
|
||||
})},
|
||||
bloburl: image
|
||||
})`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -225,11 +236,20 @@ typeof globalThis.statusBar !== "undefined" && statusBar.current.set({date:"",sa
|
||||
);
|
||||
};
|
||||
|
||||
updateImage = async ({ src, hash }: Partial<ImageAttributes>) => {
|
||||
updateImage = async ({
|
||||
hash,
|
||||
dataurl
|
||||
}: Partial<Omit<ImageAttributes, "bloburl">> & {
|
||||
dataurl: string;
|
||||
}) => {
|
||||
await this.doAsync(
|
||||
`editor && editor.commands.updateImage(${JSON.stringify({
|
||||
`const image = toBlobURL("${dataurl}", "${hash}");
|
||||
editor && editor.commands.updateImage(${JSON.stringify({
|
||||
hash
|
||||
})},${JSON.stringify({ dataurl: src, hash, preventUpdate: true })})`
|
||||
})}, {
|
||||
...${JSON.stringify({ hash, preventUpdate: true })},
|
||||
bloburl: image
|
||||
})`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
|
||||
const showEncryptionSheet = (file) => {
|
||||
presentSheet({
|
||||
@@ -230,6 +231,16 @@ const handleImageResponse = async (response, options) => {
|
||||
});
|
||||
|
||||
let fileName = image.originalFileName || image.fileName;
|
||||
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
mime: image.type,
|
||||
title: fileName,
|
||||
dataurl: b64,
|
||||
size: image.fileSize,
|
||||
filename: fileName
|
||||
});
|
||||
|
||||
if (!(await attachFile(uri, hash, image.type, fileName, options))) return;
|
||||
const isPng = /(png)/g.test(image.type);
|
||||
const isJpeg = /(jpeg|jpg)/g.test(image.type);
|
||||
@@ -243,15 +254,6 @@ const handleImageResponse = async (response, options) => {
|
||||
}
|
||||
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
editorController.current?.commands.insertImage({
|
||||
hash: hash,
|
||||
mime: image.type,
|
||||
title: fileName,
|
||||
dataurl: b64,
|
||||
size: image.fileSize,
|
||||
filename: fileName
|
||||
});
|
||||
};
|
||||
|
||||
export async function attachFile(uri, hash, type, filename, options) {
|
||||
@@ -269,6 +271,10 @@ export async function attachFile(uri, hash, type, filename, options) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.reupload && exists) {
|
||||
options.reupload = (await filesystem.getUploadedFileSize(hash)) <= 0;
|
||||
}
|
||||
|
||||
if (!exists || options?.reupload) {
|
||||
let key = await db.attachments.generateKey();
|
||||
encryptionInfo = await Sodium.encryptFile(key, {
|
||||
|
||||
@@ -52,6 +52,7 @@ export type Settings = {
|
||||
fontFamily: string;
|
||||
dateFormat: string;
|
||||
timeFormat: string;
|
||||
fontScale: number;
|
||||
};
|
||||
|
||||
export type EditorProps = {
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
KeyboardEventListener,
|
||||
NativeEventSubscription
|
||||
NativeEventSubscription,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import { WebViewMessageEvent } from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
@@ -142,6 +143,8 @@ export const useEditorEvents = (
|
||||
const handleBack = useRef<NativeEventSubscription>();
|
||||
const readonly = useEditorStore((state) => state.readonly);
|
||||
const isPremium = useUserStore((state) => state.premium);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
|
||||
const doubleSpacedLines = useSettingStore(
|
||||
(state) => state.settings?.doubleSpacedLines
|
||||
);
|
||||
@@ -195,13 +198,13 @@ export const useEditorEvents = (
|
||||
: defaultFontSize,
|
||||
fontFamily: SettingsService.get().defaultFontFamily,
|
||||
dateFormat: db.settings?.getDateFormat(),
|
||||
timeFormat: db.settings?.getTimeFormat()
|
||||
timeFormat: db.settings?.getTimeFormat(),
|
||||
fontScale
|
||||
});
|
||||
}, [
|
||||
fullscreen,
|
||||
isPremium,
|
||||
readonly,
|
||||
editor.sessionId,
|
||||
editor.loading,
|
||||
deviceMode,
|
||||
tools,
|
||||
@@ -215,7 +218,8 @@ export const useEditorEvents = (
|
||||
defaultFontFamily,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
loading
|
||||
loading,
|
||||
fontScale
|
||||
]);
|
||||
|
||||
const onBackPress = useCallback(async () => {
|
||||
@@ -224,6 +228,8 @@ export const useEditorEvents = (
|
||||
logger.info("editor handled back event", editorHandledBack);
|
||||
return;
|
||||
}
|
||||
editorState().currentlyEditing = false;
|
||||
editor.reset();
|
||||
setTimeout(async () => {
|
||||
if (deviceMode !== "mobile" && fullscreen) {
|
||||
if (fullscreen) {
|
||||
@@ -236,14 +242,10 @@ export const useEditorEvents = (
|
||||
editorState().movedAway = true;
|
||||
tabBarRef.current?.goToPage(0);
|
||||
}
|
||||
setImmediate(() => {
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 500);
|
||||
});
|
||||
editorState().currentlyEditing = false;
|
||||
editor.reset();
|
||||
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}, 500);
|
||||
}, 1);
|
||||
}, [editor, deviceMode, fullscreen]);
|
||||
|
||||
@@ -339,8 +341,12 @@ export const useEditorEvents = (
|
||||
});
|
||||
}
|
||||
|
||||
if (editorMessage.type === EventTypes.back) {
|
||||
return onBackPress();
|
||||
}
|
||||
|
||||
if (
|
||||
editorMessage.sessionId !== editor.sessionId &&
|
||||
editorMessage.sessionId !== editor.sessionId.current &&
|
||||
editorMessage.type !== EditorEvents.status
|
||||
) {
|
||||
return;
|
||||
@@ -426,9 +432,6 @@ export const useEditorEvents = (
|
||||
editorState().isFullscreen = true;
|
||||
eSendEvent(eOpenFullscreenEditor);
|
||||
break;
|
||||
case EventTypes.back:
|
||||
onBackPress();
|
||||
break;
|
||||
case EventTypes.link:
|
||||
openLinkInBrowser(editorMessage.value as string);
|
||||
break;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeEngineStore } from "@notesnook/theme";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import WebView from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
import { DatabaseLogger, db } from "../../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
@@ -63,8 +63,7 @@ export const useEditor = (
|
||||
const theme = useThemeEngineStore((state) => state.theme);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sessionId, setSessionId] = useState<string>(makeSessionId());
|
||||
const sessionIdRef = useRef(sessionId);
|
||||
const sessionIdRef = useRef(makeSessionId());
|
||||
const editorRef = useRef<WebView>(null);
|
||||
const currentNote = useRef<NoteType | null>();
|
||||
const currentContent = useRef<Content | null>();
|
||||
@@ -99,10 +98,6 @@ export const useEditor = (
|
||||
postMessage(EditorEvents.theme, theme);
|
||||
}, [theme, postMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionIdRef.current = sessionId;
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
commands.setTags(currentNote.current);
|
||||
}, [commands, tags]);
|
||||
@@ -134,20 +129,27 @@ export const useEditor = (
|
||||
|
||||
const reset = useCallback(
|
||||
async (resetState = true, resetContent = true) => {
|
||||
currentNote.current?.id && db.fs?.cancel(currentNote.current.id, null);
|
||||
currentNote.current?.id &&
|
||||
db.fs?.cancel(currentNote.current.id, "download");
|
||||
currentNote.current = null;
|
||||
loadedImages.current = {};
|
||||
currentContent.current = null;
|
||||
clearTimeout(timers.current["loading-images"]);
|
||||
sessionHistoryId.current = undefined;
|
||||
saveCount.current = 0;
|
||||
loadingState.current = undefined;
|
||||
lock.current = false;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
resetContent && postMessage(EditorEvents.title, "");
|
||||
lastContentChangeTime.current = 0;
|
||||
resetContent && (await commands.clearContent());
|
||||
resetContent && (await commands.clearTags());
|
||||
|
||||
if (resetState) {
|
||||
const newSessionId = makeSessionId();
|
||||
sessionIdRef.current = newSessionId;
|
||||
await commands.setSessionId(newSessionId);
|
||||
|
||||
isDefaultEditor &&
|
||||
useEditorStore.getState().setCurrentlyEditingNote(null);
|
||||
placeholderTip.current = TipManager.placeholderTip();
|
||||
@@ -264,7 +266,8 @@ export const useEditor = (
|
||||
saveCount.current++;
|
||||
return id;
|
||||
} catch (e) {
|
||||
console.log("Error saving note: ", e);
|
||||
console.error(e);
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
},
|
||||
[commands, isDefaultEditor, postMessage, readonly, reset]
|
||||
@@ -279,6 +282,7 @@ export const useEditor = (
|
||||
noteId: currentNote.current?.id as string
|
||||
};
|
||||
} else {
|
||||
if (!note.contentId) return;
|
||||
currentContent.current = await db.content?.raw(note.contentId);
|
||||
}
|
||||
}, []);
|
||||
@@ -355,7 +359,6 @@ export const useEditor = (
|
||||
if (item && item.type === "new") {
|
||||
currentNote.current && (await reset());
|
||||
const nextSessionId = makeSessionId(item as NoteType);
|
||||
setSessionId(nextSessionId);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
await commands.setSessionId(nextSessionId);
|
||||
@@ -366,12 +369,23 @@ export const useEditor = (
|
||||
if (!item.forced && currentNote.current?.id === item.id) return;
|
||||
state.current.movedAway = false;
|
||||
state.current.currentlyEditing = true;
|
||||
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id);
|
||||
currentNote.current && (await reset(false, false));
|
||||
|
||||
if (currentNote.current?.id !== item.id) {
|
||||
currentNote.current && (await reset(false, false));
|
||||
isDefaultEditor && editorState.setCurrentlyEditingNote(item.id);
|
||||
}
|
||||
|
||||
await loadContent(item as NoteType);
|
||||
if (loadingState.current === currentContent.current?.data) {
|
||||
|
||||
if (
|
||||
currentNote.current?.id === item.id &&
|
||||
loadingState.current &&
|
||||
currentContent.current?.data &&
|
||||
loadingState.current === currentContent.current?.data
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!currentContent.current?.data ||
|
||||
currentContent.current?.data.length < 50000
|
||||
@@ -386,20 +400,21 @@ export const useEditor = (
|
||||
}
|
||||
lastContentChangeTime.current = item.dateEdited;
|
||||
const nextSessionId = makeSessionId(item as NoteType);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
lockedSessionId.current = nextSessionId;
|
||||
sessionHistoryId.current = Date.now();
|
||||
setSessionId(nextSessionId);
|
||||
commands.setSessionId(nextSessionId);
|
||||
sessionIdRef.current = nextSessionId;
|
||||
await commands.setSessionId(nextSessionId);
|
||||
currentNote.current = item as NoteType;
|
||||
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
|
||||
await postMessage(EditorEvents.title, item.title);
|
||||
loadingState.current = currentContent.current?.data;
|
||||
await postMessage(
|
||||
EditorEvents.html,
|
||||
currentContent.current?.data,
|
||||
10000
|
||||
);
|
||||
if (currentContent.current?.data) {
|
||||
await postMessage(
|
||||
EditorEvents.html,
|
||||
currentContent.current?.data,
|
||||
10000
|
||||
);
|
||||
}
|
||||
loadingState.current = undefined;
|
||||
useEditorStore.getState().setReadonly(item.readonly);
|
||||
await commands.setTags(currentNote.current);
|
||||
@@ -525,6 +540,14 @@ export const useEditor = (
|
||||
}) => {
|
||||
if (lock.current || lockedSessionId.current === forSessionId) return;
|
||||
lastContentChangeTime.current = Date.now();
|
||||
|
||||
if (
|
||||
sessionHistoryId.current &&
|
||||
Date.now() - sessionHistoryId.current > 5 * 60 * 1000
|
||||
) {
|
||||
sessionHistoryId.current = Date.now();
|
||||
}
|
||||
|
||||
if (type === EditorEvents.content) {
|
||||
currentContent.current = {
|
||||
data: content,
|
||||
@@ -608,7 +631,7 @@ export const useEditor = (
|
||||
|
||||
useEffect(() => {
|
||||
state.current.saveCount = 0;
|
||||
}, [sessionId, loading]);
|
||||
}, [loading]);
|
||||
|
||||
const onReady = useCallback(async () => {
|
||||
if (!(await isEditorLoaded(editorRef, sessionIdRef.current))) {
|
||||
@@ -660,8 +683,7 @@ export const useEditor = (
|
||||
loading,
|
||||
setLoading,
|
||||
state,
|
||||
sessionId,
|
||||
setSessionId,
|
||||
sessionId: sessionIdRef,
|
||||
note: currentNote,
|
||||
onReady,
|
||||
saveContent,
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
|
||||
<List
|
||||
listData={notes}
|
||||
type="notes"
|
||||
screen="Notes"
|
||||
screen="Home"
|
||||
loading={loading || !isFocused}
|
||||
headerProps={{
|
||||
heading: "Notes"
|
||||
|
||||
@@ -71,7 +71,7 @@ export const SearchBar = () => {
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
ToastEvent.show({
|
||||
heading: "Error occured while searching",
|
||||
heading: "Error occurred while searching",
|
||||
message: e.message,
|
||||
type: "error"
|
||||
});
|
||||
|
||||
@@ -320,13 +320,13 @@ export const settingsGroups: SettingSection[] = [
|
||||
name: "Delete account",
|
||||
icon: "alert",
|
||||
description:
|
||||
"All your data will be removed permanantly. Make sure you have saved backup of your notes. This action is IRREVERSIBLE.",
|
||||
"All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE.",
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: "Delete account",
|
||||
paragraphColor: "red",
|
||||
paragraph:
|
||||
"All your data will be removed permanantly. Make sure you have saved backup of your notes. This action is IRREVERSIBLE.",
|
||||
"All your data will be removed permanently. Make sure you have saved backup of your notes. This action is IRREVERSIBLE.",
|
||||
positiveType: "errorShade",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter account password",
|
||||
@@ -628,7 +628,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
name: "CORS bypass proxy",
|
||||
description: "You can set a custom proxy URL to increase your privacy.",
|
||||
inputProperties: {
|
||||
defaultValue: "https://cors.notesnook.com"
|
||||
defaultValue: "https://cors.notesnook.com",
|
||||
autoCorrect: false,
|
||||
keyboardType: "url"
|
||||
},
|
||||
property: "corsProxy",
|
||||
icon: "arrow-decision-outline"
|
||||
@@ -890,16 +892,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
name: "Restore backup",
|
||||
description: "Restore backup from phone storage.",
|
||||
modifer: () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user || !user?.email) {
|
||||
ToastEvent.show({
|
||||
heading: "Login required",
|
||||
message: "Please log in to your account to restore backup",
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
return;
|
||||
}
|
||||
eSendEvent(eOpenRestoreDialog);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,6 +42,9 @@ export const Subscription = () => {
|
||||
user?.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM &&
|
||||
user?.subscription?.type !== SUBSCRIPTION_STATUS.BETA;
|
||||
|
||||
const hasCancelledPremium =
|
||||
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED === user?.subscription?.type;
|
||||
|
||||
const subscriptionProviderInfo =
|
||||
SUBSCRIPTION_PROVIDER[user?.subscription?.provider];
|
||||
|
||||
@@ -63,10 +66,7 @@ export const Subscription = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
user?.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_CANCELLED &&
|
||||
Platform.OS === "android"
|
||||
) {
|
||||
if (hasCancelledPremium && Platform.OS === "android") {
|
||||
if (user.subscription?.provider === 3) {
|
||||
ToastEvent.show({
|
||||
heading: "Subscribed on web",
|
||||
@@ -88,6 +88,13 @@ export const Subscription = () => {
|
||||
}
|
||||
};
|
||||
|
||||
function getPrice() {
|
||||
return Platform.OS === "android"
|
||||
? monthlyPlan?.product?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: monthlyPlan?.product?.localizedPrice;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{isNotPro ? (
|
||||
@@ -104,22 +111,16 @@ export const Subscription = () => {
|
||||
title={
|
||||
!user?.isEmailConfirmed
|
||||
? "Confirm your email"
|
||||
: user.subscription?.provider === 3 &&
|
||||
user.subscription?.type ===
|
||||
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED
|
||||
: user.subscription?.provider === 3 && hasCancelledPremium
|
||||
? "Manage subscription from desktop app"
|
||||
: user.subscription?.type ===
|
||||
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED &&
|
||||
: hasCancelledPremium &&
|
||||
Platform.OS === "android" &&
|
||||
Config.GITHUB_RELEASE !== "true"
|
||||
? "Resubscribe from Google Playstore"
|
||||
: user.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED
|
||||
? `Resubscribe to Pro (${
|
||||
monthlyPlan?.product?.localizedPrice || "$4.49"
|
||||
} / mo)`
|
||||
: `Get Pro (${
|
||||
monthlyPlan?.product?.localizedPrice || "$4.49"
|
||||
} / mo)`
|
||||
: user.subscription?.type ===
|
||||
SUBSCRIPTION_STATUS.PREMIUM_EXPIRED || hasCancelledPremium
|
||||
? `Resubscribe to Pro (${getPrice() || "$4.49"} / mo)`
|
||||
: `Get Pro (${getPrice() || "$4.49"} / mo)`
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -52,7 +52,7 @@ const onPressFloatingButton = () => {
|
||||
ToastEvent.show({
|
||||
heading: "Trash cleared",
|
||||
message:
|
||||
"All notes and notebooks in the trash have been removed permanantly.",
|
||||
"All notes and notebooks in the trash have been removed permanently.",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ 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 { sanitizeFilename } from "@notesnook/common";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
@@ -30,6 +29,8 @@ import { eCloseSheet } from "../utils/events";
|
||||
import { sleep } from "../utils/time";
|
||||
import { ToastEvent, eSendEvent, presentSheet } from "./event-manager";
|
||||
import SettingsService from "./settings";
|
||||
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
const MS_WEEK = MS_DAY * 7;
|
||||
@@ -152,8 +153,6 @@ async function run(progress, context) {
|
||||
let androidBackupDirectory = await checkBackupDirExists(false, context);
|
||||
if (!androidBackupDirectory) return;
|
||||
|
||||
let backup;
|
||||
|
||||
if (progress) {
|
||||
presentSheet({
|
||||
title: "Backing up your data",
|
||||
@@ -163,43 +162,67 @@ async function run(progress, context) {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
backup = await db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup
|
||||
);
|
||||
if (!backup) throw new Error("Backup returned empty.");
|
||||
} catch (e) {
|
||||
await sleep(300);
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastEvent.error(e, "Backup failed!");
|
||||
return null;
|
||||
let path;
|
||||
let backupFilePath;
|
||||
let backupFileName = "notesnook_backup_" + Date.now();
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups/");
|
||||
}
|
||||
|
||||
try {
|
||||
let backupName = "notesnook_backup_" + Date.now();
|
||||
backupName =
|
||||
sanitizeFilename(backupName, { replacement: "_" }) + ".nnbackup";
|
||||
let path;
|
||||
let backupFilePath;
|
||||
const zipSourceFolder = `${cacheDir}/${backupFileName}`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${path}/${backupFileName}.nnbackupz`
|
||||
: `${cacheDir}/${backupFileName}.nnbackupz`;
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups/");
|
||||
await RNFetchBlob.fs.writeFile(path + backupName, backup, "utf8");
|
||||
backupFilePath = path + backupName;
|
||||
} else {
|
||||
backupFilePath = await ScopedStorage.writeFile(
|
||||
androidBackupDirectory.uri,
|
||||
backup,
|
||||
backupName,
|
||||
"nnbackup/json",
|
||||
"utf8",
|
||||
false
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
try {
|
||||
for await (const file of db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup
|
||||
)) {
|
||||
console.log("Writing backup chunk of size...", file?.data?.length);
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
`${zipSourceFolder}/${file.path}`,
|
||||
file.data,
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
|
||||
console.log("Final zip:", await RNFetchBlob.fs.stat(zipOutputFile));
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
androidBackupDirectory.uri,
|
||||
`${backupFileName}.nnbackupz`,
|
||||
"application/nnbackupz"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = zipOutputFile;
|
||||
}
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
updateNextBackupTime();
|
||||
|
||||
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
|
||||
|
||||
if (context) return path;
|
||||
await sleep(300);
|
||||
if (showBackupCompleteSheet) {
|
||||
presentBackupCompleteSheet(backupFilePath);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
|
||||
ToastEvent.show({
|
||||
heading: "Backup successful",
|
||||
message: "Your backup is stored in Notesnook folder on your phone.",
|
||||
@@ -207,18 +230,10 @@ async function run(progress, context) {
|
||||
context: "global"
|
||||
});
|
||||
|
||||
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
|
||||
|
||||
if (context) return backupFilePath;
|
||||
await sleep(300);
|
||||
if (showBackupCompleteSheet) {
|
||||
presentBackupCompleteSheet(backupFilePath);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
return backupFilePath;
|
||||
return path;
|
||||
} catch (e) {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastEvent.error(e, "Backup failed!");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ async function makeHtml(note) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html"} type
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportAs(type, note, bulk) {
|
||||
let data;
|
||||
@@ -122,6 +122,9 @@ async function exportAs(type, note, bulk) {
|
||||
case "md":
|
||||
data = await db.notes.note(note.id).export("md");
|
||||
break;
|
||||
case "md-frontmatter":
|
||||
data = await db.notes.note(note.id).export("md-frontmatter");
|
||||
break;
|
||||
case "pdf":
|
||||
{
|
||||
let html = await makeHtml(note);
|
||||
@@ -159,7 +162,7 @@ async function exportAs(type, note, bulk) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html"} type
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function exportNote(note, type) {
|
||||
let path = await getPath(FolderNames[type]);
|
||||
@@ -180,7 +183,7 @@ async function exportNote(note, type) {
|
||||
filePath: path,
|
||||
type: "text/plain",
|
||||
name: "Text",
|
||||
fileName: fileName + `.${type}`
|
||||
fileName: fileName + `.${type === "md-frontmatter" ? "md" : type}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,7 +215,7 @@ function getUniqueFileName(fileName, results) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"txt" | "pdf" | "md" | "html"} type
|
||||
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
|
||||
*/
|
||||
async function bulkExport(notes, type, callback) {
|
||||
let path = await getPath(FolderNames[type]);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function setRateAppMessage() {
|
||||
|
||||
const recoveryKeyMessage = {
|
||||
visible: true,
|
||||
message: "Keep your data safe if you lose password",
|
||||
message: "Keep your data safe",
|
||||
actionText: "Save your account recovery key",
|
||||
onPress: () => {
|
||||
verifyUser(
|
||||
|
||||
@@ -39,6 +39,10 @@ import { eSendEvent, presentSheet, ToastEvent } from "./event-manager";
|
||||
|
||||
import SettingsService from "./settings";
|
||||
let premiumStatus = 0;
|
||||
|
||||
/**
|
||||
* @type {RNIap.Subscription[]}
|
||||
*/
|
||||
let products = [];
|
||||
let user = null;
|
||||
|
||||
@@ -68,7 +72,9 @@ async function setPremiumStatus() {
|
||||
}
|
||||
try {
|
||||
await RNIap.initConnection();
|
||||
products = await RNIap.getSubscriptions(itemSkus);
|
||||
products = await RNIap.getSubscriptions({
|
||||
skus: itemSkus
|
||||
});
|
||||
} catch (e) {
|
||||
console.log("subscriptions: ", e);
|
||||
}
|
||||
@@ -223,12 +229,21 @@ const showVerifyEmailDialog = () => {
|
||||
};
|
||||
|
||||
const subscriptions = {
|
||||
get: async () => {
|
||||
/**
|
||||
*
|
||||
* @returns {RNIap.Purchase} subscription
|
||||
*/
|
||||
get: () => {
|
||||
if (Platform.OS === "android") return;
|
||||
let _subscriptions = MMKV.getString("subscriptionsIOS");
|
||||
if (!_subscriptions) return [];
|
||||
return JSON.parse(_subscriptions);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {RNIap.Purchase} subscription
|
||||
* @returns
|
||||
*/
|
||||
set: async (subscription) => {
|
||||
if (Platform.OS === "android") return;
|
||||
let _subscriptions = MMKV.getString("subscriptionsIOS");
|
||||
@@ -263,6 +278,10 @@ const subscriptions = {
|
||||
MMKV.setString("subscriptionsIOS", JSON.stringify(_subscriptions));
|
||||
}
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {RNIap.Purchase} subscription
|
||||
*/
|
||||
verify: async (subscription) => {
|
||||
if (Platform.OS === "android") return;
|
||||
|
||||
@@ -280,12 +299,14 @@ const subscriptions = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
console.log("Subscription.verify", requestData);
|
||||
try {
|
||||
let result = await fetch(
|
||||
"https://payments.streetwriters.co/apple/verify",
|
||||
"http://192.168.43.5:4264/apple/verify",
|
||||
requestData
|
||||
);
|
||||
|
||||
console.log("Subscribed", result);
|
||||
let text = await result.text();
|
||||
|
||||
if (!result.ok) {
|
||||
@@ -293,6 +314,8 @@ const subscriptions = {
|
||||
await subscriptions.clear(subscription);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
await subscriptions.clear(subscription);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("subscription error", e);
|
||||
@@ -302,15 +325,18 @@ const subscriptions = {
|
||||
},
|
||||
clear: async (_subscription) => {
|
||||
if (Platform.OS === "android") return;
|
||||
let _subscriptions = await subscriptions.get();
|
||||
let _subscriptions = subscriptions.get();
|
||||
let subscription = null;
|
||||
if (_subscription) {
|
||||
subscription = _subscription;
|
||||
} else {
|
||||
subscription = _subscriptions.length > 0 ? _subscriptions[0] : null;
|
||||
}
|
||||
|
||||
if (subscription) {
|
||||
await RNIap.finishTransaction(subscription.transactionId);
|
||||
await RNIap.finishTransaction({
|
||||
purchase: subscription
|
||||
});
|
||||
await RNIap.clearTransactionIOS();
|
||||
await subscriptions.remove(subscription.transactionId);
|
||||
}
|
||||
@@ -362,7 +388,7 @@ const features_list = [
|
||||
content: "Instantly sync to unlimited devices"
|
||||
},
|
||||
{
|
||||
content: "A private vault to keep everything imporant always locked"
|
||||
content: "A private vault to keep everything important always locked"
|
||||
},
|
||||
{
|
||||
content:
|
||||
@@ -382,7 +408,7 @@ const sheet = (context, promo, trial) => {
|
||||
centered
|
||||
title="Upgrade to Notesnook"
|
||||
titlePart="Pro"
|
||||
paragraph="Manage your work on another level, enjoy seemless sync and keep all notes in one place."
|
||||
paragraph="Manage your work on another level, enjoy seamless sync and keep all notes in one place."
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
@@ -17,15 +17,14 @@ 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 { EVENTS } from "@notesnook/core/dist/common";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { db } from "../common/database";
|
||||
import { DatabaseLogger } from "../common/database/index";
|
||||
import { initAfterSync } from "../stores/index";
|
||||
import { SyncStatus, useUserStore } from "../stores/use-user-store";
|
||||
import BackgroundSync from "./background-sync";
|
||||
import { ToastEvent } from "./event-manager";
|
||||
import SettingsService from "./settings";
|
||||
import BackgroundSync from "./background-sync";
|
||||
|
||||
export const ignoredMessages = [
|
||||
"Sync already running",
|
||||
@@ -33,7 +32,7 @@ export const ignoredMessages = [
|
||||
"WebSocket failed to connect",
|
||||
"Failed to start the HttpConnection before"
|
||||
];
|
||||
|
||||
let pendingSync = undefined;
|
||||
let syncTimer = 0;
|
||||
const run = async (
|
||||
context = "global",
|
||||
@@ -43,10 +42,18 @@ const run = async (
|
||||
lastSyncTime
|
||||
) => {
|
||||
if (useUserStore.getState().syncing) {
|
||||
DatabaseLogger.log("Sync in progress");
|
||||
console.log("Sync in progress");
|
||||
DatabaseLogger.info("Sync in progress");
|
||||
pendingSync = {
|
||||
full: full
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingSync) {
|
||||
pendingSync = undefined;
|
||||
DatabaseLogger.info("Running pending sync...");
|
||||
}
|
||||
|
||||
clearTimeout(syncTimer);
|
||||
syncTimer = setTimeout(async () => {
|
||||
const status = await NetInfo.fetch();
|
||||
@@ -55,56 +62,54 @@ const run = async (
|
||||
if (!status.isInternetReachable) {
|
||||
DatabaseLogger.warn("Internet not reachable");
|
||||
}
|
||||
|
||||
if (
|
||||
!user ||
|
||||
!status.isInternetReachable ||
|
||||
SettingsService.get().disableSync
|
||||
) {
|
||||
initAfterSync();
|
||||
pendingSync = undefined;
|
||||
return onCompleted?.(false);
|
||||
}
|
||||
userstore.setSyncing(true);
|
||||
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
let res = await BackgroundSync.doInBackground(async () => {
|
||||
await BackgroundSync.doInBackground(async () => {
|
||||
try {
|
||||
await db.sync(full, forced, lastSyncTime);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
return e.message;
|
||||
}
|
||||
});
|
||||
if (!res) {
|
||||
initAfterSync();
|
||||
userstore.setSyncing(false, SyncStatus.Failed);
|
||||
return onCompleted?.(false);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (typeof res === "string") throw error;
|
||||
userstore.setSyncing(false);
|
||||
return onCompleted?.(true);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
if (
|
||||
!ignoredMessages.find((im) => e.message?.includes(im)) &&
|
||||
userstore.user
|
||||
!ignoredMessages.find((message) => e.message?.includes(message)) &&
|
||||
userstore.user &&
|
||||
status.isConnected &&
|
||||
status.isInternetReachable
|
||||
) {
|
||||
userstore.setSyncing(false, SyncStatus.Failed);
|
||||
if (status.isConnected && status.isInternetReachable) {
|
||||
ToastEvent.error(e, "Sync failed", context);
|
||||
}
|
||||
ToastEvent.error(e, "Sync failed", context);
|
||||
}
|
||||
|
||||
DatabaseLogger.error(e, "[Client] Failed to sync");
|
||||
onCompleted?.(false);
|
||||
} finally {
|
||||
initAfterSync();
|
||||
userstore.setSyncing(
|
||||
false,
|
||||
error ? SyncStatus.Failed : SyncStatus.Passed
|
||||
);
|
||||
if (full || forced) {
|
||||
db.eventManager.publish(EVENTS.syncCompleted);
|
||||
}
|
||||
onCompleted?.(error ? SyncStatus.Failed : SyncStatus.Passed);
|
||||
setImmediate(() => {
|
||||
if (pendingSync) Sync.run("global", false, pendingSync.full);
|
||||
});
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
@@ -215,7 +215,7 @@ const placeholderTips = [
|
||||
"If you read someone else's diary, you get what you deserve. - David Sedaris",
|
||||
"Take quick notes from notifications. Enable the option in Settings to try",
|
||||
"Get Notesnook on all your devices. Or even open it in browser by going to https://app.notesnook.com to access all your notes",
|
||||
"With note history, you can restore back to an older version of the note if you accidently deleted something.",
|
||||
"With note history, you can restore back to an older version of the note if you accidentally deleted something.",
|
||||
"When your heart speaks, take good notes. - Judith Campbell",
|
||||
"You can publish a note and share it with anyone. Even if they don't use Notesnook!",
|
||||
"Published notes can be encrypted. Which means only you and the person you share the password with can read them.",
|
||||
|
||||
@@ -20,6 +20,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import create from "zustand";
|
||||
import { editorController } from "../screens/editor/tiptap/utils";
|
||||
|
||||
export type AttachmentGroupProgress = {
|
||||
total: number;
|
||||
current: number;
|
||||
groupId: string;
|
||||
filename: string;
|
||||
canceled?: boolean;
|
||||
success?: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
interface AttachmentStore {
|
||||
progress?: {
|
||||
[name: string]: {
|
||||
@@ -30,6 +40,7 @@ interface AttachmentStore {
|
||||
type: "upload" | "download";
|
||||
} | null;
|
||||
};
|
||||
|
||||
encryptionProgress: number;
|
||||
setEncryptionProgress: (encryptionProgress: number) => void;
|
||||
remove: (hash: string) => void;
|
||||
@@ -40,8 +51,14 @@ interface AttachmentStore {
|
||||
recieved: number,
|
||||
type: "upload" | "download"
|
||||
) => void;
|
||||
loading: { total: number; current: number };
|
||||
setLoading: (data: { total: number; current: number }) => void;
|
||||
downloading?: {
|
||||
[groupId: string]: AttachmentGroupProgress | undefined;
|
||||
};
|
||||
setDownloading: (data: AttachmentGroupProgress) => void;
|
||||
uploading?: {
|
||||
[groupId: string]: AttachmentGroupProgress | undefined;
|
||||
};
|
||||
setUploading: (data: AttachmentGroupProgress) => void;
|
||||
}
|
||||
|
||||
export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
@@ -73,6 +90,21 @@ export const useAttachmentStore = create<AttachmentStore>((set, get) => ({
|
||||
encryptionProgress: 0,
|
||||
setEncryptionProgress: (encryptionProgress) =>
|
||||
set({ encryptionProgress: encryptionProgress }),
|
||||
loading: { total: 0, current: 0 },
|
||||
setLoading: (data) => set({ loading: data ? { ...data } : data })
|
||||
|
||||
downloading: {},
|
||||
setDownloading: (data) =>
|
||||
set({
|
||||
downloading: {
|
||||
...get().downloading,
|
||||
[data.groupId]: data?.canceled ? undefined : data
|
||||
}
|
||||
}),
|
||||
uploading: {},
|
||||
setUploading: (data) =>
|
||||
set({
|
||||
uploading: {
|
||||
...get().uploading,
|
||||
[data.groupId]: data?.canceled ? undefined : data
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
@@ -56,7 +56,7 @@ export const useSelectionStore = create<SelectionStore>((set, get) => ({
|
||||
|
||||
set({
|
||||
selectedItemsList: selectedItems,
|
||||
selectionMode: get().selectionMode
|
||||
selectionMode: selectedItems.length === 0 ? false : get().selectionMode
|
||||
});
|
||||
},
|
||||
clearSelection: () => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export const SUBSCRIPTION_STATUS_STRINGS = {
|
||||
2: Platform.OS === "ios" ? "Pro" : "Beta",
|
||||
5: "Pro",
|
||||
6: "Expired",
|
||||
7: "Pro"
|
||||
7: "Pro (cancelled)"
|
||||
};
|
||||
|
||||
export const SUBSCRIPTION_PROVIDER = {
|
||||
|
||||
@@ -67,15 +67,10 @@ export const deleteItems = async (item, context) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
item &&
|
||||
item.id &&
|
||||
useSelectionStore.getState().selectedItemsList.length === 0
|
||||
) {
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
}
|
||||
|
||||
const { selectedItemsList } = useSelectionStore.getState();
|
||||
const selectedItemsList = item
|
||||
? [item]
|
||||
: useSelectionStore.getState().selectedItemsList;
|
||||
|
||||
let notes = selectedItemsList.filter((i) => i.type === "note");
|
||||
let notebooks = selectedItemsList.filter((i) => i.type === "notebook");
|
||||
@@ -107,46 +102,43 @@ export const deleteItems = async (item, context) => {
|
||||
|
||||
if (topics?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(topics, "topic", context);
|
||||
if (result.delete) {
|
||||
for (const topic of topics) {
|
||||
if (result.deleteNotes) {
|
||||
if (!result.delete) return;
|
||||
for (const topic of topics) {
|
||||
if (result.deleteNotes) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(notebooks, "notebook", context);
|
||||
if (!result.delete) return;
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (result.deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
const topics = notebook.topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(notebooks, "notebook", context);
|
||||
|
||||
if (result.delete) {
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (result.deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
const topics = notebook.topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
@@ -182,7 +174,9 @@ export const deleteItems = async (item, context) => {
|
||||
});
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().clearSelection();
|
||||
if (!item) {
|
||||
useSelectionStore.getState().clearSelection();
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
useMenuStore.getState().setColorNotes();
|
||||
SearchService.updateAndSearch();
|
||||
|
||||
@@ -24,6 +24,12 @@ import RNFetchBlob from "react-native-blob-util";
|
||||
import { db } from "../common/database";
|
||||
import { IOS_APPGROUPID } from "./constants";
|
||||
|
||||
const santizeUri = (uri) => {
|
||||
uri = decodeURI(uri);
|
||||
uri = Platform.OS === "ios" ? uri.replace("file:///", "/") : uri;
|
||||
return uri;
|
||||
};
|
||||
|
||||
export async function attachFile(uri, hash, type, filename, options) {
|
||||
try {
|
||||
let exists = db.attachments.exists(hash);
|
||||
@@ -60,25 +66,43 @@ async function createNotes(bundle) {
|
||||
const sessionId = bundle.note.sessionId;
|
||||
const id = await db.notes.add(bundle.note);
|
||||
|
||||
for (const item of bundle.notebooks) {
|
||||
if (item.type === "notebook") {
|
||||
db.relations.add(item, { id, type: "note" });
|
||||
} else {
|
||||
db.notes.addToNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id
|
||||
},
|
||||
id
|
||||
);
|
||||
if (!bundle.notebooks || !bundle.notebooks.length) {
|
||||
const defaultNotebook = db.settings?.getDefaultNotebook();
|
||||
if (defaultNotebook) {
|
||||
if (!defaultNotebook.topic) {
|
||||
db.relations.add(
|
||||
{ type: "notebook", id: defaultNotebook.id },
|
||||
{ id, type: "note" }
|
||||
);
|
||||
} else {
|
||||
db.notes.addToNotebook(
|
||||
{
|
||||
id: defaultNotebook?.id,
|
||||
topic: defaultNotebook.topic
|
||||
},
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const item of bundle.notebooks) {
|
||||
if (item.type === "notebook") {
|
||||
db.relations.add(item, { id, type: "note" });
|
||||
} else {
|
||||
db.notes.addToNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id
|
||||
},
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const file of bundle.files) {
|
||||
const uri =
|
||||
Platform.OS === "ios"
|
||||
? `${file.value.replace("file://", "")}`
|
||||
: `${file.value}`;
|
||||
Platform.OS === "ios" ? santizeUri(file.value) : `${file.value}`;
|
||||
const hash = await Sodium.hashFile({
|
||||
uri: uri,
|
||||
type: "cache"
|
||||
|
||||
@@ -30,36 +30,10 @@ import {
|
||||
elementByText,
|
||||
sleep,
|
||||
notVisibleByText,
|
||||
visibleById
|
||||
visibleById,
|
||||
createNotebook
|
||||
} from "./utils";
|
||||
|
||||
async function createNotebook(
|
||||
title = "Notebook 1",
|
||||
description = true,
|
||||
topic = true,
|
||||
topicCount = 1
|
||||
) {
|
||||
await tapByText("Add your first notebook");
|
||||
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
|
||||
title
|
||||
);
|
||||
if (description) {
|
||||
await elementById(
|
||||
notesnook.ids.dialogs.notebook.inputs.description
|
||||
).typeText(`Description of ${title}`);
|
||||
}
|
||||
if (topic) {
|
||||
for (let i = 1; i <= topicCount; i++) {
|
||||
await elementById(notesnook.ids.dialogs.notebook.inputs.topic).typeText(
|
||||
i === 1 ? "Topic" : "Topic " + i
|
||||
);
|
||||
await tapById("topic-add-button");
|
||||
}
|
||||
}
|
||||
await tapByText("Save");
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
// async function addTopic(title = "Topic") {
|
||||
// await tapById(notesnook.buttons.add);
|
||||
// await elementById("input-title").typeText(title);
|
||||
|
||||
@@ -25,7 +25,9 @@ import {
|
||||
prepare,
|
||||
tapByText,
|
||||
notVisibleByText,
|
||||
sleep
|
||||
sleep,
|
||||
navigate,
|
||||
createNotebook
|
||||
} from "./utils";
|
||||
|
||||
async function sortBy(sorting, elementText = "Default") {
|
||||
@@ -104,6 +106,23 @@ describe("Sort & filter", () => {
|
||||
await visibleByText("Month");
|
||||
});
|
||||
|
||||
it("Sort notes in topic", async () => {
|
||||
await prepare();
|
||||
await navigate("Notebooks");
|
||||
await sleep(500);
|
||||
await createNotebook("Notebook 1", true, true);
|
||||
await sleep(500);
|
||||
await device.pressBack();
|
||||
await sleep(500);
|
||||
await tapByText("Topic");
|
||||
await createNote("A", "A letter");
|
||||
await sleep(500);
|
||||
await createNote("B", "B letter");
|
||||
await sortBy("Abc");
|
||||
await sleep(300);
|
||||
await visibleByText("N");
|
||||
});
|
||||
|
||||
it("Compact mode", async () => {
|
||||
await prepare();
|
||||
await createNote("Note 1", "Note 1");
|
||||
|
||||
@@ -129,8 +129,36 @@ async function matchSnapshot(element, name) {
|
||||
});
|
||||
}
|
||||
|
||||
async function createNotebook(
|
||||
title = "Notebook 1",
|
||||
description = true,
|
||||
topic = true,
|
||||
topicCount = 1
|
||||
) {
|
||||
await tapByText("Add your first notebook");
|
||||
await elementById(notesnook.ids.dialogs.notebook.inputs.title).typeText(
|
||||
title
|
||||
);
|
||||
if (description) {
|
||||
await elementById(
|
||||
notesnook.ids.dialogs.notebook.inputs.description
|
||||
).typeText(`Description of ${title}`);
|
||||
}
|
||||
if (topic) {
|
||||
for (let i = 1; i <= topicCount; i++) {
|
||||
await elementById(notesnook.ids.dialogs.notebook.inputs.topic).typeText(
|
||||
i === 1 ? "Topic" : "Topic " + i
|
||||
);
|
||||
await tapById("topic-add-button");
|
||||
}
|
||||
}
|
||||
await tapByText("Save");
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
export {
|
||||
matchSnapshot,
|
||||
createNotebook,
|
||||
prepare,
|
||||
LaunchApp,
|
||||
createNote,
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2059
|
||||
versionCode 2072
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -8,6 +8,10 @@ import com.ammarahmed.mmkv.MMKV;
|
||||
|
||||
public class OnClearFromRecentService extends Service {
|
||||
|
||||
static {
|
||||
System.loadLibrary("rnmmkv");
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
@@ -15,7 +19,6 @@ public class OnClearFromRecentService extends Service {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@@ -31,8 +34,10 @@ public class OnClearFromRecentService extends Service {
|
||||
MMKV mmkv = MMKV.mmkvWithID("default",MMKV.SINGLE_PROCESS_MODE);
|
||||
mmkv.removeValueForKey("appState");
|
||||
stopSelf();
|
||||
} catch (Exception e) {
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
//System.exit(0);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 33
|
||||
targetSdkVersion = 33
|
||||
ext.kotlinVersion = '1.6.10'
|
||||
ext.kotlinVersion = '1.8.0'
|
||||
androidXAnnotation = "1.1.0"
|
||||
androidXCoreVersion = "1.7.0"
|
||||
androidXCore = "1.7.0"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
- Much improved and faster sync
|
||||
- Note pinned in notifications will update automatically when content changes
|
||||
- Quick notes from notifications now respect paragraph spacing settings.
|
||||
- Many bug fixes and small improvements
|
||||
- Added support for markdown with frontmatter in exports
|
||||
- Fixed a bug that would randomly cause the app to freeze on launch after using the app for some days.
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user