mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
328 Commits
fix-codebl
...
background
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f2786e55 | ||
|
|
c6612a8e5f | ||
|
|
356142bb22 | ||
|
|
dad1742e80 | ||
|
|
c68ed0dc52 | ||
|
|
978afa0899 | ||
|
|
14d57f200b | ||
|
|
2f4f351fb6 | ||
|
|
1344ba3821 | ||
|
|
21f9930cd0 | ||
|
|
0789f0c075 | ||
|
|
8e1d7e097c | ||
|
|
f3a977c070 | ||
|
|
c6fad05c56 | ||
|
|
72a99ff751 | ||
|
|
1fcc4aa0b4 | ||
|
|
3a97cdeb3d | ||
|
|
7cf6441b9c | ||
|
|
cdd3075e4f | ||
|
|
162d16b2c7 | ||
|
|
8e0559ca85 | ||
|
|
e92e54684e | ||
|
|
9321aa1732 | ||
|
|
5daeda890c | ||
|
|
f6dbf253fa | ||
|
|
acf993f06e | ||
|
|
4b4a388f5f | ||
|
|
3fd216bfd4 | ||
|
|
a2bbde30e6 | ||
|
|
2e4035cd7b | ||
|
|
e705793e0a | ||
|
|
37c0107660 | ||
|
|
5bfcf93713 | ||
|
|
920ff08f16 | ||
|
|
6b85150acb | ||
|
|
0e243fbeb4 | ||
|
|
24a5953e0f | ||
|
|
ec1e868510 | ||
|
|
7450585737 | ||
|
|
9a09332182 | ||
|
|
3992aa5dc0 | ||
|
|
14f9d0ac7f | ||
|
|
f1a615515b | ||
|
|
d4ff1dd46c | ||
|
|
6276bd5db3 | ||
|
|
34736307c3 | ||
|
|
60056f3fab | ||
|
|
f99a00789b | ||
|
|
fcfda03fb2 | ||
|
|
cd4c2934e0 | ||
|
|
396508d4b9 | ||
|
|
ffc03460b9 | ||
|
|
4315503893 | ||
|
|
1c9e42fbfc | ||
|
|
f8f992d11e | ||
|
|
8aa36f632a | ||
|
|
ac89e3851a | ||
|
|
430ad22263 | ||
|
|
c46ba25408 | ||
|
|
ddcddbc8a8 | ||
|
|
24b1ada636 | ||
|
|
98a3b12bb8 | ||
|
|
26dca8b1d8 | ||
|
|
7dc9529981 | ||
|
|
32e36325b3 | ||
|
|
7b554bf781 | ||
|
|
4bcc08d046 | ||
|
|
77bb9726ab | ||
|
|
94f3dc0c52 | ||
|
|
be19ff1171 | ||
|
|
6d52f9a6a3 | ||
|
|
b3c6bedc13 | ||
|
|
94bc61f636 | ||
|
|
68406a2a3a | ||
|
|
8a0bf20e11 | ||
|
|
1055b6f13a | ||
|
|
4994ad77b4 | ||
|
|
e0712ce2f3 | ||
|
|
559801ba80 | ||
|
|
b69dd3b217 | ||
|
|
8328575806 | ||
|
|
30de9cabe0 | ||
|
|
506cb2dd47 | ||
|
|
2f5a3db1c1 | ||
|
|
06f4fee088 | ||
|
|
7540e34366 | ||
|
|
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 |
@@ -78,7 +78,13 @@ module.exports = {
|
||||
semi: ["error", "always"],
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"react/prop-types": "off",
|
||||
"header/header": ["error", "block", LICENSE, 1]
|
||||
"header/header": ["error", "block", LICENSE, 1],
|
||||
"@typescript-eslint/no-empty-interface": [
|
||||
"error",
|
||||
{
|
||||
allowSingleExtends: true
|
||||
}
|
||||
]
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,6 +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 "@azure/core-asynciterator-polyfill";
|
||||
import "react-native-gesture-handler";
|
||||
import SettingsService from "./services/settings";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
@@ -24,7 +25,6 @@ import {
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
@@ -38,14 +38,14 @@ import { useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import { themeTrpcClient } from "./screens/settings/theme-selector";
|
||||
|
||||
SettingsService.checkOrientation();
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useEffect(() => {
|
||||
let { appLockMode } = SettingsService.get();
|
||||
const { appLockMode } = SettingsService.get();
|
||||
if (appLockMode && appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
@@ -97,10 +97,10 @@ const App = () => {
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
: SettingsService.getProperty("lighTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element) => {
|
||||
export const withTheme = (Element: () => JSX.Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
state.colorScheme,
|
||||
@@ -16,15 +16,17 @@ 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 "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
@@ -49,15 +51,30 @@ database.host(
|
||||
}
|
||||
);
|
||||
|
||||
database.setup(
|
||||
Storage,
|
||||
Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
filesystem,
|
||||
{
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
batchSize: 500,
|
||||
sqliteOptions: {
|
||||
dialect: {
|
||||
createDriver: () =>
|
||||
new RNSqliteDriver({ async: true, dbName: "test.db" }),
|
||||
createAdapter: () => new SqliteAdapter(),
|
||||
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||
}
|
||||
// journalMode: "MEMORY",
|
||||
// synchronous: "normal",
|
||||
// pageSize: 8192,
|
||||
// cacheSize: -16000,
|
||||
// lockingMode: "exclusive"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
@@ -17,13 +17,13 @@ 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 { initialize } from "@notesnook/core/dist/logger";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
initalize(new KV(LoggerStorage));
|
||||
initialize(new KV(LoggerStorage));
|
||||
|
||||
export {};
|
||||
export { LoggerStorage };
|
||||
|
||||
123
apps/mobile/app/common/database/sqlite.kysely.ts
Normal file
123
apps/mobile/app/common/database/sqlite.kysely.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
export class RNSqliteDriver implements Driver {
|
||||
private connection?: DatabaseConnection;
|
||||
private connectionMutex = new ConnectionMutex();
|
||||
private db: QuickSQLiteConnection;
|
||||
constructor(private readonly config: Config) {
|
||||
this.db = open({
|
||||
name: config.dbName
|
||||
});
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.connection = new RNSqliteConnection(this.db);
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
// SQLite only has one single connection. We use a mutex here to wait
|
||||
// until the single connection has been released.
|
||||
await this.connectionMutex.lock();
|
||||
return this.connection!;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("begin"));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("commit"));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
||||
}
|
||||
|
||||
async releaseConnection(): Promise<void> {
|
||||
this.connectionMutex.unlock();
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionMutex {
|
||||
private promise?: Promise<void>;
|
||||
private resolve?: () => void;
|
||||
|
||||
async lock(): Promise<void> {
|
||||
while (this.promise) {
|
||||
await this.promise;
|
||||
}
|
||||
|
||||
this.promise = new Promise((resolve) => {
|
||||
this.resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
unlock(): void {
|
||||
const resolve = this.resolve;
|
||||
|
||||
this.promise = undefined;
|
||||
this.resolve = undefined;
|
||||
|
||||
resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
class RNSqliteConnection implements DatabaseConnection {
|
||||
constructor(private readonly db: QuickSQLiteConnection) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
}
|
||||
|
||||
async executeQuery<R>(
|
||||
compiledQuery: CompiledQuery<unknown>
|
||||
): Promise<QueryResult<R>> {
|
||||
const { parameters, sql, query } = compiledQuery;
|
||||
const mode =
|
||||
query.kind === "SelectQueryNode"
|
||||
? "query"
|
||||
: query.kind === "RawNode"
|
||||
? "raw"
|
||||
: "exec";
|
||||
const result = await this.db.executeAsync(sql, parameters as any[]);
|
||||
|
||||
// console.log("SQLITE result:", result?.rows?._array);
|
||||
if (mode === "query" || !result.insertId)
|
||||
return {
|
||||
rows: result.rows?._array || []
|
||||
};
|
||||
|
||||
return {
|
||||
insertId: BigInt(result.insertId),
|
||||
numAffectedRows: BigInt(result.rowsAffected),
|
||||
rows: mode === "raw" ? result.rows?._array || [] : []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -29,11 +29,12 @@ import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastEvent, presentSheet } from "../../services/event-manager";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
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,7 +184,9 @@ export default async function downloadAttachment(
|
||||
groupId: undefined
|
||||
}
|
||||
) {
|
||||
let attachment = db.attachments.attachment(hash);
|
||||
await createCacheDir();
|
||||
|
||||
let attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) {
|
||||
console.log("attachment not found");
|
||||
return;
|
||||
@@ -204,10 +208,12 @@ export default async function downloadAttachment(
|
||||
attachment.metadata.hash,
|
||||
options.groupId
|
||||
);
|
||||
await db.fs.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.metadata.hash,
|
||||
attachment.metadata.hash
|
||||
);
|
||||
if (
|
||||
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
|
||||
)
|
||||
@@ -239,7 +245,7 @@ export default async function downloadAttachment(
|
||||
);
|
||||
|
||||
if (!options.silent) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
type: "success"
|
||||
|
||||
@@ -20,19 +20,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import hosts from "@notesnook/core/dist/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { ToastManager } 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") {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastManager.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.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(() => {
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { Platform, TextInput, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { db } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
@@ -41,8 +42,8 @@ const AppLockedOverlay = () => {
|
||||
const appLocked = useUserStore((state) => state.appLocked);
|
||||
const lockApp = useUserStore((state) => state.lockApp);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const passwordInputRef = useRef();
|
||||
const password = useRef();
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const password = useRef<string>();
|
||||
const appState = useAppState();
|
||||
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
@@ -55,14 +56,14 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
useSettingStore.getState().setRequestBiometrics(true);
|
||||
|
||||
let unlocked = await BiometricService.validateUser(
|
||||
const unlocked = await BiometricService.validateUser(
|
||||
"Unlock to access your notes",
|
||||
""
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
password.current = undefined;
|
||||
}
|
||||
setTimeout(() => {
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
@@ -73,11 +74,11 @@ const AppLockedOverlay = () => {
|
||||
const onSubmit = async () => {
|
||||
if (!password.current) return;
|
||||
try {
|
||||
let unlocked = await db.user.verifyPassword(password.current);
|
||||
const unlocked = await db.user.verifyPassword(password.current);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = null;
|
||||
password.current = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -170,7 +171,9 @@ const AppLockedOverlay = () => {
|
||||
secureTextEntry
|
||||
placeholder="Enter account password"
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={onSubmit}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -17,9 +17,13 @@ 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 { formatBytes } from "@notesnook/common";
|
||||
import { Attachment, Note, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
@@ -27,14 +31,17 @@ import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseAttachmentDialog, eCloseSheet } from "../../utils/events";
|
||||
import {
|
||||
eCloseAttachmentDialog,
|
||||
eCloseSheet,
|
||||
eDBItemUpdate
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
@@ -46,28 +53,33 @@ import { Notice } from "../ui/notice";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
|
||||
const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
const Actions = ({
|
||||
attachment,
|
||||
fwdRef
|
||||
}: {
|
||||
attachment: Attachment;
|
||||
fwdRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const contextId = attachment.metadata.hash;
|
||||
const [filename, setFilename] = useState(attachment.metadata.filename);
|
||||
const contextId = attachment.hash;
|
||||
const [filename, setFilename] = useState(attachment.filename);
|
||||
const [currentProgress] = useAttachmentProgress(attachment);
|
||||
const [failed, setFailed] = useState(attachment.failed);
|
||||
const [notes, setNotes] = useState([]);
|
||||
const [loading, setLoading] = useState({
|
||||
name: null
|
||||
});
|
||||
const [failed, setFailed] = useState<string | undefined>(attachment.failed);
|
||||
const [notes, setNotes] = useState<Note[]>([]);
|
||||
const [loading, setLoading] = useState<{
|
||||
name?: string;
|
||||
}>({});
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Download",
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs.cancel(attachment.metadata.hash, "download");
|
||||
useAttachmentStore.getState().remove(attachment.metadata.hash);
|
||||
await db.fs().cancel(attachment.hash, "download");
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.metadata.hash, false);
|
||||
downloadAttachment(attachment.hash, false);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "download"
|
||||
@@ -76,7 +88,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Reupload",
|
||||
onPress: async () => {
|
||||
if (!PremiumService.get()) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Upgrade to pro",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -85,9 +97,9 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}
|
||||
await picker.pick({
|
||||
reupload: true,
|
||||
hash: attachment.metadata.hash,
|
||||
hash: attachment.hash,
|
||||
context: contextId,
|
||||
type: attachment.metadata.type
|
||||
type: attachment.type
|
||||
});
|
||||
},
|
||||
icon: "upload"
|
||||
@@ -98,28 +110,28 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
let res = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
let res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
setFailed(null);
|
||||
db.attachments.markAsFailed(attachment.id, null);
|
||||
ToastEvent.show({
|
||||
setFailed(undefined);
|
||||
db.attachments.markAsFailed(attachment.id);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
heading: "File check passed",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
|
||||
setAttachments([...db.attachments.all]);
|
||||
setLoading({
|
||||
name: null
|
||||
name: undefined
|
||||
});
|
||||
},
|
||||
icon: "file-check"
|
||||
@@ -128,19 +140,19 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
name: "Rename",
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: contextId,
|
||||
context: contextId as any,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
defaultValue: attachment.metadata.filename,
|
||||
defaultValue: attachment.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
await db.attachments.add({
|
||||
hash: attachment.metadata.hash,
|
||||
hash: attachment.hash,
|
||||
filename: value
|
||||
});
|
||||
setFilename(value);
|
||||
setAttachments([...db.attachments.all]);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
@@ -151,34 +163,22 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
{
|
||||
name: "Delete",
|
||||
onPress: async () => {
|
||||
await db.attachments.remove(attachment.metadata.hash, false);
|
||||
setAttachments([...db.attachments.all]);
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "delete-outline"
|
||||
}
|
||||
];
|
||||
|
||||
const getNotes = useCallback(() => {
|
||||
let allNotes = db.notes.all;
|
||||
let attachmentNotes = attachment.noteIds?.map((id) => {
|
||||
let index = allNotes?.findIndex((note) => id === note.id);
|
||||
if (index !== -1) {
|
||||
return allNotes[index];
|
||||
} else {
|
||||
return {
|
||||
type: "notfound",
|
||||
title: `Note with id ${id} does not exist.`,
|
||||
id: id
|
||||
};
|
||||
}
|
||||
});
|
||||
return attachmentNotes;
|
||||
}, [attachment.noteIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setNotes(getNotes());
|
||||
}, [attachment, getNotes]);
|
||||
db.relations
|
||||
.to(attachment, "note")
|
||||
.selector.items()
|
||||
.then((items) => {
|
||||
setNotes(items);
|
||||
});
|
||||
}, [attachment]);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -221,7 +221,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.metadata.type}
|
||||
{attachment.type}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
@@ -230,10 +230,10 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{formatBytes(attachment.length)}
|
||||
{formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
|
||||
{attachment.noteIds ? (
|
||||
{notes.length ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
@@ -241,14 +241,14 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.noteIds.length} note
|
||||
{attachment.noteIds.length > 1 ? "s" : ""}
|
||||
{notes.length} note
|
||||
{notes.length > 1 ? "s" : ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph
|
||||
onPress={() => {
|
||||
Clipboard.setString(attachment.metadata.hash);
|
||||
ToastEvent.show({
|
||||
Clipboard.setString(attachment.hash);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
heading: "Attachment hash copied",
|
||||
context: "local"
|
||||
@@ -257,7 +257,7 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{attachment.metadata.hash}
|
||||
{attachment.hash}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -286,21 +286,11 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
{notes.map((item) => (
|
||||
<PressableButton
|
||||
onPress={async () => {
|
||||
if (item.type === "notfound") {
|
||||
ToastEvent.show({
|
||||
heading: "Note not found",
|
||||
message:
|
||||
"A note with the given id was not found. Maybe you have deleted the note or moved it to trash already.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
await sleep(150);
|
||||
eSendEvent(eCloseAttachmentDialog);
|
||||
await sleep(300);
|
||||
openNote(item, item.type === "trash");
|
||||
openNote(item, (item as any).type === "trash");
|
||||
}}
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
@@ -321,17 +311,16 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
<Button
|
||||
key={item.name}
|
||||
buttonType={{
|
||||
text: item.on
|
||||
? colors.primary.accent
|
||||
: item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
text:
|
||||
item.name === "Delete" || item.name === "PermDelete"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
title={item.name}
|
||||
icon={item.icon}
|
||||
loading={loading?.name === item.name}
|
||||
type={item.on ? "shade" : "gray"}
|
||||
type="gray"
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
@@ -359,12 +348,14 @@ const Actions = ({ attachment, setAttachments, fwdRef }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Actions.present = (attachment, set, context) => {
|
||||
Actions.present = (
|
||||
attachment: Attachment,
|
||||
set: (attachments: VirtualizedGrouping<Attachment>) => void,
|
||||
context?: string
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref) => (
|
||||
<Actions fwdRef={ref} setAttachments={set} attachment={attachment} />
|
||||
)
|
||||
component: (ref) => <Actions fwdRef={ref} attachment={attachment} />
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
@@ -29,32 +29,47 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
|
||||
function getFileExtension(filename) {
|
||||
function getFileExtension(filename: string) {
|
||||
var ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
return ext == null ? "" : ext[1];
|
||||
}
|
||||
|
||||
export const AttachmentItem = ({
|
||||
attachment,
|
||||
id,
|
||||
attachments,
|
||||
encryption,
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
}: {
|
||||
id: string;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
encryption?: boolean;
|
||||
setAttachments: (attachments: any) => void;
|
||||
pressable?: boolean;
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments?.item);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
encryption
|
||||
);
|
||||
|
||||
const onPress = () => {
|
||||
if (!pressable) return;
|
||||
if (!pressable || !attachment) return;
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
return (hideWhenNotDownloading &&
|
||||
(!currentProgress || !(currentProgress as any).value)) ||
|
||||
!attachment ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -67,7 +82,6 @@ export const AttachmentItem = ({
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
type="grayBg"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
@@ -93,7 +107,7 @@ export const AttachmentItem = ({
|
||||
position: "absolute"
|
||||
}}
|
||||
>
|
||||
{getFileExtension(attachment.metadata.filename).toUpperCase()}
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -113,14 +127,14 @@ export const AttachmentItem = ({
|
||||
lineBreakMode="middle"
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
{attachment.metadata.filename}
|
||||
{attachment.filename}
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.length)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{(currentProgress as any)?.type
|
||||
? "(" + (currentProgress as any).type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
@@ -132,8 +146,8 @@ export const AttachmentItem = ({
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
if (encryption || !pressable) return;
|
||||
db.fs.cancel(attachment.metadata.hash);
|
||||
setCurrentProgress(null);
|
||||
db.fs().cancel(attachment.hash, currentProgress?.type as any);
|
||||
setCurrentProgress(undefined);
|
||||
}}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -21,7 +21,10 @@ import React, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
@@ -29,8 +32,19 @@ import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
@@ -39,38 +53,40 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef();
|
||||
const groupId = useRef<string>();
|
||||
|
||||
const onDownload = async () => {
|
||||
update({
|
||||
update?.({
|
||||
disableClosing: true
|
||||
});
|
||||
} as PresentSheetOptions);
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
attachments,
|
||||
(progress, statusText) => setProgress({ value: progress, statusText }),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update({
|
||||
update?.({
|
||||
disableClosing: false
|
||||
});
|
||||
} as PresentSheetOptions);
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update({
|
||||
update?.({
|
||||
disableClosing: false
|
||||
});
|
||||
} as PresentSheetOptions);
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs.cancel(groupId.current);
|
||||
await db.fs().cancel(groupId.current, "download");
|
||||
setDownloading(false);
|
||||
groupId.current = null;
|
||||
groupId.current = undefined;
|
||||
};
|
||||
|
||||
const successResults = () => {
|
||||
@@ -91,11 +107,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments.length - failedResults().length;
|
||||
attachments?.ids?.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments.length
|
||||
attachments?.ids.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
@@ -157,7 +173,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={progress.value ? progress.value / attachments.length : 0}
|
||||
progress={
|
||||
progress.value ? progress.value / attachments.ids?.length : 0
|
||||
}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
@@ -174,7 +192,7 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments : []}
|
||||
data={downloading ? attachments.ids : undefined}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
@@ -189,14 +207,15 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyExtractor={(item) => item as string}
|
||||
renderItem={({ item }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
attachment={item}
|
||||
id={item as string}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
@@ -209,7 +228,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={close}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
@@ -227,7 +248,9 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={close}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="grayBg"
|
||||
title="No"
|
||||
/>
|
||||
@@ -258,7 +281,11 @@ const DownloadAttachments = ({ close, attachments, isNote, update }) => {
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (context, attachments, isNote) => {
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
@@ -17,16 +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, { useRef, useState } from "react";
|
||||
import {
|
||||
Attachment,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
@@ -34,79 +43,77 @@ import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
isAudio,
|
||||
isDocument,
|
||||
isImage,
|
||||
isVideo
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
|
||||
export const AttachmentDialog = ({ note }) => {
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] = useState(
|
||||
note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])]
|
||||
);
|
||||
|
||||
const attachmentSearchValue = useRef();
|
||||
const searchTimer = useRef();
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
|
||||
const onChangeText = (text) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
|
||||
} else {
|
||||
db.attachments.all
|
||||
.sorted(DEFAULT_SORTING)
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [note, refresh]);
|
||||
|
||||
const onChangeText = (text: string) => {
|
||||
attachmentSearchValue.current = text;
|
||||
if (
|
||||
!attachmentSearchValue.current ||
|
||||
attachmentSearchValue.current === ""
|
||||
) {
|
||||
setAttachments([...attachments]);
|
||||
refresh();
|
||||
}
|
||||
clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => {
|
||||
let results = db.lookup.attachments(
|
||||
attachments,
|
||||
attachmentSearchValue.current
|
||||
searchTimer.current = setTimeout(async () => {
|
||||
let results = await db.lookup.attachments(
|
||||
attachmentSearchValue.current as string
|
||||
);
|
||||
if (results.length === 0) return;
|
||||
setAttachments(results);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
const renderItem = ({ item }: { item: string }) => (
|
||||
<AttachmentItem
|
||||
setAttachments={setAttachments}
|
||||
attachment={item}
|
||||
attachments={attachments}
|
||||
id={item}
|
||||
context="attachments-list"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
const checkedAttachments = [];
|
||||
for (let attachment of attachments) {
|
||||
let result = await filesystem.checkAttachment(attachment.metadata.hash);
|
||||
for (let id of attachments.ids) {
|
||||
const attachment = await attachments.item(id as string);
|
||||
if (!attachment) continue;
|
||||
|
||||
let result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (result.failed) {
|
||||
await db.attachments.markAsFailed(
|
||||
attachment.metadata.hash,
|
||||
result.failed
|
||||
);
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, null);
|
||||
await db.attachments.markAsFailed(id as string, undefined);
|
||||
}
|
||||
checkedAttachments.push(
|
||||
db.attachments.attachment(attachment.metadata.hash)
|
||||
);
|
||||
setAttachments([...checkedAttachments]);
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -133,31 +140,33 @@ export const AttachmentDialog = ({ note }) => {
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = (type) => {
|
||||
const attachments = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: [...(db.attachments.all || [])];
|
||||
isDocument;
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
|
||||
switch (type) {
|
||||
case "all":
|
||||
return attachments;
|
||||
items = db.attachments.all;
|
||||
break;
|
||||
case "images":
|
||||
return attachments.filter((attachment) =>
|
||||
isImage(attachment.metadata.type)
|
||||
);
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "images")
|
||||
: db.attachments.images;
|
||||
break;
|
||||
case "video":
|
||||
return attachments.filter((attachment) =>
|
||||
isVideo(attachment.metadata.type)
|
||||
);
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.videos;
|
||||
break;
|
||||
case "audio":
|
||||
return attachments.filter((attachment) =>
|
||||
isAudio(attachment.metadata.type)
|
||||
);
|
||||
items = db.attachments.all;
|
||||
break;
|
||||
case "documents":
|
||||
return attachments.filter((attachment) =>
|
||||
isDocument(attachment.metadata.type)
|
||||
);
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.documents;
|
||||
}
|
||||
|
||||
return await items.sorted(DEFAULT_SORTING);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -215,6 +224,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
@@ -231,7 +241,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current);
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -253,10 +263,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
<Button
|
||||
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
|
||||
key={item.title}
|
||||
title={
|
||||
item.title +
|
||||
` (${filterAttachments(item.filterBy)?.length || 0})`
|
||||
}
|
||||
title={item.title}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
@@ -266,9 +273,9 @@ export const AttachmentDialog = ({ note }) => {
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
onPress={async () => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(filterAttachments(item.filterBy));
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -299,7 +306,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments}
|
||||
data={attachments?.ids as string[]}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
@@ -322,7 +329,7 @@ export const AttachmentDialog = ({ note }) => {
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note) => {
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
});
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -23,7 +23,7 @@ import { db } from "../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
@@ -45,7 +45,7 @@ export const ChangePassword = () => {
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Email not confirmed",
|
||||
message: "Please confirm your email to change account password",
|
||||
type: "error",
|
||||
@@ -54,7 +54,7 @@ export const ChangePassword = () => {
|
||||
return;
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again.",
|
||||
type: "error",
|
||||
@@ -66,7 +66,7 @@ export const ChangePassword = () => {
|
||||
try {
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Account password updated",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -75,7 +75,7 @@ export const ChangePassword = () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Failed to change password",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
|
||||
@@ -22,7 +22,7 @@ import { View } from "react-native";
|
||||
import ActionSheet from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
|
||||
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Account email is required.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Check your email to reset password",
|
||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||
type: "success",
|
||||
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
|
||||
setSent(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Recovery email not sent",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
|
||||
@@ -24,7 +24,7 @@ import { db } from "../../common/database";
|
||||
import { MMKV } from "../../common/database/mmkv";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
ToastEvent,
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
@@ -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 () => {
|
||||
@@ -83,7 +85,7 @@ export const SessionExpired = () => {
|
||||
clearAllStores();
|
||||
setVisible(false);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -100,9 +102,9 @@ export const SessionExpired = () => {
|
||||
|
||||
const open = React.useCallback(async () => {
|
||||
try {
|
||||
let res = await db.user.tokenManager.getToken();
|
||||
let res = await db.tokenManager.getToken();
|
||||
if (!res) throw new Error("no token found");
|
||||
if (db.user.tokenManager._isTokenExpired(res))
|
||||
if (db.tokenManager._isTokenExpired(res))
|
||||
throw new Error("token expired");
|
||||
Sync.run("global", false, true, async (complete) => {
|
||||
if (!complete) {
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
|
||||
@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
setSending(false);
|
||||
} catch (e) {
|
||||
setSending(false);
|
||||
ToastEvent.error(e, "Error sending 2FA Code", "local");
|
||||
ToastManager.error(e, "Error sending 2FA Code", "local");
|
||||
}
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -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);
|
||||
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin) => {
|
||||
(!password.current && step === LoginSteps.passwordAuth) ||
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
type: "error",
|
||||
@@ -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;
|
||||
}
|
||||
@@ -115,7 +121,7 @@ export const useLogin = (onFinishLogin) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Login failed",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
@@ -129,7 +135,7 @@ export const useLogin = (onFinishLogin) => {
|
||||
PremiumService.setPremiumStatus();
|
||||
setUser(user);
|
||||
clearMessage();
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Login successful",
|
||||
message: `Logged in as ${user.email}`,
|
||||
type: "success",
|
||||
|
||||
@@ -1,40 +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, { PropsWithChildren } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ContainerHeader = (props: PropsWithChildren) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return !selectionMode ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -36,10 +36,11 @@ import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
|
||||
interface FloatingButton {
|
||||
title?: string;
|
||||
interface FloatingButtonProps {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
shouldShow?: boolean;
|
||||
alwaysVisible?: boolean;
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ const FloatingButton = ({
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false
|
||||
}: FloatingButton) => {
|
||||
}: FloatingButtonProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
@@ -118,7 +119,7 @@ const FloatingButton = ({
|
||||
<PressableButton
|
||||
testID={notesnook.buttons.add}
|
||||
type="accent"
|
||||
accentColor={colors.static[color as keyof typeof colors.static]}
|
||||
accentColor={color}
|
||||
customStyle={{
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
|
||||
@@ -21,15 +21,9 @@ import React, { PropsWithChildren } from "react";
|
||||
import { KeyboardAvoidingView, Platform } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { Header } from "../header";
|
||||
import SelectionHeader from "../selection-header";
|
||||
|
||||
export const Container = ({ children }: PropsWithChildren) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
return (
|
||||
@@ -43,13 +37,6 @@ export const Container = ({ children }: PropsWithChildren) => {
|
||||
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
}}
|
||||
>
|
||||
{!introCompleted ? null : (
|
||||
<>
|
||||
<SelectionHeader />
|
||||
<Header />
|
||||
</>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
|
||||
@@ -17,15 +17,16 @@ 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 from "react";
|
||||
import { useNoteStore } from "../../stores/use-notes-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import { AddTopicDialog } from "../dialogs/add-topic";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { LoadingDialog } from "../dialogs/loading";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
import ResultDialog from "../dialogs/result";
|
||||
import { VaultDialog } from "../dialogs/vault";
|
||||
import ImagePreview from "../image-preview";
|
||||
@@ -36,7 +37,6 @@ import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -46,7 +46,6 @@ const DialogProvider = () => {
|
||||
<>
|
||||
<LoadingDialog />
|
||||
<Dialog context="global" />
|
||||
<AddTopicDialog colors={colors} />
|
||||
<PremiumDialog colors={colors} />
|
||||
<AuthModal colors={colors} />
|
||||
<MergeConflicts />
|
||||
@@ -62,6 +61,7 @@ const DialogProvider = () => {
|
||||
<AnnouncementDialog />
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,11 +21,9 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseAddNotebookDialog,
|
||||
eCloseAddTopicDialog,
|
||||
eCloseMoveNoteDialog,
|
||||
eOpenActionSheet,
|
||||
eOpenAddNotebookDialog,
|
||||
eOpenAddTopicDialog,
|
||||
eOpenMoveNoteDialog
|
||||
} from "../../utils/events";
|
||||
|
||||
@@ -52,9 +50,3 @@ export const AddNotebookEvent = (notebook) => {
|
||||
export const HideAddNotebookEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddNotebookDialog, notebook);
|
||||
};
|
||||
export const AddTopicEvent = (topic) => {
|
||||
eSendEvent(eOpenAddTopicDialog, topic);
|
||||
};
|
||||
export const HideAddTopicEvent = (notebook) => {
|
||||
eSendEvent(eCloseAddTopicDialog, notebook);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,9 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
@@ -31,6 +32,28 @@ 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";
|
||||
|
||||
export interface BaseDialogProps extends PropsWithChildren {
|
||||
animation?: "fade" | "none" | "slide" | undefined;
|
||||
visible: boolean;
|
||||
onRequestClose?: () => void;
|
||||
onShow?: () => void;
|
||||
premium?: boolean;
|
||||
statusBarTranslucent?: boolean;
|
||||
transparent?: boolean;
|
||||
centered?: boolean;
|
||||
bottom?: boolean;
|
||||
background?: string | ColorValue;
|
||||
animated?: boolean;
|
||||
bounce?: boolean;
|
||||
closeOnTouch?: boolean;
|
||||
useSafeArea?: boolean;
|
||||
avoidKeyboardResize?: boolean;
|
||||
enableSheetKeyboardHandler?: boolean;
|
||||
}
|
||||
|
||||
const BaseDialog = ({
|
||||
visible,
|
||||
@@ -43,14 +66,18 @@ const BaseDialog = ({
|
||||
transparent,
|
||||
centered = true,
|
||||
bottom = false,
|
||||
background = null,
|
||||
animated = true,
|
||||
bounce = true,
|
||||
closeOnTouch = true,
|
||||
useSafeArea = true,
|
||||
avoidKeyboardResize = false
|
||||
}) => {
|
||||
avoidKeyboardResize = false,
|
||||
enableSheetKeyboardHandler,
|
||||
background
|
||||
}: BaseDialogProps) => {
|
||||
const floating = useIsFloatingKeyboard();
|
||||
const appState = useAppState();
|
||||
const lockEvents = useRef(false);
|
||||
const [internalVisible, setIntervalVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -58,14 +85,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 +121,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 +157,7 @@ const BaseDialog = ({
|
||||
style={[
|
||||
styles.backdrop,
|
||||
{
|
||||
alignItems: centered ? "center" : undefined,
|
||||
justifyContent: centered
|
||||
? "center"
|
||||
: bottom
|
||||
@@ -116,7 +167,7 @@ const BaseDialog = ({
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={closeOnTouch ? onRequestClose : null}
|
||||
onPress={closeOnTouch ? onRequestClose : undefined}
|
||||
style={styles.overlayButton}
|
||||
/>
|
||||
{premium}
|
||||
@@ -133,8 +184,7 @@ const styles = StyleSheet.create({
|
||||
backdrop: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
justifyContent: "center"
|
||||
},
|
||||
overlayButton: {
|
||||
width: "100%",
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (value: unknown) => void;
|
||||
positivePress?: (value: any) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
|
||||
@@ -1,190 +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, { createRef } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eCloseAddTopicDialog,
|
||||
eOnTopicSheetUpdate,
|
||||
eOpenAddTopicDialog
|
||||
} from "../../../utils/events";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { Toast } from "../../toast";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
export class AddTopicDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
titleFocused: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.ref = createRef();
|
||||
this.title;
|
||||
this.titleRef = createRef();
|
||||
this.notebook = null;
|
||||
this.toEdit = null;
|
||||
}
|
||||
|
||||
addNewTopic = async () => {
|
||||
try {
|
||||
if (!this.title || this.title?.trim() === "") {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.toEdit) {
|
||||
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
|
||||
} else {
|
||||
let topic = this.toEdit;
|
||||
topic.title = this.title;
|
||||
|
||||
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
|
||||
}
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useMenuStore.getState().setMenuPins();
|
||||
});
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
useRelationStore.getState().update();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
eSubscribeEvent(eOpenAddTopicDialog, this.open);
|
||||
eSubscribeEvent(eCloseAddTopicDialog, this.close);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
|
||||
}
|
||||
|
||||
open = async ({ notebookId, toEdit }) => {
|
||||
let id = notebookId;
|
||||
if (id) {
|
||||
this.notebook = await db.notebooks.notebook(id).data;
|
||||
}
|
||||
this.toEdit = toEdit;
|
||||
|
||||
if (this.toEdit) {
|
||||
this.title = this.toEdit.title;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
};
|
||||
close = () => {
|
||||
this.title = null;
|
||||
this.notebook = null;
|
||||
this.toEdit = null;
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible } = this.state;
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
if (this.toEdit) {
|
||||
this.titleRef.current?.setNativeProps({
|
||||
text: this.toEdit.title
|
||||
});
|
||||
}
|
||||
|
||||
this.ref.current?.animateNextTransition();
|
||||
await sleep(300);
|
||||
this.titleRef.current?.focus();
|
||||
}}
|
||||
bounce={false}
|
||||
statusBarTranslucent={false}
|
||||
visible={true}
|
||||
onRequestClose={this.close}
|
||||
>
|
||||
<DialogContainer>
|
||||
<DialogHeader
|
||||
icon="book-outline"
|
||||
title={this.toEdit ? "Edit topic" : "New topic"}
|
||||
paragraph={
|
||||
this.toEdit
|
||||
? "Edit title of the topic"
|
||||
: "Add a new topic in " + this.notebook.title
|
||||
}
|
||||
padding={12}
|
||||
/>
|
||||
<Seperator half />
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
zIndex: 10
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={this.titleRef}
|
||||
testID="input-title"
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
placeholder="Enter title"
|
||||
onSubmit={() => this.addNewTopic()}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
positiveTitle={this.toEdit ? "Save" : "Add"}
|
||||
onPressNegative={() => this.close()}
|
||||
onPressPositive={() => this.addNewTopic()}
|
||||
/>
|
||||
</DialogContainer>
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,22 @@ 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, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { FlatList, ScrollView, View } from "react-native";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { useMessageStore } from "../../../stores/use-message-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseJumpToDialog,
|
||||
@@ -36,27 +43,47 @@ import { SIZE } from "../../../utils/size";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const offsets = [];
|
||||
let timeout = null;
|
||||
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
||||
const JumpToSectionDialog = () => {
|
||||
const scrollRef = useRef<RefObject<FlatList>>();
|
||||
const [data, setData] = useState<VirtualizedGrouping<Item>>();
|
||||
const { colors } = useThemeColors();
|
||||
const notes = data;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const offsets = useRef<number[]>([]);
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
|
||||
const onPress = (item) => {
|
||||
let ind = notes.findIndex(
|
||||
(i) => i.title === item.title && i.type === "header"
|
||||
);
|
||||
scrollRef.current?.scrollToIndex({
|
||||
index: ind,
|
||||
const onPress = (item: GroupHeader) => {
|
||||
const index = notes?.ids?.findIndex((i) => {
|
||||
if (typeof i === "object") {
|
||||
return i.title === item.title && i.type === "header";
|
||||
} else {
|
||||
false;
|
||||
}
|
||||
});
|
||||
scrollRef.current?.current?.scrollToIndex({
|
||||
index: index as number,
|
||||
animated: true
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
const open = useCallback(
|
||||
({
|
||||
data,
|
||||
ref
|
||||
}: {
|
||||
data: VirtualizedGrouping<Item>;
|
||||
ref: RefObject<FlatList>;
|
||||
}) => {
|
||||
setData(data);
|
||||
scrollRef.current = ref;
|
||||
setVisible(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenJumpToDialog, open);
|
||||
eSubscribeEvent(eCloseJumpToDialog, close);
|
||||
@@ -69,50 +96,52 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const onScroll = (data) => {
|
||||
let y = data.y;
|
||||
const onScroll = (data: { x: number; y: number }) => {
|
||||
const y = data.y;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
clearTimeout(timeout.current);
|
||||
timeout.current = undefined;
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
|
||||
setCurrentIndex(index || 0);
|
||||
timeout.current = setTimeout(() => {
|
||||
setCurrentIndex(
|
||||
offsets.current?.findIndex(
|
||||
(o, i) => o <= y && offsets.current[i + 1] > y
|
||||
) || 0
|
||||
);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const open = useCallback(
|
||||
(_type) => {
|
||||
if (_type !== type) return;
|
||||
setVisible(true);
|
||||
},
|
||||
[type]
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const loadOffsets = useCallback(() => {
|
||||
notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
.map((item, index) => {
|
||||
if (typeof item === "string") return;
|
||||
|
||||
let offset = 35 * index;
|
||||
let ind = notes.ids.findIndex(
|
||||
(i) =>
|
||||
typeof i === "object" &&
|
||||
i.title === item.title &&
|
||||
i.type === "header"
|
||||
);
|
||||
const messageState = useMessageStore.getState().message;
|
||||
const msgOffset = messageState?.visible ? 60 : 10;
|
||||
|
||||
ind = ind + 1;
|
||||
ind = ind - (index + 1);
|
||||
offset = offset + ind * 100 + msgOffset;
|
||||
offsets.current.push(offset);
|
||||
});
|
||||
}, [notes]);
|
||||
|
||||
useEffect(() => {
|
||||
loadOffsets();
|
||||
}, [loadOffsets, notes]);
|
||||
|
||||
const loadOffsets = useCallback(() => {
|
||||
notes
|
||||
.filter((i) => i.type === "header")
|
||||
.map((item, index) => {
|
||||
let offset = 35 * index;
|
||||
let ind = notes.findIndex(
|
||||
(i) => i.title === item.title && i.type === "header"
|
||||
);
|
||||
let messageState = useMessageStore.getState().message;
|
||||
let msgOffset = messageState?.visible ? 60 : 10;
|
||||
ind = ind + 1;
|
||||
ind = ind - (index + 1);
|
||||
offset = offset + ind * 100 + msgOffset;
|
||||
offsets.push(offset);
|
||||
});
|
||||
}, [notes]);
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
onShow={() => {
|
||||
@@ -149,13 +178,13 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
{notes
|
||||
.filter((i) => i.type === "header")
|
||||
{notes?.ids
|
||||
.filter((i) => typeof i === "object" && i.type === "header")
|
||||
.map((item, index) => {
|
||||
return item.title ? (
|
||||
return typeof item === "object" && item.title ? (
|
||||
<PressableButton
|
||||
key={item.title}
|
||||
onPress={() => onPress(item, index)}
|
||||
onPress={() => onPress(item)}
|
||||
type={currentIndex === index ? "selected" : "transparent"}
|
||||
customStyle={{
|
||||
minWidth: "20%",
|
||||
@@ -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:
|
||||
|
||||
@@ -27,14 +27,12 @@ import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastEvent
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eClearEditor,
|
||||
@@ -45,6 +43,7 @@ import {
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
@@ -55,10 +54,6 @@ import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
let Keychain;
|
||||
const passInputRef = createRef();
|
||||
const confirmPassRef = createRef();
|
||||
const changePassInputRef = createRef();
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -88,6 +83,11 @@ export class VaultDialog extends Component {
|
||||
deleteVault: false,
|
||||
deleteAll: false
|
||||
};
|
||||
|
||||
this.passInputRef = createRef();
|
||||
this.confirmPassRef = createRef();
|
||||
this.changePassInputRef = createRef();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.newPassword = null;
|
||||
@@ -148,9 +148,6 @@ export class VaultDialog extends Component {
|
||||
* @param {import('../../../services/event-manager').vaultType} data
|
||||
*/
|
||||
open = async (data) => {
|
||||
if (!Keychain) {
|
||||
Keychain = require("react-native-keychain");
|
||||
}
|
||||
let biometry = await BiometricService.isBiometryAvailable();
|
||||
let available = false;
|
||||
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
||||
@@ -199,7 +196,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
close = () => {
|
||||
if (this.state.loading) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: this.state.title,
|
||||
message: "Please wait and do not close the app.",
|
||||
type: "success",
|
||||
@@ -212,7 +209,6 @@ export class VaultDialog extends Component {
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
SearchService.updateAndSearch();
|
||||
this.setState({
|
||||
visible: false,
|
||||
note: {},
|
||||
@@ -236,7 +232,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (!this.password) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Password not entered",
|
||||
message: "Enter a password for the vault and try again.",
|
||||
type: "error",
|
||||
@@ -245,7 +241,7 @@ export class VaultDialog extends Component {
|
||||
return;
|
||||
}
|
||||
if (this.password && this.password.length < 3) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Password too short",
|
||||
message: "Password must be longer than 3 characters.",
|
||||
type: "error",
|
||||
@@ -257,7 +253,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Passwords do not match",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -283,7 +279,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.biometricUnlock) {
|
||||
this._enrollFingerprint(this.newPassword);
|
||||
}
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Vault password updated successfully",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -295,17 +291,19 @@ export class VaultDialog extends Component {
|
||||
loading: false
|
||||
});
|
||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
ToastManager.error(e);
|
||||
}
|
||||
});
|
||||
} else if (this.state.locked) {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
@@ -355,7 +353,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
this.close();
|
||||
} else {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Account password incorrect",
|
||||
message: "Please enter correct password for your account.",
|
||||
type: "error",
|
||||
@@ -382,7 +380,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Vault password incorrect",
|
||||
message: "Please enter correct password to clear vault.",
|
||||
type: "error",
|
||||
@@ -396,7 +394,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _lockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -408,7 +406,7 @@ export class VaultDialog extends Component {
|
||||
eSendEvent(eClearEditor);
|
||||
}
|
||||
this.close();
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
message: "Note locked successfully",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -421,7 +419,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _unlockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
type: "error",
|
||||
@@ -442,6 +440,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,14 +451,13 @@ export class VaultDialog extends Component {
|
||||
await this._copyNote(note);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
};
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems(this.state.note);
|
||||
await deleteItems([this.state.note.id], "note");
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
@@ -467,44 +465,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");
|
||||
ToastManager.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();
|
||||
ToastManager.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() {
|
||||
@@ -521,7 +515,7 @@ export class VaultDialog extends Component {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Note added to vault",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -529,7 +523,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
} else {
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Vault created successfully",
|
||||
type: "success",
|
||||
context: "global"
|
||||
@@ -542,8 +536,8 @@ export class VaultDialog extends Component {
|
||||
db.vault
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastEvent.show({
|
||||
heading: "Note permanantly unlocked.",
|
||||
ToastManager.show({
|
||||
heading: "Note permanently unlocked.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -557,7 +551,9 @@ export class VaultDialog extends Component {
|
||||
_openInEditor(note) {
|
||||
this.close();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
eSendEvent(eOnLoadNote, note);
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1);
|
||||
}
|
||||
@@ -566,7 +562,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString(await convertNoteToText(note));
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Note copied",
|
||||
type: "success",
|
||||
message: "Note has been copied to clipboard!",
|
||||
@@ -598,7 +594,7 @@ export class VaultDialog extends Component {
|
||||
visible: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -613,13 +609,13 @@ export class VaultDialog extends Component {
|
||||
try {
|
||||
await BiometricService.resetCredentials();
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Biometric unlocking disabled!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Failed to disable Biometric unlocking.",
|
||||
message: e.message,
|
||||
type: "success",
|
||||
@@ -671,7 +667,8 @@ export class VaultDialog extends Component {
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
passInputRef.current?.focus();
|
||||
|
||||
this.passInputRef.current?.focus();
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
@@ -706,7 +703,7 @@ export class VaultDialog extends Component {
|
||||
!this.state.revokeFingerprintAccess ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passInputRef}
|
||||
fwdRef={this.passInputRef}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
@@ -723,7 +720,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
onSubmit={() => {
|
||||
changePassword
|
||||
? changePassInputRef.current?.focus()
|
||||
? this.confirmPassRef.current?.focus()
|
||||
: this.onPress;
|
||||
}}
|
||||
autoComplete="password"
|
||||
@@ -775,7 +772,7 @@ export class VaultDialog extends Component {
|
||||
<>
|
||||
<Seperator half />
|
||||
<Input
|
||||
ref={changePassInputRef}
|
||||
ref={this.confirmPassRef}
|
||||
editable={!loading}
|
||||
testID={notesnook.ids.dialogs.vault.changePwd}
|
||||
autoCapitalize="none"
|
||||
@@ -795,7 +792,7 @@ export class VaultDialog extends Component {
|
||||
{!novault ? (
|
||||
<View>
|
||||
<Input
|
||||
fwdRef={passInputRef}
|
||||
fwdRef={this.passInputRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
@@ -806,13 +803,13 @@ export class VaultDialog extends Component {
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
onSubmit={() => {
|
||||
confirmPassRef.current?.focus();
|
||||
this.confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder="Password"
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPassRef}
|
||||
fwdRef={this.confirmPassRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
||||
secureTextEntry
|
||||
|
||||
@@ -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,42 +17,73 @@ 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, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { SearchBar } from "../../screens/search/search-bar";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import useNavigationStore, {
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
import { Title } from "./title";
|
||||
|
||||
const _Header = () => {
|
||||
type HeaderRightButton = {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
renderedInRoute,
|
||||
onLeftMenuButtonPress,
|
||||
title,
|
||||
titleHiddenOnRender,
|
||||
headerRightButtons,
|
||||
id,
|
||||
accentColor,
|
||||
isBeta,
|
||||
canGoBack,
|
||||
onPressDefaultRightButton,
|
||||
hasSearch,
|
||||
onSearch
|
||||
}: {
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
title: string;
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
titleHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
canGoBack?: boolean;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [hide, setHide] = useState(true);
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const currentScreen = useNavigationStore(
|
||||
(state) => state.currentScreen?.name
|
||||
);
|
||||
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
|
||||
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number }) => {
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
if (data.y > 150) {
|
||||
if (!hide) return;
|
||||
setHide(false);
|
||||
if (!borderHidden) return;
|
||||
setBorderHidden(false);
|
||||
} else {
|
||||
if (hide) return;
|
||||
setHide(true);
|
||||
if (borderHidden) return;
|
||||
setBorderHidden(true);
|
||||
}
|
||||
},
|
||||
[hide]
|
||||
[borderHidden, id, renderedInRoute]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,9 +91,9 @@ const _Header = () => {
|
||||
return () => {
|
||||
eUnSubscribeEvent(eScrollEvent, onScroll);
|
||||
};
|
||||
}, [hide, onScroll]);
|
||||
}, [borderHidden, onScroll]);
|
||||
|
||||
return selectionMode ? null : (
|
||||
return selectionMode && isFocused ? null : (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
@@ -72,29 +103,42 @@ const _Header = () => {
|
||||
backgroundColor: colors.primary.background,
|
||||
overflow: "hidden",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: hide
|
||||
borderBottomColor: borderHidden
|
||||
? "transparent"
|
||||
: colors.secondary.background,
|
||||
justifyContent: "space-between"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{currentScreen === "Search" ? (
|
||||
<SearchBar />
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.leftBtnContainer}>
|
||||
<LeftMenus />
|
||||
<Title />
|
||||
</View>
|
||||
<RightMenus />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<View style={styles.leftBtnContainer}>
|
||||
<LeftMenus
|
||||
canGoBack={canGoBack}
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
|
||||
<Title
|
||||
isHiddenOnRender={titleHiddenOnRender}
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
accentColor={accentColor}
|
||||
title={title}
|
||||
isBeta={isBeta}
|
||||
/>
|
||||
</View>
|
||||
<RightMenus
|
||||
renderedInRoute={renderedInRoute}
|
||||
id={id}
|
||||
headerRightButtons={headerRightButtons}
|
||||
onPressDefaultRightButton={onPressDefaultRightButton}
|
||||
search={hasSearch}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const Header = React.memo(_Header, () => true);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
@@ -17,23 +17,29 @@ 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 from "react";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const LeftMenus = () => {
|
||||
export const LeftMenus = ({
|
||||
canGoBack,
|
||||
onLeftButtonPress
|
||||
}: {
|
||||
canGoBack?: boolean;
|
||||
onLeftButtonPress?: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const canGoBack = useNavigationStore((state) => state.canGoBack);
|
||||
const isTablet = deviceMode === "tablet";
|
||||
|
||||
const onLeftButtonPress = () => {
|
||||
const _onLeftButtonPress = () => {
|
||||
if (onLeftButtonPress) return onLeftButtonPress();
|
||||
|
||||
if (!canGoBack) {
|
||||
if (tabBarRef.current?.isDrawerOpen()) {
|
||||
Navigation.closeDrawer();
|
||||
@@ -54,21 +60,17 @@ export const LeftMenus = () => {
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginLeft: -5,
|
||||
marginRight: DDS.isLargeTablet() ? 10 : 7
|
||||
}}
|
||||
left={40}
|
||||
top={40}
|
||||
right={DDS.isLargeTablet() ? 10 : 10}
|
||||
onPress={onLeftButtonPress}
|
||||
onPress={_onLeftButtonPress}
|
||||
onLongPress={() => {
|
||||
Navigation.popToTop();
|
||||
}}
|
||||
name={canGoBack ? "arrow-left" : "menu"}
|
||||
color={colors.primary.paragraph}
|
||||
iconStyle={{
|
||||
marginLeft: canGoBack ? -5 : 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,43 +20,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useRef } from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Menu from "react-native-reanimated-material-menu";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import {
|
||||
HeaderRightButton,
|
||||
RouteName
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
export const RightMenus = () => {
|
||||
export const RightMenus = ({
|
||||
headerRightButtons,
|
||||
renderedInRoute,
|
||||
id,
|
||||
onPressDefaultRightButton,
|
||||
search,
|
||||
onSearch
|
||||
}: {
|
||||
headerRightButtons?: HeaderRightButton[];
|
||||
renderedInRoute: RouteName;
|
||||
id?: string;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
search?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors: contextMenuColors } = useThemeColors("contextMenu");
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const buttons = useNavigationStore((state) => state.headerRightButtons);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
|
||||
const buttonAction = useNavigationStore((state) => state.buttonAction);
|
||||
const menuRef = useRef<{
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
}>();
|
||||
const menuRef = useRef<Menu>(null);
|
||||
|
||||
return (
|
||||
<View style={styles.rightBtnContainer}>
|
||||
{!currentScreen.startsWith("Settings") ? (
|
||||
{search ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
SearchService.prepareSearch();
|
||||
Navigation.navigate(
|
||||
{
|
||||
name: "Search"
|
||||
},
|
||||
{}
|
||||
);
|
||||
}}
|
||||
onPress={onSearch}
|
||||
testID="icon-search"
|
||||
name="magnify"
|
||||
color={colors.primary.paragraph}
|
||||
@@ -66,9 +69,9 @@ export const RightMenus = () => {
|
||||
|
||||
{deviceMode !== "mobile" ? (
|
||||
<Button
|
||||
onPress={buttonAction}
|
||||
onPress={onPressDefaultRightButton}
|
||||
testID={notesnook.ids.default.addBtn}
|
||||
icon={currentScreen === "Trash" ? "delete" : "plus"}
|
||||
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
|
||||
iconSize={SIZE.xl}
|
||||
type="shade"
|
||||
hitSlop={{
|
||||
@@ -89,7 +92,7 @@ export const RightMenus = () => {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{buttons && buttons.length > 0 ? (
|
||||
{headerRightButtons && headerRightButtons.length > 0 ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
animationDuration={200}
|
||||
@@ -111,7 +114,7 @@ export const RightMenus = () => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{buttons.map((item) => (
|
||||
{headerRightButtons.map((item) => (
|
||||
<Button
|
||||
style={{
|
||||
width: 150,
|
||||
|
||||
@@ -20,103 +20,74 @@ 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 { Platform } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Tag from "../ui/tag";
|
||||
import Heading from "../ui/typography/heading";
|
||||
|
||||
const titleState: { [name: string]: boolean } = {};
|
||||
|
||||
export const Title = () => {
|
||||
export const Title = ({
|
||||
title,
|
||||
isHiddenOnRender,
|
||||
accentColor,
|
||||
isBeta,
|
||||
renderedInRoute,
|
||||
id
|
||||
}: {
|
||||
title: string;
|
||||
isHiddenOnRender?: boolean;
|
||||
accentColor?: string;
|
||||
isBeta?: boolean;
|
||||
renderedInRoute: string;
|
||||
id?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const isNotebook = currentScreen.name === "Notebook";
|
||||
const isTopic = currentScreen?.name === "TopicNotes";
|
||||
const [hide, setHide] = useState(
|
||||
isNotebook
|
||||
? typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
: true
|
||||
: false
|
||||
);
|
||||
const isHidden = titleState[currentScreen.id as string];
|
||||
const notebook =
|
||||
isTopic && currentScreen.notebookId
|
||||
? db.notebooks?.notebook(currentScreen.notebookId)?.data
|
||||
: null;
|
||||
const title = currentScreen.title;
|
||||
const isTag = currentScreen?.name === "TaggedNotes";
|
||||
|
||||
const [visible, setVisible] = useState(isHiddenOnRender);
|
||||
const isTag = title.startsWith("#");
|
||||
const onScroll = useCallback(
|
||||
(data: { x: number; y: number }) => {
|
||||
if (currentScreen.name !== "Notebook") {
|
||||
setHide(false);
|
||||
return;
|
||||
}
|
||||
(data: { x: number; y: number; id?: string; route: string }) => {
|
||||
if (data.route !== renderedInRoute || data.id !== id) return;
|
||||
if (data.y > 150) {
|
||||
if (!hide) return;
|
||||
titleState[currentScreen.id as string] = false;
|
||||
setHide(false);
|
||||
if (!visible) return;
|
||||
setVisible(false);
|
||||
} else {
|
||||
if (hide) return;
|
||||
titleState[currentScreen.id as string] = true;
|
||||
setHide(true);
|
||||
if (visible) return;
|
||||
setVisible(true);
|
||||
}
|
||||
},
|
||||
[currentScreen.id, currentScreen.name, hide]
|
||||
[id, renderedInRoute, visible]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentScreen.name === "Notebook") {
|
||||
const value =
|
||||
typeof titleState[currentScreen.id as string] === "boolean"
|
||||
? titleState[currentScreen.id as string]
|
||||
: true;
|
||||
setHide(value);
|
||||
} else {
|
||||
setHide(titleState[currentScreen.id as string]);
|
||||
}
|
||||
}, [currentScreen.id, currentScreen.name]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eScrollEvent, onScroll);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eScrollEvent, onScroll);
|
||||
};
|
||||
}, [hide, onScroll]);
|
||||
}, [visible, onScroll]);
|
||||
|
||||
function navigateToNotebook() {
|
||||
if (!isTopic) return;
|
||||
Notebook.navigate(notebook, true);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!hide && !isHidden ? (
|
||||
{!visible ? (
|
||||
<Heading
|
||||
onPress={navigateToNotebook}
|
||||
numberOfLines={1}
|
||||
size={SIZE.lg}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginTop: Platform.OS === "ios" ? -1 : 0
|
||||
}}
|
||||
color={currentScreen.color || colors.primary.heading}
|
||||
color={accentColor || colors.primary.heading}
|
||||
>
|
||||
{isTag ? (
|
||||
<Heading size={SIZE.xl} color={colors.primary.accent}>
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{title}{" "}
|
||||
{isTag ? title.slice(1) : title}{" "}
|
||||
<Tag
|
||||
visible={currentScreen.beta}
|
||||
visible={isBeta}
|
||||
text="BETA"
|
||||
style={{
|
||||
backgroundColor: "transparent"
|
||||
|
||||
@@ -32,23 +32,22 @@ import {
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
const ImagePreview = () => {
|
||||
const { colors } = useThemeColors("dialog");
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [image, setImage] = useState("");
|
||||
const [image, setImage] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("ImagePreview", open);
|
||||
|
||||
return () => {
|
||||
eUnSubscribeEvent("ImagePreview", open);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const open = async (image) => {
|
||||
const open = async (image: ImageAttributes) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
@@ -63,6 +62,7 @@ const ImagePreview = () => {
|
||||
});
|
||||
}
|
||||
if (!hash) return;
|
||||
//@ts-ignore // FIX ME
|
||||
const uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true
|
||||
@@ -74,7 +74,7 @@ const ImagePreview = () => {
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
setImage(null);
|
||||
setImage(undefined);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ const ImagePreview = () => {
|
||||
backgroundColor: "black"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
{loading || !image ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -77,14 +77,9 @@ export const WelcomeNotice = () => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace(
|
||||
{
|
||||
name: "Notes"
|
||||
},
|
||||
{
|
||||
canGoBack: false
|
||||
}
|
||||
);
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
style={{
|
||||
@@ -106,14 +101,9 @@ export const WelcomeNotice = () => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace(
|
||||
{
|
||||
name: "Notes"
|
||||
},
|
||||
{
|
||||
canGoBack: false
|
||||
}
|
||||
);
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
await sleep(1000);
|
||||
eSendEvent(eCloseLoading);
|
||||
}}
|
||||
|
||||
@@ -17,57 +17,39 @@ 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 from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useMessageStore } from "../../../stores/use-message-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { Announcement } from "../../announcements/announcement";
|
||||
import { Card } from "../../list/card";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
|
||||
export type ListHeaderProps = {
|
||||
noAnnouncement?: boolean;
|
||||
color?: string;
|
||||
messageCard?: boolean;
|
||||
screen?: string;
|
||||
shouldShow?: boolean;
|
||||
};
|
||||
|
||||
export const Header = React.memo(
|
||||
({
|
||||
type,
|
||||
messageCard = true,
|
||||
color,
|
||||
shouldShow = false,
|
||||
noAnnouncement,
|
||||
warning
|
||||
}) => {
|
||||
screen
|
||||
}: ListHeaderProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
|
||||
return selectionMode ? null : (
|
||||
<>
|
||||
{warning ? (
|
||||
<View
|
||||
style={{
|
||||
padding: 12,
|
||||
backgroundColor: colors.error.background,
|
||||
width: "95%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="sync-alert"
|
||||
size={SIZE.md}
|
||||
color={colors.error.icon}
|
||||
f
|
||||
/>
|
||||
<Paragraph style={{ marginLeft: 5 }} color={colors.error.icon}>
|
||||
{warning.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : announcements.length !== 0 && !noAnnouncement ? (
|
||||
{announcements.length !== 0 && !noAnnouncement ? (
|
||||
<Announcement color={color || colors.primary.accent} />
|
||||
) : type === "search" ? null : !shouldShow ? (
|
||||
) : (screen as any) === "Search" ? null : !shouldShow ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 5,
|
||||
@@ -78,11 +60,7 @@ export const Header = React.memo(
|
||||
}}
|
||||
>
|
||||
{messageCard ? (
|
||||
<Card
|
||||
color={
|
||||
ColorValues[color?.toLowerCase()] || colors.primary.accent
|
||||
}
|
||||
/>
|
||||
<Card color={color || colors.primary.accent} />
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
@@ -17,27 +17,33 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { Notebook } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { ToastEvent } from "../../../services/event-manager";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
|
||||
export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
export const NotebookHeader = ({
|
||||
notebook,
|
||||
onEditNotebook,
|
||||
totalNotes = 0
|
||||
}: {
|
||||
notebook: Notebook;
|
||||
onEditNotebook: () => void;
|
||||
totalNotes: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
|
||||
db.shortcuts.exists(notebook.id)
|
||||
);
|
||||
const setMenuPins = useMenuStore((state) => state.setMenuPins);
|
||||
const totalNotes = getTotalNotes(notebook);
|
||||
const shortcutRef = useRef();
|
||||
|
||||
const onPinNotebook = async () => {
|
||||
try {
|
||||
@@ -50,7 +56,7 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
type: "notebook"
|
||||
}
|
||||
});
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Shortcut created",
|
||||
type: "success"
|
||||
});
|
||||
@@ -76,7 +82,7 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{new Date(notebook.dateEdited).toLocaleString()}
|
||||
{getFormattedDate(notebook.dateModified, "date-time")}
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
@@ -103,7 +109,6 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
name={isPinnedToMenu ? "link-variant-off" : "link-variant"}
|
||||
onPress={onPinNotebook}
|
||||
tooltipText={"Create shortcut in side menu"}
|
||||
fwdRef={shortcutRef}
|
||||
customStyle={{
|
||||
marginRight: 15,
|
||||
width: 40,
|
||||
@@ -138,15 +143,15 @@ export const NotebookHeader = ({ notebook, onEditNotebook }) => {
|
||||
style={{
|
||||
marginTop: 10,
|
||||
fontStyle: "italic",
|
||||
fontFamily: null
|
||||
fontFamily: undefined
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{notebook.topics.length === 1
|
||||
{/* {notebook.topics.length === 1
|
||||
? "1 topic"
|
||||
: `${notebook.topics.length} topics`}
|
||||
,{" "}
|
||||
,{" "} */}
|
||||
{notebook && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
@@ -17,65 +17,57 @@ 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 { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { db } from "../../../common/database";
|
||||
import { eOpenJumpToDialog } from "../../../utils/events";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
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 }) {
|
||||
type SectionHeaderProps = {
|
||||
item: GroupHeader;
|
||||
index: number;
|
||||
dataType: ItemType;
|
||||
color?: string;
|
||||
screen?: RouteName;
|
||||
groupOptions: GroupOptions;
|
||||
onOpenJumpToDialog: () => void;
|
||||
};
|
||||
|
||||
export const SectionHeader = React.memo<
|
||||
React.FunctionComponent<SectionHeaderProps>
|
||||
>(
|
||||
function SectionHeader({
|
||||
item,
|
||||
index,
|
||||
dataType,
|
||||
color,
|
||||
screen,
|
||||
groupOptions,
|
||||
onOpenJumpToDialog
|
||||
}: SectionHeaderProps) {
|
||||
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
|
||||
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
|
||||
);
|
||||
const jumpToRef = useRef();
|
||||
const sortRef = useRef();
|
||||
const compactModeRef = useRef();
|
||||
|
||||
const notebooksListMode = useSettingStore(
|
||||
(state) => state.settings.notebooksListMode
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook"
|
||||
);
|
||||
const notesListMode = useSettingStore(
|
||||
(state) => state.settings.notesListMode
|
||||
);
|
||||
const listMode = type === "notebooks" ? notebooksListMode : notesListMode;
|
||||
|
||||
groupBy = !groupBy
|
||||
? "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={{
|
||||
@@ -93,9 +85,8 @@ export const SectionHeader = React.memo(
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
eSendEvent(eOpenJumpToDialog, type);
|
||||
onOpenJumpToDialog();
|
||||
}}
|
||||
ref={jumpToRef}
|
||||
activeOpacity={0.9}
|
||||
hitSlop={{ top: 10, left: 10, right: 30, bottom: 15 }}
|
||||
style={{
|
||||
@@ -104,7 +95,10 @@ export const SectionHeader = React.memo(
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={ColorValues[color?.toLowerCase()] || colors.primary.accent}
|
||||
color={
|
||||
ColorValues[color?.toLowerCase() as keyof typeof ColorValues] ||
|
||||
colors.primary.accent
|
||||
}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
minWidth: 60,
|
||||
@@ -126,13 +120,13 @@ export const SectionHeader = React.memo(
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
console.log("Opening Sort sheet", screen, dataType);
|
||||
presentSheet({
|
||||
component: <Sort screen={screen} type={type} />
|
||||
component: <Sort screen={screen} type={dataType} />
|
||||
});
|
||||
}}
|
||||
hidden={screen === "Reminders"}
|
||||
tooltipText="Change sorting of items in list"
|
||||
fwdRef={sortRef}
|
||||
title={groupBy}
|
||||
icon={
|
||||
groupOptions.sortDirection === "asc"
|
||||
@@ -145,7 +139,9 @@ export const SectionHeader = React.memo(
|
||||
paddingHorizontal: 0,
|
||||
backgroundColor: "transparent",
|
||||
marginRight:
|
||||
type === "notes" || type === "home" || type === "notebooks"
|
||||
dataType === "note" ||
|
||||
screen === "Notes" ||
|
||||
dataType === "notebook"
|
||||
? 10
|
||||
: 0
|
||||
}}
|
||||
@@ -159,24 +155,26 @@ export const SectionHeader = React.memo(
|
||||
height: 25
|
||||
}}
|
||||
hidden={
|
||||
type !== "notes" && type !== "notebooks" && type !== "home"
|
||||
dataType !== "note" &&
|
||||
dataType !== "notebook" &&
|
||||
screen !== "Notes"
|
||||
}
|
||||
testID="icon-compact-mode"
|
||||
tooltipText={
|
||||
listMode == "compact"
|
||||
isCompactModeEnabled
|
||||
? "Switch to normal mode"
|
||||
: "Switch to compact mode"
|
||||
}
|
||||
fwdRef={compactModeRef}
|
||||
color={colors.secondary.icon}
|
||||
name={listMode == "compact" ? "view-list" : "view-list-outline"}
|
||||
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
|
||||
onPress={() => {
|
||||
let settings = {};
|
||||
settings[
|
||||
type !== "notebooks" ? "notesListMode" : "notebooksListMode"
|
||||
] = listMode === "normal" ? "compact" : "normal";
|
||||
|
||||
SettingsService.set(settings);
|
||||
SettingsService.set({
|
||||
[dataType !== "notebook"
|
||||
? "notesListMode"
|
||||
: "notebooksListMode"]: !isCompactModeEnabled
|
||||
? "compact"
|
||||
: "normal"
|
||||
});
|
||||
}}
|
||||
size={SIZE.lg - 2}
|
||||
/>
|
||||
@@ -188,6 +186,10 @@ export const SectionHeader = React.memo(
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.item.title !== next.item.title) return false;
|
||||
if (prev.groupOptions?.groupBy !== next.groupOptions.groupBy) return false;
|
||||
if (prev.groupOptions?.sortDirection !== next.groupOptions.sortDirection)
|
||||
return false;
|
||||
if (prev.groupOptions?.sortBy !== next.groupOptions.sortBy) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -17,21 +17,30 @@ 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 { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
|
||||
import { decode, EntityLevel } from "entities";
|
||||
import {
|
||||
BaseTrashItem,
|
||||
Color,
|
||||
Note,
|
||||
Reminder,
|
||||
TrashItem
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { EntityLevel, decode } from "entities";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import Notebook from "../../../screens/notebook";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../list/list-item.wrapper";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
@@ -39,79 +48,39 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
|
||||
function navigateToTag(item) {
|
||||
const tag = db.tags.tag(item.id);
|
||||
if (!tag) return;
|
||||
TaggedNotes.navigate(tag, true);
|
||||
}
|
||||
|
||||
const showActionSheet = (item) => {
|
||||
Properties.present(item);
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
index: number;
|
||||
tags?: TagsWithDateEdited;
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
color?: Color;
|
||||
reminder?: Reminder;
|
||||
attachmentsCount: number;
|
||||
date: number;
|
||||
isTrash?: boolean;
|
||||
noOpen?: boolean;
|
||||
};
|
||||
|
||||
function getNotebook(item) {
|
||||
const isTrash = item.type === "trash";
|
||||
const currentId = useNavigationStore.getState().currentScreen.id;
|
||||
if (isTrash) return [];
|
||||
const items = [];
|
||||
const notebooks = db.relations.to(item, "notebook") || [];
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
if (items.length > 1) break;
|
||||
if (notebook.id === currentId) continue;
|
||||
items.push(notebook);
|
||||
}
|
||||
|
||||
if (item.notebooks) {
|
||||
for (let nb of item.notebooks) {
|
||||
if (items.length > 1) break;
|
||||
const notebook = db.notebooks?.notebook(nb.id)?.data;
|
||||
if (!notebook) continue;
|
||||
for (let topicId of nb.topics) {
|
||||
if (items.length > 1) break;
|
||||
if (topicId === currentId) continue;
|
||||
const topic = notebook.topics.find((t) => t.id === topicId);
|
||||
if (!topic) continue;
|
||||
items.push(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function getTags(item) {
|
||||
const noteTags = item.tags?.slice(0, 3) || [];
|
||||
const tags = [];
|
||||
for (const tagName of noteTags) {
|
||||
const tag = db.tags.tag(tagName);
|
||||
if (!tag) continue;
|
||||
tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
const NoteItem = ({
|
||||
item,
|
||||
isTrash,
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
date,
|
||||
color,
|
||||
notebooks,
|
||||
reminder,
|
||||
tags,
|
||||
attachmentsCount,
|
||||
noOpen
|
||||
}: NoteItemProps) => {
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
const _update = useRelationStore((state) => state.updater);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
|
||||
const reminders = db.relations.from(item, "reminder");
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const noteColor = ColorValues[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
const primaryColors =
|
||||
currentEditingNote === item.id ? colors.selected : colors.primary;
|
||||
|
||||
@@ -134,44 +103,45 @@ const NoteItem = ({
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{notebooks?.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "topic" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
Notebook.navigate(item);
|
||||
{notebooks?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
item.id !== useNavigationStore.getState().focusedRouteId
|
||||
)
|
||||
.map((item) => (
|
||||
<Button
|
||||
title={
|
||||
item.title.length > 25
|
||||
? item.title.slice(0, 25) + "..."
|
||||
: item.title
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
tooltipText={item.title}
|
||||
key={item.id}
|
||||
height={25}
|
||||
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
iconSize={SIZE.sm}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
NotebookScreen.navigate(item, true);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ReminderTime
|
||||
reminder={reminder}
|
||||
color={noteColor}
|
||||
color={color?.colorCode}
|
||||
onPress={() => {
|
||||
Properties.present(reminder);
|
||||
}}
|
||||
@@ -185,9 +155,7 @@ const NoteItem = ({
|
||||
{compactMode ? (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
@@ -198,9 +166,7 @@ const NoteItem = ({
|
||||
) : (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
color={color?.colorCode || primaryColors.heading}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
@@ -253,13 +219,11 @@ const NoteItem = ({
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={
|
||||
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
|
||||
}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
|
||||
{attachmentCount > 0 ? (
|
||||
{attachmentsCount > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
@@ -276,7 +240,7 @@ const NoteItem = ({
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{attachmentCount}
|
||||
{attachmentsCount}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -289,10 +253,7 @@ const NoteItem = ({
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] ||
|
||||
primaryColors.accent
|
||||
}
|
||||
color={color?.colorCode || primaryColors.accent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -321,10 +282,10 @@ const NoteItem = ({
|
||||
) : null}
|
||||
|
||||
{!isTrash && !compactMode && tags
|
||||
? tags.map((item) =>
|
||||
? tags.items?.map((item) =>
|
||||
item.id ? (
|
||||
<Button
|
||||
title={"#" + item.alias}
|
||||
title={"#" + item.title}
|
||||
key={item.id}
|
||||
height={23}
|
||||
type="gray"
|
||||
@@ -338,9 +299,9 @@ const NoteItem = ({
|
||||
paddingHorizontal: 6,
|
||||
marginRight: 4,
|
||||
zIndex: 10,
|
||||
maxWidth: tags.length > 1 ? 130 : null
|
||||
maxWidth: tags.items?.length > 1 ? 130 : null
|
||||
}}
|
||||
onPress={() => navigateToTag(item)}
|
||||
onPress={() => TaggedNotes.navigate(item, true)}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
@@ -368,7 +329,8 @@ const NoteItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
{(item as TrashItem).itemType[0].toUpperCase() +
|
||||
(item as TrashItem).itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
@@ -424,8 +386,8 @@ const NoteItem = ({
|
||||
color: colors.secondary.paragraph,
|
||||
marginRight: 6
|
||||
}}
|
||||
time={item[dateBy]}
|
||||
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
|
||||
time={date}
|
||||
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -435,7 +397,7 @@ const NoteItem = ({
|
||||
color={primaryColors.paragraph}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
onPress={() => !noOpen && Properties.present(item)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
@@ -17,6 +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 { BaseTrashItem, Color, Note, Reminder } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import NoteItem from ".";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
@@ -32,50 +33,43 @@ import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../../utils/events";
|
||||
import { tabBarRef } from "../../../utils/global-refs";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import type {
|
||||
NotebooksWithDateEdited,
|
||||
TagsWithDateEdited
|
||||
} from "../../list/list-item.wrapper";
|
||||
import NotePreview from "../../note-history/preview";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
const present = () =>
|
||||
presentDialog({
|
||||
title: "Note not synced",
|
||||
negativeText: "Ok",
|
||||
paragraph: "Please sync again to open this note for editing"
|
||||
});
|
||||
|
||||
export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
|
||||
let _note = item;
|
||||
export const openNote = async (
|
||||
item: Note,
|
||||
isTrash?: boolean,
|
||||
isSheet?: boolean
|
||||
) => {
|
||||
let note: Note = item;
|
||||
if (isSheet) hideSheet();
|
||||
|
||||
if (!isTrash) {
|
||||
_note = db.notes.note(item.id).data;
|
||||
if (!db.notes.note(item.id)?.synced()) {
|
||||
present();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!db.trash.synced(item.id)) {
|
||||
present();
|
||||
return;
|
||||
}
|
||||
note = (await db.notes.note(item.id)) as Note;
|
||||
}
|
||||
const { selectedItemsList, selectionMode, clearSelection } =
|
||||
|
||||
const { selectedItemsList, selectionMode, clearSelection, setSelectedItem } =
|
||||
useSelectionStore.getState();
|
||||
|
||||
if (selectedItemsList.length > 0 && selectionMode) {
|
||||
setSelectedItem && setSelectedItem(_note);
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(note.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
if (_note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, _note);
|
||||
if (note.conflicted) {
|
||||
eSendEvent(eShowMergeDialog, note);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_note.locked) {
|
||||
if (note.locked) {
|
||||
openVault({
|
||||
item: _note,
|
||||
item: note,
|
||||
novault: true,
|
||||
locked: true,
|
||||
goToEditor: true,
|
||||
@@ -85,48 +79,81 @@ export const openNote = async (item, isTrash, setSelectedItem, isSheet) => {
|
||||
return;
|
||||
}
|
||||
if (isTrash) {
|
||||
const content = await db.content.get(item.contentId);
|
||||
if (!note.contentId) return;
|
||||
|
||||
const content = await db.content.get(note.contentId as string);
|
||||
presentSheet({
|
||||
component: (
|
||||
<NotePreview note={item} content={{ type: "tiptap", data: content }} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
useEditorStore.getState().setReadonly(_note?.readonly);
|
||||
eSendEvent(eOnLoadNote, _note);
|
||||
useEditorStore.getState().setReadonly(note?.readonly);
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
tabBarRef.current?.goToPage(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo(
|
||||
function NoteWrapper({ item, index, dateBy, isSheet }) {
|
||||
type NoteWrapperProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
index: number;
|
||||
tags?: TagsWithDateEdited;
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
color?: Color;
|
||||
reminder?: Reminder;
|
||||
attachmentsCount: number;
|
||||
date: number;
|
||||
isRenderedInActionSheet: boolean;
|
||||
};
|
||||
|
||||
export const NoteWrapper = React.memo<
|
||||
React.FunctionComponent<NoteWrapperProps>
|
||||
>(
|
||||
function NoteWrapper({
|
||||
item,
|
||||
index,
|
||||
isRenderedInActionSheet,
|
||||
...restProps
|
||||
}: NoteWrapperProps) {
|
||||
const isTrash = item.type === "trash";
|
||||
const setSelectedItem = useSelectionStore((state) => state.setSelectedItem);
|
||||
|
||||
return (
|
||||
<SelectionWrapper
|
||||
index={index}
|
||||
height={100}
|
||||
testID={notesnook.ids.note.get(index)}
|
||||
onPress={() => openNote(item, isTrash, setSelectedItem, isSheet)}
|
||||
isSheet={isSheet}
|
||||
onPress={() => openNote(item as Note, isTrash, isRenderedInActionSheet)}
|
||||
isSheet={isRenderedInActionSheet}
|
||||
item={item}
|
||||
color={restProps.color?.colorCode}
|
||||
>
|
||||
<NoteItem item={item} dateBy={dateBy} isTrash={isTrash} />
|
||||
<NoteItem {...restProps} item={item} index={index} isTrash={isTrash} />
|
||||
</SelectionWrapper>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.dateBy !== next.dateBy) {
|
||||
return false;
|
||||
}
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
if (prev.date !== next.date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.item !== next.item) {
|
||||
if (
|
||||
prev.tags?.dateEdited !== next.tags?.dateEdited ||
|
||||
prev.tags?.items?.length !== next.tags?.items?.length
|
||||
)
|
||||
return false;
|
||||
|
||||
if (
|
||||
prev.notebooks?.dateEdited !== next.notebooks?.dateEdited ||
|
||||
prev.notebooks?.items?.length !== next.notebooks?.items?.length
|
||||
)
|
||||
return false;
|
||||
|
||||
if (prev.color !== next.color) return false;
|
||||
if (prev.reminder?.id !== next.reminder?.id) return false;
|
||||
if (prev.attachmentsCount !== next.attachmentsCount) return false;
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,41 +17,38 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { BaseTrashItem, Notebook, TrashItem } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
|
||||
const showActionSheet = (item) => {
|
||||
Properties.present(item);
|
||||
};
|
||||
|
||||
const navigateToTopic = (topic) => {
|
||||
if (useSelectionStore.getState().selectedItemsList.length > 0) return;
|
||||
TopicNotes.navigate(topic, true);
|
||||
type NotebookItemProps = {
|
||||
item: Notebook | BaseTrashItem<Notebook>;
|
||||
totalNotes: number;
|
||||
date: number;
|
||||
index: number;
|
||||
isTrash: boolean;
|
||||
};
|
||||
|
||||
export const NotebookItem = ({
|
||||
item,
|
||||
isTopic = false,
|
||||
isTrash,
|
||||
dateBy,
|
||||
date,
|
||||
totalNotes
|
||||
}) => {
|
||||
}: NotebookItemProps) => {
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const topics = item.topics?.slice(0, 3) || [];
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -83,7 +80,7 @@ export const NotebookItem = ({
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{isTopic || !item.description || compactMode ? null : (
|
||||
{!item.description || compactMode ? null : (
|
||||
<Paragraph
|
||||
size={SIZE.sm}
|
||||
numberOfLines={2}
|
||||
@@ -95,42 +92,6 @@ export const NotebookItem = ({
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{isTopic || compactMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{topics.map((topic) => (
|
||||
<Button
|
||||
title={topic.title}
|
||||
key={topic.id}
|
||||
height={null}
|
||||
textStyle={{
|
||||
marginRight: 0
|
||||
}}
|
||||
type="grayBg"
|
||||
fontSize={SIZE.xs}
|
||||
icon="bookmark-outline"
|
||||
iconSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
maxWidth: 120,
|
||||
borderWidth: 0.5,
|
||||
paddingVertical: 2.5,
|
||||
borderColor: colors.primary.border,
|
||||
paddingHorizontal: 6,
|
||||
marginVertical: 5,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => navigateToTopic(topic)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!compactMode ? (
|
||||
<View
|
||||
style={{
|
||||
@@ -152,7 +113,9 @@ export const NotebookItem = ({
|
||||
}}
|
||||
>
|
||||
{"Deleted on " +
|
||||
new Date(item.dateDeleted).toISOString().slice(0, 10)}
|
||||
new Date((item as TrashItem).dateDeleted)
|
||||
.toISOString()
|
||||
.slice(0, 10)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
color={colors.primary.accent}
|
||||
@@ -162,7 +125,8 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
|
||||
{(item as TrashItem).itemType[0].toUpperCase() +
|
||||
(item as TrashItem).itemType.slice(1)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
@@ -173,7 +137,7 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{getFormattedDate(item[dateBy], "date")}
|
||||
{getFormattedDate(date, "date")}
|
||||
</Paragraph>
|
||||
)}
|
||||
<Paragraph
|
||||
@@ -233,7 +197,7 @@ export const NotebookItem = ({
|
||||
name="dots-horizontal"
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
size={SIZE.xl}
|
||||
onPress={() => showActionSheet(item)}
|
||||
onPress={() => Properties.present(item)}
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
height: 35,
|
||||
@@ -17,41 +17,24 @@ 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 { BaseTrashItem, Notebook } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { NotebookItem } from ".";
|
||||
import { TopicNotes } from "../../../screens/notes/topic-notes";
|
||||
import { ToastEvent } from "../../../services/event-manager";
|
||||
import { db } from "../../../common/database";
|
||||
import NotebookScreen from "../../../screens/notebook";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
const navigateToNotebook = (item, canGoBack) => {
|
||||
if (!item) return;
|
||||
|
||||
Navigation.navigate(
|
||||
{
|
||||
title: item.title,
|
||||
name: "Notebook",
|
||||
id: item.id,
|
||||
type: "notebook"
|
||||
},
|
||||
{
|
||||
title: item.title,
|
||||
item: item,
|
||||
canGoBack
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const openNotebookTopic = (item) => {
|
||||
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
const isTrash = item.type === "trash";
|
||||
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
|
||||
useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode) {
|
||||
setSelectedItem(item);
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
@@ -59,15 +42,15 @@ export const openNotebookTopic = (item) => {
|
||||
|
||||
if (isTrash) {
|
||||
presentDialog({
|
||||
title: `Restore ${item.itemType}`,
|
||||
paragraph: `Restore or delete ${item.itemType} forever`,
|
||||
title: `Restore notebook`,
|
||||
paragraph: `Restore or delete notebook forever`,
|
||||
positiveText: "Restore",
|
||||
negativeText: "Delete",
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
@@ -75,9 +58,9 @@ export const openNotebookTopic = (item) => {
|
||||
onClose: async () => {
|
||||
await db.trash.delete(item.id);
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -85,29 +68,30 @@ export const openNotebookTopic = (item) => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (item.type === "topic") {
|
||||
TopicNotes.navigate(item, true);
|
||||
} else {
|
||||
navigateToNotebook(item, true);
|
||||
}
|
||||
NotebookScreen.navigate(item, true);
|
||||
};
|
||||
|
||||
type NotebookWrapperProps = {
|
||||
item: Notebook | BaseTrashItem<Notebook>;
|
||||
totalNotes: number;
|
||||
date: number;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export const NotebookWrapper = React.memo(
|
||||
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
|
||||
function NotebookWrapper({
|
||||
item,
|
||||
index,
|
||||
date,
|
||||
totalNotes
|
||||
}: NotebookWrapperProps) {
|
||||
const isTrash = item.type === "trash";
|
||||
|
||||
return (
|
||||
<SelectionWrapper
|
||||
pinned={item.pinned}
|
||||
index={index}
|
||||
onPress={() => openNotebookTopic(item)}
|
||||
height={item.type === "topic" ? 80 : 110}
|
||||
item={item}
|
||||
>
|
||||
<SelectionWrapper onPress={() => openNotebook(item)} item={item}>
|
||||
<NotebookItem
|
||||
isTopic={item.type === "topic"}
|
||||
item={item}
|
||||
dateBy={dateBy}
|
||||
date={date}
|
||||
index={index}
|
||||
isTrash={isTrash}
|
||||
totalNotes={totalNotes}
|
||||
@@ -117,17 +101,8 @@ export const NotebookWrapper = React.memo(
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.totalNotes !== next.totalNotes) return false;
|
||||
if (prev.item.title !== next.item.title) return false;
|
||||
if (prev.dateBy !== next.dateBy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
|
||||
return false;
|
||||
}
|
||||
if (prev.date !== next.date) return false;
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -16,20 +16,21 @@ 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 { Reminder } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import type { Reminder } from "../../../services/notifications";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ReminderSheet from "../../sheets/reminder";
|
||||
import { ReminderTime } from "../../ui/reminder-time";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
@@ -43,17 +44,23 @@ const ReminderItem = React.memo(
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const openReminder = () => {
|
||||
const {
|
||||
selectedItemsList,
|
||||
setSelectedItem,
|
||||
selectionMode,
|
||||
clearSelection
|
||||
} = useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
ReminderSheet.present(item, undefined, isSheet);
|
||||
};
|
||||
return (
|
||||
<SelectionWrapper
|
||||
//@ts-ignore
|
||||
index={index}
|
||||
height={100}
|
||||
onPress={openReminder}
|
||||
item={item}
|
||||
isSheet={isSheet}
|
||||
>
|
||||
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
@@ -131,7 +138,11 @@ const ReminderItem = React.memo(
|
||||
height: 30
|
||||
}}
|
||||
>
|
||||
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
|
||||
<Icon
|
||||
name="reload"
|
||||
size={SIZE.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
|
||||
@@ -22,8 +22,9 @@ import React from "react";
|
||||
import { View } from "react-native";
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useEditorStore } from "../../../stores/use-editor-store";
|
||||
import { Item } from "@notesnook/core";
|
||||
|
||||
export const Filler = ({ item }) => {
|
||||
export const Filler = ({ item, color }: { item: Item; color?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
@@ -41,8 +42,8 @@ export const Filler = ({ item }) => {
|
||||
borderLeftWidth: 5,
|
||||
borderLeftColor:
|
||||
currentEditingNote === item.id
|
||||
? item.color
|
||||
? colors.static[item.color]
|
||||
? color
|
||||
? color
|
||||
: colors.selected.accent
|
||||
: "transparent"
|
||||
}}
|
||||
@@ -18,34 +18,45 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef } from "react";
|
||||
import React, { PropsWithChildren, useRef } from "react";
|
||||
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import { Filler } from "./back-fill";
|
||||
import { SelectionIcon } from "./selection";
|
||||
import { Item, TrashItem } from "@notesnook/core";
|
||||
|
||||
type SelectionWrapperProps = PropsWithChildren<{
|
||||
item: Item;
|
||||
onPress: () => void;
|
||||
testID?: string;
|
||||
isSheet?: boolean;
|
||||
color?: string;
|
||||
}>;
|
||||
|
||||
const SelectionWrapper = ({
|
||||
children,
|
||||
item,
|
||||
background,
|
||||
onPress,
|
||||
testID,
|
||||
isSheet
|
||||
}) => {
|
||||
isSheet,
|
||||
children,
|
||||
color
|
||||
}: SelectionWrapperProps) => {
|
||||
const itemId = useRef(item.id);
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
|
||||
if (item.id !== itemId.current) {
|
||||
itemId.current = item.id;
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (!useSelectionStore.getState().selectionMode) {
|
||||
useSelectionStore.getState().setSelectionMode(true);
|
||||
if (useSelectionStore.getState().selectionMode !== item.type) {
|
||||
useSelectionStore.getState().setSelectionMode(item.type);
|
||||
}
|
||||
useSelectionStore.getState().setSelectedItem(item);
|
||||
useSelectionStore.getState().setSelectedItem(item.id);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -69,14 +80,8 @@ const SelectionWrapper = ({
|
||||
marginBottom: isSheet ? 12 : undefined
|
||||
}}
|
||||
>
|
||||
{item.type === "note" ? (
|
||||
<Filler background={background} item={item} />
|
||||
) : null}
|
||||
<SelectionIcon
|
||||
compactMode={compactMode}
|
||||
item={item}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
{item.type === "note" ? <Filler item={item} color={color} /> : null}
|
||||
<SelectionIcon item={item} />
|
||||
{children}
|
||||
</PressableButton>
|
||||
);
|
||||
@@ -25,13 +25,16 @@ import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enab
|
||||
import useIsSelected from "../../../hooks/use-selected";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Item, TrashItem } from "@notesnook/core";
|
||||
|
||||
export const SelectionIcon = ({ item }) => {
|
||||
export const SelectionIcon = ({ item }: { item: Item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const [selected] = useIsSelected(item);
|
||||
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const compactMode = useIsCompactModeEnabled(
|
||||
(item as TrashItem).itemType || item.type
|
||||
);
|
||||
|
||||
return selectionMode ? (
|
||||
<View
|
||||
@@ -17,44 +17,58 @@ 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 { Tag } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { TaggedNotes } from "../../../screens/notes/tagged";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Properties } from "../../properties";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper from "../selection-wrapper";
|
||||
|
||||
const TagItem = React.memo(
|
||||
({ item, index }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
({
|
||||
item,
|
||||
index,
|
||||
totalNotes
|
||||
}: {
|
||||
item: Tag;
|
||||
index: number;
|
||||
totalNotes: number;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const onPress = () => {
|
||||
const {
|
||||
selectedItemsList,
|
||||
setSelectedItem,
|
||||
selectionMode,
|
||||
clearSelection
|
||||
} = useSelectionStore.getState();
|
||||
if (selectedItemsList.length > 0 && selectionMode === item.type) {
|
||||
setSelectedItem(item.id);
|
||||
return;
|
||||
} else {
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
TaggedNotes.navigate(item, true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
<SelectionWrapper
|
||||
onPress={onPress}
|
||||
selectedColor={colors.secondary.background}
|
||||
item={item}
|
||||
testID={notesnook.ids.tag.get(index)}
|
||||
alpha={!isDark ? -0.02 : 0.02}
|
||||
opacity={1}
|
||||
customStyle={{
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
paddingVertical: 12,
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxWidth: "92%"
|
||||
flexGrow: 1,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>
|
||||
@@ -66,7 +80,7 @@ const TagItem = React.memo(
|
||||
>
|
||||
#
|
||||
</Heading>
|
||||
{item.alias}
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
@@ -75,10 +89,10 @@ const TagItem = React.memo(
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{item && item.noteIds.length && item.noteIds.length > 1
|
||||
? item.noteIds.length + " notes"
|
||||
: item.noteIds.length === 1
|
||||
? item.noteIds.length + " note"
|
||||
{totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
@@ -99,14 +113,11 @@ const TagItem = React.memo(
|
||||
alignItems: "center"
|
||||
}}
|
||||
/>
|
||||
</PressableButton>
|
||||
</SelectionWrapper>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.item?.dateEdited !== next.item?.dateEdited) {
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
|
||||
if (prev.item?.dateModified !== next.item?.dateModified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -27,13 +27,15 @@ import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Card = ({ color, warning }) => {
|
||||
export const Card = ({ color }: { color?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
color = color ? color : colors.primary.accent;
|
||||
const messageBoardState = useMessageStore((state) => state.message);
|
||||
const announcement = useMessageStore((state) => state.announcement);
|
||||
const announcements = useMessageStore((state) => state.announcements);
|
||||
const fontScale = Dimensions.get("window").fontScale;
|
||||
|
||||
return !messageBoardState.visible || announcement || warning ? null : (
|
||||
return !messageBoardState.visible ||
|
||||
(announcements && announcements.length) ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "95%"
|
||||
@@ -44,7 +46,6 @@ export const Card = ({ color, warning }) => {
|
||||
type="gray"
|
||||
customStyle={{
|
||||
paddingVertical: 12,
|
||||
width: "95%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
@@ -54,17 +55,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 +77,7 @@ export const Card = ({ color, warning }) => {
|
||||
color={
|
||||
messageBoardState.type === "error" ? colors.error.icon : color
|
||||
}
|
||||
allowFontScaling
|
||||
name={messageBoardState.icon}
|
||||
/>
|
||||
</View>
|
||||
@@ -91,7 +94,7 @@ export const Card = ({ color, warning }) => {
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
flexWrap: "nowrap",
|
||||
flexShrink: 1
|
||||
}}
|
||||
color={colors.primary.heading}
|
||||
@@ -101,22 +104,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>
|
||||
);
|
||||
@@ -1,167 +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 from "react";
|
||||
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { useTip } from "../../services/tip-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Tip } from "../tip";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Empty = React.memo(
|
||||
function Empty({
|
||||
loading = true,
|
||||
placeholderData,
|
||||
headerProps,
|
||||
type,
|
||||
screen
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const { height } = useWindowDimensions();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
|
||||
const tip = useTip(
|
||||
screen === "Notes" && introCompleted
|
||||
? "first-note"
|
||||
: placeholderData.type || type,
|
||||
screen === "Notes" ? "notes" : null
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: height - (140 + insets.top),
|
||||
width: "80%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{!loading ? (
|
||||
<>
|
||||
<Tip
|
||||
color={
|
||||
ColorValues[headerProps.color?.toLowerCase()]
|
||||
? headerProps.color
|
||||
: "accent"
|
||||
}
|
||||
tip={tip || { text: placeholderData.paragraph }}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
{placeholderData.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="grayAccent"
|
||||
title={placeholderData.button}
|
||||
iconPosition="right"
|
||||
icon="arrow-right"
|
||||
onPress={placeholderData.action}
|
||||
buttonType={{
|
||||
text:
|
||||
colors.static[headerProps.color] || colors.primary.accent
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 5,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
alignItems: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Heading>{placeholderData.heading}</Heading>
|
||||
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
|
||||
{placeholderData.loading}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<ActivityIndicator
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
ColorValues[headerProps.color?.toLowerCase()] ||
|
||||
colors.primary.accent
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
(prev, next) => {
|
||||
if (prev.loading === next.loading) return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Make a tips manager.
|
||||
* The tip manager stores many tips. Each tip has following values
|
||||
* 1. Text
|
||||
* 2. contexts: An array of context strings. // Places where the tip can be shown
|
||||
* 3. Button if any.
|
||||
* 4. Image/Gif asset.
|
||||
*
|
||||
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
|
||||
*
|
||||
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
|
||||
* render tips.
|
||||
*
|
||||
* Where can the tips be shown and how?
|
||||
* 1. When transitioning, show tips in a sheet. Make sure its useful
|
||||
* 2. Replace placeholders with tips.
|
||||
* 3. Show tips in editor placeholder.
|
||||
* 4. Show tips between list items?
|
||||
*
|
||||
* Tooltips.
|
||||
* Small tooltips can be shown in initial render first time.
|
||||
* Especially for items that are not shown on blank page. Should be
|
||||
* in places where it makes sense and does not interrupt the user.
|
||||
*
|
||||
* Can also be shown when first time entering a screen that
|
||||
* has something that the user might not know of. Like sorting and side menu.
|
||||
*
|
||||
* Todo:
|
||||
* 1. Make a tip manager.
|
||||
* 2. Make a list of tips.
|
||||
* 3. Add images for those tips.
|
||||
* 4. Show tips
|
||||
*/
|
||||
140
apps/mobile/app/components/list/empty.tsx
Normal file
140
apps/mobile/app/components/list/empty.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
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 from "react";
|
||||
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { TTip, useTip } from "../../services/tip-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Tip } from "../tip";
|
||||
import { Button } from "../ui/button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export type PlaceholderData = {
|
||||
title: string;
|
||||
paragraph: string;
|
||||
button?: string;
|
||||
action?: () => void;
|
||||
loading?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type EmptyListProps = {
|
||||
loading?: boolean;
|
||||
placeholder?: PlaceholderData;
|
||||
title?: string;
|
||||
color?: string;
|
||||
dataType: string;
|
||||
screen?: string;
|
||||
};
|
||||
|
||||
export const Empty = React.memo(function Empty({
|
||||
loading = true,
|
||||
placeholder,
|
||||
title,
|
||||
color,
|
||||
dataType,
|
||||
screen
|
||||
}: EmptyListProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const { height } = useWindowDimensions();
|
||||
const introCompleted = useSettingStore(
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const tip = useTip(
|
||||
screen === "Notes" && introCompleted
|
||||
? "first-note"
|
||||
: placeholder?.type || ((dataType + "s") as any),
|
||||
screen === "Notes" ? "notes" : "list"
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
height: height - (140 + insets.top),
|
||||
width: "80%",
|
||||
justifyContent: "center",
|
||||
alignSelf: "center"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{!loading ? (
|
||||
<>
|
||||
<Tip
|
||||
color={color}
|
||||
tip={
|
||||
screen !== "Search"
|
||||
? tip || ({ text: placeholder?.paragraph } as TTip)
|
||||
: ({ text: placeholder?.paragraph } as TTip)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
{placeholder?.button && (
|
||||
<Button
|
||||
testID={notesnook.buttons.add}
|
||||
type="grayAccent"
|
||||
title={placeholder?.button}
|
||||
iconPosition="right"
|
||||
icon="arrow-right"
|
||||
onPress={placeholder?.action}
|
||||
buttonType={{
|
||||
text: color || colors.primary.accent
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 5,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
alignItems: "flex-start",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Heading>{placeholder?.title}</Heading>
|
||||
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
|
||||
{placeholder?.loading}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<ActivityIndicator
|
||||
size={SIZE.lg}
|
||||
color={color || colors.primary.accent}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -1,241 +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 } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { db } from "../../common/database";
|
||||
import { eScrollEvent } from "../../utils/events";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { Footer } from "../list-items/footer";
|
||||
import { Header } from "../list-items/headers/header";
|
||||
import { SectionHeader } from "../list-items/headers/section-header";
|
||||
import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import TagItem from "../list-items/tag";
|
||||
import { Empty } from "./empty";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
|
||||
const renderItems = {
|
||||
note: NoteWrapper,
|
||||
notebook: NotebookWrapper,
|
||||
topic: NotebookWrapper,
|
||||
tag: TagItem,
|
||||
section: SectionHeader,
|
||||
header: SectionHeader,
|
||||
reminder: ReminderItem
|
||||
};
|
||||
|
||||
const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
if (!item) return <View />;
|
||||
const Item = renderItems[item.itemType || item.type] || View;
|
||||
const totalNotes = getTotalNotes(item);
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
index={index}
|
||||
type={type}
|
||||
totalNotes={totalNotes}
|
||||
{...restArgs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
const List = ({
|
||||
listData,
|
||||
type,
|
||||
refreshCallback,
|
||||
placeholderData,
|
||||
loading,
|
||||
headerProps = {
|
||||
heading: "Home",
|
||||
color: null
|
||||
},
|
||||
screen,
|
||||
ListHeader,
|
||||
warning,
|
||||
isSheet = false,
|
||||
onMomentumScrollEnd,
|
||||
handlers,
|
||||
ScrollComponent
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const scrollRef = useRef();
|
||||
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
|
||||
state.settings.notesListMode,
|
||||
state.settings.notebooksListMode
|
||||
]);
|
||||
const isCompactModeEnabled =
|
||||
(type === "notes" && notesListMode === "compact") ||
|
||||
type === "notebooks" ||
|
||||
notebooksListMode === "compact";
|
||||
const groupType =
|
||||
screen === "Notes" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
|
||||
const groupOptions = db.settings?.getGroupOptions(groupType);
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item, index }) => (
|
||||
<RenderItem
|
||||
item={item}
|
||||
index={index}
|
||||
color={headerProps?.color}
|
||||
title={headerProps?.heading}
|
||||
dateBy={dateBy}
|
||||
type={
|
||||
screen === "Notes"
|
||||
? "home"
|
||||
: screen === "Favorites"
|
||||
? "favorites"
|
||||
: type
|
||||
}
|
||||
screen={screen}
|
||||
isSheet={isSheet}
|
||||
/>
|
||||
),
|
||||
[headerProps?.color, headerProps?.heading, screen, type, isSheet, dateBy]
|
||||
);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
if (refreshCallback) {
|
||||
refreshCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const _onScroll = React.useCallback(
|
||||
(event) => {
|
||||
if (!event) return;
|
||||
let y = event.nativeEvent.contentOffset.y;
|
||||
eSendEvent(eScrollEvent, {
|
||||
y,
|
||||
screen
|
||||
});
|
||||
},
|
||||
[screen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: 0,
|
||||
screen
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
let styles = {
|
||||
width: "100%",
|
||||
minHeight: 1,
|
||||
minWidth: 1
|
||||
};
|
||||
|
||||
const ListView = ScrollComponent ? ScrollComponent : FlashList;
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
entering={type === "search" ? undefined : FadeInDown}
|
||||
>
|
||||
<ListView
|
||||
{...handlers}
|
||||
style={styles}
|
||||
ref={scrollRef}
|
||||
testID={notesnook.list.id}
|
||||
data={listData}
|
||||
renderItem={renderItem}
|
||||
onScroll={_onScroll}
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
tabBarRef.current?.unlock();
|
||||
onMomentumScrollEnd?.();
|
||||
}}
|
||||
getItemType={(item) => item.itemType || item.type}
|
||||
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
|
||||
directionalLockEnabled={true}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={colors.primary.accent}
|
||||
colors={[colors.primary.accent]}
|
||||
progressBackgroundColor={colors.secondary.background}
|
||||
onRefresh={_onRefresh}
|
||||
refreshing={false}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
placeholderData ? (
|
||||
<Empty
|
||||
loading={loading}
|
||||
placeholderData={placeholderData}
|
||||
headerProps={headerProps}
|
||||
type={type}
|
||||
screen={screen}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListFooterComponent={<Footer />}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{ListHeader ? (
|
||||
ListHeader
|
||||
) : !headerProps ? null : (
|
||||
<Header
|
||||
title={headerProps.heading}
|
||||
color={headerProps.color}
|
||||
type={type}
|
||||
screen={screen}
|
||||
warning={warning}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
{listData ? (
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
||||
227
apps/mobile/app/components/list/index.tsx
Normal file
227
apps/mobile/app/components/list/index.tsx
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
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 {
|
||||
GroupHeader,
|
||||
GroupingKey,
|
||||
Item,
|
||||
VirtualizedGrouping,
|
||||
isGroupHeader
|
||||
} from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
RefreshControl
|
||||
} from "react-native";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eOpenJumpToDialog, eScrollEvent } from "../../utils/events";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { Footer } from "../list-items/footer";
|
||||
import { Header } from "../list-items/headers/header";
|
||||
import { SectionHeader } from "../list-items/headers/section-header";
|
||||
import { Empty, PlaceholderData } from "./empty";
|
||||
import { ListItemWrapper } from "./list-item.wrapper";
|
||||
|
||||
type ListProps = {
|
||||
data: VirtualizedGrouping<Item> | undefined;
|
||||
dataType: Item["type"];
|
||||
onRefresh?: () => void;
|
||||
loading?: boolean;
|
||||
headerTitle?: string;
|
||||
customAccentColor?: string;
|
||||
renderedInRoute?: RouteName;
|
||||
CustomLisHeader?: React.JSX.Element;
|
||||
isRenderedInActionSheet?: boolean;
|
||||
CustomListComponent?: React.JSX.ElementType;
|
||||
placeholder?: PlaceholderData;
|
||||
};
|
||||
|
||||
export default function List(props: ListProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const scrollRef = useRef();
|
||||
const [notesListMode, notebooksListMode] = useSettingStore((state) => [
|
||||
state.settings.notesListMode,
|
||||
state.settings.notebooksListMode
|
||||
]);
|
||||
|
||||
const isCompactModeEnabled =
|
||||
(props.dataType === "note" && notesListMode === "compact") ||
|
||||
props.dataType === "notebook" ||
|
||||
notebooksListMode === "compact";
|
||||
|
||||
const groupType =
|
||||
props.renderedInRoute === "Notes"
|
||||
? "home"
|
||||
: props.renderedInRoute === "Favorites"
|
||||
? "favorites"
|
||||
: props.renderedInRoute === "Trash"
|
||||
? "trash"
|
||||
: `${props.dataType}s`;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
Sync.run("global", false, true, () => {
|
||||
props.onRefresh?.();
|
||||
});
|
||||
};
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item, index }: { item: string | GroupHeader; index: number }) => {
|
||||
if (isGroupHeader(item)) {
|
||||
return (
|
||||
<SectionHeader
|
||||
screen={props.renderedInRoute}
|
||||
item={item}
|
||||
index={index}
|
||||
dataType={props.dataType}
|
||||
color={props.customAccentColor}
|
||||
groupOptions={groupOptions}
|
||||
onOpenJumpToDialog={() => {
|
||||
eSendEvent(eOpenJumpToDialog, {
|
||||
ref: scrollRef,
|
||||
data: props.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ListItemWrapper
|
||||
id={item}
|
||||
index={index}
|
||||
isSheet={props.isRenderedInActionSheet || false}
|
||||
items={props.data}
|
||||
group={groupType as GroupingKey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
groupOptions,
|
||||
groupType,
|
||||
props.customAccentColor,
|
||||
props.data,
|
||||
props.dataType,
|
||||
props.isRenderedInActionSheet,
|
||||
props.renderedInRoute
|
||||
]
|
||||
);
|
||||
|
||||
const onListScroll = React.useCallback(
|
||||
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (!event) return;
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: event.nativeEvent.contentOffset.y,
|
||||
screen: props.renderedInRoute
|
||||
});
|
||||
},
|
||||
[props.renderedInRoute]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSendEvent(eScrollEvent, {
|
||||
y: 0,
|
||||
screen: props.renderedInRoute
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const styles = {
|
||||
width: "100%",
|
||||
minHeight: 1,
|
||||
minWidth: 1
|
||||
};
|
||||
|
||||
const ListView = props.CustomListComponent
|
||||
? props.CustomListComponent
|
||||
: FlashList;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Animated.View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
entering={props.renderedInRoute === "Search" ? undefined : FadeInDown}
|
||||
>
|
||||
<ListView
|
||||
style={styles}
|
||||
ref={scrollRef}
|
||||
testID={notesnook.list.id}
|
||||
data={props.data?.ids || []}
|
||||
renderItem={renderItem}
|
||||
onScroll={onListScroll}
|
||||
nestedScrollEnabled={true}
|
||||
onMomentumScrollEnd={() => {
|
||||
tabBarRef.current?.unlock();
|
||||
}}
|
||||
getItemType={(item: any) => (isGroupHeader(item) ? "header" : "item")}
|
||||
estimatedItemSize={isCompactModeEnabled ? 60 : 100}
|
||||
directionalLockEnabled={true}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={colors.primary.accent}
|
||||
colors={[colors.primary.accent]}
|
||||
progressBackgroundColor={colors.secondary.background}
|
||||
onRefresh={_onRefresh}
|
||||
refreshing={false}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
props.placeholder ? (
|
||||
<Empty
|
||||
loading={props.loading}
|
||||
title={props.headerTitle}
|
||||
dataType={props.dataType}
|
||||
color={props.customAccentColor}
|
||||
placeholder={props.placeholder}
|
||||
screen={props.renderedInRoute}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListFooterComponent={<Footer />}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{props.CustomLisHeader ? (
|
||||
props.CustomLisHeader
|
||||
) : !props.headerTitle ? null : (
|
||||
<Header
|
||||
color={props.customAccentColor}
|
||||
screen={props.renderedInRoute}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
269
apps/mobile/app/components/list/list-item.wrapper.tsx
Normal file
269
apps/mobile/app/components/list/list-item.wrapper.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
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 from "react";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import {
|
||||
GroupingKey,
|
||||
Item,
|
||||
VirtualizedGrouping,
|
||||
Color,
|
||||
Reminder,
|
||||
Notebook,
|
||||
Tag,
|
||||
TrashItem,
|
||||
ItemType,
|
||||
Note
|
||||
} from "@notesnook/core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { db } from "../../common/database";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import TagItem from "../list-items/tag";
|
||||
|
||||
export type WithDateEdited<T> = { items: T[]; dateEdited: number };
|
||||
export type NotebooksWithDateEdited = WithDateEdited<Notebook>;
|
||||
export type TagsWithDateEdited = WithDateEdited<Tag>;
|
||||
|
||||
type ListItemWrapperProps<TItem = Item> = {
|
||||
group?: GroupingKey;
|
||||
items: VirtualizedGrouping<TItem> | undefined;
|
||||
id: string;
|
||||
isSheet: boolean;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export function ListItemWrapper(props: ListItemWrapperProps) {
|
||||
const { id, items, group, isSheet, index } = props;
|
||||
const [item, setItem] = useState<Item>();
|
||||
const tags = useRef<TagsWithDateEdited>();
|
||||
const notebooks = useRef<NotebooksWithDateEdited>();
|
||||
const reminder = useRef<Reminder>();
|
||||
const color = useRef<Color>();
|
||||
const totalNotes = useRef<number>(0);
|
||||
const attachmentsCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const { item, data } = (await items?.item(id, resolveItems)) || {};
|
||||
if (!item) return;
|
||||
if (item.type === "note" && isNoteResolvedData(data)) {
|
||||
tags.current = data.tags;
|
||||
notebooks.current = data.notebooks;
|
||||
reminder.current = data.reminder;
|
||||
color.current = data.color;
|
||||
attachmentsCount.current = data.attachmentsCount;
|
||||
} else if (item.type === "notebook" && typeof data === "number") {
|
||||
totalNotes.current = data;
|
||||
} else if (item.type === "tag" && typeof data === "number") {
|
||||
totalNotes.current = data;
|
||||
}
|
||||
setItem(item);
|
||||
})();
|
||||
}, [id, items]);
|
||||
|
||||
if (!item) return <View style={{ height: 100, width: "100%" }} />;
|
||||
|
||||
const type = ((item as TrashItem).itemType || item.type) as ItemType;
|
||||
switch (type) {
|
||||
case "note": {
|
||||
return (
|
||||
<NoteWrapper
|
||||
item={item as Note}
|
||||
tags={tags.current}
|
||||
color={color.current}
|
||||
notebooks={notebooks.current}
|
||||
reminder={reminder.current}
|
||||
attachmentsCount={attachmentsCount.current}
|
||||
date={getDate(item, group)}
|
||||
isRenderedInActionSheet={isSheet}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "notebook":
|
||||
return (
|
||||
<NotebookWrapper
|
||||
item={item as Notebook}
|
||||
totalNotes={totalNotes.current}
|
||||
date={getDate(item, group)}
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
|
||||
case "reminder":
|
||||
return (
|
||||
<ReminderItem item={item as Reminder} index={index} isSheet={isSheet} />
|
||||
);
|
||||
case "tag":
|
||||
return (
|
||||
<TagItem
|
||||
item={item as Tag}
|
||||
index={index}
|
||||
totalNotes={totalNotes.current}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function withDateEdited<
|
||||
T extends { dateEdited: number } | { dateModified: number }
|
||||
>(items: T[]): WithDateEdited<T> {
|
||||
let latestDateEdited = 0;
|
||||
items.forEach((item) => {
|
||||
const date = "dateEdited" in item ? item.dateEdited : item.dateModified;
|
||||
if (latestDateEdited < date) latestDateEdited = date;
|
||||
});
|
||||
return { dateEdited: latestDateEdited, items };
|
||||
}
|
||||
|
||||
function getDate(item: Item, groupType?: GroupingKey): number {
|
||||
return (
|
||||
getSortValue(
|
||||
groupType
|
||||
? db.settings.getGroupOptions(groupType)
|
||||
: {
|
||||
groupBy: "default",
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
},
|
||||
item
|
||||
) || 0
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveItems(ids: string[], items: Record<string, Item>) {
|
||||
const { type } = items[ids[0]];
|
||||
if (type === "note") return resolveNotes(ids);
|
||||
else if (type === "notebook") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids) data[id] = await db.notebooks.totalNotes(id);
|
||||
return data;
|
||||
} else if (type === "tag") {
|
||||
const data: Record<string, number> = {};
|
||||
for (const id of ids)
|
||||
data[id] = await db.relations.from({ id, type: "tag" }, "note").count();
|
||||
return data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
type NoteResolvedData = {
|
||||
notebooks?: NotebooksWithDateEdited;
|
||||
reminder?: Reminder;
|
||||
color?: Color;
|
||||
tags?: TagsWithDateEdited;
|
||||
attachmentsCount: number;
|
||||
};
|
||||
async function resolveNotes(ids: string[]) {
|
||||
console.time("relations");
|
||||
const relations = [
|
||||
...(await db.relations
|
||||
.to({ type: "note", ids }, ["notebook", "tag", "color"])
|
||||
.get()),
|
||||
...(await db.relations.from({ type: "note", ids }, "reminder").get())
|
||||
];
|
||||
console.timeEnd("relations");
|
||||
|
||||
const relationIds: {
|
||||
notebooks: Set<string>;
|
||||
colors: Set<string>;
|
||||
tags: Set<string>;
|
||||
reminders: Set<string>;
|
||||
} = {
|
||||
colors: new Set(),
|
||||
notebooks: new Set(),
|
||||
tags: new Set(),
|
||||
reminders: new Set()
|
||||
};
|
||||
|
||||
const grouped: Record<
|
||||
string,
|
||||
{
|
||||
notebooks: string[];
|
||||
color?: string;
|
||||
tags: string[];
|
||||
reminder?: string;
|
||||
}
|
||||
> = {};
|
||||
for (const relation of relations) {
|
||||
const noteId =
|
||||
relation.toType === "relation" ? relation.fromId : relation.toId;
|
||||
const data = grouped[noteId] || {
|
||||
notebooks: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
if (relation.toType === "relation" && !data.reminder) {
|
||||
data.reminder = relation.fromId;
|
||||
relationIds.reminders.add(relation.fromId);
|
||||
} else if (relation.fromType === "notebook" && data.notebooks.length < 2) {
|
||||
data.notebooks.push(relation.fromId);
|
||||
relationIds.notebooks.add(relation.fromId);
|
||||
} else if (relation.fromType === "tag" && data.tags.length < 3) {
|
||||
data.tags.push(relation.fromId);
|
||||
relationIds.tags.add(relation.fromId);
|
||||
} else if (relation.fromType === "color" && !data.color) {
|
||||
data.color = relation.fromId;
|
||||
relationIds.colors.add(relation.fromId);
|
||||
}
|
||||
grouped[relation.toId] = data;
|
||||
}
|
||||
|
||||
console.time("resolve");
|
||||
const resolved = {
|
||||
notebooks: await db.notebooks.all.records(
|
||||
Array.from(relationIds.notebooks)
|
||||
),
|
||||
tags: await db.tags.all.records(Array.from(relationIds.tags)),
|
||||
colors: await db.colors.all.records(Array.from(relationIds.colors)),
|
||||
reminders: await db.reminders.all.records(Array.from(relationIds.reminders))
|
||||
};
|
||||
console.timeEnd("resolve");
|
||||
|
||||
const data: Record<string, NoteResolvedData> = {};
|
||||
for (const noteId in grouped) {
|
||||
const group = grouped[noteId];
|
||||
data[noteId] = {
|
||||
color: group.color ? resolved.colors[group.color] : undefined,
|
||||
reminder: group.reminder ? resolved.reminders[group.reminder] : undefined,
|
||||
tags: withDateEdited(group.tags.map((id) => resolved.tags[id])),
|
||||
notebooks: withDateEdited(
|
||||
group.notebooks.map((id) => resolved.notebooks[id])
|
||||
),
|
||||
attachmentsCount:
|
||||
(await db.attachments?.ofNote(noteId, "all").ids())?.length || 0
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function isNoteResolvedData(data: unknown): data is NoteResolvedData {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
!!data &&
|
||||
"notebooks" in data &&
|
||||
"reminder" in data &&
|
||||
"color" in data &&
|
||||
"tags" in data
|
||||
);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ const MergeConflicts = () => {
|
||||
if (editorController.current?.note?.id === note.id) {
|
||||
// reload the note in editor
|
||||
eSendEvent(eOnLoadNote, {
|
||||
...editorController.current?.note,
|
||||
item: editorController.current?.note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
@@ -320,10 +324,12 @@ const MergeConflicts = () => {
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictPrimary", {
|
||||
...note,
|
||||
content: {
|
||||
...content.current,
|
||||
isPreview: true
|
||||
item: {
|
||||
...note,
|
||||
content: {
|
||||
...content.current,
|
||||
isPreview: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -355,15 +361,17 @@ const MergeConflicts = () => {
|
||||
if (!note) return;
|
||||
await sleep(300);
|
||||
eSendEvent(eOnLoadNote + ":conflictSecondary", {
|
||||
...note,
|
||||
content: { ...content.current.conflicted, isPreview: true }
|
||||
item: {
|
||||
...note,
|
||||
content: { ...content.current.conflicted, isPreview: true }
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Modal>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,26 +17,29 @@ 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 from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import Editor from "../../screens/editor";
|
||||
import EditorOverlay from "../../screens/editor/loading";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, ToastEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useEditorStore } from "../../stores/use-editor-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useTrashStore } from "../../stores/use-trash-store";
|
||||
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Dialog } from "../dialog";
|
||||
import DialogHeader from "../dialog/dialog-header";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function NotePreview({ session, content, note }) {
|
||||
const { colors } = useThemeColors();
|
||||
const editorId = ":noteHistory";
|
||||
@@ -46,7 +49,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
@@ -57,7 +60,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
|
||||
if (editorController.current?.note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
...editorController.current?.note,
|
||||
item: editorController.current?.note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
@@ -66,7 +69,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
ToastEvent.show({
|
||||
ToastManager.show({
|
||||
heading: "Note restored successfully",
|
||||
type: "success"
|
||||
});
|
||||
@@ -83,8 +86,8 @@ export default function NotePreview({ session, content, note }) {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().setTrash();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastEvent.show({
|
||||
heading: "Permanantly deleted items",
|
||||
ToastManager.show({
|
||||
heading: "Permanently deleted items",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -117,10 +120,12 @@ export default function NotePreview({ session, content, note }) {
|
||||
onLoad={async () => {
|
||||
const _note = note || db.notes.note(session?.noteId)?.data;
|
||||
eSendEvent(eOnLoadNote + editorId, {
|
||||
..._note,
|
||||
content: {
|
||||
...content,
|
||||
isPreview: true
|
||||
item: {
|
||||
..._note,
|
||||
content: {
|
||||
...content,
|
||||
isPreview: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -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,12 +20,12 @@ 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,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
@@ -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,9 +515,9 @@ 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({
|
||||
ToastManager.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
@@ -356,9 +525,9 @@ export const PricingPlans = ({
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastEvent.show({
|
||||
ToastManager.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(() => {});
|
||||
}}
|
||||
@@ -23,6 +23,11 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
export const ProTag = ({ width, size, background }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
|
||||
@@ -17,7 +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, { useState } from "react";
|
||||
import { DefaultColors } from "@notesnook/core/dist/collections/colors";
|
||||
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
@@ -25,45 +28,56 @@ import { db } from "../../common/database";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useMenuStore } from "../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { refreshNotesPage } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ColorTags = ({ item }) => {
|
||||
export const ColorTags = ({ item }: { item: Note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [note, setNote] = useState(item);
|
||||
const setColorNotes = useMenuStore((state) => state.setColorNotes);
|
||||
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
|
||||
const changeColor = async (color) => {
|
||||
if (note.color === color.name) {
|
||||
await db.notes.note(note.id).uncolor();
|
||||
} else {
|
||||
await db.notes.note(note.id).color(color.name);
|
||||
const updater = useRelationStore((state) => state.updater);
|
||||
|
||||
const getColorInfo = async (colorCode: string) => {
|
||||
const dbColor = await db.colors.all.find((v) =>
|
||||
v.and([v(`colorCode`, "==", colorCode)])
|
||||
);
|
||||
let isLinked = false;
|
||||
|
||||
if (dbColor) {
|
||||
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
|
||||
if (hasRelation) {
|
||||
isLinked = true;
|
||||
}
|
||||
}
|
||||
let _note = db.notes.note(note.id).data;
|
||||
setNote({ ..._note });
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(refreshNotesPage);
|
||||
|
||||
return {
|
||||
linked: isLinked,
|
||||
item: dbColor
|
||||
};
|
||||
};
|
||||
|
||||
const _renderColor = (c) => {
|
||||
const color = {
|
||||
name: c,
|
||||
value: ColorValues[c?.toLowerCase()]
|
||||
};
|
||||
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
|
||||
const color = DefaultColors[name];
|
||||
const [colorInfo, setColorInfo] = useState<{
|
||||
linked: boolean;
|
||||
item: Color | undefined;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
getColorInfo(color).then((info) => setColorInfo(info));
|
||||
}, [color]);
|
||||
|
||||
return (
|
||||
<PressableButton
|
||||
type="accent"
|
||||
accentColor={colors.static[color.name?.toLowerCase()]}
|
||||
accentColor={color}
|
||||
accentText={colors.static.white}
|
||||
testID={notesnook.ids.dialogs.actionsheet.color(c)}
|
||||
key={color.value}
|
||||
onPress={() => changeColor(color)}
|
||||
testID={notesnook.ids.dialogs.actionsheet.color(name)}
|
||||
key={color}
|
||||
onPress={() => changeColor(name)}
|
||||
customStyle={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
@@ -73,13 +87,40 @@ export const ColorTags = ({ item }) => {
|
||||
marginRight: isTablet ? 10 : undefined
|
||||
}}
|
||||
>
|
||||
{note.color?.toLowerCase() === color.name ? (
|
||||
{colorInfo?.linked ? (
|
||||
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
|
||||
) : null}
|
||||
</PressableButton>
|
||||
);
|
||||
};
|
||||
|
||||
const changeColor = async (color: string) => {
|
||||
const colorInfo = await getColorInfo(DefaultColors[color]);
|
||||
|
||||
if (colorInfo.item) {
|
||||
if (colorInfo.linked) {
|
||||
await db.relations.unlink(colorInfo.item, item);
|
||||
} else {
|
||||
await db.relations.add(colorInfo.item, item);
|
||||
}
|
||||
} else {
|
||||
const colorId = await db.colors.add({
|
||||
title: color,
|
||||
colorCode: DefaultColors[color]
|
||||
});
|
||||
|
||||
const dbColor = await db.colors.color(colorId);
|
||||
if (dbColor) {
|
||||
await db.relations.add(dbColor as unknown as ItemReference, item);
|
||||
}
|
||||
}
|
||||
|
||||
useRelationStore.getState().update();
|
||||
setColorNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(refreshNotesPage);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -92,7 +133,9 @@ export const ColorTags = ({ item }) => {
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
}}
|
||||
>
|
||||
{Object.keys(ColorValues).map(_renderColor)}
|
||||
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
|
||||
return <ColorItem key={name} name={name} />;
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -53,23 +53,24 @@ export const DateMeta = ({ item }) => {
|
||||
return keys.filter((key) => key.startsWith("date") && key !== "date");
|
||||
}
|
||||
|
||||
const renderItem = (key) => (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
const renderItem = (key) =>
|
||||
!item[key] ? null : (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 3
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -16,14 +16,13 @@ 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 from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import SearchService from "../../services/search";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { ColorValues } from "../../utils/colors";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -34,7 +33,8 @@ import { DateMeta } from "./date-meta";
|
||||
import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { Synced } from "./synced";
|
||||
import { Tags, TagStrip } from "./tags";
|
||||
import { TagStrip, Tags } from "./tags";
|
||||
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
@@ -52,9 +52,7 @@ const Line = ({ top = 6, bottom = 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 +60,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="always"
|
||||
@@ -90,9 +89,13 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{alias}
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
{item.type === "notebook" && item.description ? (
|
||||
<Paragraph>{item.description}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.type === "note" ? (
|
||||
<TagStrip close={close} item={item} />
|
||||
) : null}
|
||||
@@ -130,11 +133,9 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
buttons={buttons}
|
||||
close={() => {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
SearchService.updateAndSearch();
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Synced item={item} close={close} />
|
||||
|
||||
{DDS.isTab ? (
|
||||
@@ -151,7 +152,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
Properties.present = (item, buttons = [], isSheet) => {
|
||||
Properties.present = async (item, buttons = [], isSheet) => {
|
||||
if (!item) return;
|
||||
let type = item?.type;
|
||||
let props = [];
|
||||
@@ -163,7 +164,7 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
break;
|
||||
case "note":
|
||||
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
|
||||
props[0] = db.notes.note(item.id).data;
|
||||
props[0] = await db.notes.note(item.id);
|
||||
props.push([
|
||||
"notebooks",
|
||||
"add-reminder",
|
||||
@@ -188,33 +189,34 @@ Properties.present = (item, buttons = [], isSheet) => {
|
||||
]);
|
||||
break;
|
||||
case "notebook":
|
||||
props[0] = db.notebooks.notebook(item.id).data;
|
||||
props[0] = await db.notebooks.notebook(item.id);
|
||||
props.push([
|
||||
"edit-notebook",
|
||||
"pin",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-notebook"
|
||||
]);
|
||||
break;
|
||||
case "topic":
|
||||
props[0] = db.notebooks
|
||||
.notebook(item.notebookId)
|
||||
.topics.topic(item.id)._topic;
|
||||
props.push([
|
||||
"move-notes",
|
||||
"edit-topic",
|
||||
"add-shortcut",
|
||||
"trash",
|
||||
"default-topic"
|
||||
"default-notebook",
|
||||
"add-notebook"
|
||||
]);
|
||||
break;
|
||||
// case "topic":
|
||||
// props[0] = db.notebooks
|
||||
// .notebook(item.notebookId)
|
||||
// .topics.topic(item.id)._topic;
|
||||
// props.push([
|
||||
// "move-notes",
|
||||
// "edit-topic",
|
||||
// "add-shortcut",
|
||||
// "trash",
|
||||
// "default-topic"
|
||||
// ]);
|
||||
// break;
|
||||
case "tag":
|
||||
props[0] = db.tags.tag(item.id);
|
||||
props[0] = await db.tags.tag(item.id);
|
||||
props.push(["add-shortcut", "trash", "rename-tag"]);
|
||||
break;
|
||||
case "reminder": {
|
||||
props[0] = db.reminders.reminder(item.id);
|
||||
props[0] = await db.reminders.reminder(item.id);
|
||||
props.push(["edit-reminder", "trash", "disable-reminder"]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -28,14 +28,17 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Items = ({ item, buttons, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
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 +69,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling
|
||||
name={item.icon}
|
||||
size={DDS.isTab ? SIZE.xxl : SIZE.lg}
|
||||
color={
|
||||
@@ -78,7 +82,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 +147,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 +159,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,16 +176,39 @@ export const Items = ({ item, buttons, close }) => {
|
||||
"copy",
|
||||
"share",
|
||||
"export",
|
||||
"lock-unlock",
|
||||
"publish"
|
||||
"lock-unlock"
|
||||
];
|
||||
const topBarItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) > -1
|
||||
);
|
||||
|
||||
const bottomGridItems = data.filter(
|
||||
(item) => topBarItemsList.indexOf(item.id) === -1
|
||||
);
|
||||
if (!shouldShrink) {
|
||||
topBarItemsList.push("publish");
|
||||
}
|
||||
|
||||
const topBarItems = data
|
||||
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
|
||||
.sort((a, b) =>
|
||||
topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1
|
||||
);
|
||||
|
||||
const bottomBarItemsList = [
|
||||
"notebooks",
|
||||
"add-reminder",
|
||||
"pin-to-notifications",
|
||||
"duplicate",
|
||||
"read-only",
|
||||
"local-only",
|
||||
"history",
|
||||
"reminders",
|
||||
"attachments",
|
||||
"trash"
|
||||
];
|
||||
|
||||
const bottomGridItems = data
|
||||
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
|
||||
.sort((a, b) =>
|
||||
bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id)
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
|
||||
const topBarItemWidth =
|
||||
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;
|
||||
|
||||
@@ -17,76 +17,46 @@ 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 { ScrollView, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import Notebook from "../../screens/notebook";
|
||||
import { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { useNotebookStore } from "../../stores/use-notebook-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import NotebookScreen from "../../screens/notebook";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
|
||||
export default function Notebooks({ note, close, full }) {
|
||||
const { colors } = useThemeColors();
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
function getNotebooks(item) {
|
||||
let filteredNotebooks = [];
|
||||
const relations = db.relations.to(note, "notebook");
|
||||
filteredNotebooks.push(
|
||||
...relations.map((notebook) => ({
|
||||
...notebook,
|
||||
topics: []
|
||||
}))
|
||||
);
|
||||
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
|
||||
|
||||
for (let notebookReference of item.notebooks) {
|
||||
let notebook = {
|
||||
...(notebooks.find((item) => item.id === notebookReference.id) || {})
|
||||
};
|
||||
if (notebook.id) {
|
||||
notebook.topics = notebook.topics.filter((topic) => {
|
||||
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
|
||||
});
|
||||
const index = filteredNotebooks.findIndex(
|
||||
(item) => item.id === notebook.id
|
||||
);
|
||||
if (index > -1) {
|
||||
filteredNotebooks[index].topics = notebook.topics;
|
||||
} else {
|
||||
filteredNotebooks.push(notebook);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredNotebooks;
|
||||
async function getNotebooks() {
|
||||
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
|
||||
return filteredNotebooks || [];
|
||||
}
|
||||
const noteNotebooks = getNotebooks(note);
|
||||
const [noteNotebooks, setNoteNotebooks] = useState([]);
|
||||
useEffect(() => {
|
||||
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
|
||||
});
|
||||
|
||||
const navigateNotebook = (id) => {
|
||||
let item = db.notebooks.notebook(id)?.data;
|
||||
const navigateNotebook = async (id) => {
|
||||
let item = await db.notebooks.notebook(id);
|
||||
if (!item) return;
|
||||
Notebook.navigate(item, true);
|
||||
NotebookScreen.navigate(item, true);
|
||||
};
|
||||
|
||||
const navigateTopic = (id, notebookId) => {
|
||||
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
|
||||
if (!item) return;
|
||||
TopicNotes.navigate(item, true);
|
||||
};
|
||||
const renderItem = (item) => (
|
||||
<View
|
||||
<PressableButton
|
||||
key={item.id}
|
||||
style={{
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
type={full ? "transparent" : "grayBg"}
|
||||
customStyle={{
|
||||
justifyContent: "flex-start",
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
@@ -94,10 +64,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
padding: 6,
|
||||
borderWidth: full ? 0 : 1,
|
||||
borderColor: colors.primary.background,
|
||||
borderRadius: 10,
|
||||
backgroundColor: full ? "transparent" : colors.secondary.background,
|
||||
minHeight: 42
|
||||
}}
|
||||
>
|
||||
@@ -115,65 +82,10 @@ export default function Notebooks({ note, close, full }) {
|
||||
maxWidth: "50%"
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
onPress={() => {
|
||||
navigateNotebook(item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
{item.title}
|
||||
</Heading>
|
||||
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginLeft: 8,
|
||||
borderLeftColor: colors.primary.hover,
|
||||
borderLeftWidth: 1,
|
||||
paddingLeft: 8
|
||||
}}
|
||||
>
|
||||
{item.topics.map((topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
onPress={() => {
|
||||
navigateTopic(topic.id, item.id);
|
||||
eSendEvent(eClearEditor);
|
||||
close();
|
||||
}}
|
||||
onLongPress={async () => {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.id,
|
||||
topic: topic.id
|
||||
},
|
||||
note
|
||||
);
|
||||
useNotebookStore.getState().setNotebooks();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
ToastEvent.show({
|
||||
heading: "Note removed from topic",
|
||||
context: "local",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title={topic.title}
|
||||
type="gray"
|
||||
height={30}
|
||||
fontSize={SIZE.xs}
|
||||
icon="bookmark-outline"
|
||||
style={{
|
||||
marginRight: 5,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 8
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<View style={{ width: 10 }} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
</PressableButton>
|
||||
);
|
||||
|
||||
return noteNotebooks.length === 0 ? null : (
|
||||
@@ -190,7 +102,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`See all linked notebooks`}
|
||||
title={`View all linked notebooks`}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { sleep } from "../../utils/time";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
|
||||
export const Tags = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
@@ -47,7 +48,7 @@ export const Tags = ({ item, close }) => {
|
||||
>
|
||||
<Button
|
||||
onPress={async () => {
|
||||
ManageTagsSheet.present([item]);
|
||||
ManageTagsSheet.present([item.id]);
|
||||
}}
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
|
||||
@@ -1,70 +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 from "react";
|
||||
import { View } from "react-native";
|
||||
import { TopicNotes } from "../../screens/notes/topic-notes";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
export const Topics = ({ item, close }) => {
|
||||
const open = (topic) => {
|
||||
close();
|
||||
TopicNotes.navigate(topic, true);
|
||||
};
|
||||
|
||||
const renderItem = (topic) => (
|
||||
<Button
|
||||
key={topic.id}
|
||||
title={topic.title}
|
||||
type="grayBg"
|
||||
// buttonType={{
|
||||
// text: colors.primary.accent
|
||||
// }}
|
||||
height={30}
|
||||
onPress={() => open(topic)}
|
||||
icon="bookmark-outline"
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 5,
|
||||
paddingHorizontal: 8,
|
||||
borderRadius: 100,
|
||||
marginVertical: 5
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return item &&
|
||||
item.type === "notebook" &&
|
||||
item.topics &&
|
||||
item.topics.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginTop: 5,
|
||||
width: "100%",
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
{item.topics
|
||||
.sort((a, b) => a.dateEdited - b.dateEdited)
|
||||
.slice(0, 6)
|
||||
.map(renderItem)}
|
||||
</View>
|
||||
) : null;
|
||||
};
|
||||
@@ -1,380 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { BackHandler, Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SearchService from "../../services/search";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
|
||||
export const SelectionHeader = React.memo(() => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
const currentScreen = useNavigationStore((state) => state.currentScreen);
|
||||
const screen = currentScreen.name;
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
SearchService.prepareSearch?.();
|
||||
const allItems = SearchService.getSearchInformation()?.get() || [];
|
||||
const allSelected = allItems.length === selectedItemsList.length;
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
tabBarRef.current?.lock();
|
||||
} else {
|
||||
tabBarRef.current?.unlock();
|
||||
}
|
||||
}, [selectionMode]);
|
||||
|
||||
const addToFavorite = async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
selectedItemsList.forEach((item) => {
|
||||
db.notes.note(item.id).favorite();
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
const restoreItem = async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
let noteIds = [];
|
||||
selectedItemsList.forEach((item) => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.restore(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastEvent.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1
|
||||
? "these items permanently?"
|
||||
: "this item permanently?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
let noteIds = [];
|
||||
selectedItemsList.forEach((item) => {
|
||||
noteIds.push(item.id);
|
||||
});
|
||||
await db.trash.delete(...noteIds);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
};
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
clearSelection();
|
||||
return true;
|
||||
}, [clearSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
BackHandler.addEventListener("hardwareBackPress", onBackPress);
|
||||
} else {
|
||||
BackHandler.removeEventListener("hardwareBackPress", onBackPress);
|
||||
}
|
||||
}, [onBackPress, selectionMode]);
|
||||
|
||||
return !selectionMode ? null : (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: Platform.OS === "android" ? 50 + insets.top : 50,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
zIndex: 999,
|
||||
paddingHorizontal: 12,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
type="grayBg"
|
||||
onPress={() => {
|
||||
setSelectionMode(!selectionMode);
|
||||
}}
|
||||
size={SIZE.xl}
|
||||
color={colors.primary.icon}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.primary.accent}>
|
||||
{selectedItemsList.length}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
useSelectionStore
|
||||
.getState()
|
||||
.setAll(allSelected ? [] : [...allItems]);
|
||||
}}
|
||||
tooltipText="Select all"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="select-all"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
{screen === "Trash" ||
|
||||
screen === "Notebooks" ||
|
||||
screen === "Notebook" ||
|
||||
screen === "Reminders" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
name="pound"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
//setSelectionMode(false);
|
||||
await sleep(100);
|
||||
MoveNoteSheet.present();
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Add to notebooks"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{screen === "TopicNotes" || screen === "Notebook" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
const currentScreen =
|
||||
useNavigationStore.getState().currentScreen;
|
||||
|
||||
if (screen === "Notebook") {
|
||||
for (const item of selectedItemsList) {
|
||||
await db.relations.unlink(
|
||||
{ type: "notebook", id: currentScreen.id },
|
||||
item
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: currentScreen.notebookId,
|
||||
topic: currentScreen.id
|
||||
},
|
||||
...selectedItemsList.map((item) => item.id)
|
||||
);
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from ${
|
||||
screen === "Notebook" ? "notebook" : "topic"
|
||||
}`}
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.primary.paragraph}
|
||||
name="minus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{screen === "Favorites" ? (
|
||||
<IconButton
|
||||
onPress={addToFavorite}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Remove from favorites"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="star-off"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{screen === "Trash" ? null : (
|
||||
<IconButton
|
||||
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;
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
color={colors.primary.paragraph}
|
||||
name="delete"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "Trash" ? (
|
||||
<>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={restoreItem}
|
||||
name="delete-restore"
|
||||
tooltipText="Restore"
|
||||
tooltipPosition={4}
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={deleteItem}
|
||||
tooltipText="Delete"
|
||||
tooltipPosition={4}
|
||||
name="delete"
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
SelectionHeader.displayName = "SelectionHeader";
|
||||
|
||||
export default SelectionHeader;
|
||||
363
apps/mobile/app/components/selection-header/index.tsx
Normal file
363
apps/mobile/app/components/selection-header/index.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
/*
|
||||
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 { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import {
|
||||
BackHandler,
|
||||
NativeEventSubscription,
|
||||
Platform,
|
||||
View
|
||||
} from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { deleteItems } from "../../utils/functions";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { updateNotebook } from "../../utils/notebooks";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import MoveNoteSheet from "../sheets/add-to";
|
||||
import ExportNotesSheet from "../sheets/export-notes";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Animated, { FadeInUp } from "react-native-reanimated";
|
||||
|
||||
export const SelectionHeader = React.memo(
|
||||
({
|
||||
items,
|
||||
type,
|
||||
id,
|
||||
renderedInRoute
|
||||
}: {
|
||||
items?: VirtualizedGrouping<Item>;
|
||||
id?: string;
|
||||
type?: ItemType;
|
||||
renderedInRoute?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const selectionMode = useSelectionStore((state) => state.selectionMode);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const allSelected =
|
||||
items?.ids?.filter((id) => typeof id === "string").length ===
|
||||
selectedItemsList.length;
|
||||
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectionMode) {
|
||||
tabBarRef.current?.lock();
|
||||
} else {
|
||||
tabBarRef.current?.unlock();
|
||||
}
|
||||
}, [selectionMode]);
|
||||
|
||||
const addToFavorite = async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
db.notes.favorite(true, ...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const restoreItem = async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.restore(...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1
|
||||
? "these items permanently?"
|
||||
: "this item permanently?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.delete(...selectedItemsList);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onBackPress = () => {
|
||||
clearSelection();
|
||||
return true;
|
||||
};
|
||||
let sub: NativeEventSubscription | undefined;
|
||||
if (selectionMode) {
|
||||
sub = BackHandler.addEventListener("hardwareBackPress", onBackPress);
|
||||
}
|
||||
|
||||
return () => {
|
||||
sub?.remove();
|
||||
};
|
||||
}, [clearSelection, selectionMode]);
|
||||
|
||||
return selectionMode !== type || focusedRouteId !== id ? null : (
|
||||
<Animated.View
|
||||
entering={FadeInUp}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: Platform.OS === "android" ? 50 + insets.top : 50,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : null,
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
zIndex: 999,
|
||||
paddingHorizontal: 12,
|
||||
marginVertical: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
type="grayBg"
|
||||
onPress={() => {
|
||||
clearSelection();
|
||||
}}
|
||||
size={SIZE.xl}
|
||||
color={colors.primary.icon}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 16,
|
||||
justifyContent: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md} color={colors.primary.accent}>
|
||||
{selectedItemsList.length}
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
useSelectionStore
|
||||
.getState()
|
||||
.setAll(
|
||||
allSelected
|
||||
? []
|
||||
: [
|
||||
...((items?.ids.filter(
|
||||
(id) => typeof id !== "object"
|
||||
) as string[]) || [])
|
||||
]
|
||||
);
|
||||
}}
|
||||
tooltipText="Select all"
|
||||
tooltipPosition={4}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={
|
||||
allSelected ? colors.primary.accent : colors.primary.paragraph
|
||||
}
|
||||
name="select-all"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
|
||||
{type !== "note" ? null : (
|
||||
<>
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
tooltipText="Manage tags"
|
||||
tooltipPosition={4}
|
||||
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 () => {
|
||||
MoveNoteSheet.present();
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Add to notebooks"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="plus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{renderedInRoute === "Notebook" ? (
|
||||
<IconButton
|
||||
onPress={async () => {
|
||||
if (selectedItemsList.length > 0) {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(id, ...selectedItemsList);
|
||||
updateNotebook(id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
clearSelection();
|
||||
}
|
||||
}}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText={`Remove from Notebook`}
|
||||
tooltipPosition={4}
|
||||
testID="select-minus"
|
||||
color={colors.primary.paragraph}
|
||||
name="minus"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{focusedRouteId === "Favorites" ? (
|
||||
<IconButton
|
||||
onPress={addToFavorite}
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
tooltipText="Remove from favorites"
|
||||
tooltipPosition={4}
|
||||
color={colors.primary.paragraph}
|
||||
name="star-off"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{type === "trash" ? null : (
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
deleteItems(
|
||||
undefined,
|
||||
useSelectionStore.getState().selectionMode
|
||||
);
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
color={colors.primary.paragraph}
|
||||
name="delete"
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "trash" ? (
|
||||
<>
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={restoreItem}
|
||||
name="delete-restore"
|
||||
tooltipText="Restore"
|
||||
tooltipPosition={4}
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={deleteItem}
|
||||
tooltipText="Delete"
|
||||
tooltipPosition={4}
|
||||
name="delete"
|
||||
size={SIZE.xl - 3}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SelectionHeader.displayName = "SelectionHeader";
|
||||
|
||||
export default SelectionHeader;
|
||||
@@ -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}
|
||||
|
||||
@@ -1,553 +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, { createRef } from "react";
|
||||
import {
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import { ToastEvent, presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE, ph, pv } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
|
||||
let refs = [];
|
||||
export class AddNotebookSheet extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
refs = [];
|
||||
this.state = {
|
||||
notebook: props.notebook,
|
||||
topics:
|
||||
props.notebook?.topics?.map((item) => {
|
||||
return item.title;
|
||||
}) || [],
|
||||
description: null,
|
||||
titleFocused: false,
|
||||
descFocused: false,
|
||||
count: 0,
|
||||
topicInputFocused: false,
|
||||
editTopic: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
this.title = props.notebook?.title;
|
||||
this.description = props.notebook?.description;
|
||||
this.listRef;
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentSelectedInput = null;
|
||||
this.id = props.notebook?.id;
|
||||
this.backPressCount = 0;
|
||||
this.currentInputValue = null;
|
||||
this.titleRef;
|
||||
this.descriptionRef;
|
||||
this.topicsToDelete = [];
|
||||
this.hiddenInput = createRef();
|
||||
this.topicInputRef = createRef();
|
||||
this.addingTopic = false;
|
||||
this.actionSheetRef = props.actionSheetRef;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
refs = [];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
sleep(300).then(() => {
|
||||
!this.state.notebook && this.titleRef?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
refs = [];
|
||||
this.props.close(true);
|
||||
};
|
||||
|
||||
onDelete = (index) => {
|
||||
let { topics } = this.state;
|
||||
let prevTopics = topics;
|
||||
refs = [];
|
||||
prevTopics.splice(index, 1);
|
||||
let edit = this.state.notebook;
|
||||
if (edit && edit.id) {
|
||||
let topicToDelete = edit.topics[index];
|
||||
|
||||
if (topicToDelete) {
|
||||
this.topicsToDelete.push(topicToDelete.id);
|
||||
}
|
||||
}
|
||||
let nextTopics = [...prevTopics];
|
||||
if (this.prevIndex === index) {
|
||||
this.prevIndex = null;
|
||||
this.prevItem = null;
|
||||
this.currentInputValue = null;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: null
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
topics: nextTopics
|
||||
});
|
||||
};
|
||||
|
||||
addNewNotebook = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
let { topics, notebook } = this.state;
|
||||
|
||||
if (!this.title || this.title?.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
let toEdit = null;
|
||||
if (notebook) {
|
||||
toEdit = db.notebooks.notebook(notebook.id).data;
|
||||
}
|
||||
|
||||
let prevTopics = [...topics];
|
||||
|
||||
if (this.currentInputValue && this.currentInputValue.trim().length !== 0) {
|
||||
if (this.prevItem != null) {
|
||||
prevTopics[this.prevIndex] = this.currentInputValue;
|
||||
} else {
|
||||
prevTopics.push(this.currentInputValue);
|
||||
this.currentInputValue = null;
|
||||
}
|
||||
}
|
||||
let newNotebookId = null;
|
||||
if (notebook) {
|
||||
if (this.topicsToDelete?.length > 0) {
|
||||
await db.notebooks
|
||||
.notebook(toEdit.id)
|
||||
.topics.delete(...this.topicsToDelete);
|
||||
toEdit = db.notebooks.notebook(toEdit.id).data;
|
||||
}
|
||||
|
||||
await db.notebooks.add({
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
id: notebook.id
|
||||
});
|
||||
|
||||
let nextTopics = toEdit.topics.map((topic, index) => {
|
||||
let copy = { ...topic };
|
||||
copy.title = prevTopics[index];
|
||||
return copy;
|
||||
});
|
||||
|
||||
prevTopics.forEach((title, index) => {
|
||||
if (!nextTopics[index]) {
|
||||
nextTopics.push(title);
|
||||
}
|
||||
});
|
||||
|
||||
await db.notebooks.notebook(toEdit.id).topics.add(...nextTopics);
|
||||
this.close();
|
||||
} else {
|
||||
newNotebookId = await db.notebooks.add({
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
topics: prevTopics,
|
||||
id: null
|
||||
});
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
MoveNotes.present(db.notebooks.notebook(newNotebookId).data);
|
||||
};
|
||||
|
||||
onSubmit = (forward = true) => {
|
||||
this.hiddenInput.current?.focus();
|
||||
let willFocus = true;
|
||||
let { topics } = this.state;
|
||||
if (!this.currentInputValue || this.currentInputValue?.trim().length === 0)
|
||||
return;
|
||||
|
||||
let prevTopics = [...topics];
|
||||
if (this.prevItem === null) {
|
||||
prevTopics.push(this.currentInputValue);
|
||||
this.setState({
|
||||
topics: prevTopics
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
}, 30);
|
||||
this.currentInputValue = null;
|
||||
} else {
|
||||
prevTopics[this.prevIndex] = this.currentInputValue;
|
||||
this.setState({
|
||||
topics: prevTopics
|
||||
});
|
||||
this.currentInputValue = null;
|
||||
|
||||
if (this.state.editTopic) {
|
||||
this.topicInputRef.current?.blur();
|
||||
Keyboard.dismiss();
|
||||
this.setState({
|
||||
editTopic: false
|
||||
});
|
||||
willFocus = false;
|
||||
}
|
||||
this.prevItem = null;
|
||||
this.prevIndex = null;
|
||||
this.currentInputValue = null;
|
||||
|
||||
if (forward) {
|
||||
setTimeout(() => {
|
||||
this.listRef.current?.scrollToEnd?.({ animated: true });
|
||||
}, 30);
|
||||
}
|
||||
}
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: ""
|
||||
});
|
||||
willFocus && this.topicInputRef.current?.focus();
|
||||
};
|
||||
|
||||
renderTopicItem = ({ item, index }) => (
|
||||
<TopicItem
|
||||
item={item}
|
||||
onPress={(item, index) => {
|
||||
this.prevIndex = index;
|
||||
this.prevItem = item;
|
||||
this.topicInputRef.current?.setNativeProps({
|
||||
text: item
|
||||
});
|
||||
this.topicInputRef.current?.focus();
|
||||
this.currentInputValue = item;
|
||||
this.setState({
|
||||
editTopic: true
|
||||
});
|
||||
}}
|
||||
onDelete={this.onDelete}
|
||||
index={index}
|
||||
colors={this.props.colors}
|
||||
/>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const { topics, topicInputFocused, notebook } = this.state;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
ref={this.hiddenInput}
|
||||
style={{
|
||||
width: 1,
|
||||
height: 1,
|
||||
opacity: 0,
|
||||
position: "absolute"
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook && notebook.dateCreated
|
||||
? "Edit Notebook"
|
||||
: "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title="Save"
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={this.addNewNotebook}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.titleRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
this.title = value;
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
this.descriptionRef.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={(ref) => (this.descriptionRef = ref)}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
this.description = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
onSubmit={() => {
|
||||
this.topicInputRef.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : null}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.topicInputRef}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.topic}
|
||||
onChangeText={(value) => {
|
||||
this.currentInputValue = value;
|
||||
if (this.prevItem !== null) {
|
||||
refs[this.prevIndex].setNativeProps({
|
||||
text: value,
|
||||
style: {
|
||||
borderBottomColor: colors.primary.accent
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
onSubmit={() => {
|
||||
this.onSubmit();
|
||||
}}
|
||||
blurOnSubmit={false}
|
||||
button={{
|
||||
testID: "topic-add-button",
|
||||
icon: this.state.editTopic ? "check" : "plus",
|
||||
onPress: this.onSubmit,
|
||||
color: topicInputFocused
|
||||
? colors.primary.accent
|
||||
: colors.secondary.icon
|
||||
}}
|
||||
placeholder="Add a topic"
|
||||
/>
|
||||
|
||||
<FlatList
|
||||
data={topics}
|
||||
ref={(ref) => (this.listRef = ref)}
|
||||
nestedScrollEnabled
|
||||
keyExtractor={(item, index) => item + index.toString()}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="interactive"
|
||||
ListFooterComponent={
|
||||
topics.length === 0 ? null : <View style={{ height: 50 }} />
|
||||
}
|
||||
renderItem={this.renderTopicItem}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNotebookSheet.present = (notebook) => {
|
||||
presentSheet({
|
||||
component: (ref, close, _update, colors) => (
|
||||
<AddNotebookSheet
|
||||
actionSheetRef={ref}
|
||||
notebook={notebook}
|
||||
close={close}
|
||||
colors={colors}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
const TopicItem = ({ item, index, colors, onPress, onDelete }) => {
|
||||
const topicRef = (ref) => (refs[index] = ref);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginVertical: 5
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "80%",
|
||||
backgroundColor: "transparent",
|
||||
zIndex: 10,
|
||||
position: "absolute",
|
||||
height: 30
|
||||
}}
|
||||
onPress={() => {
|
||||
onPress(item, index);
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
ref={topicRef}
|
||||
editable={false}
|
||||
style={[
|
||||
styles.topicInput,
|
||||
{
|
||||
color: colors.primary.paragraph
|
||||
}
|
||||
]}
|
||||
defaultValue={item}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 80,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
onPress(item, index);
|
||||
}}
|
||||
name="pencil"
|
||||
size={SIZE.lg - 5}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
onDelete(index);
|
||||
}}
|
||||
name="minus"
|
||||
size={SIZE.lg}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrapper: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: "rgba(0,0,0,0.3)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
overlay: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
},
|
||||
headingContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
},
|
||||
headingText: {
|
||||
marginLeft: 5,
|
||||
fontSize: SIZE.xl
|
||||
},
|
||||
input: {
|
||||
paddingRight: 12,
|
||||
paddingHorizontal: 0,
|
||||
borderRadius: 0,
|
||||
minHeight: 45,
|
||||
fontSize: SIZE.md,
|
||||
padding: pv - 2,
|
||||
borderBottomWidth: 1,
|
||||
marginTop: 10,
|
||||
marginBottom: 5
|
||||
},
|
||||
addBtn: {
|
||||
width: "12%",
|
||||
minHeight: 45,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
right: 0
|
||||
},
|
||||
buttonContainer: {
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
},
|
||||
|
||||
topicContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: 10
|
||||
},
|
||||
topicInput: {
|
||||
padding: pv - 5,
|
||||
fontSize: SIZE.sm,
|
||||
//fontFamily: "sans-serif",
|
||||
paddingHorizontal: ph,
|
||||
paddingRight: 40,
|
||||
paddingVertical: 10,
|
||||
width: "100%",
|
||||
maxWidth: "100%"
|
||||
},
|
||||
topicBtn: {
|
||||
borderRadius: 5,
|
||||
width: 40,
|
||||
height: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
right: 0
|
||||
}
|
||||
});
|
||||
189
apps/mobile/app/components/sheets/add-notebook/index.tsx
Normal file
189
apps/mobile/app/components/sheets/add-notebook/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
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 { Notebook } from "@notesnook/core";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TextInput, View } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { useMenuStore } from "../../../stores/use-menu-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { MoveNotes } from "../move-notes/movenote";
|
||||
import { eOnNotebookUpdated } from "../../../utils/events";
|
||||
import { getParentNotebookId } from "../../../utils/notebooks";
|
||||
|
||||
export const AddNotebookSheet = ({
|
||||
notebook,
|
||||
parentNotebook,
|
||||
close
|
||||
}: {
|
||||
notebook?: Notebook;
|
||||
parentNotebook?: Notebook;
|
||||
close: ((context?: string | undefined) => void) | undefined;
|
||||
}) => {
|
||||
const title = useRef(notebook?.title);
|
||||
const description = useRef(notebook?.description);
|
||||
const titleInput = useRef<TextInput>(null);
|
||||
const descriptionInput = useRef<TextInput>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSaveChanges = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
if (!title.current || title?.current.trim().length === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
title: title.current,
|
||||
description: description.current,
|
||||
id: notebook?.id
|
||||
});
|
||||
|
||||
if (parentNotebook) {
|
||||
await db.relations.add(parentNotebook, {
|
||||
type: "notebook",
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
useMenuStore.getState().setMenuPins();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
|
||||
const parent =
|
||||
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
|
||||
|
||||
eSendEvent(eOnNotebookUpdated, parent);
|
||||
if (notebook) {
|
||||
setImmediate(() => {
|
||||
console.log(parent, notebook.id);
|
||||
eSendEvent(eOnNotebookUpdated, notebook.id);
|
||||
});
|
||||
}
|
||||
|
||||
if (!notebook) {
|
||||
MoveNotes.present(await db.notebooks.notebook(id));
|
||||
} else {
|
||||
close?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: DDS.isTab ? "90%" : "97%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook ? "Edit Notebook" : "New Notebook"}
|
||||
</Heading>
|
||||
<Button
|
||||
title={notebook ? "Save" : "Add"}
|
||||
type="accent"
|
||||
height={40}
|
||||
style={{
|
||||
paddingHorizontal: 24
|
||||
}}
|
||||
onPress={onSaveChanges}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
fwdRef={titleInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.title}
|
||||
onChangeText={(value) => {
|
||||
title.current = value;
|
||||
}}
|
||||
onLayout={() => {
|
||||
setImmediate(() => {
|
||||
titleInput?.current?.focus();
|
||||
});
|
||||
}}
|
||||
placeholder="Enter a title"
|
||||
onSubmit={() => {
|
||||
descriptionInput.current?.focus();
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.title : ""}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={descriptionInput}
|
||||
testID={notesnook.ids.dialogs.notebook.inputs.description}
|
||||
onChangeText={(value) => {
|
||||
description.current = value;
|
||||
}}
|
||||
placeholder="Describe your notebook."
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : ""}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AddNotebookSheet.present = (
|
||||
notebook?: Notebook,
|
||||
parentNotebook?: Notebook,
|
||||
context?: string
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close) => (
|
||||
<AddNotebookSheet
|
||||
notebook={notebook}
|
||||
parentNotebook={parentNotebook}
|
||||
close={close}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { ListHeaderInputItem } from "./list-header-item.js";
|
||||
|
||||
export const FilteredList = ({
|
||||
data,
|
||||
itemType,
|
||||
onAddItem,
|
||||
hasHeaderSearch,
|
||||
listRef,
|
||||
...restProps
|
||||
}) => {
|
||||
const [filtered, setFiltered] = useState(data);
|
||||
const query = useRef();
|
||||
const onChangeText = useCallback(
|
||||
(value) => {
|
||||
query.current = value;
|
||||
try {
|
||||
if (!value) return setFiltered(data);
|
||||
const results = db.lookup[itemType + "s"]([...data], value);
|
||||
setFiltered(results);
|
||||
} catch (e) {
|
||||
console.warn(e.message);
|
||||
}
|
||||
},
|
||||
[data, itemType]
|
||||
);
|
||||
const onSubmit = async (value) => {
|
||||
return await onAddItem(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onChangeText(query.current);
|
||||
}, [data, onChangeText]);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
{...restProps}
|
||||
data={filtered}
|
||||
ref={listRef}
|
||||
ListHeaderComponent={
|
||||
hasHeaderSearch ? (
|
||||
<ListHeaderInputItem
|
||||
onSubmit={onSubmit}
|
||||
onChangeText={onChangeText}
|
||||
itemType={itemType}
|
||||
testID={"list-input" + itemType}
|
||||
placeholder={`Search or add a new ${itemType}`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,486 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../../common/database";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet,
|
||||
ToastEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SearchService from "../../../services/search";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { eOnTopicSheetUpdate } from "../../../utils/events";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { SelectionProvider } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { ListItem } from "./list-item";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
|
||||
const MoveNoteSheet = ({ note, actionSheetRef }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const notebooks = useNotebookStore((state) =>
|
||||
state.notebooks.filter((n) => n?.type === "notebook")
|
||||
);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
|
||||
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const onAddNotebook = async (title) => {
|
||||
if (!title || title.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Notebook title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.add({
|
||||
title: title,
|
||||
description: null,
|
||||
topics: [],
|
||||
id: null
|
||||
});
|
||||
setNotebooks();
|
||||
return true;
|
||||
};
|
||||
|
||||
const openAddTopicDialog = (item) => {
|
||||
presentDialog({
|
||||
context: "move_note",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter title",
|
||||
title: "New topic",
|
||||
paragraph: "Add a new topic in " + item.title,
|
||||
positiveText: "Add",
|
||||
positivePress: (value) => {
|
||||
return onAddTopic(value, item);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onAddTopic = useCallback(
|
||||
async (value, item) => {
|
||||
if (!value || value.trim().length === 0) {
|
||||
ToastEvent.show({
|
||||
heading: "Topic title is required",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.notebooks.notebook(item.id).topics.add(value);
|
||||
setNotebooks();
|
||||
return true;
|
||||
},
|
||||
[setNotebooks]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const getSelectedNotesCountInItem = React.useCallback(
|
||||
(item) => {
|
||||
switch (item.type) {
|
||||
case "notebook": {
|
||||
const notes = db.relations.from(item, "note");
|
||||
if (notes.length === 0) return 0;
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
notes.findIndex((note) => note.id === item.id) > -1
|
||||
? count++
|
||||
: undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
case "topic": {
|
||||
const noteIds = db.notes?.topicReferences.get(item.id);
|
||||
let count = 0;
|
||||
selectedItemsList.forEach((item) =>
|
||||
noteIds.indexOf(item.id) > -1 ? count++ : undefined
|
||||
);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedItemsList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resetItemState();
|
||||
return () => {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
useItemSelectionStore.getState().setItemState({});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const resetItemState = useCallback(
|
||||
(state) => {
|
||||
const itemState = {};
|
||||
const notebooks = db.notebooks.all;
|
||||
let count = 0;
|
||||
for (let notebook of notebooks) {
|
||||
itemState[notebook.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[notebook.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(notebook);
|
||||
} else {
|
||||
contextValue.deselect(notebook);
|
||||
}
|
||||
for (let topic of notebook.topics) {
|
||||
itemState[topic.id] = state
|
||||
? state
|
||||
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
|
||||
getSelectedNotesCountInItem(topic, selectedItemsList)
|
||||
? "selected"
|
||||
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
|
||||
? "intermediate"
|
||||
: "deselected";
|
||||
if (itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
contextValue.select(topic);
|
||||
} else {
|
||||
contextValue.deselect(topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
} else {
|
||||
useItemSelectionStore.getState().setMultiSelect(false);
|
||||
}
|
||||
useItemSelectionStore.getState().setItemState(itemState);
|
||||
},
|
||||
[contextValue, getSelectedNotesCountInItem, selectedItemsList]
|
||||
);
|
||||
|
||||
const getItemsForItem = (item) => {
|
||||
switch (item.type) {
|
||||
case "notebook":
|
||||
return item.topics?.filter((t) => t.type === "topic");
|
||||
}
|
||||
};
|
||||
|
||||
function areAllSelectedItemsInNotebook(notebook, items) {
|
||||
const notes = db.relations.from(notebook, "note");
|
||||
if (notes.length === 0) return false;
|
||||
return items.every((item) => {
|
||||
return notes.find((note) => note.id === item.id);
|
||||
});
|
||||
}
|
||||
|
||||
function areAllSelectedItemsInTopic(topic, items) {
|
||||
return items.every((item) => {
|
||||
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
|
||||
});
|
||||
}
|
||||
|
||||
const updateItemState = useCallback(function (item, state) {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
const mergeState = {
|
||||
[item.id]: state
|
||||
};
|
||||
useItemSelectionStore.getState().setItemState({
|
||||
...itemState,
|
||||
...mergeState
|
||||
});
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
toggleSelection: (item) => {
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
if (itemState[item.id] === "selected") {
|
||||
updateItemState(item, "deselected");
|
||||
} else {
|
||||
updateItemState(item, "selected");
|
||||
}
|
||||
},
|
||||
deselect: (item) => {
|
||||
updateItemState(item, "deselected");
|
||||
},
|
||||
select: (item) => {
|
||||
updateItemState(item, "selected");
|
||||
},
|
||||
deselectAll: (state) => {
|
||||
resetItemState(state);
|
||||
}
|
||||
}),
|
||||
[resetItemState, updateItemState]
|
||||
);
|
||||
|
||||
const getItemFromId = (id) => {
|
||||
for (const nb of notebooks) {
|
||||
if (nb.id === id) return nb;
|
||||
for (const tp of nb.topics) {
|
||||
if (tp.id === id) return tp;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
|
||||
const itemState = useItemSelectionStore.getState().itemState;
|
||||
for (const id in itemState) {
|
||||
const item = getItemFromId(id);
|
||||
if (itemState[id] === "selected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.add(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.addToNotebook(
|
||||
{
|
||||
topic: item.id,
|
||||
id: item.notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
} else if (itemState[id] === "deselected") {
|
||||
if (item.type === "notebook") {
|
||||
for (let noteId of noteIds) {
|
||||
await db.relations.unlink(item, { id: noteId, type: "note" });
|
||||
}
|
||||
} else {
|
||||
await db.notes.removeFromNotebook(
|
||||
{
|
||||
id: item.notebookId,
|
||||
topic: item.id,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setNotebooks();
|
||||
eSendEvent(eOnTopicSheetUpdate);
|
||||
SearchService.updateAndSearch();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog context="move_note" />
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
}}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
style={{
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select topics you want to add note(s) to."
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Reset selection"
|
||||
height={30}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
resetItemState();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<SelectionProvider value={contextValue}>
|
||||
<FilteredList
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
maxHeight: dimensions.height * 0.85
|
||||
}}
|
||||
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>
|
||||
)
|
||||
}
|
||||
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();
|
||||
}}
|
||||
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 }} />}
|
||||
/>
|
||||
</SelectionProvider>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
257
apps/mobile/app/components/sheets/add-to/index.tsx
Normal file
257
apps/mobile/app/components/sheets/add-to/index.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
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 { GroupHeader, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useCallback, useEffect } from "react";
|
||||
import { Keyboard, TouchableOpacity, View } from "react-native";
|
||||
import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
|
||||
import { db } from "../../../common/database";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { ItemSelection } from "../../../stores/item-selection-store";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { useRelationStore } from "../../../stores/use-relation-store";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { updateNotebook } from "../../../utils/notebooks";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import SheetProvider from "../../sheet-provider";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { NotebookItem } from "./notebook-item";
|
||||
import { useNotebookItemSelectionStore } from "./store";
|
||||
|
||||
async function updateInitialSelectionState(items: string[]) {
|
||||
const relations = await db.relations
|
||||
.to(
|
||||
{
|
||||
type: "note",
|
||||
ids: items
|
||||
},
|
||||
"notebook"
|
||||
)
|
||||
.get();
|
||||
|
||||
const initialSelectionState: ItemSelection = {};
|
||||
const notebookIds = [
|
||||
...new Set(relations.map((relation) => relation.fromId))
|
||||
];
|
||||
|
||||
for (const id of notebookIds) {
|
||||
const all = items.every((noteId) => {
|
||||
return (
|
||||
relations.findIndex(
|
||||
(relation) => relation.fromId === id && relation.toId === noteId
|
||||
) > -1
|
||||
);
|
||||
});
|
||||
if (all) {
|
||||
initialSelectionState[id] = "selected";
|
||||
} else {
|
||||
initialSelectionState[id] = "intermediate";
|
||||
}
|
||||
}
|
||||
useNotebookItemSelectionStore.setState({
|
||||
initialState: initialSelectionState,
|
||||
selection: { ...initialSelectionState },
|
||||
multiSelect: relations.length > 1
|
||||
});
|
||||
}
|
||||
|
||||
const MoveNoteSheet = ({
|
||||
note,
|
||||
actionSheetRef
|
||||
}: {
|
||||
note: Note | undefined;
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const notebooks = useNotebookStore((state) => state.notebooks);
|
||||
const dimensions = useSettingStore((state) => state.dimensions);
|
||||
const selectedItemsList = useSelectionStore(
|
||||
(state) => state.selectedItemsList
|
||||
);
|
||||
|
||||
const multiSelect = useNotebookItemSelectionStore(
|
||||
(state) => state.multiSelect
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const items = note ? [note.id] : selectedItemsList;
|
||||
updateInitialSelectionState(items);
|
||||
return () => {
|
||||
useNotebookItemSelectionStore.setState({
|
||||
initialState: {},
|
||||
selection: {},
|
||||
multiSelect: false,
|
||||
canEnableMultiSelectMode: true
|
||||
});
|
||||
};
|
||||
}, [note, selectedItemsList]);
|
||||
|
||||
const onSave = async () => {
|
||||
const noteIds = note ? [note.id] : selectedItemsList;
|
||||
|
||||
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
|
||||
|
||||
for (const id in changedNotebooks) {
|
||||
const item = await db.notebooks.notebook(id);
|
||||
if (!item) continue;
|
||||
if (changedNotebooks[id] === "selected") {
|
||||
for (const id of noteIds) {
|
||||
await db.relations.add(item, { id: id, type: "note" });
|
||||
updateNotebook(item.id);
|
||||
}
|
||||
} else if (changedNotebooks[id] === "deselected") {
|
||||
for (const id of noteIds) {
|
||||
await db.relations.unlink(item, { id: id, type: "note" });
|
||||
updateNotebook(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
actionSheetRef.current?.hide();
|
||||
};
|
||||
|
||||
const renderNotebook = useCallback(
|
||||
({ item, index }: { item: string | GroupHeader; index: number }) =>
|
||||
(item as GroupHeader).type === "header" ? null : (
|
||||
<NotebookItem items={notebooks} id={item as string} index={index} />
|
||||
),
|
||||
[notebooks]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog context="move_note" />
|
||||
<SheetProvider context="link-notebooks" />
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute"
|
||||
}}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
style={{
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select topics you want to add note(s) to."
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
columnGap: 10
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="restore"
|
||||
type="grayAccent"
|
||||
onPress={() => {
|
||||
const items = note ? [note.id] : selectedItemsList;
|
||||
updateInitialSelectionState(items);
|
||||
}}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
height={40}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 0,
|
||||
maxHeight: dimensions.height * 0.85,
|
||||
height: dimensions.height * 0.85,
|
||||
paddingTop: 6
|
||||
}}
|
||||
>
|
||||
<FlashList
|
||||
data={notebooks?.ids?.filter((id) => typeof id === "string")}
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
estimatedItemSize={50}
|
||||
keyExtractor={(item) => item as string}
|
||||
renderItem={renderNotebook}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={<View style={{ height: 50 }} />}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MoveNoteSheet.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
|
||||
enableGesturesInScrollView: false,
|
||||
noBottomPadding: true
|
||||
});
|
||||
};
|
||||
export default MoveNoteSheet;
|
||||
@@ -1,88 +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, { useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Input from "../../ui/input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
|
||||
export const ListHeaderInputItem = ({
|
||||
onSubmit,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
testID
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
|
||||
const [inputValue, setInputValue] = useState();
|
||||
const inputRef = useRef();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 10,
|
||||
marginBottom: 5
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
onChangeText={(value) => {
|
||||
setInputValue(value);
|
||||
onChangeText?.(value);
|
||||
}}
|
||||
testID={testID}
|
||||
blurOnSubmit={false}
|
||||
onFocusInput={() => {
|
||||
setFocused(true);
|
||||
}}
|
||||
onBlurInput={() => {
|
||||
setFocused(false);
|
||||
}}
|
||||
button={{
|
||||
icon: inputValue ? "plus" : "magnify",
|
||||
color: focused ? colors.selected.icon : colors.secondary.icon,
|
||||
onPress: async () => {
|
||||
const result = await onSubmit(inputValue);
|
||||
if (result) {
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
marginBottom={5}
|
||||
/>
|
||||
{inputValue ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.shade,
|
||||
padding: 5,
|
||||
borderRadius: 5,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.primary.accent}>
|
||||
Tap on + to add {`"${inputValue}"`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,292 +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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../../common/database";
|
||||
import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useSelectionContext } from "./context";
|
||||
import { FilteredList } from "./filtered-list";
|
||||
import { useItemSelectionStore } from "./store";
|
||||
|
||||
const SelectionIndicator = ({
|
||||
item,
|
||||
hasNotes,
|
||||
selectItem,
|
||||
onPress,
|
||||
onChange
|
||||
}) => {
|
||||
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
|
||||
const isSelected = itemState === "selected";
|
||||
const isIntermediate = itemState === "intermediate";
|
||||
const isRemoved = !isSelected && hasNotes;
|
||||
const { colors } = useThemeColors("sheet");
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemState]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size={22}
|
||||
customStyle={{
|
||||
marginRight: 5,
|
||||
width: 23,
|
||||
height: 23
|
||||
}}
|
||||
color={
|
||||
isRemoved
|
||||
? colors.static.red
|
||||
: isIntermediate || isSelected
|
||||
? colors.selected.icon
|
||||
: colors.primary.icon
|
||||
}
|
||||
onPress={() => {
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
testID={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
name={
|
||||
isRemoved
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: isIntermediate
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ListItem = ({
|
||||
item,
|
||||
index,
|
||||
icon,
|
||||
infoText,
|
||||
hasSubList,
|
||||
onPress,
|
||||
onScrollEnd,
|
||||
getListItems,
|
||||
style,
|
||||
type,
|
||||
sublistItemType,
|
||||
onAddItem,
|
||||
getSublistItemProps,
|
||||
hasHeaderSearch,
|
||||
onAddSublistItem,
|
||||
hasNotes,
|
||||
onChange,
|
||||
sheetRef
|
||||
}) => {
|
||||
const { toggleSelection } = useSelectionContext();
|
||||
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
|
||||
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
function selectItem() {
|
||||
toggleSelection(item);
|
||||
}
|
||||
|
||||
const getSelectedNotesCountInNotebookTopics = (item) => {
|
||||
if (item.type === "topic") return;
|
||||
|
||||
let count = 0;
|
||||
const noteIds = [];
|
||||
for (let topic of item.topics) {
|
||||
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
|
||||
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
useSelectionStore.getState().selectedItemsList.forEach((item) => {
|
||||
if (noteIds.indexOf(item.id) > -1) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
}, [item]);
|
||||
|
||||
const onChangeSubItem = () => {
|
||||
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
marginBottom: 10,
|
||||
...style
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
onPress={() => {
|
||||
if (hasSubList) return setExpanded(!expanded);
|
||||
if (multiSelect) return selectItem();
|
||||
onPress?.(item);
|
||||
}}
|
||||
type={type}
|
||||
onLongPress={() => {
|
||||
useItemSelectionStore.getState().setMultiSelect(true);
|
||||
selectItem();
|
||||
}}
|
||||
customStyle={{
|
||||
height: style?.height || 50,
|
||||
width: "100%",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<SelectionIndicator
|
||||
hasNotes={hasNotes}
|
||||
onPress={onPress}
|
||||
item={item}
|
||||
onChange={onChange}
|
||||
selectItem={selectItem}
|
||||
/>
|
||||
<View>
|
||||
{hasSubList && expanded ? (
|
||||
<Heading size={SIZE.md}>{item.title}</Heading>
|
||||
) : (
|
||||
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
|
||||
)}
|
||||
|
||||
{infoText ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
|
||||
{infoText}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{showSelectedIndicator ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.accent,
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 100,
|
||||
marginRight: 12
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{onAddSublistItem ? (
|
||||
<IconButton
|
||||
name={"plus"}
|
||||
testID="add-item-icon"
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.xl}
|
||||
onPress={() => {
|
||||
onAddSublistItem(item);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{icon ? (
|
||||
<IconButton
|
||||
name={icon(expanded).name}
|
||||
color={icon(expanded).color}
|
||||
size={icon(expanded).size || SIZE.xl}
|
||||
onPress={
|
||||
hasSubList
|
||||
? () => setExpanded(!expanded)
|
||||
: icon(expanded).onPress
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{expanded && hasSubList ? (
|
||||
<FilteredList
|
||||
nestedScrollEnabled
|
||||
data={getListItems(item)}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
onMomentumScrollEnd={onScrollEnd}
|
||||
style={{
|
||||
width: "95%",
|
||||
alignSelf: "flex-end",
|
||||
maxHeight: 250
|
||||
}}
|
||||
itemType={sublistItemType}
|
||||
hasHeaderSearch={hasHeaderSearch}
|
||||
renderItem={({ item, index }) => (
|
||||
<ListItem
|
||||
item={item}
|
||||
{...getSublistItemProps(item)}
|
||||
index={index}
|
||||
onChange={onChangeSubItem}
|
||||
onScrollEnd={onScrollEnd}
|
||||
/>
|
||||
)}
|
||||
onAddItem={onAddItem}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
247
apps/mobile/app/components/sheets/add-to/notebook-item.tsx
Normal file
247
apps/mobile/app/components/sheets/add-to/notebook-item.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useMemo } from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { useTotalNotes } from "../../../hooks/use-db-item";
|
||||
import { useNotebook } from "../../../hooks/use-notebook";
|
||||
import useNavigationStore from "../../../stores/use-navigation-store";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { PressableButton } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { AddNotebookSheet } from "../add-notebook";
|
||||
import {
|
||||
useNotebookExpandedStore,
|
||||
useNotebookItemSelectionStore
|
||||
} from "./store";
|
||||
|
||||
type NotebookParentProp = {
|
||||
parent?: NotebookParentProp;
|
||||
item?: Notebook;
|
||||
};
|
||||
|
||||
export const NotebookItem = ({
|
||||
id,
|
||||
currentLevel = 0,
|
||||
index,
|
||||
parent,
|
||||
items
|
||||
}: {
|
||||
id: string;
|
||||
currentLevel?: number;
|
||||
index: number;
|
||||
parent?: NotebookParentProp;
|
||||
items?: VirtualizedGrouping<Notebook>;
|
||||
}) => {
|
||||
const { nestedNotebooks, notebook: item } = useNotebook(id, items);
|
||||
const ids = useMemo(() => (id ? [id] : []), [id]);
|
||||
const { totalNotes: totalNotes } = useTotalNotes(ids, "notebook");
|
||||
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
|
||||
const { colors } = useThemeColors("sheet");
|
||||
const selection = useNotebookItemSelectionStore((state) =>
|
||||
id ? state.selection[id] : undefined
|
||||
);
|
||||
const isSelected = selection === "selected";
|
||||
const isFocused = focusedRouteId === id;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
|
||||
|
||||
const onPress = () => {
|
||||
if (!item) return;
|
||||
const state = useNotebookItemSelectionStore.getState();
|
||||
|
||||
if (isSelected) {
|
||||
state.markAs(item, !state.initialState[id] ? undefined : "deselected");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.multiSelect) {
|
||||
const keys = Object.keys(state.selection);
|
||||
const nextState: any = {};
|
||||
for (const key in keys) {
|
||||
nextState[key] = !state.initialState[key] ? undefined : "deselected";
|
||||
}
|
||||
console.log("Single item selection");
|
||||
state.setSelection({
|
||||
[item.id]: "selected",
|
||||
...nextState
|
||||
});
|
||||
} else {
|
||||
console.log("Multi item selection");
|
||||
state.markAs(item, "selected");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<PressableButton
|
||||
type={"transparent"}
|
||||
onLongPress={() => {
|
||||
if (!item) return;
|
||||
useNotebookItemSelectionStore.setState({
|
||||
multiSelect: true
|
||||
});
|
||||
const state = useNotebookItemSelectionStore.getState();
|
||||
useNotebookItemSelectionStore
|
||||
.getState()
|
||||
.markAs(
|
||||
item,
|
||||
!isSelected
|
||||
? "selected"
|
||||
: !state.initialState[id]
|
||||
? undefined
|
||||
: "deselected"
|
||||
);
|
||||
}}
|
||||
testID={`add-to-notebook-item-${currentLevel}-${index}`}
|
||||
onPress={onPress}
|
||||
customStyle={{
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
borderRadius: 0,
|
||||
height: 45
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{nestedNotebooks?.ids.length ? (
|
||||
<IconButton
|
||||
size={SIZE.xl}
|
||||
color={isSelected ? colors.selected.icon : colors.primary.icon}
|
||||
onPress={() => {
|
||||
useNotebookExpandedStore.getState().setExpanded(id);
|
||||
}}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
customStyle={{
|
||||
width: 35,
|
||||
height: 35
|
||||
}}
|
||||
name={expanded ? "chevron-down" : "chevron-right"}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
size={SIZE.xl}
|
||||
color={
|
||||
isSelected
|
||||
? colors.selected.icon
|
||||
: selection === "deselected"
|
||||
? colors.error.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
onPress={onPress}
|
||||
top={0}
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
customStyle={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
name={
|
||||
selection === "deselected"
|
||||
? "close-circle-outline"
|
||||
: isSelected
|
||||
? "check-circle-outline"
|
||||
: selection === "intermediate"
|
||||
? "minus-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={
|
||||
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
|
||||
}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{item?.title}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
columnGap: 10,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{totalNotes?.(id) ? (
|
||||
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
|
||||
{totalNotes(id)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<IconButton
|
||||
name="plus"
|
||||
customStyle={{
|
||||
width: 40 * fontScale,
|
||||
height: 40 * fontScale
|
||||
}}
|
||||
testID={notesnook.ids.notebook.menu}
|
||||
onPress={() => {
|
||||
if (!item) return;
|
||||
AddNotebookSheet.present(undefined, item, "link-notebooks");
|
||||
}}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
top={0}
|
||||
color={colors.primary.icon}
|
||||
size={SIZE.xl}
|
||||
/>
|
||||
</View>
|
||||
</PressableButton>
|
||||
|
||||
{!expanded
|
||||
? null
|
||||
: nestedNotebooks?.ids.map((id, index) => (
|
||||
<NotebookItem
|
||||
key={id as string}
|
||||
id={id as string}
|
||||
index={index}
|
||||
currentLevel={currentLevel + 1}
|
||||
items={nestedNotebooks}
|
||||
parent={{
|
||||
parent: parent,
|
||||
item: item
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -16,27 +16,24 @@ 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 create, { State } from "zustand";
|
||||
import create from "zustand";
|
||||
import { createItemSelectionStore } from "../../../stores/item-selection-store";
|
||||
|
||||
type SelectionItemState = Record<
|
||||
string,
|
||||
"intermediate" | "selected" | "deselected"
|
||||
>;
|
||||
|
||||
export interface SelectionStore extends State {
|
||||
itemState: SelectionItemState;
|
||||
setItemState: (state: SelectionItemState) => void;
|
||||
multiSelect: boolean;
|
||||
setMultiSelect: (multiSelect: boolean) => void;
|
||||
}
|
||||
|
||||
export const useItemSelectionStore = create<SelectionStore>((set) => ({
|
||||
itemState: {},
|
||||
setItemState: (itemState) => {
|
||||
export const useNotebookExpandedStore = create<{
|
||||
expanded: {
|
||||
[id: string]: boolean;
|
||||
};
|
||||
setExpanded: (id: string) => void;
|
||||
}>((set, get) => ({
|
||||
expanded: {},
|
||||
setExpanded(id: string) {
|
||||
set({
|
||||
itemState
|
||||
expanded: {
|
||||
...get().expanded,
|
||||
[id]: !get().expanded[id]
|
||||
}
|
||||
});
|
||||
},
|
||||
multiSelect: false,
|
||||
setMultiSelect: (multiSelect) => set({ multiSelect })
|
||||
}
|
||||
}));
|
||||
|
||||
export const useNotebookItemSelectionStore = createItemSelectionStore(true);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user