mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
125 Commits
mobile/fix
...
fix/sortin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e651a63ee | ||
|
|
3f1e9e31d4 | ||
|
|
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 | ||
|
|
e8e50bebcf | ||
|
|
fa0610e392 | ||
|
|
124489a5a5 | ||
|
|
22fcdc0fb1 | ||
|
|
1375300a91 | ||
|
|
e6f4aea4d7 | ||
|
|
d72c4057ff | ||
|
|
02d80ee4fa | ||
|
|
2da3e9e0be | ||
|
|
8333e7bdb3 | ||
|
|
cef2472aae | ||
|
|
a4946f7e89 | ||
|
|
3e95e0feae | ||
|
|
b81843cdf7 | ||
|
|
eedd50fc64 | ||
|
|
1cb00bca0d | ||
|
|
8c233570a7 | ||
|
|
8a3c8b85bb | ||
|
|
2ee8a807dc | ||
|
|
ec85f748fa | ||
|
|
051b7fcb8c | ||
|
|
9af88811d8 | ||
|
|
6c4672037c | ||
|
|
d59e0d9ee2 | ||
|
|
5a001a1457 | ||
|
|
44d6474b36 | ||
|
|
0607e005f5 | ||
|
|
308eb4b55f | ||
|
|
6abb6207c5 | ||
|
|
89189d52ae | ||
|
|
e653ce69f8 | ||
|
|
9b89b328f6 | ||
|
|
77ae71fab1 | ||
|
|
7f7c1e6b93 | ||
|
|
30341bea1c | ||
|
|
82d44ea890 | ||
|
|
5f0907168f | ||
|
|
48061962b1 | ||
|
|
24141b66cf | ||
|
|
41beee45ba | ||
|
|
cb7c70b0d6 | ||
|
|
2aa5a57557 | ||
|
|
17582bcefb | ||
|
|
1f5a5afb10 | ||
|
|
d1c724563f | ||
|
|
72aa5eccb7 | ||
|
|
95e0c4b0d6 | ||
|
|
9ec6ca0ded | ||
|
|
85b7611214 | ||
|
|
d256d435e4 | ||
|
|
577a976e01 | ||
|
|
07453834b0 | ||
|
|
59f6b5e411 | ||
|
|
ac9be8360a | ||
|
|
32d0b07f09 | ||
|
|
7b57639076 | ||
|
|
1fa174bab7 | ||
|
|
6c52b5a77d | ||
|
|
d4a6157ed3 | ||
|
|
a86358bbcd | ||
|
|
20d0e6d047 | ||
|
|
51fc4bf3e4 | ||
|
|
61d0dafd21 | ||
|
|
89398cb5e5 | ||
|
|
72ec4063de | ||
|
|
f8423eeea8 | ||
|
|
d91203990e | ||
|
|
fc84c1af51 | ||
|
|
58de07073d | ||
|
|
cafa5ff821 | ||
|
|
1859799ca3 | ||
|
|
271aa11f74 | ||
|
|
e42f1a65f5 | ||
|
|
73f3a31614 | ||
|
|
4d7792d06a | ||
|
|
1a0f17c059 | ||
|
|
89d941401a | ||
|
|
a076ee640d | ||
|
|
8c1629f7dd | ||
|
|
9a31c07ac5 | ||
|
|
11f39316b5 | ||
|
|
f157229e00 | ||
|
|
a0f25c4e6f | ||
|
|
003b3d2a84 | ||
|
|
1775e0bda1 | ||
|
|
d5038ac631 |
@@ -19,7 +19,6 @@ runs:
|
||||
packages/crypto/package-lock.json
|
||||
packages/sodium/package-lock.json
|
||||
packages/clipper/package-lock.json
|
||||
packages/crypto-worker/package-lock.json
|
||||
packages/editor-mobile/package-lock.json
|
||||
packages/editor/package-lock.json
|
||||
packages/logger/package-lock.json
|
||||
|
||||
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 }}
|
||||
|
||||
@@ -103,7 +103,7 @@ All commits must include valid scopes in the commit message. **Valid commit scop
|
||||
|
||||
**Packages:**
|
||||
|
||||
1. `crypto`: changes related to the cryptographic core (including `@notesnook/crypto-worker`)
|
||||
1. `crypto`: changes related to the cryptographic core
|
||||
2. `editor`: changes related to the editor (including `@notesnook/editor-mobile`)
|
||||
3. `logger`: changes related to the logger
|
||||
4. `theme`: changes related to the theme
|
||||
|
||||
@@ -46,7 +46,6 @@ Notesnook is built using the following technologies:
|
||||
| `@notesnook/editor-mobile` | [/packages/editor-mobile](/packages/editor-mobile) | A very thin wrapper around `@notesnook/editor` for mobile clients |
|
||||
| `@notesnook/logger` | [/packages/logger](/packages/logger) | Simple & pluggable logger |
|
||||
| `@notesnook/sodium` | [/packages/sodium](/packages/sodium) | Wrapper around libsodium to support Node.js & Browser |
|
||||
| `@notesnook/crypto-worker` | [/packages/crypto-worker](/packages/crypto-worker) | Helpers to use `@notesnook/crypto` from a Worker |
|
||||
| `@notesnook/streamable-fs` | [/packages/streamable-fs](/packages/streamable-fs) | Streaming interface around an IndexedDB based file system |
|
||||
| `@notesnook/theme` | [/packages/theme](/packages/theme) | The core theme used in web & desktop clients |
|
||||
|
||||
|
||||
108
apps/desktop/package-lock.json
generated
108
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -14,7 +14,7 @@
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.1",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
@@ -24,8 +24,8 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^26.1.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron": "25.8.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
@@ -270,13 +270,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/notarize": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-1.2.4.tgz",
|
||||
"integrity": "sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.1.0.tgz",
|
||||
"integrity": "sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"fs-extra": "^9.0.1"
|
||||
"fs-extra": "^9.0.1",
|
||||
"promise-retry": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
@@ -331,9 +332,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@electron/universal": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.3.4.tgz",
|
||||
"integrity": "sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.4.1.tgz",
|
||||
"integrity": "sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
@@ -1040,15 +1041,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/app-builder-lib": {
|
||||
"version": "24.6.3",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.3.tgz",
|
||||
"integrity": "sha512-++0Zp7vcCHfXMBGVj7luFxpqvMPk5mcWeTuw7OK0xNAaNtYQTTN0d9YfWRsb1MvviTOOhyHeULWz1CaixrdrDg==",
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.6.4.tgz",
|
||||
"integrity": "sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@develar/schema-utils": "~2.6.5",
|
||||
"@electron/notarize": "^1.2.3",
|
||||
"@electron/osx-sign": "^1.0.4",
|
||||
"@electron/universal": "1.3.4",
|
||||
"@electron/notarize": "2.1.0",
|
||||
"@electron/osx-sign": "1.0.5",
|
||||
"@electron/universal": "1.4.1",
|
||||
"@malept/flatpak-bundler": "^0.4.0",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"7zip-bin": "~5.1.1",
|
||||
@@ -1678,12 +1679,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
"version": "24.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.3.tgz",
|
||||
"integrity": "sha512-O7KNT7OKqtV54fMYUpdlyTOCP5DoPuRMLqMTgxxV2PO8Hj/so6zOl5o8GTs8pdDkeAhJzCFOUNB3BDhgXbUbJg==",
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz",
|
||||
"integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.6.3",
|
||||
"app-builder-lib": "24.6.4",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
@@ -1750,9 +1751,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-26.1.0.tgz",
|
||||
"integrity": "sha512-qEh19H09Pysn3ibms5nZ0haIh5pFoOd7/5Ww7gzmAwDQOulRi8Sa2naeueOyIb1GKpf+6L4ix3iceYRAuA5r5Q==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-25.8.0.tgz",
|
||||
"integrity": "sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron/get": "^2.0.0",
|
||||
@@ -1767,16 +1768,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-builder": {
|
||||
"version": "24.6.3",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.3.tgz",
|
||||
"integrity": "sha512-O6PqhRXwfxCNTXI4BlhELSeYYO6/tqlxRuy+4+xKBokQvwDDjDgZMMoSgAmanVSCuzjE7MZldI9XYrKFk+EQDw==",
|
||||
"version": "24.6.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz",
|
||||
"integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.6.3",
|
||||
"app-builder-lib": "24.6.4",
|
||||
"builder-util": "24.5.0",
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"chalk": "^4.1.2",
|
||||
"dmg-builder": "24.6.3",
|
||||
"dmg-builder": "24.6.4",
|
||||
"fs-extra": "^10.1.0",
|
||||
"is-ci": "^3.0.0",
|
||||
"lazy-val": "^1.0.5",
|
||||
@@ -1896,9 +1897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-updater": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.1.tgz",
|
||||
"integrity": "sha512-IBT3zJ4yO5UZMF2gOTC9HrlmG4OYSRtOiHKzNAShJvfuicdx6UaXoa6AvhcTxdx6zf/rJyFMRBISS9jhVwTfow==",
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.1.4.tgz",
|
||||
"integrity": "sha512-yYAJc6RQjjV4WtInZVn+ZcLyXRhbVXoomKEfUUwDqIk5s2wxzLhWaor7lrNgxODyODhipjg4SVPMhJHi5EnsCA==",
|
||||
"dependencies": {
|
||||
"builder-util-runtime": "9.2.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
@@ -1907,7 +1908,7 @@
|
||||
"lodash.escaperegexp": "^4.1.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"semver": "^7.3.8",
|
||||
"typed-emitter": "^2.1.0"
|
||||
"tiny-typed-emitter": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
@@ -1931,6 +1932,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/err-code": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@@ -3102,6 +3109,19 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-retry": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
|
||||
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"err-code": "^2.0.2",
|
||||
"retry": "^0.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@@ -3331,6 +3351,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -3674,9 +3703,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz",
|
||||
"integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==",
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
|
||||
"integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
@@ -3700,6 +3729,11 @@
|
||||
"fs-extra": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-typed-emitter": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.4",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -15,7 +15,7 @@
|
||||
"@trpc/client": "10.31.0",
|
||||
"@trpc/server": "10.31.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.1",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
@@ -25,8 +25,8 @@
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^26.1.0",
|
||||
"electron-builder": "^24.6.3",
|
||||
"electron": "25.8.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.5.1",
|
||||
"esbuild": "^0.17.19",
|
||||
"tree-kill": "^1.2.2",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index 3ba5d6a..cf8e147 100644
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -105,6 +105,7 @@ class LinuxTargetHelper {
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
@@ -10,7 +10,7 @@ index 3ba5d6a..cf8e147 100644
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -148,6 +149,23 @@ class LinuxTargetHelper {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
@@ -117,6 +117,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 }) => {
|
||||
|
||||
@@ -53,6 +53,13 @@ if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("uncaughtException:", error);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
console.error("unhandledRejection:", reason);
|
||||
});
|
||||
|
||||
app.commandLine.appendSwitch("lang", "en-US");
|
||||
|
||||
async function createWindow() {
|
||||
|
||||
@@ -31,7 +31,6 @@ declare global {
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
console.log("HELLO!");
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
|
||||
@@ -62,14 +62,9 @@ function registerProtocol() {
|
||||
headers: { "Content-Type": extensionToMimeType[fileExtension] }
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
return net.fetch(request, {
|
||||
bypassCustomProtocolHandlers: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return Response.error();
|
||||
return net.fetch(request, {
|
||||
bypassCustomProtocolHandlers: true
|
||||
});
|
||||
}
|
||||
});
|
||||
console.info(`${SCHEME} protocol inteception "successful"`);
|
||||
|
||||
@@ -94,6 +94,12 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
let currTheme =
|
||||
useThemeStore.getState().colorScheme === "dark"
|
||||
? SettingsService.getProperty("darkTheme")
|
||||
: SettingsService.getProperty("lightTheme");
|
||||
useThemeEngineStore.getState().setTheme(currTheme);
|
||||
|
||||
export const withTheme = (Element) => {
|
||||
return function AppWithThemeProvider() {
|
||||
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
|
||||
@@ -125,11 +131,13 @@ export const withTheme = (Element) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const nextTheme = colorScheme === "dark" ? darkTheme : lightTheme;
|
||||
if (JSON.stringify(nextTheme) !== JSON.stringify(currTheme)) {
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(colorScheme === "dark" ? darkTheme : lightTheme);
|
||||
}, [colorScheme, darkTheme, lightTheme]);
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
|
||||
return <Element />;
|
||||
};
|
||||
|
||||
@@ -17,18 +17,14 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { database } from "@notesnook/common";
|
||||
import { initalize, logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { Platform } from "react-native";
|
||||
import { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import filesystem from "../filesystem";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import Storage, { KV } from "./storage";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
import filesystem from "../filesystem";
|
||||
import "./logger";
|
||||
import Storage from "./storage";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
@@ -63,7 +59,5 @@ database.setup(
|
||||
}
|
||||
);
|
||||
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export const db = database;
|
||||
export const DatabaseLogger = dbLogger;
|
||||
|
||||
29
apps/mobile/app/common/database/logger.js
Normal file
29
apps/mobile/app/common/database/logger.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 { MMKVLoader } from "react-native-mmkv-storage";
|
||||
import { initalize } from "@notesnook/core/dist/logger";
|
||||
import { KV } from "./storage";
|
||||
|
||||
const LoggerStorage = new MMKVLoader()
|
||||
.withInstanceID("notesnook_logs")
|
||||
.initialize();
|
||||
|
||||
initalize(new KV(LoggerStorage));
|
||||
|
||||
export {};
|
||||
@@ -26,4 +26,5 @@ export const MMKV = new MMKVLoader()
|
||||
? ProcessingModes.MULTI_PROCESS
|
||||
: ProcessingModes.SINGLE_PROCESS
|
||||
)
|
||||
.disableIndexing()
|
||||
.initialize();
|
||||
|
||||
@@ -66,9 +66,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -128,7 +128,9 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
|
||||
fontSize={SIZE.md}
|
||||
buttonType={{
|
||||
color: color ? color : colors.primary.accent,
|
||||
text: colors.static.white,
|
||||
text: color
|
||||
? colors.static.white
|
||||
: colors.primary.accentForeground,
|
||||
selected: color ? color : colors.primary.accent,
|
||||
opacity: 1
|
||||
}}
|
||||
|
||||
@@ -214,6 +214,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
height: 60,
|
||||
|
||||
@@ -140,7 +140,7 @@ const FloatingButton = ({
|
||||
>
|
||||
<Icon
|
||||
name={title === "Clear all trash" ? "delete" : "plus"}
|
||||
color="white"
|
||||
color={colors.primary.accentForeground}
|
||||
size={SIZE.xxl}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -114,7 +114,15 @@ export const Title = () => {
|
||||
#
|
||||
</Heading>
|
||||
) : null}
|
||||
{title} <Tag visible={currentScreen.beta} text="BETA" />
|
||||
{title}{" "}
|
||||
<Tag
|
||||
visible={currentScreen.beta}
|
||||
text="BETA"
|
||||
style={{
|
||||
backgroundColor: "transparent"
|
||||
}}
|
||||
textColor={colors.primary.accent}
|
||||
/>
|
||||
</Heading>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -17,35 +17,25 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { TouchableOpacity, useWindowDimensions, View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
presentSheet
|
||||
} from "../../../services/event-manager";
|
||||
import React, { useRef } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
import { ColorValues } from "../../../utils/colors";
|
||||
import { db } from "../../../common/database";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { eOpenJumpToDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Button } from "../../ui/button";
|
||||
import Sort from "../../sheets/sort";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const SectionHeader = React.memo(
|
||||
function SectionHeader({ item, index, type, color, screen }) {
|
||||
function SectionHeader({ item, index, type, color, screen, groupOptions }) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions(type)
|
||||
);
|
||||
let groupBy = Object.keys(GROUP).find(
|
||||
(key) => GROUP[key] === groupOptions.groupBy
|
||||
);
|
||||
@@ -65,17 +55,6 @@ export const SectionHeader = React.memo(
|
||||
? "Default"
|
||||
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
|
||||
|
||||
const onUpdate = useCallback(() => {
|
||||
setGroupOptions({ ...db.settings?.getGroupOptions(type) });
|
||||
}, [type]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [onUpdate]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -40,6 +40,7 @@ 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);
|
||||
@@ -98,6 +99,9 @@ const NoteItem = ({
|
||||
dateBy = "dateCreated",
|
||||
noOpen = false
|
||||
}) => {
|
||||
const currentEditingNote = useEditorStore(
|
||||
(state) => state.currentEditingNote
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
const compactMode = useIsCompactModeEnabled(item);
|
||||
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
|
||||
@@ -108,6 +112,9 @@ const NoteItem = ({
|
||||
const reminder = getUpcomingReminder(reminders);
|
||||
const noteColor = ColorValues[item.color?.toLowerCase()];
|
||||
const tags = getTags(item);
|
||||
const primaryColors =
|
||||
currentEditingNote === item.id ? colors.selected : colors.primary;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
@@ -148,7 +155,7 @@ const NoteItem = ({
|
||||
borderRadius: 5,
|
||||
marginRight: 5,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.primary.border,
|
||||
borderColor: primaryColors.border,
|
||||
paddingHorizontal: 6,
|
||||
marginBottom: 5
|
||||
}}
|
||||
@@ -179,7 +186,7 @@ const NoteItem = ({
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || colors.primary.heading
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
@@ -192,7 +199,7 @@ const NoteItem = ({
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] || colors.primary.heading
|
||||
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
|
||||
}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
@@ -208,6 +215,7 @@ const NoteItem = ({
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={primaryColors.paragraph}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{decode(item.headline, {
|
||||
@@ -262,7 +270,7 @@ const NoteItem = ({
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={SIZE.md}
|
||||
color={colors.primary.icon}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
@@ -283,7 +291,7 @@ const NoteItem = ({
|
||||
}}
|
||||
color={
|
||||
ColorValues[item.color?.toLowerCase()] ||
|
||||
colors.primary.accent
|
||||
primaryColors.accent
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -296,7 +304,7 @@ const NoteItem = ({
|
||||
style={{
|
||||
marginRight: 6
|
||||
}}
|
||||
color={colors.primary.icon}
|
||||
color={primaryColors.icon}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -354,7 +362,7 @@ const NoteItem = ({
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
color={colors.primary.accent}
|
||||
color={primaryColors.accent}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginRight: 6
|
||||
@@ -424,7 +432,7 @@ const NoteItem = ({
|
||||
|
||||
<IconButton
|
||||
testID={notesnook.listitem.menu}
|
||||
color={colors.primary.paragraph}
|
||||
color={primaryColors.paragraph}
|
||||
name="dots-horizontal"
|
||||
size={SIZE.xl}
|
||||
onPress={() => !noOpen && showActionSheet(item, isTrash)}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { Empty } from "./empty";
|
||||
import { getTotalNotes } from "@notesnook/common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import { useGroupOptions } from "../../hooks/use-group-options";
|
||||
|
||||
const renderItems = {
|
||||
note: NoteWrapper,
|
||||
@@ -53,15 +54,10 @@ const renderItems = {
|
||||
const RenderItem = ({ item, index, type, ...restArgs }) => {
|
||||
if (!item) return <View />;
|
||||
const Item = renderItems[item.itemType || item.type] || View;
|
||||
const groupOptions = db.settings?.getGroupOptions(type);
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
const totalNotes = getTotalNotes(item);
|
||||
|
||||
return (
|
||||
<Item
|
||||
item={item}
|
||||
dateBy={dateBy}
|
||||
index={index}
|
||||
type={type}
|
||||
totalNotes={totalNotes}
|
||||
@@ -103,6 +99,13 @@ const List = ({
|
||||
(type === "notes" && notesListMode === "compact") ||
|
||||
type === "notebooks" ||
|
||||
notebooksListMode === "compact";
|
||||
const groupType =
|
||||
screen === "Home" ? "home" : screen === "Favorites" ? "favorites" : type;
|
||||
|
||||
const groupOptions = useGroupOptions(groupType);
|
||||
|
||||
const dateBy =
|
||||
groupOptions.sortBy !== "title" ? groupOptions.sortBy : "dateEdited";
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({ item, index }) => (
|
||||
@@ -111,18 +114,22 @@ const List = ({
|
||||
index={index}
|
||||
color={headerProps?.color}
|
||||
title={headerProps?.heading}
|
||||
type={
|
||||
screen === "Notes"
|
||||
? "home"
|
||||
: screen === "Favorites"
|
||||
? "favorites"
|
||||
: type
|
||||
}
|
||||
dateBy={dateBy}
|
||||
type={groupType}
|
||||
screen={screen}
|
||||
isSheet={isSheet}
|
||||
groupOptions={groupOptions}
|
||||
/>
|
||||
),
|
||||
[headerProps?.color, headerProps?.heading, screen, type, isSheet]
|
||||
[
|
||||
headerProps?.color,
|
||||
headerProps?.heading,
|
||||
screen,
|
||||
isSheet,
|
||||
dateBy,
|
||||
groupType,
|
||||
groupOptions
|
||||
]
|
||||
);
|
||||
|
||||
const _onRefresh = async () => {
|
||||
@@ -228,7 +235,7 @@ const List = ({
|
||||
<JumpToSectionDialog
|
||||
screen={screen}
|
||||
data={listData}
|
||||
type={screen === "Notes" ? "home" : type}
|
||||
type={screen === "Home" ? "home" : type}
|
||||
scrollRef={scrollRef}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -317,23 +317,8 @@ export const SelectionHeader = React.memo(() => {
|
||||
customStyle={{
|
||||
marginLeft: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${
|
||||
selectedItemsList.length > 1 ? "items" : "item"
|
||||
}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1 ? "these items?" : "this item?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
positivePress: () => {
|
||||
deleteItems();
|
||||
},
|
||||
positiveType: "errorShade"
|
||||
});
|
||||
|
||||
return;
|
||||
onPress={() => {
|
||||
deleteItems();
|
||||
}}
|
||||
tooltipText="Move to trash"
|
||||
tooltipPosition={1}
|
||||
|
||||
@@ -198,7 +198,6 @@ const SheetProvider = ({ context = "global" }) => {
|
||||
key={data.actionText}
|
||||
title={data.actionText}
|
||||
accentColor={data.iconColor}
|
||||
accentText="light"
|
||||
type="accent"
|
||||
height={40}
|
||||
width={250}
|
||||
|
||||
@@ -35,10 +35,14 @@ import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const body = useRef(defaultBody);
|
||||
const title = useRef(defaultTitle);
|
||||
|
||||
const body = useStoredRef("issueBody", defaultBody);
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
@@ -70,6 +74,8 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
body.reset();
|
||||
title.reset();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Issue reported",
|
||||
|
||||
@@ -64,7 +64,7 @@ export const Progress = () => {
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={currentProgress || 0.1}
|
||||
indeterminate
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
@@ -74,7 +74,7 @@ export const Progress = () => {
|
||||
{progress ? (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{progress.type?.slice(0, 1).toUpperCase() + progress.type?.slice(1)}
|
||||
ing {progress?.current}/{progress?.total}
|
||||
ing {progress?.current}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -22,7 +22,9 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import DocumentPicker, {
|
||||
DocumentPickerResponse
|
||||
} from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
@@ -45,6 +47,8 @@ import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -129,75 +133,71 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 300);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setRestoring(true);
|
||||
let prefix = Platform.OS === "ios" ? "" : "file:/";
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(item.uri, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(prefix + item.path, "utf8");
|
||||
}
|
||||
backup = JSON.parse(backup);
|
||||
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
await restoreBackup(backup, value);
|
||||
close();
|
||||
setRestoring(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = (onsubmit, onclose = () => {}) => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file to restore it",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: onclose,
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password) => {
|
||||
try {
|
||||
return await onsubmit(password);
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to backup data",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
return false;
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file to restore it",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password) => {
|
||||
resolve(password);
|
||||
resolved = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -217,21 +217,164 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password) => {
|
||||
await db.backup.import(backup, password);
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
throw new Error("Backup file is invalid");
|
||||
}
|
||||
|
||||
let password;
|
||||
|
||||
console.log(`Found ${backupFiles?.length} files to restore from backup`);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
password = await withPassword();
|
||||
if (!password) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
await db.initCollections();
|
||||
initialize();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const password = await withPassword();
|
||||
if (password) {
|
||||
try {
|
||||
await restoreBackup(backup, password);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
@@ -271,76 +414,6 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const restoreBackup = async (backup, password) => {
|
||||
await db.backup.import(backup, password);
|
||||
setRestoring(false);
|
||||
initialize();
|
||||
ToastEvent.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
ToastEvent.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentPicker.pickSingle()
|
||||
.then((r) => {
|
||||
setRestoring(true);
|
||||
fetch(r.uri)
|
||||
.then(async (r) => {
|
||||
try {
|
||||
let backup = await r.json();
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
withPassword(
|
||||
async (value) => {
|
||||
try {
|
||||
restoreBackup(backup, value).then(() => {
|
||||
close();
|
||||
setRestoring(false);
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setRestoring(false);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
})
|
||||
.catch(console.log);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
|
||||
@@ -17,20 +17,18 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import ToggleSwitch from "toggle-switch-react-native";
|
||||
import Navigation from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { normalize, SIZE } from "../../utils/size";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { PressableButton } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import Tag from "../ui/tag";
|
||||
|
||||
export const MenuItem = React.memo(
|
||||
function MenuItem({ item, index, testID, rightBtn }) {
|
||||
@@ -40,6 +38,7 @@ export const MenuItem = React.memo(
|
||||
);
|
||||
const screenId = item.name.toLowerCase() + "_navigation";
|
||||
let isFocused = headerTextState?.id === screenId;
|
||||
const primaryColors = isFocused ? colors.selected : colors.primary;
|
||||
|
||||
const _onPress = () => {
|
||||
if (item.func) {
|
||||
@@ -128,14 +127,28 @@ export const MenuItem = React.memo(
|
||||
<Paragraph size={SIZE.md}>{item.name}</Paragraph>
|
||||
)}
|
||||
|
||||
<Tag visible={item.isBeta} text="BETA" />
|
||||
{item.isBeta ? (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
backgroundColor: primaryColors.accent,
|
||||
paddingHorizontal: 4,
|
||||
marginLeft: 5,
|
||||
paddingVertical: 2
|
||||
}}
|
||||
>
|
||||
<Paragraph color={primaryColors.accentForeground} size={SIZE.xxs}>
|
||||
BETA
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{item.switch ? (
|
||||
<ToggleSwitch
|
||||
isOn={item.on}
|
||||
onColor={colors.primary.accent}
|
||||
offColor={colors.primary.icon}
|
||||
onColor={primaryColors.accent}
|
||||
offColor={primaryColors.icon}
|
||||
size="small"
|
||||
animationSpeed={150}
|
||||
onToggle={_onPress}
|
||||
|
||||
@@ -108,9 +108,12 @@ export const PinItem = React.memo(
|
||||
alias = item?.alias || item?.title;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [headerTextState, setHeaderTextState] = useState(null);
|
||||
const primaryColors =
|
||||
headerTextState?.id === item.id ? colors.selected : colors.primary;
|
||||
|
||||
const color =
|
||||
headerTextState?.id === item.id
|
||||
? colors.primary.accent
|
||||
? colors.selected.accent
|
||||
: colors.primary.icon;
|
||||
const fwdRef = useRef();
|
||||
|
||||
@@ -230,7 +233,7 @@ export const PinItem = React.memo(
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
color={colors.primary.heading}
|
||||
color={primaryColors.heading}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{alias}
|
||||
@@ -238,7 +241,7 @@ export const PinItem = React.memo(
|
||||
) : (
|
||||
<Paragraph
|
||||
numberOfLines={1}
|
||||
color={colors.primary.paragraph}
|
||||
color={primaryColors.paragraph}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{alias}
|
||||
|
||||
@@ -135,7 +135,7 @@ export const UserStatus = () => {
|
||||
? "Last sync failed, tap to try again"
|
||||
: syncing
|
||||
? `Syncing your notes${
|
||||
progress ? ` (${progress.current}/${progress.total})` : ""
|
||||
progress ? ` (${progress.current})` : ""
|
||||
}`
|
||||
: "Tap here to sync your notes."}
|
||||
</Paragraph>
|
||||
|
||||
@@ -77,7 +77,9 @@ export const Tip = ({
|
||||
alignSelf: "flex-start",
|
||||
borderRadius: 100,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.static[color as never] || colors.primary[color as never] as string
|
||||
borderColor:
|
||||
colors.static[color as never] ||
|
||||
(colors.primary[color as never] as string)
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -139,7 +141,7 @@ export const Tip = ({
|
||||
icon={tip.button.icon}
|
||||
buttonType={{
|
||||
color: colors.static[color as never],
|
||||
text: colors.static.white
|
||||
text: colors.primary.accentForeground
|
||||
}}
|
||||
style={{
|
||||
marginTop: 10
|
||||
|
||||
@@ -108,7 +108,7 @@ export const useButton = ({
|
||||
},
|
||||
accent: {
|
||||
primary: accent || colors.primary.accent,
|
||||
text: text || colors.primary.paragraph,
|
||||
text: text || colors.primary.accentForeground,
|
||||
selected: accent || colors.primary.accent
|
||||
},
|
||||
inverted: {
|
||||
|
||||
@@ -16,9 +16,9 @@ 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, ViewStyle } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { ViewStyle } from "react-native";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function Tag({
|
||||
textAlign: "center",
|
||||
...style
|
||||
}}
|
||||
color={textColor || colors.static.white}
|
||||
color={textColor || colors.primary.accentForeground}
|
||||
size={SIZE.xxs}
|
||||
>
|
||||
{text}
|
||||
|
||||
@@ -101,13 +101,6 @@ const onSyncAborted = () => {
|
||||
useUserStore.getState().setSyncing(false, SyncStatus.Failed);
|
||||
};
|
||||
|
||||
const onSyncProgress = ({ type, total, current }) => {
|
||||
if (type !== "download") return;
|
||||
if (total < 10 || current % 10 === 0) {
|
||||
initAfterSync();
|
||||
}
|
||||
};
|
||||
|
||||
const onFileEncryptionProgress = ({ total, progress }) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
@@ -167,13 +160,15 @@ const onUserSubscriptionStatusChanged = async (userStatus) => {
|
||||
useMessageStore.getState().setAnnouncement();
|
||||
};
|
||||
|
||||
const onRequestPartialSync = async (full, force) => {
|
||||
const onRequestPartialSync = async (full, force, lastSyncTime) => {
|
||||
if (SettingsService.get().disableAutoSync) return;
|
||||
DatabaseLogger.info(`onRequestPartialSync full:${full}, force:${force}`);
|
||||
DatabaseLogger.info(
|
||||
`onRequestPartialSync full:${full}, force:${force}, lastSyncTime:${lastSyncTime}`
|
||||
);
|
||||
if (full || force) {
|
||||
await Sync.run("global", force, full);
|
||||
await Sync.run("global", force, full, undefined, lastSyncTime);
|
||||
} else {
|
||||
await Sync.run("global", false, false);
|
||||
await Sync.run("global", false, false, undefined, lastSyncTime);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -253,7 +248,6 @@ export const useAppEvents = () => {
|
||||
]);
|
||||
|
||||
const syncedOnLaunch = useRef(false);
|
||||
const { isDark } = useThemeColors();
|
||||
const refValues = useRef({
|
||||
subsriptionSuccessListener: null,
|
||||
subsriptionErrorListener: null,
|
||||
@@ -278,7 +272,6 @@ export const useAppEvents = () => {
|
||||
const eventManager = db?.eventManager;
|
||||
eventSubscriptions = [
|
||||
eventManager?.subscribe(EVENTS.syncCompleted, onSyncComplete),
|
||||
eventManager?.subscribe(EVENTS.syncProgress, onSyncProgress),
|
||||
eventManager?.subscribe(
|
||||
EVENTS.databaseSyncRequested,
|
||||
onRequestPartialSync
|
||||
@@ -533,6 +526,12 @@ export const useAppEvents = () => {
|
||||
if (refValues.current?.isReconnecting || !refValues.current?.isUserReady)
|
||||
return;
|
||||
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
console.log("AppDidEnterForegroundAfterAction");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
refValues.current.isReconnecting = false;
|
||||
return;
|
||||
|
||||
49
apps/mobile/app/hooks/use-group-options.ts
Normal file
49
apps/mobile/app/hooks/use-group-options.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import { db } from "../common/database";
|
||||
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
|
||||
export function useGroupOptions(type: any) {
|
||||
const [groupOptions, setGroupOptions] = useState(
|
||||
db.settings?.getGroupOptions(type)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onUpdate = () => {
|
||||
const options = db.settings?.getGroupOptions(type) as any;
|
||||
if (
|
||||
groupOptions?.groupBy !== options.groupBy ||
|
||||
groupOptions?.sortBy !== options.sortBy ||
|
||||
groupOptions?.sortDirection !== groupOptions?.sortDirection
|
||||
) {
|
||||
setGroupOptions({ ...options });
|
||||
Navigation.queueRoutesForUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
eSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
return () => {
|
||||
eUnSubscribeEvent("groupOptionsUpdate", onUpdate);
|
||||
};
|
||||
}, [type, groupOptions]);
|
||||
|
||||
return groupOptions;
|
||||
}
|
||||
52
apps/mobile/app/hooks/use-stored-ref.ts
Normal file
52
apps/mobile/app/hooks/use-stored-ref.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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 { useRef } from "react";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
|
||||
export function useStoredRef<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): {
|
||||
current: T;
|
||||
reset(): void;
|
||||
} {
|
||||
const refKey = `storedRef:${key}`;
|
||||
const value = useRef(
|
||||
MMKV.getMap<{ current: T }>(refKey)?.current || initialValue
|
||||
);
|
||||
const frameRef = useRef(0);
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return value.current;
|
||||
},
|
||||
set current(next: T) {
|
||||
value.current = next;
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
frameRef.current = requestAnimationFrame(() => {
|
||||
MMKV.setMap(refKey, {
|
||||
current: value.current
|
||||
});
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
MMKV.removeItem(refKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
49
apps/mobile/app/hooks/use-stored-state.ts
Normal file
49
apps/mobile/app/hooks/use-stored-state.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useRef, useState } from "react";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
|
||||
export function useStoredValue<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
): { value: T; reset(): void } {
|
||||
const refKey = `storedState:${key}`;
|
||||
const [value, setValue] = useState<T>(
|
||||
MMKV.getMap<{ value: T }>(refKey)?.value || initialValue
|
||||
);
|
||||
const frameRef = useRef(0);
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
set value(next: T) {
|
||||
setValue(next);
|
||||
cancelAnimationFrame(frameRef.current);
|
||||
frameRef.current = requestAnimationFrame(() => {
|
||||
MMKV.setMap(refKey, {
|
||||
value: value
|
||||
});
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
MMKV.removeItem(refKey);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import PremiumService from "../../../services/premium";
|
||||
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { useSettingStore } from "../../../stores/use-setting-store";
|
||||
|
||||
const showEncryptionSheet = (file) => {
|
||||
presentSheet({
|
||||
@@ -62,6 +63,7 @@ const file = async (fileOptions) => {
|
||||
|
||||
let file;
|
||||
try {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
file = await DocumentPicker.pick(options);
|
||||
} catch (e) {
|
||||
return;
|
||||
@@ -135,6 +137,7 @@ const file = async (fileOptions) => {
|
||||
const camera = async (options) => {
|
||||
try {
|
||||
await db.attachments.generateKey();
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
launchCamera(
|
||||
{
|
||||
includeBase64: true,
|
||||
@@ -156,6 +159,7 @@ const camera = async (options) => {
|
||||
const gallery = async (options) => {
|
||||
try {
|
||||
await db.attachments.generateKey();
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
launchImageLibrary(
|
||||
{
|
||||
includeBase64: true,
|
||||
|
||||
@@ -141,6 +141,7 @@ export const useEditor = (
|
||||
clearTimeout(timers.current["loading-images"]);
|
||||
sessionHistoryId.current = undefined;
|
||||
saveCount.current = 0;
|
||||
lock.current = false;
|
||||
useEditorStore.getState().setReadonly(false);
|
||||
resetContent && postMessage(EditorEvents.title, "");
|
||||
lastContentChangeTime.current = 0;
|
||||
|
||||
@@ -55,6 +55,7 @@ export const EditorWrapper = ({ width }) => {
|
||||
prevState.current = state;
|
||||
return;
|
||||
}
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) return;
|
||||
if (state === "active") {
|
||||
editorController.current.onReady();
|
||||
editorController.current.overlay(false);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
|
||||
<List
|
||||
listData={notes}
|
||||
type="notes"
|
||||
screen="Notes"
|
||||
screen="Home"
|
||||
loading={loading || !isFocused}
|
||||
headerProps={{
|
||||
heading: "Notes"
|
||||
|
||||
@@ -42,7 +42,8 @@ export async function verifyUser(
|
||||
negativeText: closeText || "Cancel",
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
let verified = await db.user.verifyPassword(value);
|
||||
const user = await db.user.getUser();
|
||||
let verified = !user ? true : await db.user.verifyPassword(value);
|
||||
if (verified) {
|
||||
sleep(300).then(async () => {
|
||||
await onsuccess();
|
||||
@@ -58,7 +59,7 @@ export async function verifyUser(
|
||||
}
|
||||
} catch (e) {
|
||||
ToastEvent.show({
|
||||
heading: "Failed to backup data",
|
||||
heading: "Failed to verify",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
|
||||
@@ -27,6 +27,8 @@ import PremiumService from "../../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { verifyUser } from "../functions";
|
||||
import { Dialog } from "../../../components/dialog";
|
||||
|
||||
interface PickerOptions<T> {
|
||||
getValue: () => T;
|
||||
@@ -37,6 +39,7 @@ interface PickerOptions<T> {
|
||||
options: T[];
|
||||
premium?: boolean;
|
||||
onCheckOptionIsPremium?: (item: T) => boolean;
|
||||
requiresVerification?: () => boolean;
|
||||
}
|
||||
|
||||
export function SettingsPicker<T>({
|
||||
@@ -47,7 +50,8 @@ export function SettingsPicker<T>({
|
||||
options,
|
||||
getItemKey,
|
||||
premium,
|
||||
onCheckOptionIsPremium = () => true
|
||||
onCheckOptionIsPremium = () => true,
|
||||
requiresVerification = () => false
|
||||
}: PickerOptions<T>) {
|
||||
const { colors } = useThemeColors("contextMenu");
|
||||
const menuRef = useRef<any>();
|
||||
@@ -118,11 +122,18 @@ export function SettingsPicker<T>({
|
||||
</PressableButton>
|
||||
}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
{options.map((item) => (
|
||||
<MenuItem
|
||||
key={getItemKey(item)}
|
||||
onPress={async () => {
|
||||
onChange(item);
|
||||
if (requiresVerification?.()) {
|
||||
verifyUser("local", () => {
|
||||
onChange(item);
|
||||
});
|
||||
} else {
|
||||
onChange(item);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: compareValue(currentValue, item)
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createSettingsPicker } from ".";
|
||||
import { getFontById, getFonts } from "@notesnook/editor/dist/utils/font";
|
||||
import { DATE_FORMATS, TIME_FORMATS } from "@notesnook/core/dist/common";
|
||||
import dayjs from "dayjs";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
export const FontPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.defaultFontFamily,
|
||||
@@ -127,6 +128,12 @@ export const BackupReminderPicker = createSettingsPicker({
|
||||
options: ["useroff", "daily", "weekly", "monthly"],
|
||||
compareValue: (current, item) => current === item,
|
||||
premium: true,
|
||||
requiresVerification: () => {
|
||||
return (
|
||||
!useSettingStore.getState().settings.encryptedBackup &&
|
||||
useUserStore.getState().user
|
||||
);
|
||||
},
|
||||
onCheckOptionIsPremium: (item) => {
|
||||
return item !== "useroff";
|
||||
}
|
||||
|
||||
@@ -782,10 +782,11 @@ export const settingsGroups: SettingSection[] = [
|
||||
description: "Create a backup of your data",
|
||||
modifer: async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
if (!user) {
|
||||
if (!user || SettingsService.getProperty("encryptedBackup")) {
|
||||
await BackupService.run(true);
|
||||
return;
|
||||
}
|
||||
|
||||
verifyUser(null, () => BackupService.run(true));
|
||||
}
|
||||
},
|
||||
@@ -869,9 +870,17 @@ export const settingsGroups: SettingSection[] = [
|
||||
});
|
||||
return;
|
||||
}
|
||||
SettingsService.set({
|
||||
encryptedBackup: !settings.encryptedBackup
|
||||
});
|
||||
if (settings.encryptedBackup) {
|
||||
await verifyUser(null, () => {
|
||||
SettingsService.set({
|
||||
encryptedBackup: false
|
||||
});
|
||||
});
|
||||
} else {
|
||||
SettingsService.set({
|
||||
encryptedBackup: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -901,7 +910,9 @@ export const settingsGroups: SettingSection[] = [
|
||||
description:
|
||||
"Export all notes as pdf, markdown, html or text in a single zip file",
|
||||
modifer: () => {
|
||||
ExportNotesSheet.present(undefined, true);
|
||||
verifyUser(null, () => {
|
||||
ExportNotesSheet.present(undefined, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -748,7 +748,7 @@ const ThemeSetter = ({
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.accent} size={SIZE.md}>
|
||||
<Heading color={colors.accentForeground} size={SIZE.md}>
|
||||
{darkTheme.id === theme.id
|
||||
? "Applied as dark theme"
|
||||
: "Applied as light theme"}
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import FileViewer from "react-native-file-viewer";
|
||||
@@ -30,6 +29,8 @@ import { eCloseSheet } from "../utils/events";
|
||||
import { sleep } from "../utils/time";
|
||||
import { ToastEvent, eSendEvent, presentSheet } from "./event-manager";
|
||||
import SettingsService from "./settings";
|
||||
import { cacheDir, copyFileAsync } from "../common/filesystem/utils";
|
||||
import { zip } from "react-native-zip-archive";
|
||||
|
||||
const MS_DAY = 86400000;
|
||||
const MS_WEEK = MS_DAY * 7;
|
||||
@@ -152,8 +153,6 @@ async function run(progress, context) {
|
||||
let androidBackupDirectory = await checkBackupDirExists(false, context);
|
||||
if (!androidBackupDirectory) return;
|
||||
|
||||
let backup;
|
||||
|
||||
if (progress) {
|
||||
presentSheet({
|
||||
title: "Backing up your data",
|
||||
@@ -163,43 +162,67 @@ async function run(progress, context) {
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
backup = await db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup
|
||||
);
|
||||
if (!backup) throw new Error("Backup returned empty.");
|
||||
} catch (e) {
|
||||
await sleep(300);
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastEvent.error(e, "Backup failed!");
|
||||
return null;
|
||||
let path;
|
||||
let backupFilePath;
|
||||
let backupFileName = "notesnook_backup_" + Date.now();
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups/");
|
||||
}
|
||||
|
||||
try {
|
||||
let backupName = "notesnook_backup_" + Date.now();
|
||||
backupName =
|
||||
sanitizeFilename(backupName, { replacement: "_" }) + ".nnbackup";
|
||||
let path;
|
||||
let backupFilePath;
|
||||
const zipSourceFolder = `${cacheDir}/${backupFileName}`;
|
||||
const zipOutputFile =
|
||||
Platform.OS === "ios"
|
||||
? `${path}/${backupFileName}.nnbackupz`
|
||||
: `${cacheDir}/${backupFileName}.nnbackupz`;
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
path = await storage.checkAndCreateDir("/backups/");
|
||||
await RNFetchBlob.fs.writeFile(path + backupName, backup, "utf8");
|
||||
backupFilePath = path + backupName;
|
||||
} else {
|
||||
backupFilePath = await ScopedStorage.writeFile(
|
||||
androidBackupDirectory.uri,
|
||||
backup,
|
||||
backupName,
|
||||
"nnbackup/json",
|
||||
"utf8",
|
||||
false
|
||||
if (await RNFetchBlob.fs.exists(zipSourceFolder))
|
||||
await RNFetchBlob.fs.unlink(zipSourceFolder);
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
try {
|
||||
for await (const file of db.backup.export(
|
||||
"mobile",
|
||||
SettingsService.get().encryptedBackup
|
||||
)) {
|
||||
console.log("Writing backup chunk of size...", file?.data?.length);
|
||||
await RNFetchBlob.fs.writeFile(
|
||||
`${zipSourceFolder}/${file.path}`,
|
||||
file.data,
|
||||
"utf8"
|
||||
);
|
||||
}
|
||||
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
|
||||
console.log("Final zip:", await RNFetchBlob.fs.stat(zipOutputFile));
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
androidBackupDirectory.uri,
|
||||
`${backupFileName}.nnbackupz`,
|
||||
"application/nnbackupz"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
path = file.uri;
|
||||
} else {
|
||||
path = zipOutputFile;
|
||||
}
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
updateNextBackupTime();
|
||||
|
||||
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
|
||||
|
||||
if (context) return path;
|
||||
await sleep(300);
|
||||
if (showBackupCompleteSheet) {
|
||||
presentBackupCompleteSheet(backupFilePath);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
|
||||
ToastEvent.show({
|
||||
heading: "Backup successful",
|
||||
message: "Your backup is stored in Notesnook folder on your phone.",
|
||||
@@ -207,18 +230,10 @@ async function run(progress, context) {
|
||||
context: "global"
|
||||
});
|
||||
|
||||
let showBackupCompleteSheet = SettingsService.get().showBackupCompleteSheet;
|
||||
|
||||
if (context) return backupFilePath;
|
||||
await sleep(300);
|
||||
if (showBackupCompleteSheet) {
|
||||
presentBackupCompleteSheet(backupFilePath);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
return backupFilePath;
|
||||
return path;
|
||||
} catch (e) {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastEvent.error(e, "Backup failed!");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ import NetInfo from "@react-native-community/netinfo";
|
||||
import { encodeNonAsciiHTML } from "entities";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
|
||||
|
||||
export type Reminder = {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -142,6 +141,7 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
const ReminderNotify =
|
||||
require("../components/sheets/reminder-notify").default;
|
||||
ReminderNotify.present(reminder);
|
||||
useRelationStore.getState().update();
|
||||
return;
|
||||
}
|
||||
editorState().movedAway = false;
|
||||
|
||||
@@ -39,7 +39,8 @@ const run = async (
|
||||
context = "global",
|
||||
forced = false,
|
||||
full = true,
|
||||
onCompleted
|
||||
onCompleted,
|
||||
lastSyncTime
|
||||
) => {
|
||||
if (useUserStore.getState().syncing) {
|
||||
DatabaseLogger.log("Sync in progress");
|
||||
@@ -68,7 +69,7 @@ const run = async (
|
||||
try {
|
||||
let res = await BackgroundSync.doInBackground(async () => {
|
||||
try {
|
||||
await db.sync(full, forced);
|
||||
await db.sync(full, forced, lastSyncTime);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
||||
@@ -103,6 +103,8 @@ export interface SettingStore extends State {
|
||||
sheetKeyboardHandler: boolean;
|
||||
requestBiometrics: boolean;
|
||||
setRequestBiometrics: (requestBiometrics: boolean) => void;
|
||||
appDidEnterBackgroundForAction: boolean;
|
||||
setAppDidEnterBackgroundForAction: (value: boolean) => void;
|
||||
insets: Insets;
|
||||
setInsets: (insets: Insets) => void;
|
||||
timeFormat: string;
|
||||
@@ -155,7 +157,7 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
darkTheme: ThemeDark
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set) => ({
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
settings: { ...defaultSettings },
|
||||
sheetKeyboardHandler: true,
|
||||
fullscreen: false,
|
||||
@@ -174,6 +176,12 @@ export const useSettingStore = create<SettingStore>((set) => ({
|
||||
setInsets: (insets) => set({ insets }),
|
||||
timeFormat: "12-hour",
|
||||
dateFormat: "DD-MM-YYYY",
|
||||
setAppDidEnterBackgroundForAction: (value: boolean) => {
|
||||
set({
|
||||
appDidEnterBackgroundForAction: value
|
||||
});
|
||||
},
|
||||
appDidEnterBackgroundForAction: false,
|
||||
insets: initialWindowMetrics?.insets
|
||||
? initialWindowMetrics.insets
|
||||
: { top: 0, right: 0, left: 0, bottom: 0 }
|
||||
|
||||
@@ -107,46 +107,43 @@ export const deleteItems = async (item, context) => {
|
||||
|
||||
if (topics?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(topics, "topic", context);
|
||||
if (result.delete) {
|
||||
for (const topic of topics) {
|
||||
if (result.deleteNotes) {
|
||||
if (!result.delete) return;
|
||||
for (const topic of topics) {
|
||||
if (result.deleteNotes) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(notebooks, "notebook", context);
|
||||
if (!result.delete) return;
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (result.deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
const topics = notebook.topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
useMenuStore.getState().setMenuPins();
|
||||
ToastEvent.show({
|
||||
heading: `${topics.length > 1 ? "Topics" : "Topic"} deleted`,
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (notebooks?.length > 0) {
|
||||
const result = await confirmDeleteAllNotes(notebooks, "notebook", context);
|
||||
|
||||
if (result.delete) {
|
||||
let ids = notebooks.map((i) => i.id);
|
||||
if (result.deleteNotes) {
|
||||
for (let id of ids) {
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
const topics = notebook.topics.all;
|
||||
for (let topic of topics) {
|
||||
const notes = db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.topic(topic.id).all;
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
const notes = db.relations.from(notebook.data, "note");
|
||||
await db.notes.delete(...notes.map((note) => note.id));
|
||||
}
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
await db.notebooks.delete(...ids);
|
||||
useMenuStore.getState().setMenuPins();
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
visibleById
|
||||
} from "./utils";
|
||||
|
||||
async function createNotebook(
|
||||
export async function createNotebook(
|
||||
title = "Notebook 1",
|
||||
description = true,
|
||||
topic = true,
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { notesnook } from "../test.ids";
|
||||
import { createNotebook } from "./notebook.e2e";
|
||||
import {
|
||||
tapById,
|
||||
visibleByText,
|
||||
@@ -25,7 +26,8 @@ import {
|
||||
prepare,
|
||||
tapByText,
|
||||
notVisibleByText,
|
||||
sleep
|
||||
sleep,
|
||||
navigate
|
||||
} from "./utils";
|
||||
|
||||
async function sortBy(sorting, elementText = "Default") {
|
||||
@@ -104,6 +106,23 @@ describe("Sort & filter", () => {
|
||||
await visibleByText("Month");
|
||||
});
|
||||
|
||||
it("Sort notes in topic", async () => {
|
||||
await prepare();
|
||||
await navigate("Notebooks");
|
||||
await sleep(500);
|
||||
await createNotebook("Notebook 1", true, true);
|
||||
await sleep(500);
|
||||
await device.pressBack();
|
||||
await sleep(500);
|
||||
await tapByText("Topic");
|
||||
await createNote("A", "A letter");
|
||||
await sleep(500);
|
||||
await createNote("B", "B letter");
|
||||
await sortBy("Abc");
|
||||
await sleep(300);
|
||||
await visibleByText("N");
|
||||
});
|
||||
|
||||
it("Compact mode", async () => {
|
||||
await prepare();
|
||||
await createNote("Note 1", "Note 1");
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2057
|
||||
versionCode 2063
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
- Added support for custom themes
|
||||
- Syntax highlighting and rendered Math in exports
|
||||
- Improved copy/pasting to and from Notesnook
|
||||
- Support copying link & link text in directly from the Link toolbar
|
||||
- Many other bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
- New backup format nnbackupz with upto 90% smaller backups
|
||||
- Improve UI of code blocks & language selection popup
|
||||
- Fixed some bugs in sync v2
|
||||
@@ -33,6 +33,8 @@
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationDictionaryVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>5</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
|
||||
@@ -997,7 +997,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1071,7 +1071,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1102,7 +1102,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1176,7 +1176,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1335,7 +1335,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1347,7 +1347,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1378,7 +1378,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1391,7 +1391,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1421,7 +1421,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1495,7 +1495,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1526,7 +1526,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2050;
|
||||
CURRENT_PROJECT_VERSION = 2056;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1601,7 +1601,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.6.1;
|
||||
MARKETING_VERSION = 2.6.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "NSZombieEnabled"
|
||||
value = "YES"
|
||||
value = "NO"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
@@ -77,13 +77,6 @@
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
<AdditionalOption
|
||||
key = "NSZombieEnabled"
|
||||
value = "YES"
|
||||
isEnabled = "YES">
|
||||
</AdditionalOption>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -327,7 +327,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-keep-awake (1.2.0):
|
||||
- React-Core
|
||||
- react-native-mmkv-storage (0.10.0-alpha.6):
|
||||
- react-native-mmkv-storage (0.10.0-alpha.9):
|
||||
- MMKV (~> 1.3.1)
|
||||
- React
|
||||
- React-Core
|
||||
@@ -347,7 +347,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-share-extension (2.5.6):
|
||||
- React
|
||||
- react-native-sodium (1.5.1):
|
||||
- react-native-sodium (1.5.2):
|
||||
- React
|
||||
- react-native-webview (11.26.1):
|
||||
- React-Core
|
||||
@@ -875,14 +875,14 @@ SPEC CHECKSUMS:
|
||||
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
|
||||
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
|
||||
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
|
||||
react-native-mmkv-storage: a38beae34b1f7906a2f5e715f54216f9ddab14c0
|
||||
react-native-mmkv-storage: d4ad55ab411b7f0d4e6269d801b31821ef48970b
|
||||
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
|
||||
react-native-notification-sounds: da78c828fe1bcbb92d8b505d5261890ed315ff39
|
||||
react-native-orientation: f1caf84d65f1a4fd4511a18f2b924e634ad7a628
|
||||
react-native-pdf: 33c622cbdf776a649929e8b9d1ce2d313347c4fa
|
||||
react-native-safe-area-context: 36cc67648134e89465663b8172336a19eeda493d
|
||||
react-native-share-extension: df66a2ee48a62277d79898375e2142bde0782063
|
||||
react-native-sodium: 08d459dfcb5d246363a30712d816acea56ab8f1c
|
||||
react-native-sodium: 7d2bbb9ca840dc58d63f661b2ff733a38170fec5
|
||||
react-native-webview: 9f111dfbcfc826084d6c507f569e5e03342ee1c1
|
||||
React-NativeModulesApple: 1d81d927ef1a67a3545a01e14c2e98500bf9b199
|
||||
React-perflogger: 684a11499a0589cc42135d6d5cc04d0e4e0e261a
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"react-native-image-picker": "4.1.2",
|
||||
"react-native-in-app-review": "4.3.3",
|
||||
"react-native-keychain": "4.0.5",
|
||||
"react-native-mmkv-storage": "^0.10.0-alpha.7",
|
||||
"react-native-mmkv-storage": "^0.10.0-alpha.9",
|
||||
"react-native-modal-datetime-picker": "14.0.0",
|
||||
"react-native-navigation-bar-color": "2.0.2",
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
|
||||
566
apps/mobile/package-lock.json
generated
566
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.5",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -43,4 +43,4 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,8 +602,9 @@ const ShareView = ({ quicknote = false }) => {
|
||||
marginBottom: 0
|
||||
}}
|
||||
loading={loading}
|
||||
iconColor={colors.static.white}
|
||||
iconColor={colors.primary.accentForeground}
|
||||
onPress={onPress}
|
||||
textColor={colors.primary.accentForeground}
|
||||
textStyle={{
|
||||
fontSize: 16,
|
||||
marginLeft: 0
|
||||
|
||||
@@ -42,20 +42,24 @@ let appSettings = MMKV.getString(StorageKeys.appSettings);
|
||||
if (appSettings) {
|
||||
appSettings = JSON.parse(appSettings);
|
||||
}
|
||||
|
||||
const systemColorScheme = Appearance.getColorScheme();
|
||||
const appColorScheme = appSettings?.colorScheme;
|
||||
const useSystemTheme = appSettings?.useSystemTheme;
|
||||
const currentColorScheme = useSystemTheme ? systemColorScheme : appColorScheme;
|
||||
|
||||
const theme =
|
||||
Appearance.getColorScheme() !== "dark"
|
||||
? appSettings?.darkThem
|
||||
currentColorScheme === "dark"
|
||||
? appSettings?.darkTheme
|
||||
: appSettings?.lightTheme;
|
||||
|
||||
useThemeEngineStore
|
||||
.getState()
|
||||
.setTheme(
|
||||
theme || (Appearance.getColorScheme() === "dark" ? ThemeDark : ThemeLight)
|
||||
);
|
||||
const currentTheme =
|
||||
theme || (currentColorScheme === "dark" ? ThemeDark : ThemeLight);
|
||||
|
||||
useThemeEngineStore.getState().setTheme(currentTheme);
|
||||
|
||||
export const useShareStore = create((set) => ({
|
||||
theme:
|
||||
theme || (Appearance.getColorScheme() === "dark" ? ThemeDark : ThemeLight),
|
||||
theme: currentTheme,
|
||||
appendNote: null,
|
||||
setAppendNote: (note) => {
|
||||
MMKV.setItem(StorageKeys.appendNote, JSON.stringify(note));
|
||||
|
||||
12
apps/theme-builder/package-lock.json
generated
12
apps/theme-builder/package-lock.json
generated
@@ -210,17 +210,6 @@
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../packages/crypto-worker": {
|
||||
"name": "@notesnook/crypto-worker",
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../crypto",
|
||||
"comlink": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../packages/editor": {
|
||||
"name": "@notesnook/editor",
|
||||
"version": "1.6.1",
|
||||
@@ -1402,7 +1391,6 @@
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/crypto-worker": "file:../../packages/crypto-worker",
|
||||
"@notesnook/desktop": "file:../desktop",
|
||||
"@notesnook/editor": "file:../../packages/editor",
|
||||
"@notesnook/logger": "file:../../packages/logger",
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
ThemeDefinition,
|
||||
THEME_SCOPES,
|
||||
Variants,
|
||||
validateTheme
|
||||
validateTheme,
|
||||
ALPHA_COLORS
|
||||
} from "@notesnook/theme";
|
||||
import { Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import FileSaver from "file-saver";
|
||||
@@ -44,14 +45,15 @@ const JSON_SCHEMA_URL =
|
||||
"https://raw.githubusercontent.com/streetwriters/notesnook-themes/main/schemas/v1.schema.json";
|
||||
const ThemeInfoTemplate: Omit<
|
||||
ThemeDefinition,
|
||||
"authors" | "compatibilityVersion" | "colorScheme" | "codeBlockCSS" | "scopes"
|
||||
"authors" | "compatibilityVersion" | "colorScheme" | "scopes"
|
||||
> = {
|
||||
name: "",
|
||||
id: "",
|
||||
version: 0,
|
||||
license: "",
|
||||
homepage: "",
|
||||
description: ""
|
||||
description: "",
|
||||
codeBlockCSS: "https://github.com/PrismJS/prism-themes/tree/master/themes"
|
||||
};
|
||||
|
||||
export default function ThemeBuilder() {
|
||||
@@ -302,7 +304,7 @@ export default function ThemeBuilder() {
|
||||
paddingX: "10px"
|
||||
}}
|
||||
>
|
||||
{Object.keys(ThemeInfoTemplate).map((key) => {
|
||||
{Object.keys(ThemeInfoTemplate).map((key: string) => {
|
||||
return (
|
||||
<Field
|
||||
key={`${getThemeKey(currentTheme)}-${key}`}
|
||||
@@ -355,7 +357,7 @@ export default function ThemeBuilder() {
|
||||
key="compatibilityVersion"
|
||||
/>
|
||||
|
||||
{authors.map((author, index) => (
|
||||
{authors.map((author, index: number) => (
|
||||
<Flex
|
||||
key={`${getThemeKey(currentTheme)}-${author.name}`}
|
||||
sx={{
|
||||
@@ -524,9 +526,7 @@ export default function ThemeBuilder() {
|
||||
}
|
||||
}}
|
||||
title={
|
||||
/hover|shade|backdrop|textSelection/g.test(
|
||||
colorName
|
||||
)
|
||||
ALPHA_COLORS.includes(colorName)
|
||||
? `Hex RGB & ARGB values both are supported. (e.g. #dbdbdb99)`
|
||||
: `Only Hex RGB values are supported. No Alpha. (e.g. #f33ff3)`
|
||||
}
|
||||
@@ -555,9 +555,7 @@ export default function ThemeBuilder() {
|
||||
);
|
||||
}}
|
||||
title={
|
||||
/hover|shade|backdrop|textSelection/g.test(
|
||||
colorName
|
||||
)
|
||||
ALPHA_COLORS.includes(colorName)
|
||||
? `Only Hex RGB values are supported. No Alpha. (e.g. #f33ff3)`
|
||||
: `Hex RGB & ARGB values both are supported. (e.g. #dbdbdb99)`
|
||||
}
|
||||
@@ -645,13 +643,25 @@ function exportTheme(theme: ThemeDefinition) {
|
||||
}),
|
||||
`${theme.id}.json`
|
||||
);
|
||||
const confirmed = window.confirm(
|
||||
"Do you also want to download code-block.css for this theme?"
|
||||
);
|
||||
if (confirmed) {
|
||||
FileSaver.saveAs(
|
||||
new Blob([theme.codeBlockCSS], {
|
||||
type: "text/plain"
|
||||
}),
|
||||
`code-block.css`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function themeToJSON(theme: ThemeDefinition) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
$schema: JSON_SCHEMA_URL,
|
||||
...theme
|
||||
...theme,
|
||||
codeBlockCSS: undefined
|
||||
},
|
||||
undefined,
|
||||
2
|
||||
@@ -698,7 +708,10 @@ function getThemeFromFormData(
|
||||
function toTitleCase(value: string) {
|
||||
return (
|
||||
value.slice(0, 1).toUpperCase() +
|
||||
value.slice(1).replace(/([A-Z]+)*([A-Z][a-z])/g, "$1 $2")
|
||||
value
|
||||
.slice(1)
|
||||
.replace(/([A-Z]+)/g, " $1")
|
||||
.replace(/([A-Z][a-z])/g, " $1")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
15
apps/vericrypt/package-lock.json
generated
15
apps/vericrypt/package-lock.json
generated
@@ -11,7 +11,6 @@
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.9",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/crypto-worker": "file:../../packages/crypto-worker",
|
||||
"@notesnook/theme": "1.2.0",
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
@@ -42,16 +41,6 @@
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../packages/crypto-worker": {
|
||||
"name": "@notesnook/crypto-worker",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../crypto",
|
||||
"comlink": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
|
||||
@@ -915,10 +904,6 @@
|
||||
"resolved": "../../packages/crypto",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@notesnook/crypto-worker": {
|
||||
"resolved": "../../packages/crypto-worker",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@notesnook/theme": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@notesnook/theme/-/theme-1.2.0.tgz",
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "^7.0.9",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@notesnook/crypto-worker": "file:../../packages/crypto-worker",
|
||||
"@notesnook/theme": "1.2.0",
|
||||
"@theme-ui/components": "^0.14.7",
|
||||
"@theme-ui/core": "^0.14.7",
|
||||
|
||||
@@ -74,3 +74,30 @@ test("restore an encrypted backup", async ({ page }) => {
|
||||
const notebooks = await app.goToNotebooks();
|
||||
expect(await notebooks.isEmpty()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("turning off encrypted backups should ask for password", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.auth.goto();
|
||||
await app.auth.login(USER.CURRENT);
|
||||
const settings = await app.goToSettings();
|
||||
|
||||
await settings.toggleBackupEncryption(USER.CURRENT.password);
|
||||
|
||||
expect(await settings.isBackupEncryptionEnabled(false)).toBe(false);
|
||||
});
|
||||
|
||||
test("turning on encrypted backups should not ask for password", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.auth.goto();
|
||||
await app.auth.login(USER.CURRENT);
|
||||
const settings = await app.goToSettings();
|
||||
await settings.toggleBackupEncryption(USER.CURRENT.password);
|
||||
|
||||
await settings.toggleBackupEncryption();
|
||||
|
||||
expect(await settings.isBackupEncryptionEnabled(true)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -84,16 +84,32 @@ export class SettingsViewModel {
|
||||
return !!(await item?.getTitle());
|
||||
}
|
||||
|
||||
async createBackup(password?: string) {
|
||||
async isBackupEncryptionEnabled(state: boolean) {
|
||||
const encyptBackups = this.page
|
||||
.locator(getTestId("setting-encrypt-backups"))
|
||||
.locator(
|
||||
state ? `input[data-checked="true"]` : `input[data-checked="false"]`
|
||||
);
|
||||
await encyptBackups.waitFor({ state: "visible" });
|
||||
return (await encyptBackups.getAttribute("data-checked")) === "true";
|
||||
}
|
||||
|
||||
async toggleBackupEncryption(password?: string) {
|
||||
const item = await this.navigation.findItem("Backup & export");
|
||||
await item?.click();
|
||||
|
||||
if (password) {
|
||||
const encyptBackups = this.page
|
||||
.locator(getTestId("setting-encrypt-backups"))
|
||||
.locator("label");
|
||||
await encyptBackups.click();
|
||||
}
|
||||
const encyptBackups = this.page
|
||||
.locator(getTestId("setting-encrypt-backups"))
|
||||
.locator("label");
|
||||
|
||||
await encyptBackups.click();
|
||||
|
||||
if (password) await fillPasswordDialog(this.page, password);
|
||||
}
|
||||
|
||||
async createBackup(password?: string) {
|
||||
const item = await this.navigation.findItem("Backup & export");
|
||||
await item?.click();
|
||||
|
||||
const backupData = this.page
|
||||
.locator(getTestId("setting-create-backup"))
|
||||
|
||||
1570
apps/web/package-lock.json
generated
1570
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "2.6.1",
|
||||
"version": "2.6.5",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -1,154 +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/>.
|
||||
*/
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
const map = new Map();
|
||||
|
||||
// This should be called once per download
|
||||
// Each event has a dataChannel that the data will be piped through
|
||||
self.onmessage = (event) => {
|
||||
// We send a heartbeat every x second to keep the
|
||||
// service worker alive if a transferable stream is not sent
|
||||
if (event.data === "ping") {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
const downloadUrl =
|
||||
data.url ||
|
||||
self.registration.scope +
|
||||
Math.random() +
|
||||
"/" +
|
||||
(typeof data === "string" ? data : data.filename);
|
||||
const port = event.ports[0];
|
||||
const metadata = new Array(3); // [stream, data, port]
|
||||
|
||||
metadata[1] = data;
|
||||
metadata[2] = port;
|
||||
|
||||
if (event.data.transferringReadable) {
|
||||
port.onmessage = (evt) => {
|
||||
port.onmessage = null;
|
||||
metadata[0] = evt.data.readableStream;
|
||||
};
|
||||
} else {
|
||||
metadata[0] = createStream(port);
|
||||
}
|
||||
|
||||
map.set(downloadUrl, metadata);
|
||||
port.postMessage({ download: downloadUrl });
|
||||
};
|
||||
|
||||
function createStream(port) {
|
||||
// ReadableStream is only supported by chrome 52
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
// When we receive data on the messageChannel, we write
|
||||
port.onmessage = ({ data }) => {
|
||||
if (data === "end") {
|
||||
return controller.close();
|
||||
}
|
||||
|
||||
if (data === "abort") {
|
||||
controller.error("Aborted the download");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(data);
|
||||
};
|
||||
},
|
||||
cancel(reason) {
|
||||
console.log("user aborted", reason);
|
||||
port.postMessage({ abort: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.onfetch = (event) => {
|
||||
const url = event.request.url;
|
||||
|
||||
// this only works for Firefox
|
||||
if (url.endsWith("/ping")) {
|
||||
return event.respondWith(new Response("pong"));
|
||||
}
|
||||
|
||||
const metadata = map.get(url);
|
||||
if (!metadata) return null;
|
||||
|
||||
const [stream, data, port] = metadata;
|
||||
|
||||
map.delete(url);
|
||||
|
||||
// Not comfortable letting any user control all headers
|
||||
// so we only copy over the length & disposition
|
||||
const responseHeaders = new Headers({
|
||||
"Content-Type": "application/octet-stream; charset=utf-8",
|
||||
|
||||
// To be on the safe side, The link can be opened in a iframe.
|
||||
// but octet-stream should stop it.
|
||||
"Content-Security-Policy": "default-src 'none'",
|
||||
"X-Content-Security-Policy": "default-src 'none'",
|
||||
"X-WebKit-CSP": "default-src 'none'",
|
||||
"X-XSS-Protection": "1; mode=block"
|
||||
});
|
||||
|
||||
let headers = new Headers(data.headers || {});
|
||||
|
||||
if (headers.has("Content-Length")) {
|
||||
responseHeaders.set("Content-Length", headers.get("Content-Length"));
|
||||
}
|
||||
|
||||
if (headers.has("Content-Disposition")) {
|
||||
responseHeaders.set(
|
||||
"Content-Disposition",
|
||||
headers.get("Content-Disposition")
|
||||
);
|
||||
}
|
||||
|
||||
// data, data.filename and size should not be used anymore
|
||||
if (data.size) {
|
||||
console.warn("Depricated");
|
||||
responseHeaders.set("Content-Length", data.size);
|
||||
}
|
||||
|
||||
let fileName = typeof data === "string" ? data : data.filename;
|
||||
if (fileName) {
|
||||
console.warn("Depricated");
|
||||
// Make filename RFC5987 compatible
|
||||
fileName = encodeURIComponent(fileName)
|
||||
.replace(/['()]/g, escape)
|
||||
.replace(/\*/g, "%2A");
|
||||
responseHeaders.set(
|
||||
"Content-Disposition",
|
||||
"attachment; filename*=UTF-8''" + fileName
|
||||
);
|
||||
}
|
||||
|
||||
event.respondWith(new Response(stream, { headers: responseHeaders }));
|
||||
|
||||
port.postMessage({ debug: "Download started" });
|
||||
};
|
||||
@@ -11,7 +11,7 @@
|
||||
.allotment-module_splitView__L-yRc.allotment-module_separatorBorder__x-rDS
|
||||
> .allotment-module_splitViewContainer__rQnVa
|
||||
> .allotment-module_splitViewView__MGZ6O:not(:first-child)::before {
|
||||
background-color: var(--border);
|
||||
background-color: transparent;
|
||||
}
|
||||
/* open-sans-regular - vietnamese_latin-ext_latin_hebrew_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
@@ -138,25 +138,25 @@ textarea,
|
||||
}
|
||||
|
||||
.rpv-core__text-layer-text::selection {
|
||||
background-color: var(--textSelection) !important;
|
||||
background-color: var(--background-selected) !important;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.rpv-core__text-layer-text::-moz-selection {
|
||||
/* Code for Firefox */
|
||||
background-color: var(--textSelection) !important;
|
||||
background-color: var(--background-selected) !important;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
/* Code for Firefox */
|
||||
background-color: var(--textSelection);
|
||||
color: var(--paragraph);
|
||||
background-color: var(--background-selected);
|
||||
color: var(--paragraph-selected);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--textSelection);
|
||||
color: var(--paragraph);
|
||||
background-color: var(--background-selected);
|
||||
color: var(--paragraph-selected);
|
||||
}
|
||||
|
||||
*::-moz-focus-inner {
|
||||
|
||||
@@ -177,7 +177,8 @@ function DesktopAppContents({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
bg: "background"
|
||||
bg: "background",
|
||||
borderRight: "1px solid var(--separator)"
|
||||
}}
|
||||
>
|
||||
{isAppLoaded && <CachedRouter />}
|
||||
|
||||
@@ -63,3 +63,17 @@ function attachListener(event: string) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function createWritableStream(path: string) {
|
||||
const resolvedPath = await desktop.integration.resolvePath.query({
|
||||
filePath: path
|
||||
});
|
||||
if (!resolvedPath) throw new Error("invalid path.");
|
||||
const fs = require("fs");
|
||||
const { Writable } = require("stream");
|
||||
return new WritableStream(
|
||||
Writable.toWeb(
|
||||
fs.createWriteStream(resolvedPath, { encoding: "utf-8" })
|
||||
).getWriter()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +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 { createWriteStream } from "../../utils/stream-saver";
|
||||
import { type desktop as bridge } from "./index.desktop";
|
||||
|
||||
export const desktop: typeof bridge | undefined = undefined;
|
||||
export function createWritableStream(filename: string) {
|
||||
return createWriteStream(filename);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export async function exportToPDF(
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const iframe = document.createElement("iframe");
|
||||
iframe.srcdoc = content.replaceAll(/<p(.+?)><\/p>/gm, "<p$1><br/></p>");
|
||||
|
||||
iframe.srcdoc = content;
|
||||
iframe.style.position = "fixed";
|
||||
iframe.style.right = "0";
|
||||
iframe.style.bottom = "0";
|
||||
|
||||
@@ -1,217 +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 {
|
||||
showFeatureDialog,
|
||||
showLoadingDialog,
|
||||
showPasswordDialog,
|
||||
showReminderDialog
|
||||
} from "../common/dialog-controller";
|
||||
import Config from "../utils/config";
|
||||
import { hashNavigate, getCurrentHash } from "../navigation";
|
||||
import { db } from "./db";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { store as userstore } from "../stores/user-store";
|
||||
import FileSaver from "file-saver";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { SUBSCRIPTION_STATUS } from "./constants";
|
||||
import { showFilePicker } from "../utils/file-picker";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PATHS } from "@notesnook/desktop";
|
||||
import { TaskManager } from "./task-manager";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { desktop } from "./desktop-bridge";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
onClick: () => hashNavigate("/notebooks/create", { replace: true })
|
||||
},
|
||||
topics: {
|
||||
title: "Create a topic",
|
||||
onClick: () => hashNavigate(`/topics/create`, { replace: true })
|
||||
},
|
||||
tags: {
|
||||
title: "Create a tag",
|
||||
onClick: () => hashNavigate(`/tags/create`, { replace: true })
|
||||
},
|
||||
reminders: {
|
||||
title: "Add a reminder",
|
||||
onClick: () => hashNavigate(`/reminders/create`, { replace: true })
|
||||
}
|
||||
};
|
||||
|
||||
export async function introduceFeatures() {
|
||||
const hash = getCurrentHash().replace("#", "");
|
||||
if (!!hash || IS_TESTING) return;
|
||||
const features = [];
|
||||
for (let feature of features) {
|
||||
if (!Config.get(`feature:${feature}`)) {
|
||||
await showFeatureDialog(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup() {
|
||||
const encryptBackups =
|
||||
userstore.get().isLoggedIn && Config.get("encryptBackups", false);
|
||||
const data = await showLoadingDialog({
|
||||
title: "Creating backup",
|
||||
subtitle: "We are creating a backup of your data. Please wait...",
|
||||
action: async () => {
|
||||
return await db.backup.export("web", encryptBackups);
|
||||
}
|
||||
});
|
||||
if (!data) {
|
||||
showToast("error", "Could not create a backup of your data.");
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = sanitizeFilename(`notesnook-backup-${getFormattedDate()}`);
|
||||
|
||||
const ext = "nnbackup";
|
||||
if (IS_DESKTOP_APP) {
|
||||
const directory = Config.get(
|
||||
"backupStorageLocation",
|
||||
PATHS.backupsDirectory
|
||||
);
|
||||
const filePath = `${directory}/${filename}.${ext}`;
|
||||
await desktop?.integration.saveFile.query({ filePath, data });
|
||||
showToast("success", `Backup saved at ${filePath}.`);
|
||||
} else {
|
||||
FileSaver.saveAs(new Blob([Buffer.from(data)]), `${filename}.${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectBackupFile() {
|
||||
const file = await showFilePicker({
|
||||
acceptedFileTypes: ".nnbackup,application/json"
|
||||
});
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
const backup = await new Promise((resolve) => {
|
||||
reader.addEventListener("load", (event) => {
|
||||
const text = event.target.result;
|
||||
try {
|
||||
resolve(JSON.parse(text));
|
||||
} catch (e) {
|
||||
alert(
|
||||
"Error: Could not read the backup file provided. Either it's corrupted or invalid."
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
return { file, backup };
|
||||
}
|
||||
|
||||
export async function importBackup() {
|
||||
const { backup } = await selectBackupFile();
|
||||
await restoreBackupFile(backup);
|
||||
}
|
||||
|
||||
export async function restoreBackupFile(backup) {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
await showPasswordDialog("ask_backup_password", async ({ password }) => {
|
||||
const error = await restoreWithProgress(backup, password);
|
||||
return !error;
|
||||
});
|
||||
} else {
|
||||
await restoreWithProgress(backup);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreWithProgress(backup, password) {
|
||||
await TaskManager.startTask({
|
||||
title: "Restoring backup",
|
||||
subtitle: "This might take a while",
|
||||
type: "modal",
|
||||
action: (report) => {
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
({ collection, total, current }) => {
|
||||
report({
|
||||
text: `Restoring ${collection}...`,
|
||||
current,
|
||||
total
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
report({ text: `Restoring...` });
|
||||
return restore(backup, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyAccount() {
|
||||
if (!(await db.user.getUser())) return true;
|
||||
return showPasswordDialog("verify_account", ({ password }) => {
|
||||
return db.user.verifyPassword(password);
|
||||
});
|
||||
}
|
||||
|
||||
export function totalSubscriptionConsumed(user) {
|
||||
if (!user) return 0;
|
||||
const start = user.subscription?.start;
|
||||
const end = user.subscription?.expiry;
|
||||
if (!start || !end) return 0;
|
||||
|
||||
const total = end - start;
|
||||
const consumed = Date.now() - start;
|
||||
|
||||
return Math.round((consumed / total) * 100);
|
||||
}
|
||||
|
||||
export async function showUpgradeReminderDialogs() {
|
||||
if (IS_TESTING) return;
|
||||
|
||||
const user = userstore.get().user;
|
||||
if (!user || !user.subscription || user.subscription?.expiry === 0) return;
|
||||
|
||||
const consumed = totalSubscriptionConsumed(user);
|
||||
const isTrial = user?.subscription?.type === SUBSCRIPTION_STATUS.TRIAL;
|
||||
const isBasic = user?.subscription?.type === SUBSCRIPTION_STATUS.BASIC;
|
||||
if (isBasic && consumed >= 100) {
|
||||
await showReminderDialog("trialexpired");
|
||||
} else if (isTrial && consumed >= 75) {
|
||||
await showReminderDialog("trialexpiring");
|
||||
}
|
||||
}
|
||||
|
||||
async function restore(backup, password) {
|
||||
try {
|
||||
await db.backup.import(backup, password);
|
||||
showToast("success", "Backup restored!");
|
||||
} catch (e) {
|
||||
logger.error(e, "Could not restore the backup");
|
||||
showToast("error", `Could not restore the backup: ${e.message || e}`);
|
||||
}
|
||||
}
|
||||
288
apps/web/src/common/index.ts
Normal file
288
apps/web/src/common/index.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
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 {
|
||||
showFeatureDialog,
|
||||
showPasswordDialog,
|
||||
showReminderDialog
|
||||
} from "./dialog-controller";
|
||||
import Config from "../utils/config";
|
||||
import { hashNavigate, getCurrentHash } from "../navigation";
|
||||
import { db } from "./db";
|
||||
import { sanitizeFilename } from "@notesnook/common";
|
||||
import { store as userstore } from "../stores/user-store";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { SUBSCRIPTION_STATUS } from "./constants";
|
||||
import { readFile, showFilePicker } from "../utils/file-picker";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PATHS } from "@notesnook/desktop";
|
||||
import { TaskManager } from "./task-manager";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { createWritableStream } from "./desktop-bridge";
|
||||
import { ZipStream } from "../utils/streams/zip-stream";
|
||||
import { FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { Reader } from "../utils/zip-reader";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Add a note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
},
|
||||
notebooks: {
|
||||
title: "Create a notebook",
|
||||
onClick: () => hashNavigate("/notebooks/create", { replace: true })
|
||||
},
|
||||
topics: {
|
||||
title: "Create a topic",
|
||||
onClick: () => hashNavigate(`/topics/create`, { replace: true })
|
||||
},
|
||||
tags: {
|
||||
title: "Create a tag",
|
||||
onClick: () => hashNavigate(`/tags/create`, { replace: true })
|
||||
},
|
||||
reminders: {
|
||||
title: "Add a reminder",
|
||||
onClick: () => hashNavigate(`/reminders/create`, { replace: true })
|
||||
}
|
||||
};
|
||||
|
||||
export async function introduceFeatures() {
|
||||
const hash = getCurrentHash().replace("#", "");
|
||||
if (!!hash || IS_TESTING) return;
|
||||
const features: FeatureKeys[] = [];
|
||||
for (const feature of features) {
|
||||
if (!Config.get(`feature:${feature}`)) {
|
||||
await showFeatureDialog(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_CONTEXT = { colors: [], tags: [], notebook: {} };
|
||||
|
||||
export async function createBackup() {
|
||||
const encryptBackups =
|
||||
userstore.get().isLoggedIn && Config.get("encryptBackups", false);
|
||||
|
||||
const filename = sanitizeFilename(
|
||||
`notesnook-backup-${getFormattedDate(Date.now())}`
|
||||
);
|
||||
const directory = Config.get("backupStorageLocation", PATHS.backupsDirectory);
|
||||
const ext = "nnbackupz";
|
||||
const filePath = IS_DESKTOP_APP
|
||||
? `${directory}/${filename}.${ext}`
|
||||
: `${filename}.${ext}`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const error = await TaskManager.startTask<Error | void>({
|
||||
type: "modal",
|
||||
title: "Creating backup",
|
||||
subtitle: "We are creating a backup of your data. Please wait...",
|
||||
action: async (report) => {
|
||||
const writeStream = await createWritableStream(filePath);
|
||||
|
||||
await new ReadableStream({
|
||||
start() {},
|
||||
async pull(controller) {
|
||||
for await (const file of db.backup!.export("web", encryptBackups)) {
|
||||
report({
|
||||
text: `Saving chunk ${file.path}`
|
||||
});
|
||||
controller.enqueue({
|
||||
path: file.path,
|
||||
data: encoder.encode(file.data)
|
||||
});
|
||||
}
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
.pipeThrough(new ZipStream())
|
||||
.pipeTo(writeStream);
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
showToast(
|
||||
"error",
|
||||
`Could not create a backup of your data: ${(error as Error).message}`
|
||||
);
|
||||
console.error(error);
|
||||
} else {
|
||||
showToast("success", `Backup saved at ${filePath}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function selectBackupFile() {
|
||||
const file = await showFilePicker({
|
||||
acceptedFileTypes: ".nnbackup,.nnbackupz"
|
||||
});
|
||||
if (!file) return;
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function importBackup() {
|
||||
const backupFile = await selectBackupFile();
|
||||
if (!backupFile) return;
|
||||
await restoreBackupFile(backupFile);
|
||||
}
|
||||
|
||||
export async function restoreBackupFile(backupFile: File) {
|
||||
const isLegacy = !backupFile.name.endsWith(".nnbackupz");
|
||||
|
||||
if (isLegacy) {
|
||||
const backup = JSON.parse(await readFile(backupFile));
|
||||
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
await showPasswordDialog("ask_backup_password", async ({ password }) => {
|
||||
if (!password) return false;
|
||||
const error = await restoreWithProgress(backup, password);
|
||||
return !error;
|
||||
});
|
||||
} else {
|
||||
await restoreWithProgress(backup);
|
||||
}
|
||||
} else {
|
||||
const error = await TaskManager.startTask<Error | void>({
|
||||
title: "Restoring backup",
|
||||
subtitle: "Please wait while we restore your backup...",
|
||||
type: "modal",
|
||||
action: async (report) => {
|
||||
let cachedPassword: string | undefined = undefined;
|
||||
const { read, totalFiles } = await Reader(backupFile);
|
||||
let filesProcessed = 0;
|
||||
for await (const entry of read()) {
|
||||
if (filesProcessed++ === 0 && entry.name !== ".nnbackup")
|
||||
throw new Error("Invalid backup.");
|
||||
else if (entry.name === ".nnbackup") continue;
|
||||
|
||||
const backup = JSON.parse(await entry.text());
|
||||
if (backup.encrypted) {
|
||||
if (!cachedPassword) {
|
||||
const result = await showPasswordDialog(
|
||||
"ask_backup_password",
|
||||
async ({ password }) => {
|
||||
if (!password) return false;
|
||||
await db.backup?.import(backup, password);
|
||||
cachedPassword = password;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
if (!result) break;
|
||||
} else await db.backup?.import(backup, cachedPassword);
|
||||
} else {
|
||||
await db.backup?.import(backup, null);
|
||||
}
|
||||
|
||||
report({
|
||||
total: totalFiles,
|
||||
text: `Processed ${entry.name}`,
|
||||
current: filesProcessed
|
||||
});
|
||||
}
|
||||
await db.initCollections();
|
||||
}
|
||||
});
|
||||
if (error) {
|
||||
console.error(error);
|
||||
showToast("error", `Failed to restore backup: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreWithProgress(
|
||||
backup: Record<string, unknown>,
|
||||
password?: string
|
||||
) {
|
||||
return await TaskManager.startTask<Error | void>({
|
||||
title: "Restoring backup",
|
||||
subtitle: "This might take a while",
|
||||
type: "modal",
|
||||
action: (report) => {
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
({
|
||||
collection,
|
||||
total,
|
||||
current
|
||||
}: {
|
||||
collection: string;
|
||||
total: number;
|
||||
current: number;
|
||||
}) => {
|
||||
report({
|
||||
text: `Restoring ${collection}...`,
|
||||
current,
|
||||
total
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
report({ text: `Restoring...` });
|
||||
return restore(backup, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function verifyAccount() {
|
||||
if (!(await db.user?.getUser())) return true;
|
||||
return showPasswordDialog("verify_account", ({ password }) => {
|
||||
return db.user?.verifyPassword(password) || false;
|
||||
});
|
||||
}
|
||||
|
||||
export function totalSubscriptionConsumed(user: User) {
|
||||
if (!user) return 0;
|
||||
const start = user.subscription?.start;
|
||||
const end = user.subscription?.expiry;
|
||||
if (!start || !end) return 0;
|
||||
|
||||
const total = end - start;
|
||||
const consumed = Date.now() - start;
|
||||
|
||||
return Math.round((consumed / total) * 100);
|
||||
}
|
||||
|
||||
export async function showUpgradeReminderDialogs() {
|
||||
if (IS_TESTING) return;
|
||||
|
||||
const user = userstore.get().user;
|
||||
if (!user || !user.subscription || user.subscription?.expiry === 0) return;
|
||||
|
||||
const consumed = totalSubscriptionConsumed(user);
|
||||
const isTrial = user?.subscription?.type === SUBSCRIPTION_STATUS.TRIAL;
|
||||
const isBasic = user?.subscription?.type === SUBSCRIPTION_STATUS.BASIC;
|
||||
if (isBasic && consumed >= 100) {
|
||||
await showReminderDialog("trialexpired");
|
||||
} else if (isTrial && consumed >= 75) {
|
||||
await showReminderDialog("trialexpiring");
|
||||
}
|
||||
}
|
||||
|
||||
async function restore(backup: Record<string, unknown>, password?: string) {
|
||||
try {
|
||||
await db.backup?.import(backup, password);
|
||||
showToast("success", "Backup restored!");
|
||||
} catch (e) {
|
||||
logger.error(e as Error, "Could not restore the backup");
|
||||
showToast(
|
||||
"error",
|
||||
`Could not restore the backup: ${(e as Error).message || e}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -159,10 +159,9 @@ function ListContainer(props: ListContainerProps) {
|
||||
onKeyDown={(e) => onKeyDown(e.nativeEvent)}
|
||||
components={{
|
||||
Scroller: CustomScrollbarsVirtualList,
|
||||
Item: (props) => (
|
||||
Item: ({ item: _item, ...props }) => (
|
||||
<div
|
||||
{...props}
|
||||
style={{ paddingBottom: 1 }}
|
||||
onFocus={() => onFocus(props["data-item-index"])}
|
||||
onMouseDown={(e) =>
|
||||
onMouseDown(e.nativeEvent, props["data-item-index"])
|
||||
|
||||
@@ -31,8 +31,8 @@ import { alpha } from "@theme-ui/color";
|
||||
|
||||
type ListItemProps = {
|
||||
colors?: {
|
||||
heading: SchemeColors;
|
||||
accent: SchemeColors;
|
||||
paragraph: SchemeColors;
|
||||
background: SchemeColors;
|
||||
};
|
||||
isFocused?: boolean;
|
||||
@@ -53,9 +53,9 @@ type ListItemProps = {
|
||||
|
||||
function ListItem(props: ListItemProps) {
|
||||
const {
|
||||
colors: { paragraph, background, accent } = {
|
||||
colors: { heading, background, accent } = {
|
||||
heading: "heading",
|
||||
accent: "accent",
|
||||
paragraph: "paragraph",
|
||||
background: "background"
|
||||
},
|
||||
isFocused,
|
||||
@@ -76,6 +76,7 @@ function ListItem(props: ListItemProps) {
|
||||
? store.selectedItems.length > 1 && isInSelection
|
||||
: isInSelection;
|
||||
});
|
||||
const selected = isSelected || isMenuTarget || isFocused;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
@@ -112,7 +113,7 @@ function ListItem(props: ListItemProps) {
|
||||
pl={1}
|
||||
pr={2}
|
||||
py={1}
|
||||
mb={isCompact ? 0 : 1}
|
||||
mb={isCompact ? 0 : 0}
|
||||
tabIndex={-1}
|
||||
dir="auto"
|
||||
sx={{
|
||||
@@ -130,24 +131,20 @@ function ListItem(props: ListItemProps) {
|
||||
|
||||
borderLeft: "5px solid",
|
||||
borderLeftColor: isFocused ? accent : "transparent",
|
||||
ml: "2px",
|
||||
mr: "1px",
|
||||
|
||||
backgroundColor:
|
||||
isSelected || isMenuTarget || isFocused
|
||||
? "background-selected"
|
||||
: background,
|
||||
backgroundColor: selected ? "background-selected" : background,
|
||||
|
||||
":hover": {
|
||||
backgroundColor: isSelected ? "hover-selected" : "hover"
|
||||
backgroundColor: selected ? "hover-selected" : "hover"
|
||||
},
|
||||
":focus": {
|
||||
backgroundColor: isSelected ? "hover-selected" : "hover"
|
||||
backgroundColor: selected ? "hover-selected" : "hover"
|
||||
},
|
||||
":focus-visible": {
|
||||
outline: `1px solid`,
|
||||
outlineColor: accent === "accent" ? "accent" : alpha("accent", 0.7),
|
||||
backgroundColor: isSelected ? "textSelection" : background
|
||||
backgroundColor:
|
||||
isSelected || isFocused ? "background-selected" : background
|
||||
}
|
||||
}}
|
||||
onKeyPress={(e) => {
|
||||
@@ -172,7 +169,8 @@ function ListItem(props: ListItemProps) {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
fontWeight: isCompact || isSimple ? "body" : "bold",
|
||||
color: paragraph,
|
||||
color:
|
||||
selected && heading === "heading" ? `${heading}-selected` : heading,
|
||||
display: "block"
|
||||
}}
|
||||
>
|
||||
@@ -185,6 +183,7 @@ function ListItem(props: ListItemProps) {
|
||||
variant="body"
|
||||
data-test-id={`description`}
|
||||
sx={{
|
||||
color: selected ? "paragraph-selected" : "paragraph",
|
||||
lineHeight: `1.2rem`,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
|
||||
@@ -141,7 +141,20 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
}, [location, previousLocation, state]);
|
||||
|
||||
return (
|
||||
<ScopedThemeProvider scope="navigationMenu" injectCssVars={false}>
|
||||
<ScopedThemeProvider
|
||||
scope="navigationMenu"
|
||||
sx={{
|
||||
display: "flex",
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
bg: "background",
|
||||
borderRight: "1px solid var(--separator)"
|
||||
}}
|
||||
>
|
||||
<AnimatedFlex
|
||||
id="navigation-menu"
|
||||
data-test-id="navigation-menu"
|
||||
@@ -154,14 +167,10 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
}}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
bg: "background"
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
px={0}
|
||||
>
|
||||
@@ -218,7 +227,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
/>
|
||||
))}
|
||||
<Box
|
||||
bg="border"
|
||||
bg="separator"
|
||||
my={1}
|
||||
sx={{ width: "85%", height: "0.8px", alignSelf: "center" }}
|
||||
/>
|
||||
@@ -301,7 +310,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
<Button
|
||||
variant={"icon"}
|
||||
title="Toggle dark/light mode"
|
||||
sx={{ borderLeft: "1px solid var(--border)" }}
|
||||
sx={{ borderLeft: "1px solid var(--separator)" }}
|
||||
onClick={() => {
|
||||
setFollowSystemTheme(false);
|
||||
toggleNightMode();
|
||||
|
||||
@@ -131,7 +131,7 @@ function Note(props: NoteProps) {
|
||||
}}
|
||||
colors={{
|
||||
accent: primary,
|
||||
paragraph: note.color ? primary : "heading",
|
||||
heading: note.color ? primary : "heading",
|
||||
background: "background"
|
||||
}}
|
||||
menuItems={menuItems}
|
||||
@@ -179,7 +179,7 @@ function Note(props: NoteProps) {
|
||||
<Flex
|
||||
sx={{
|
||||
fontSize: "subBody",
|
||||
color: "var(--paragraph-secondary)",
|
||||
color: "paragraph-secondary",
|
||||
alignItems: "center",
|
||||
gap: 1
|
||||
}}
|
||||
|
||||
@@ -19,13 +19,50 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { Context, useTip } from "../../hooks/use-tip";
|
||||
import { Info } from "../icons";
|
||||
import { Info, Sync } from "../icons";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { toTitleCase } from "@notesnook/common";
|
||||
|
||||
type PlaceholderProps = { context: Context; text?: string };
|
||||
function Placeholder(props: PlaceholderProps) {
|
||||
const { context, text } = props;
|
||||
const tip = useTip(context);
|
||||
const syncStatus = useAppStore((store) => store.syncStatus);
|
||||
|
||||
if (syncStatus.key === "syncing" && context === "notes") {
|
||||
return (
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{
|
||||
position: "relative",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "flex-start",
|
||||
alignSelf: "stretch",
|
||||
px: 6
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
border: "1px solid var(--accent)",
|
||||
alignItems: "center",
|
||||
borderRadius: 50,
|
||||
p: 1,
|
||||
py: "1.5px"
|
||||
}}
|
||||
>
|
||||
<Sync color="accent" size={12} sx={{ mr: "small" }} />
|
||||
<Text variant="subBody" sx={{ fontSize: 10 }} color="accent">
|
||||
Syncing your {context}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Text variant="subBody" sx={{ fontSize: "body", mt: 1 }}>
|
||||
{toTitleCase(syncStatus.type || "syncing")}ing {syncStatus.progress}{" "}
|
||||
items
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
if (!tip) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -102,7 +102,7 @@ function Header(props: RouteContainerProps) {
|
||||
{!isMobile && buttons?.create && (
|
||||
<Plus
|
||||
data-test-id={`${type}-action-button`}
|
||||
color="white"
|
||||
color="accentForeground"
|
||||
size={18}
|
||||
sx={{
|
||||
bg: "accent",
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
import useStatus from "../../hooks/use-status";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { checkForUpdate, installUpdate } from "../../utils/updater";
|
||||
import { toTitleCase } from "@notesnook/common";
|
||||
|
||||
function StatusBar() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -56,7 +57,7 @@ function StatusBar() {
|
||||
bg="background"
|
||||
sx={{
|
||||
borderTop: "1px solid",
|
||||
borderTopColor: "border",
|
||||
borderTopColor: "separator",
|
||||
justifyContent: "space-between",
|
||||
display: ["none", "none", "flex"]
|
||||
}}
|
||||
@@ -230,9 +231,7 @@ function SyncStatus() {
|
||||
title={status.tooltip}
|
||||
data-test-id={`sync-status-${status.key}`}
|
||||
>
|
||||
{syncStatus.progress ? (
|
||||
<Text variant={"subBody"}>{syncStatus.progress}</Text>
|
||||
) : (
|
||||
{!syncStatus.progress && (
|
||||
<status.icon
|
||||
size={12}
|
||||
rotate={status.loading}
|
||||
@@ -253,6 +252,12 @@ function SyncStatus() {
|
||||
</>
|
||||
) : null}
|
||||
</Text>
|
||||
{syncStatus.progress && (
|
||||
<Text variant={"subBody"} ml={1}>
|
||||
{" "}
|
||||
({syncStatus.progress})
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -309,7 +314,7 @@ const syncStatusFilters: SyncStatusFilter[] = [
|
||||
isActive: (syncStatus) => syncStatus === "syncing",
|
||||
icon: Sync,
|
||||
loading: true,
|
||||
text: ({ type }) => <>{type || "sync"}ing</>,
|
||||
text: ({ type }) => <>{toTitleCase(type || "sync")}ing</>,
|
||||
tooltip: "Syncing your notes..."
|
||||
},
|
||||
{
|
||||
|
||||
@@ -127,7 +127,7 @@ export function ThemePreview(props: ThemePreviewProps) {
|
||||
Notes
|
||||
</Text>
|
||||
<Plus
|
||||
color="white"
|
||||
color={theme.previewColors.list.accentForeground}
|
||||
size={8}
|
||||
sx={{
|
||||
bg: theme.previewColors.list.accent,
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Text, Flex, Button } from "@theme-ui/components";
|
||||
import { Text, Flex, Button, Link } from "@theme-ui/components";
|
||||
import { Cross, Check, Loading } from "../../components/icons";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
@@ -461,7 +461,21 @@ function SelectedPlan(props: SelectedPlanProps) {
|
||||
>
|
||||
{metadata.title}
|
||||
</Text>
|
||||
|
||||
{plan.period === "education" && (
|
||||
<Link
|
||||
href="https://notesnook.com/education"
|
||||
target="_blank"
|
||||
variant="text.body"
|
||||
mt={1}
|
||||
sx={{
|
||||
textDecorationColor: "primary",
|
||||
color: "primary",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Apply here to get your Education discount code.
|
||||
</Link>
|
||||
)}
|
||||
{pricingInfo ? (
|
||||
<>
|
||||
<Text data-test-id={`checkout-plan-country-${pricingInfo.country}`} />
|
||||
@@ -602,7 +616,9 @@ function CheckoutPricing(props: CheckoutPricingProps) {
|
||||
{currentTotal}
|
||||
</Text>
|
||||
<Text as="div" sx={{ fontSize: "body", color: "paragraph" }}>
|
||||
{isDiscounted
|
||||
{period === "education" && discount.amount > 0
|
||||
? "for one year"
|
||||
: isDiscounted
|
||||
? "forever"
|
||||
: `first ${getFullPeriod(period)} then ${recurringTotal}`}
|
||||
</Text>
|
||||
|
||||
@@ -141,7 +141,11 @@ function RecurringPricing(props: RecurringPricingProps) {
|
||||
}
|
||||
|
||||
export function formatPeriod(period: Period) {
|
||||
return period === "monthly" ? "/mo" : period === "yearly" ? "/yr" : "";
|
||||
return period === "monthly"
|
||||
? "/mo"
|
||||
: period === "yearly" || period === "education"
|
||||
? "/yr"
|
||||
: "";
|
||||
}
|
||||
|
||||
export function getFullPeriod(period: Period) {
|
||||
|
||||
@@ -26,6 +26,15 @@ type PlanMetadata = {
|
||||
subtitle: string;
|
||||
};
|
||||
|
||||
export const EDUCATION_PLAN: Plan = {
|
||||
id: import.meta.env.DEV || IS_TESTING ? "50305" : "658759",
|
||||
period: "education",
|
||||
country: "US",
|
||||
currency: "USD",
|
||||
discount: 0,
|
||||
price: { gross: 9.99, net: 0, tax: 0 }
|
||||
};
|
||||
|
||||
export const DEFAULT_PLANS: Plan[] = [
|
||||
{
|
||||
period: "monthly",
|
||||
@@ -42,12 +51,17 @@ export const DEFAULT_PLANS: Plan[] = [
|
||||
discount: 0,
|
||||
id: import.meta.env.DEV || IS_TESTING ? "50305" : "658759",
|
||||
price: { gross: 49.99, net: 0, tax: 0 }
|
||||
}
|
||||
},
|
||||
EDUCATION_PLAN
|
||||
];
|
||||
|
||||
export const PLAN_METADATA: Record<Period, PlanMetadata> = {
|
||||
monthly: { title: "Monthly", subtitle: `Pay once a month.` },
|
||||
yearly: { title: "Yearly", subtitle: `Pay once a year.` }
|
||||
yearly: { title: "Yearly", subtitle: `Pay once a year.` },
|
||||
education: {
|
||||
title: "Education",
|
||||
subtitle: "Special offer for students & teachers."
|
||||
}
|
||||
};
|
||||
|
||||
let CACHED_PLANS: Plan[];
|
||||
@@ -59,6 +73,7 @@ export async function getPlans(): Promise<Plan[] | null> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
const plans = (await response.json()) as Plan[];
|
||||
plans.push(EDUCATION_PLAN);
|
||||
CACHED_PLANS = plans;
|
||||
return plans;
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
export type Period = "monthly" | "yearly";
|
||||
export type Period = "monthly" | "yearly" | "education";
|
||||
|
||||
export enum PaddleEvents {
|
||||
/** Checkout has been initialized on the page **/
|
||||
|
||||
@@ -91,19 +91,9 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Custom themes",
|
||||
title: "Much faster & more reliable sync",
|
||||
subtitle:
|
||||
"You can now customize how each part of Notesnook looks to your hearts desire. To access all available themes just go to Settings > Appearance."
|
||||
},
|
||||
{
|
||||
title: "Improved copy/pasting formatting",
|
||||
subtitle:
|
||||
"The copy/pasting has been significantly improved and should now cause much less frustration due to bad formatting. Give it a try!"
|
||||
},
|
||||
{
|
||||
title: "More reliable attachment uploads",
|
||||
subtitle:
|
||||
"No more sudden and unexpected error popups due to broken attachment uploads. We have fixed lots of bugs in this release to make upload of attachment as smooth as possible."
|
||||
"Syncing should now work more reliably & also much, much faster. Like 10x faster than before."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
|
||||
@@ -49,7 +49,10 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
useSettingStore.getState().encryptBackups
|
||||
)
|
||||
useSettingStore.getState().toggleEncryptBackups();
|
||||
if (await verifyAccount()) await createBackup();
|
||||
const verified =
|
||||
useSettingStore.getState().encryptBackups ||
|
||||
(await verifyAccount());
|
||||
if (verified) await createBackup();
|
||||
},
|
||||
variant: "secondary"
|
||||
}
|
||||
@@ -92,10 +95,15 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
],
|
||||
selectedOption: () =>
|
||||
useSettingStore.getState().backupReminderOffset.toString(),
|
||||
onSelectionChanged: (value) =>
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setBackupReminderOffset(parseInt(value))
|
||||
onSelectionChanged: async (value) => {
|
||||
const verified =
|
||||
useSettingStore.getState().encryptBackups ||
|
||||
(await verifyAccount());
|
||||
if (verified)
|
||||
useSettingStore
|
||||
.getState()
|
||||
.setBackupReminderOffset(parseInt(value));
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -114,7 +122,12 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
isToggled: () =>
|
||||
!!useUserStore.getState().isLoggedIn &&
|
||||
useSettingStore.getState().encryptBackups,
|
||||
toggle: () => useSettingStore.getState().toggleEncryptBackups()
|
||||
toggle: async () => {
|
||||
const verified =
|
||||
!useSettingStore.getState().encryptBackups ||
|
||||
(await verifyAccount());
|
||||
if (verified) useSettingStore.getState().toggleEncryptBackups();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -128,6 +141,11 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
type: "button",
|
||||
title: "Select directory",
|
||||
action: async () => {
|
||||
const verified =
|
||||
useSettingStore.getState().encryptBackups ||
|
||||
(await verifyAccount());
|
||||
if (!verified) return;
|
||||
|
||||
const backupStorageLocation =
|
||||
useSettingStore.getState().backupStorageLocation ||
|
||||
PATHS.backupsDirectory;
|
||||
@@ -167,11 +185,11 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
selectedOption: () => "-",
|
||||
onSelectionChanged: async (value) => {
|
||||
if (!db.notes || value === "-") return;
|
||||
|
||||
await exportNotes(
|
||||
value as "txt" | "md" | "html",
|
||||
db.notes.all.map((n) => n.id)
|
||||
);
|
||||
if (await verifyAccount())
|
||||
await exportNotes(
|
||||
value as "txt" | "md" | "html",
|
||||
db.notes.all.map((n) => n.id)
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { httpBatchLink } from "@trpc/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Box, Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import { CheckCircleOutline, Loading } from "../../../components/icons";
|
||||
import {
|
||||
@@ -74,7 +74,6 @@ function ThemesList() {
|
||||
);
|
||||
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
const parentScrollRef = useRef<HTMLElement>();
|
||||
const setCurrentTheme = useThemeStore((store) => store.setTheme);
|
||||
const user = useUserStore((store) => store.user);
|
||||
const darkTheme = useThemeStore((store) => store.darkTheme);
|
||||
@@ -198,11 +197,6 @@ function ThemesList() {
|
||||
</Flex>
|
||||
|
||||
<Box
|
||||
ref={(ref) => {
|
||||
if (ref && ref instanceof HTMLElement)
|
||||
parentScrollRef.current =
|
||||
(ref.closest(".ms-container") as HTMLElement) || undefined;
|
||||
}}
|
||||
sx={{
|
||||
".virtuoso-grid-list": {
|
||||
display: "grid",
|
||||
@@ -216,7 +210,9 @@ function ThemesList() {
|
||||
<Loader title={"Loading themes..."} />
|
||||
) : (
|
||||
<VirtuosoGrid
|
||||
customScrollParent={parentScrollRef.current || undefined}
|
||||
customScrollParent={
|
||||
document.getElementById("settings-scrollbar") || undefined
|
||||
}
|
||||
data={[
|
||||
{
|
||||
...darkTheme,
|
||||
|
||||
@@ -476,6 +476,7 @@ function SettingItem(props: { item: Setting }) {
|
||||
disabled={isWorking}
|
||||
onChange={() => workWithLoading(component.toggle)}
|
||||
checked={component.isToggled()}
|
||||
data-checked={component.isToggled()}
|
||||
/>
|
||||
);
|
||||
case "dropdown":
|
||||
|
||||
@@ -23,6 +23,7 @@ import { logger } from "./utils/logger";
|
||||
import { loadDatabase } from "./hooks/use-database";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { BaseThemeProvider } from "./components/theme-provider";
|
||||
import { register } from "./utils/stream-saver/mitm";
|
||||
|
||||
renderApp();
|
||||
|
||||
@@ -66,6 +67,9 @@ async function initializeServiceWorker() {
|
||||
AppEventManager.publish(AppEvents.updateDownloadCompleted, {
|
||||
version: formatted
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
register();
|
||||
}
|
||||
});
|
||||
// window.addEventListener("beforeinstallprompt", () => showInstallNotice());
|
||||
|
||||
@@ -86,9 +86,11 @@ function registerValidSW(
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
if (config.onSuccess) config.onSuccess(registration);
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
if (config.onSuccess) config.onSuccess(registration);
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
|
||||
@@ -80,6 +80,8 @@ registerRoute(
|
||||
})
|
||||
);
|
||||
|
||||
const downloads = new Map<string, any[]>();
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener("message", (event) => {
|
||||
@@ -87,6 +89,10 @@ self.addEventListener("message", (event) => {
|
||||
if (!data) return;
|
||||
|
||||
switch (data.type) {
|
||||
// We send a heartbeat every x second to keep the
|
||||
// service worker alive if a transferable stream is not sent
|
||||
case "PING":
|
||||
break;
|
||||
case "SKIP_WAITING":
|
||||
self.skipWaiting();
|
||||
break;
|
||||
@@ -100,7 +106,125 @@ self.addEventListener("message", (event) => {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "REGISTER_DOWNLOAD":
|
||||
{
|
||||
console.log("register download", data);
|
||||
const downloadUrl =
|
||||
data.url ||
|
||||
self.registration.scope +
|
||||
Math.random() +
|
||||
"/" +
|
||||
(typeof data === "string" ? data : data.filename);
|
||||
const port = event.ports[0];
|
||||
const metadata = new Array(3); // [stream, data, port]
|
||||
|
||||
metadata[1] = data;
|
||||
metadata[2] = port;
|
||||
|
||||
if (event.data.transferringReadable) {
|
||||
port.onmessage = (evt) => {
|
||||
port.onmessage = null;
|
||||
metadata[0] = evt.data.readableStream;
|
||||
};
|
||||
} else {
|
||||
metadata[0] = createStream(port);
|
||||
}
|
||||
|
||||
downloads.set(downloadUrl, metadata);
|
||||
port.postMessage({ download: downloadUrl });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = event.request.url;
|
||||
|
||||
// this only works for Firefox
|
||||
if (url.endsWith("/ping")) {
|
||||
return event.respondWith(new Response("pong"));
|
||||
}
|
||||
|
||||
const metadata = downloads.get(url);
|
||||
if (!metadata) return null;
|
||||
|
||||
const [stream, data, port] = metadata;
|
||||
|
||||
downloads.delete(url);
|
||||
|
||||
// Not comfortable letting any user control all headers
|
||||
// so we only copy over the length & disposition
|
||||
const responseHeaders = new Headers({
|
||||
"Content-Type": "application/octet-stream; charset=utf-8",
|
||||
|
||||
// To be on the safe side, The link can be opened in a iframe.
|
||||
// but octet-stream should stop it.
|
||||
"Content-Security-Policy": "default-src 'none'",
|
||||
"X-Content-Security-Policy": "default-src 'none'",
|
||||
"X-WebKit-CSP": "default-src 'none'",
|
||||
"X-XSS-Protection": "1; mode=block"
|
||||
});
|
||||
|
||||
const headers = new Headers(data.headers || {});
|
||||
|
||||
if (headers.has("Content-Length")) {
|
||||
responseHeaders.set("Content-Length", headers.get("Content-Length")!);
|
||||
}
|
||||
|
||||
if (headers.has("Content-Disposition")) {
|
||||
responseHeaders.set(
|
||||
"Content-Disposition",
|
||||
headers.get("Content-Disposition")!
|
||||
);
|
||||
}
|
||||
|
||||
// data, data.filename and size should not be used anymore
|
||||
if (data.size) {
|
||||
console.warn("Depricated");
|
||||
responseHeaders.set("Content-Length", data.size);
|
||||
}
|
||||
|
||||
let fileName = typeof data === "string" ? data : data.filename;
|
||||
if (fileName) {
|
||||
console.warn("Depricated");
|
||||
// Make filename RFC5987 compatible
|
||||
fileName = encodeURIComponent(fileName)
|
||||
.replace(/['()]/g, escape)
|
||||
.replace(/\*/g, "%2A");
|
||||
responseHeaders.set(
|
||||
"Content-Disposition",
|
||||
"attachment; filename*=UTF-8''" + fileName
|
||||
);
|
||||
}
|
||||
|
||||
event.respondWith(new Response(stream, { headers: responseHeaders }));
|
||||
|
||||
port.postMessage({ debug: "Download started" });
|
||||
});
|
||||
|
||||
function createStream(port: MessagePort) {
|
||||
// ReadableStream is only supported by chrome 52
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
// When we receive data on the messageChannel, we write
|
||||
port.onmessage = ({ data }) => {
|
||||
if (data === "end") {
|
||||
return controller.close();
|
||||
}
|
||||
|
||||
if (data === "abort") {
|
||||
controller.error("Aborted the download");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.enqueue(data);
|
||||
};
|
||||
},
|
||||
cancel(reason) {
|
||||
console.log("user aborted", reason);
|
||||
port.postMessage({ abort: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user