Compare commits

...

87 Commits

Author SHA1 Message Date
Ammar Ahmed
5294131370 mobile: fix loading empty conent in editor on tablets 2024-01-26 23:00:48 +05:00
Abdullah Atta
a3464255ec core: always use attachment mime type from backup if possible 2024-01-26 19:21:55 +05:00
Abdullah Atta
088c6670a9 core: fix crash if note title only contains whitespace 2024-01-26 19:12:41 +05:00
Abdullah Atta
27840b9bd6 editor: fix link pasting (#4178) 2024-01-26 18:57:59 +05:00
Muhammad Ali
ae819504f0 editor: fix input rules not working at the end of note (#4130)
* web: added condition in getDeletedNodes

* editor: some corrections

* editor: merge if conditions

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:43:19 +05:00
Muhammad Ali
af792e394c web: add support for moving locked notes to trash (#4134)
* web: added support for single/multi trash notes

* web: show error when trying to preview a locked note in trash

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:37:54 +05:00
Muhammad Ali
76a4d5c2a5 editor: fix subtasks getting unchecked on drag/drop reorder (#4135)
* editor: add a condition to check nested item state

* editor: force coerce value to boolean

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2024-01-26 16:31:23 +05:00
Muhammad Ali
4e23acd608 web: fix realtime editor sync cannot be disabled (#4140) 2024-01-26 16:22:56 +05:00
Ammar Ahmed
3c65dcdd8b ci: bump android build tools version 2024-01-15 12:31:37 +05:00
Ammar Ahmed
efea6e3972 theme: add missing alpha colors 2024-01-15 12:31:11 +05:00
Ammar Ahmed
185cca7880 mobile: release v2.6.15 2024-01-12 20:10:58 +05:00
Abdullah Atta
fbded93405 global: update lockfiles 2024-01-05 22:10:05 +05:00
Abdullah Atta
674881b873 web: add zip64 support for backups, export & imports (#4079)
* web: use @zip.js/zip.js for reading zip files

* web: always use zip streams for writing zips

* core: fix types

* theme: update lockfile
2024-01-05 22:05:04 +05:00
Abdullah Atta
248fb6dd2d web: bump version to 2.6.14 2024-01-05 21:55:18 +05:00
Abdullah Atta
3211e7a149 global: update lockfiles 2024-01-05 21:54:06 +05:00
Abdullah Atta
08496c699e editor: fix font size changes back after exiting list (fixes #3602) 2024-01-05 21:48:00 +05:00
Abdullah Atta
060fa25814 core: support underline when exporting as md (fix #3990) 2024-01-05 20:53:14 +05:00
Abdullah Atta
a6b1effc77 core: remove relations auto cleanup 2024-01-05 16:24:02 +05:00
Abdullah Atta
5cf9c4808e web: show resubscribe button if subscription is canceled 2024-01-05 15:55:36 +05:00
Muhammad Ali
d5a44067fd web: fix editor header title input not becoming readonly (#4064) 2024-01-05 11:08:16 +05:00
Muhammad Ali
0de463526f editor: add support for +/- font-size using shortcut keys (#4029) 2024-01-05 10:40:29 +05:00
Muhammad Ali
5fd79875ef editor: fix math/formula block getting cut off on mobile (#4023) 2024-01-05 10:39:20 +05:00
Muhammad Ali
bdb19ad325 web: add license to settings (#4020) 2024-01-05 10:38:13 +05:00
Ammar Ahmed
7a12efc823 mobile: release v2.6.14 2023-12-22 11:58:34 +05:00
Abdullah Atta
2f3bddf860 web: bump version to 2.6.13 2023-12-22 11:51:11 +05:00
Abdullah Atta
6127eb97af editor: do not mark task list as completed if there are no items 2023-12-22 11:49:10 +05:00
Abdullah Atta
86386e14f3 web: update test snapshots 2023-12-22 11:41:42 +05:00
Abdullah Atta
9cc17c29f6 web: use Cmd-a on macOS to select all items in list 2023-12-22 11:38:40 +05:00
Abdullah Atta
a31e863c7b global: update lockfiles 2023-12-22 11:16:26 +05:00
Muhammad Ali
15a32ac83a editor: fix pasting a word inside a list adds newlines around it (#3843)
* editor: added openstart, openEnd from old slice

* editor: removed extra import

* editor: update test snapshots

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-22 11:11:03 +05:00
Ammar Ahmed
fa5b84ec0d mobile: minor fixes 2023-12-22 11:10:19 +05:00
Muhammad Ali
7905584eac editor: disable invalid tools when inside a code block (#3997)
* editor: removed fontSize property from code-text style

* editor: TO BE CONTINUED

* editor: some comments for testing

* editor: comment removed

* editor: diabled font size and family in code-block

* editor: disable invalid tools when inside code block

* editor: remove unused console.log

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-22 10:26:25 +05:00
Abdullah Atta
afc9b96a11 web: make importer error resistant 2023-12-21 09:08:04 +05:00
Abdullah Atta
4e6466fc18 editor: handle md links with parentheses (#3996) 2023-12-19 14:54:22 +05:00
Abdullah Atta
233baf78ce web: handle file picker dialog cancelation 2023-12-19 14:39:45 +05:00
Abdullah Atta
42d0bee37f desktop: bump electron to 25.9.8 2023-12-19 13:07:34 +05:00
Abdullah Atta
ccadab6a10 web: add md + frontmatter to export all options 2023-12-19 12:44:23 +05:00
Muhammad Ali
bfe5a5cb96 editor: correctly style other types of lists in outline list (#3889) 2023-12-19 12:15:36 +05:00
Muhammad Ali
88fc24c4cf editor: fix code blocks pasting in a single line without formatting (#3910)
* editor: added code support for w3school

* editor: fixed newline issue in per tag code
2023-12-19 12:13:59 +05:00
Abdullah Atta
8be4fea7a3 ui: use symbols for shortcut keys on macOS 2023-12-19 11:12:20 +05:00
Muhammad Ali
a3d39d8b6d editor: add Ctrl/Cmd+\ keyboard shortcut for "Clear all formatting" (#3989)
* editor: added short-cut key in keyap

* editor: use Mod key instead of Ctrl

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-19 11:11:35 +05:00
Muhammad Ali
ff36f0259a editor: add support for converting md link syntax to link on paste (#3919) 2023-12-18 16:23:56 +05:00
Muhammad Ali
d13654088a editor: add support for checking/unchecking all items in a task list (#3975) 2023-12-18 16:21:44 +05:00
Muhammad Ali
8a6b97c757 editor: add special readonly mode for toolbar (#3913)
* editor: added a toolbar display on mobile in readonly mode

* editor: hide editable tools from readonly toolbar

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-18 16:04:02 +05:00
Muhammad Ali
d71e0c109b web: going back in search after editing a notebook opens notebook edit dialog again (#3979)
* web: changed afterAction()

* web: fix back navigation for dialogs that replace

* web: remove unused import

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-18 15:58:20 +05:00
Ammar Ahmed
7e345303fe mobile: fix app lock hides login page on enter background 2023-12-18 10:49:57 +05:00
Abdullah Atta
d881bdb77c web: fix attachment dialog not opening on error 2023-12-11 15:09:38 +05:00
Ammar Ahmed
4961102d8f mobile: fix subscriptions not verified on ios 2023-12-11 10:54:45 +05:00
Abdullah Atta
4f2ab4f1da docs: fix incorrect noun (fixes #3934)
Signed-off-by: Abdullah Atta <abdullahatta@streetwriters.co>
2023-12-07 09:56:55 +05:00
Ammar Ahmed
7540e34366 mobile: update provisioning profiles 2023-11-27 08:22:47 +05:00
Abdullah Atta
4d7a8b9ea0 web: bump version to 2.6.12 2023-11-25 22:20:39 +05:00
Abdullah Atta
812ecf93c8 web: fix tag tests 2023-11-25 22:18:05 +05:00
Ammar Ahmed
74d99d97bc mobile: release v2.6.12 2023-11-25 15:44:43 +05:00
Muhammad Ali
5bbc453268 web: fix new tags do not appear under "recent" tags (#3827) 2023-11-25 15:39:38 +05:00
Ammar Ahmed
d71b9edc7f mobile: add support for markdown + frontmatter 2023-11-25 15:32:31 +05:00
Abdullah Atta
134904b551 web: add md with frontmatter export 2023-11-25 15:32:31 +05:00
Abdullah Atta
3eeb292d70 core: add new md + frontmatter export format 2023-11-25 15:32:31 +05:00
Abdullah Atta
04125bbb98 web: minor ui fixes 2023-11-25 15:19:50 +05:00
Ammar Ahmed
cfc3a8341c mobile: update mmkv package 2023-11-25 15:19:27 +05:00
Abdullah Atta
cbb0a511a8 core: fix logs sorting 2023-11-25 15:19:27 +05:00
Abdullah Atta
dff1c7ed32 core: optimize logger 2023-11-25 15:19:27 +05:00
Abdullah Atta
7987ef81a6 web: use removeMulti to bulk remove old logs 2023-11-25 15:19:27 +05:00
Ammar Ahmed
8d8d4ee782 mobile: fix app freeze when deleting old logs 2023-11-25 15:19:27 +05:00
Ammar Ahmed
3a50c73cab core: add removeMulti to storage 2023-11-25 15:19:27 +05:00
Abdullah Atta
4ee0071eca core: stop localOnly items from always getting synced 2023-11-25 15:12:33 +05:00
Muhammad Ali
248192f404 editor: hide link popup in readonly mode on mobile (#3853) 2023-11-25 11:29:02 +05:00
Krishna Bharadwaj
e56b766c3a web: open testimonial source links in new tab (#3863)
Signed-off-by: Dharmavineta <krishnabharadwaj1995@gmail.com>
2023-11-25 11:27:24 +05:00
Abdullah Atta
170b2b24ab web: fix logged in status alignment 2023-11-25 11:26:19 +05:00
Ammar Ahmed
50137e0994 mobile: fix subscriptions not fulfilling on ios 2023-11-25 10:32:24 +05:00
Abdullah Atta
8b5babd5f8 desktop: use node.js fetch instead of net.fetch 2023-11-25 10:20:19 +05:00
Abdullah Atta
39e83200c8 web: fix attachment upload progress on desktop 2023-11-25 10:19:57 +05:00
Abdullah Atta
ead0e0107e core: allow deleting unuploaded attachments 2023-11-25 10:19:38 +05:00
Abdullah Atta
1c888bd4fd web: always open announcement link in browser on desktop app 2023-11-25 10:18:20 +05:00
Krishna Bharadwaj
ff92530339 web: hide email address from status bar (#3859) 2023-11-24 11:18:46 +05:00
Ammar Ahmed
ee38032403 mobile: release v2.6.11 2023-11-22 14:45:16 +05:00
Ammar Ahmed
2ce245659a mobile: fix pricing plans 2023-11-22 14:27:20 +05:00
Abdullah Atta
2c635eb4d7 web: bump version to 2.6.11 2023-11-22 10:44:39 +05:00
Abdullah Atta
d653223df0 editor: load images threadsafely 2023-11-22 10:36:48 +05:00
Muhammad Ali
290b0dfc78 web: close reminder dialog on setting snooze time (#3842) 2023-11-21 10:53:53 +05:00
Muhammad Ali
06bb9e654b web: fix weird janky animation when opening dialogs (#3826) 2023-11-21 08:20:26 +05:00
Muhammad Ali
576fe78e33 clipper: hide clip preview button when there's no clip data (#3772) 2023-11-21 08:18:28 +05:00
Abdullah Atta
0c16bfb5ff web: fix issue with service worker not getting registered 2023-11-20 12:34:07 +05:00
Abdullah Atta
a101dbdf31 web: use bloburl when previewing images 2023-11-20 11:12:02 +05:00
Abdullah Atta
682609044f editor: add support for previewing SVG images 2023-11-20 11:12:02 +05:00
Abdullah Atta
c0a7fb3a5c misc: remove unnecessary files 2023-11-20 10:01:08 +05:00
Abdullah Atta
a153aa791c web: bump version to 2.6.10 2023-11-20 09:49:31 +05:00
Abdullah Atta
76b3dd1f96 web: check if file exists before starting upload 2023-11-20 09:41:35 +05:00
141 changed files with 3444 additions and 1562 deletions

View File

@@ -40,6 +40,7 @@ jobs:
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
BUILD_TOOLS_VERSION: "33.0.0"
- name: Publish to Playstore
id: deploy
@@ -64,6 +65,7 @@ jobs:
alias: ${{ secrets.PUBLIC_ALIAS }}
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
BUILD_TOOLS_VERSION: "33.0.0"
- name: Rename apk files
run: |

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "2.6.9",
"version": "2.6.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "2.6.9",
"version": "2.6.14",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -24,8 +24,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",
@@ -173,12 +173,11 @@
}
},
"node_modules/@electron/asar": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.4.tgz",
"integrity": "sha512-lykfY3TJRRWFeTxccEKdf1I6BLl2Plw81H0bbp4Fc5iEc67foDCa5pjJQULVgo0wF+Dli75f3xVcdb/67FFZ/g==",
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"dev": true,
"dependencies": {
"chromium-pickle-js": "^0.2.0",
"commander": "^5.0.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4"
@@ -869,9 +868,9 @@
}
},
"node_modules/@types/debug": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
"integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
"dev": true,
"dependencies": {
"@types/ms": "*"
@@ -902,9 +901,9 @@
}
},
"node_modules/@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==",
"version": "0.7.34",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz",
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
"dev": true
},
"node_modules/@types/node": {
@@ -970,9 +969,9 @@
}
},
"node_modules/7zip-bin": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.1.1.tgz",
"integrity": "sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true
},
"node_modules/agent-base": {
@@ -1054,9 +1053,9 @@
"dev": true
},
"node_modules/app-builder-lib": {
"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==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.9.1.tgz",
"integrity": "sha512-Q1nYxZcio4r+W72cnIRVYofEAyjBd3mG47o+zms8HlD51zWtA/YxJb01Jei5F+jkWhge/PTQK+uldsPh6d0/4g==",
"dev": true,
"dependencies": {
"@develar/schema-utils": "~2.6.5",
@@ -1065,15 +1064,15 @@
"@electron/universal": "1.4.1",
"@malept/flatpak-bundler": "^0.4.0",
"@types/fs-extra": "9.0.13",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"async-exit-hook": "^2.0.1",
"bluebird-lst": "^1.0.9",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chromium-pickle-js": "^0.2.0",
"debug": "^4.3.4",
"ejs": "^3.1.8",
"electron-publish": "24.5.0",
"electron-publish": "24.8.1",
"form-data": "^4.0.0",
"fs-extra": "^10.1.0",
"hosted-git-info": "^4.1.0",
@@ -1092,6 +1091,19 @@
"node": ">=14.0.0"
}
},
"node_modules/app-builder-lib/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -1116,9 +1128,9 @@
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==",
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==",
"dev": true
},
"node_modules/async-exit-hook": {
@@ -1280,16 +1292,16 @@
"dev": true
},
"node_modules/builder-util": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.5.0.tgz",
"integrity": "sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.8.1.tgz",
"integrity": "sha512-ibmQ4BnnqCnJTNrdmdNlnhF48kfqhNzSeqFMXHLIl+o9/yhn6QfOaVrloZ9YUu3m0k3rexvlT5wcki6LWpjTZw==",
"dev": true,
"dependencies": {
"@types/debug": "^4.1.6",
"7zip-bin": "~5.1.1",
"7zip-bin": "~5.2.0",
"app-builder-bin": "4.0.0",
"bluebird-lst": "^1.0.9",
"builder-util-runtime": "9.2.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"cross-spawn": "^7.0.3",
"debug": "^4.3.4",
@@ -1315,6 +1327,19 @@
"node": ">=12.0.0"
}
},
"node_modules/builder-util/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -1413,9 +1438,9 @@
"dev": true
},
"node_modules/ci-info": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz",
"integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==",
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
@@ -1702,14 +1727,14 @@
}
},
"node_modules/dmg-builder": {
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.6.4.tgz",
"integrity": "sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.9.1.tgz",
"integrity": "sha512-huC+O6hvHd24Ubj3cy2GMiGLe2xGFKN3klqVMLAdcbB6SWMd1yPSdZvV8W1O01ICzCCRlZDHiv4VrNUgnPUfbQ==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
@@ -1718,6 +1743,19 @@
"dmg-license": "^1.0.11"
}
},
"node_modules/dmg-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/dmg-license": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
@@ -1774,9 +1812,9 @@
}
},
"node_modules/electron": {
"version": "25.9.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.3.tgz",
"integrity": "sha512-dacaHg/PuwVcFRgPDCM5j7UDzqGJWOsbBRdS5wPKLNS/ejPeccIjuNUT1cqcrpvCJKAFW8swHWg9kdizNSEDHQ==",
"version": "25.9.8",
"resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz",
"integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -1792,16 +1830,16 @@
}
},
"node_modules/electron-builder": {
"version": "24.6.4",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.6.4.tgz",
"integrity": "sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.9.1.tgz",
"integrity": "sha512-v7BuakDuY6sKMUYM8mfQGrwyjBpZ/ObaqnenU0H+igEL10nc6ht049rsCw2HghRBdEwJxGIBuzs3jbEhNaMDmg==",
"dev": true,
"dependencies": {
"app-builder-lib": "24.6.4",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"app-builder-lib": "24.9.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"dmg-builder": "24.6.4",
"dmg-builder": "24.9.1",
"fs-extra": "^10.1.0",
"is-ci": "^3.0.0",
"lazy-val": "^1.0.5",
@@ -1863,6 +1901,19 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true
},
"node_modules/electron-builder/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-notarize": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/electron-notarize/-/electron-notarize-1.2.2.tgz",
@@ -1893,20 +1944,33 @@
}
},
"node_modules/electron-publish": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz",
"integrity": "sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA==",
"version": "24.8.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.8.1.tgz",
"integrity": "sha512-IFNXkdxMVzUdweoLJNXSupXkqnvgbrn3J4vognuOY06LaS/m0xvfFYIf+o1CM8if6DuWYWoQFKPcWZt/FUjZPw==",
"dev": true,
"dependencies": {
"@types/fs-extra": "^9.0.11",
"builder-util": "24.5.0",
"builder-util-runtime": "9.2.1",
"builder-util": "24.8.1",
"builder-util-runtime": "9.2.3",
"chalk": "^4.1.2",
"fs-extra": "^10.1.0",
"lazy-val": "^1.0.5",
"mime": "^2.5.2"
}
},
"node_modules/electron-publish/node_modules/builder-util-runtime": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz",
"integrity": "sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-trpc": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/electron-trpc/-/electron-trpc-0.5.2.tgz",

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "2.6.9",
"version": "2.6.14",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/index.js",
@@ -25,8 +25,8 @@
"@types/node": "18.16.1",
"@types/yargs": "^17.0.24",
"chokidar": "^3.5.3",
"electron": "25.9.3",
"electron-builder": "^24.6.4",
"electron": "25.9.8",
"electron-builder": "^24.9.1",
"electron-builder-notarize": "^1.5.1",
"esbuild": "^0.17.19",
"tree-kill": "^1.2.2",

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { protocol, net } from "electron";
import { protocol } from "electron";
import { isDevelopment } from "./index";
import { createReadStream } from "fs";
import { extname, normalize } from "path";
@@ -62,8 +62,25 @@ function registerProtocol() {
headers: { "Content-Type": extensionToMimeType[fileExtension] }
});
} else {
return net.fetch(request, {
bypassCustomProtocolHandlers: true
if (request.headers.has("X-Content-Length")) {
request.headers.set(
"Content-Length",
request.headers.get("X-Content-Length") || "0"
);
request.headers.delete("X-Content-Length");
}
const headers = Object.fromEntries(request.headers.entries());
return await fetch(request.url, {
signal: request.signal,
mode: request.mode,
headers,
method: request.method,
body: request.body,
credentials: request.credentials,
referrer: (request as any).referrer,
duplex: "half",
redirect: "manual"
});
}
});

View File

@@ -16,8 +16,8 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { MMKVLoader } from "react-native-mmkv-storage";
import { initalize } from "@notesnook/core/dist/logger";
import { MMKVLoader } from "react-native-mmkv-storage";
import { KV } from "./storage";
const LoggerStorage = new MMKVLoader()
@@ -26,4 +26,4 @@ const LoggerStorage = new MMKVLoader()
initalize(new KV(LoggerStorage));
export {};
export { LoggerStorage };

View File

@@ -90,6 +90,10 @@ export class KV {
return this.storage.removeItem(key);
}
async removeMulti(keys) {
return this.storage.removeItems(...keys);
}
async clear() {
return this.storage.clearStore();
}
@@ -145,6 +149,7 @@ export default {
clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
writeMulti: (items) => DefaultStorage.writeMulti(items),
removeMulti: (keys) => DefaultStorage.removeMulti(keys),
encrypt,
decrypt,
decryptMulti,

View File

@@ -34,10 +34,69 @@ import BaseDialog from "../dialog/base-dialog";
import { allowedOnPlatform, renderItem } from "./functions";
import { useCallback } from "react";
/**
* Test announcement
* {
id: "some-announcement",
type: "dialog",
body: [
{
type: "title",
text: "This is a title",
platforms: ["all"]
},
{
type: "description",
text: "Most of you are too busy to keep up to date with what's happening in Notesnook. That is unfortunate because Notesnook has come a looooong way.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "To solve this, we are launching the Notesnook Digest — a newsletter to help you stay updated about Notesnook development. And to keep things interesting I'll also sprinkle this newsletter with other interesting stuff like privacy tips & news, interesting books, things I am looking forward to etc.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "So be sure to subscribe. There won't be a proper schedule to this (yet) maybe once or twice a month. I promise no spam — only more awesomeness.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "description",
text: "— May privacy reign.",
style: {
marginBottom: 1
},
platforms: ["all"]
},
{
type: "callToActions",
actions: [
{
type: "promo",
title: "15% Off",
platforms: ["android"],
data: "com.streetwriters.notesnook.sub.yr.15"
}
],
platforms: ["all"]
}
]
}
*/
export const AnnouncementDialog = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState(null);
const [info, setInfo] = useState();
const remove = useMessageStore((state) => state.remove);
useEffect(() => {
@@ -51,7 +110,9 @@ export const AnnouncementDialog = () => {
const open = (data) => {
setInfo(data);
setVisible(true);
setImmediate(() => {
setVisible(true);
});
};
const close = useCallback(() => {

View File

@@ -17,14 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { Platform, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
@@ -34,7 +36,6 @@ import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
export const AuthMode = {
login: 0,
@@ -68,6 +69,9 @@ const AuthModal = () => {
}
const close = () => {
useUserStore.setState({
disableAppLockRequests: false
});
actionSheetRef.current?.hide();
setCurrentAuthMode(AuthMode.login);
setVisible(false);
@@ -77,6 +81,11 @@ const AuthModal = () => {
<BaseDialog
overlayOpacity={0}
statusBarTranslucent={false}
onShow={() => {
useUserStore.setState({
disableAppLockRequests: true
});
}}
onRequestClose={currentAuthMode !== AuthMode.welcomeSignup && close}
visible={true}
onClose={close}
@@ -86,6 +95,7 @@ const AuthModal = () => {
transparent={false}
animated={false}
centered={false}
enableSheetKeyboardHandler
>
<KeyboardAwareScrollView
style={{

View File

@@ -66,6 +66,7 @@ const BaseDialog = ({
}, []);
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (appState === "background") {
setIntervalVisible(false);

View File

@@ -99,8 +99,8 @@ const NoteItem = ({
dateBy = "dateCreated",
noOpen = false
}) => {
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
@@ -112,8 +112,7 @@ 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;
const primaryColors = isEditingNote ? colors.selected : colors.primary;
return (
<>

View File

@@ -25,13 +25,13 @@ import { useEditorStore } from "../../../stores/use-editor-store";
export const Filler = ({ item }) => {
const { colors } = useThemeColors();
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
const isEditingNote = useEditorStore(
(state) => state.currentEditingNote === item.id
);
const [selected] = useIsSelected(item);
return currentEditingNote === item.id || selected ? (
return isEditingNote || selected ? (
<View
style={{
position: "absolute",
@@ -39,12 +39,11 @@ export const Filler = ({ item }) => {
height: "150%",
backgroundColor: colors.selected.background,
borderLeftWidth: 5,
borderLeftColor:
currentEditingNote === item.id
? item.color
? colors.static[item.color]
: colors.selected.accent
: "transparent"
borderLeftColor: isEditingNote
? item.color
? colors.static[item.color]
: colors.selected.accent
: "transparent"
}}
collapsable={false}
/>

View File

@@ -48,23 +48,23 @@ export const Expiring = () => {
const [visible, setVisible] = useState(false);
const [status, setStatus] = useState({
title: "Your trial is ending soon",
offer: null,
offer: "Get 30% off",
extend: true
});
const pricing = usePricing("yearly");
const promo = status.offer
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
const promo =
status.offer && pricing?.info
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
useEffect(() => {
eSubscribeEvent(eOpenTrialEndingDialog, open);

View File

@@ -28,7 +28,8 @@ import RNIap from "react-native-iap";
export const PricingItem = ({
product,
onPress,
compact
compact,
strikethrough
}: {
product: {
type: "yearly" | "monthly";
@@ -36,6 +37,7 @@ export const PricingItem = ({
info: string;
offerType?: "yearly" | "monthly";
};
strikethrough?: boolean;
onPress?: () => void;
compact?: boolean;
}) => {
@@ -50,8 +52,10 @@ export const PricingItem = ({
paddingHorizontal: 12,
paddingVertical: compact ? 15 : 10,
width: compact ? null : "100%",
minWidth: 150
minWidth: 150,
opacity: strikethrough ? 0.7 : 1
}}
disabled={strikethrough}
>
{!compact && (
<View>
@@ -67,8 +71,18 @@ export const PricingItem = ({
)}
<View>
<Paragraph size={SIZE.sm}>
<Heading size={SIZE.lg - 2}>
<Paragraph
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={SIZE.sm}
>
<Heading
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={SIZE.lg - 2}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails[0].pricingPhases

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import * as RNIap from "react-native-iap";
import { db } from "../../common/database";
import { DatabaseLogger, db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
@@ -49,6 +49,20 @@ import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { useSettingStore } from "../../stores/use-setting-store";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
const UUID_VARIANT = "a";
function toUUID(str: string) {
return [
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
str.substring(2, 6), // # next 4 oid digits
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
str.substring(12)
].join("-");
}
const promoCyclesMonthly = {
1: "first month",
2: "first 2 months",
@@ -177,11 +191,15 @@ export const PricingPlans = ({
.offerToken
: null;
DatabaseLogger.info(
`Subscription Requested initiated for user ${toUUID(user.id)}`
);
await RNIap.requestSubscription({
sku: product?.productId,
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
appAccountToken: user.id,
appAccountToken: toUUID(user.id),
andDangerouslyFinishTransactionAutomaticallyIOS: false,
subscriptionOffers: androidOfferToken
? [
@@ -213,6 +231,35 @@ export const PricingPlans = ({
}
};
function getStandardPrice() {
if (!product) return;
const productType = product.offerType;
if (Platform.OS === "android") {
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
if (!pricingPhaseListItem) {
const product =
productType === "monthly"
? monthlyPlan?.product
: yearlyPlan?.product;
return (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
?.formattedPrice;
}
return pricingPhaseListItem?.formattedPrice;
} else {
const productDefault =
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
return (
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
);
}
}
return loading ? (
<View
style={{
@@ -292,34 +339,84 @@ export const PricingPlans = ({
) : (
<>
{product?.type === "promo" ? (
<Heading
<View
style={{
paddingVertical: 15,
alignSelf: "center",
textAlign: "center"
alignItems: "center"
}}
size={SIZE.lg - 4}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0].formattedPrice
: (product.data as RNIap.SubscriptionIOS)?.introductoryPrice}
<Paragraph
{product?.offerType === "monthly" ? (
<PricingItem
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay once a month, cancel anytime."
}}
strikethrough={true}
/>
) : (
<PricingItem
onPress={() => {
if (!monthlyPlan?.product) return;
buySubscription(monthlyPlan?.product);
}}
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay once a year, cancel anytime."
}}
strikethrough={true}
/>
)}
<Heading
style={{
textDecorationLine: "line-through",
color: colors.secondary.paragraph
paddingTop: 15,
fontSize: SIZE.lg
}}
size={SIZE.sm}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[1]?.pricingPhases
.pricingPhaseList?.[1]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS)?.localizedPrice}
</Paragraph>{" "}
for {product.cycleText}
</Heading>
Special offer for you
</Heading>
<View
style={{
paddingVertical: 20,
paddingBottom: 10
}}
>
<Heading
style={{
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.xxl}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails[0].pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS)
?.introductoryPrice ||
(product.data as RNIap.SubscriptionIOS)
?.localizedPrice}{" "}
{product?.cycleText
? `for ${product.cycleText}`
: product?.offerType}
</Heading>
{product?.cycleText ? (
<Paragraph
style={{
color: colors.secondary.paragraph,
alignSelf: "center",
textAlign: "center"
}}
size={SIZE.md}
>
then {getStandardPrice()} {product?.offerType}.
</Paragraph>
) : null}
</View>
</View>
) : null}
{user && !product ? (
@@ -334,7 +431,12 @@ export const PricingPlans = ({
marginBottom: 20
}}
>
<Heading color={colors.primary.accent}>
<Heading
style={{
textAlign: "center"
}}
color={colors.primary.accent}
>
Get {monthlyPlan?.info?.discount}% off in{" "}
{monthlyPlan?.info?.country}
</Heading>
@@ -369,7 +471,7 @@ export const PricingPlans = ({
product={{
type: "monthly",
data: monthlyPlan?.product,
info: "Pay monthly, cancel anytime."
info: "Pay once a month, cancel anytime."
}}
/>
@@ -391,7 +493,7 @@ export const PricingPlans = ({
product={{
type: "yearly",
data: yearlyPlan?.product,
info: "Pay yearly"
info: "Pay once a year, cancel anytime."
}}
/>
</View>
@@ -463,7 +565,7 @@ export const PricingPlans = ({
width={250}
style={{
paddingHorizontal: 12,
marginTop: 30,
marginTop: product?.type === "promo" ? 0 : 30,
marginBottom: 10
}}
/>

View File

@@ -97,6 +97,16 @@ const ExportNotesSheet = ({ notes, update }) => {
id: notesnook.ids.dialogs.export.md,
pro: premium
},
{
title: "Markdown + Frontmatter",
func: async () => {
await save("md-frontmatter");
},
icon: "language-markdown",
desc: "View in any text or markdown editor",
id: notesnook.ids.dialogs.export.md,
pro: premium
},
{
title: "Plain Text",
func: async () => {
@@ -178,7 +188,9 @@ const ExportNotesSheet = ({ notes, update }) => {
>
<Icon
name={item.icon}
color={item.pro ? colors.primary.accent : colors.primary.icon}
color={
item.pro ? colors.primary.accent : colors.primary.icon
}
size={SIZE.xxxl + 10}
/>
</View>

View File

@@ -85,6 +85,7 @@ const SheetWrapper = ({
};
useEffect(() => {
if (useUserStore.getState().disableAppLockRequests) return;
if (SettingsService.get().appLockMode === "background") {
if (appState === "background") {
const ref = fwdRef || localRef;

View File

@@ -460,7 +460,6 @@ export const useAppEvents = () => {
}
clearMessage();
subscribeToIAPListeners();
if (!login) {
user = await db.user.fetchUser();
setUser(user);
@@ -479,7 +478,11 @@ export const useAppEvents = () => {
userEmailConfirmed: true
});
}
subscribeToIAPListeners();
} catch (e) {
DatabaseLogger.error(error);
ToastEvent.error(e, "An error occurred", "global");
}
@@ -503,14 +506,15 @@ export const useAppEvents = () => {
);
const subscribeToIAPListeners = useCallback(async () => {
RNIap.flushFailedPurchasesCachedAsPendingAndroid()
.catch(() => {})
.then(() => {
refValues.current.subsriptionSuccessListener =
RNIap.purchaseUpdatedListener(onSuccessfulSubscription);
refValues.current.subsriptionErrorListener =
RNIap.purchaseErrorListener(onSubscriptionError);
});
if (Platform.OS === "android") {
try {
await RNIap.flushFailedPurchasesCachedAsPendingAndroid();
} catch (e) {}
}
refValues.current.subsriptionSuccessListener =
RNIap.purchaseUpdatedListener(onSuccessfulSubscription);
refValues.current.subsriptionErrorListener =
RNIap.purchaseErrorListener(onSubscriptionError);
}, []);
const unSubscribeFromIAPListeners = () => {

View File

@@ -1,82 +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 { useEffect, useState } from "react";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { useEditorStore } from "../stores/use-editor-store";
import { useTagStore } from "../stores/use-tag-store";
import { db } from "../common/database";
import { NoteType } from "app/utils/types";
import { useCallback } from "react";
/**
* A hook that injects/removes tags from tags bar in editor
*/
const useEditorTags = () => {
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const tags = useTagStore((state) => state.tags);
const [note, setNote] = useState<NoteType | null>(null);
const [noteTags, setNoteTags] = useState<string[]>([]);
const refreshNote = useCallback(() => {
const current = useEditorStore.getState().currentEditingNote;
if (!current) {
setNote(null);
setNoteTags([]);
return;
}
const note = db.notes?.note(current)?.data as NoteType;
setNote(note ? { ...note } : null);
getTags(note);
}, []);
useEffect(() => {
refreshNote();
}, [currentEditingNote, refreshNote, tags]);
const load = useCallback(() => {
if (!note) return;
// tiny.call(EditorWebView, renderTags(noteTags));
}, [note]);
useEffect(() => {
eSubscribeEvent("updateTags", load);
return () => {
eUnSubscribeEvent("updateTags", load);
};
}, [load, noteTags]);
function getTags(note: NoteType) {
if (!note || !note.tags) return [];
const tags = note.tags
.map((t) => (db.tags?.tag(t) ? { ...db.tags.tag(t) } : null))
.filter((t) => t !== null);
setNoteTags(tags);
}
useEffect(() => {
load();
}, [load, noteTags]);
return [];
};
export default useEditorTags;

View File

@@ -189,7 +189,7 @@ export const useEditorEvents = (
readonly: readonly || editorPropReadonly,
tools: tools || getDefaultPresets().default,
noHeader: noHeader,
noToolbar: readonly || editorPropReadonly || noToolbar,
noToolbar: noToolbar,
doubleSpacedLines: doubleSpacedLines,
corsProxy: corsProxy,
fontSize:

View File

@@ -408,13 +408,13 @@ export const useEditor = (
await commands.setStatus(getFormattedDate(item.dateEdited), "Saved");
await postMessage(EditorEvents.title, item.title);
loadingState.current = currentContent.current?.data;
if (currentContent.current?.data) {
await postMessage(
EditorEvents.html,
currentContent.current?.data,
10000
);
}
await postMessage(
EditorEvents.html,
currentContent.current?.data || "",
10000
);
loadingState.current = undefined;
useEditorStore.getState().setReadonly(item.readonly);
await commands.setTags(currentNote.current);

View File

@@ -42,6 +42,9 @@ export const Subscription = () => {
user?.subscription?.type !== SUBSCRIPTION_STATUS.PREMIUM &&
user?.subscription?.type !== SUBSCRIPTION_STATUS.BETA;
const hasCancelledPremium =
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED === user?.subscription?.type;
const subscriptionProviderInfo =
SUBSCRIPTION_PROVIDER[user?.subscription?.provider];
@@ -63,10 +66,7 @@ export const Subscription = () => {
return;
}
if (
user?.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_CANCELLED &&
Platform.OS === "android"
) {
if (hasCancelledPremium && Platform.OS === "android") {
if (user.subscription?.provider === 3) {
ToastEvent.show({
heading: "Subscribed on web",
@@ -111,16 +111,14 @@ export const Subscription = () => {
title={
!user?.isEmailConfirmed
? "Confirm your email"
: user.subscription?.provider === 3 &&
user.subscription?.type ===
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED
: user.subscription?.provider === 3 && hasCancelledPremium
? "Manage subscription from desktop app"
: user.subscription?.type ===
SUBSCRIPTION_STATUS.PREMIUM_CANCELLED &&
: hasCancelledPremium &&
Platform.OS === "android" &&
Config.GITHUB_RELEASE !== "true"
? "Resubscribe from Google Playstore"
: user.subscription?.type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED
: user.subscription?.type ===
SUBSCRIPTION_STATUS.PREMIUM_EXPIRED || hasCancelledPremium
? `Resubscribe to Pro (${getPrice() || "$4.49"} / mo)`
: `Get Pro (${getPrice() || "$4.49"} / mo)`
}

View File

@@ -109,7 +109,7 @@ async function makeHtml(note) {
/**
*
* @param {"txt" | "pdf" | "md" | "html"} type
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
*/
async function exportAs(type, note, bulk) {
let data;
@@ -122,6 +122,9 @@ async function exportAs(type, note, bulk) {
case "md":
data = await db.notes.note(note.id).export("md");
break;
case "md-frontmatter":
data = await db.notes.note(note.id).export("md-frontmatter");
break;
case "pdf":
{
let html = await makeHtml(note);
@@ -159,7 +162,7 @@ async function exportAs(type, note, bulk) {
/**
*
* @param {"txt" | "pdf" | "md" | "html"} type
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
*/
async function exportNote(note, type) {
let path = await getPath(FolderNames[type]);
@@ -180,7 +183,7 @@ async function exportNote(note, type) {
filePath: path,
type: "text/plain",
name: "Text",
fileName: fileName + `.${type}`
fileName: fileName + `.${type === "md-frontmatter" ? "md" : type}`
};
}
@@ -212,7 +215,7 @@ function getUniqueFileName(fileName, results) {
/**
*
* @param {"txt" | "pdf" | "md" | "html"} type
* @param {"txt" | "pdf" | "md" | "html" | "md-frontmatter"} type
*/
async function bulkExport(notes, type, callback) {
let path = await getPath(FolderNames[type]);

View File

@@ -233,7 +233,7 @@ const subscriptions = {
*
* @returns {RNIap.Purchase} subscription
*/
get: async () => {
get: () => {
if (Platform.OS === "android") return;
let _subscriptions = MMKV.getString("subscriptionsIOS");
if (!_subscriptions) return [];
@@ -299,12 +299,16 @@ const subscriptions = {
"Content-Type": "application/json"
}
};
console.log("Subscription.verify", requestData);
try {
let result = await fetch(
"https://payments.streetwriters.co/apple/verify",
__DEV__
? "http://192.168.43.5:4264/apple/verify"
: "https://payments.streetwriters.co/apple/verify",
requestData
);
console.log("Subscribed", result);
let text = await result.text();
if (!result.ok) {
@@ -312,6 +316,8 @@ const subscriptions = {
await subscriptions.clear(subscription);
}
return;
} else {
await subscriptions.clear(subscription);
}
} catch (e) {
console.log("subscription error", e);
@@ -321,15 +327,18 @@ const subscriptions = {
},
clear: async (_subscription) => {
if (Platform.OS === "android") return;
let _subscriptions = await subscriptions.get();
let _subscriptions = subscriptions.get();
let subscription = null;
if (_subscription) {
subscription = _subscription;
} else {
subscription = _subscriptions.length > 0 ? _subscriptions[0] : null;
}
if (subscription) {
await RNIap.finishTransaction(subscription.transactionId);
await RNIap.finishTransaction({
purchase: subscription
});
await RNIap.clearTransactionIOS();
await subscriptions.remove(subscription.transactionId);
}

View File

@@ -75,7 +75,7 @@ export const SUBSCRIPTION_STATUS_STRINGS = {
2: Platform.OS === "ios" ? "Pro" : "Beta",
5: "Pro",
6: "Expired",
7: "Pro"
7: "Pro (cancelled)"
};
export const SUBSCRIPTION_PROVIDER = {

View File

@@ -111,7 +111,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 2070
versionCode 2074
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -1,7 +1,3 @@
- Fixed HTML & PDF exports
- Improved task lists in editor
- Improved loading images
- Allow restoring backups when logged out
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -997,7 +997,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2061;
CURRENT_PROJECT_VERSION = 2066;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1071,7 +1071,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.10;
MARKETING_VERSION = 2.6.15;
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 = 2061;
CURRENT_PROJECT_VERSION = 2066;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1176,7 +1176,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.10;
MARKETING_VERSION = 2.6.15;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1186,7 +1186,7 @@
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook;
PRODUCT_NAME = Notesnook;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Distribution 2023";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook App Distribution 2024";
SWIFT_OBJC_BRIDGING_HEADER = "Notesnook-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1335,7 +1335,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2061;
CURRENT_PROJECT_VERSION = 2066;
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.10;
MARKETING_VERSION = 2.6.15;
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 = 2061;
CURRENT_PROJECT_VERSION = 2066;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1391,12 +1391,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.10;
MARKETING_VERSION = 2.6.15;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2023";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Widget Distribution 2024";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -1421,7 +1421,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2061;
CURRENT_PROJECT_VERSION = 2066;
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.10;
MARKETING_VERSION = 2.6.15;
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 = 2061;
CURRENT_PROJECT_VERSION = 2066;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1601,12 +1601,12 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.10;
MARKETING_VERSION = 2.6.15;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distrubution 2023";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Notesnook Share Distribution 2024";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Make Note/Make Note-Bridging-Header.h";
SWIFT_VERSION = 5.0;

View File

@@ -327,7 +327,7 @@ PODS:
- React-Core
- react-native-keep-awake (1.2.0):
- React-Core
- react-native-mmkv-storage (0.10.0-alpha.9):
- react-native-mmkv-storage (0.10.0-alpha.11):
- MMKV (~> 1.3.1)
- React
- React-Core
@@ -875,7 +875,7 @@ SPEC CHECKSUMS:
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4
react-native-keep-awake: caee3ff89eaa21dfe29010f0d143566874a04441
react-native-mmkv-storage: d4ad55ab411b7f0d4e6269d801b31821ef48970b
react-native-mmkv-storage: e798639f91601896f1c84f4eb3d6f19af2f4a160
react-native-netinfo: ccbe1085dffd16592791d550189772e13bf479e2
react-native-notification-sounds: da78c828fe1bcbb92d8b505d5261890ed315ff39
react-native-orientation: f1caf84d65f1a4fd4511a18f2b924e634ad7a628
@@ -931,4 +931,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: b0cadd188aa428b98d1eaa0b84b3b6208e714119
COCOAPODS: 1.12.1
COCOAPODS: 1.14.2

File diff suppressed because one or more lines are too long

View File

@@ -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.9",
"react-native-mmkv-storage": "^0.10.0-alpha.11",
"react-native-modal-datetime-picker": "14.0.0",
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "2.6.9",
"version": "2.6.14",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "2.6.9",
"version": "2.6.14",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -62,7 +62,7 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -133,6 +133,7 @@
"@tiptap/extension-underline": "2.1.12",
"@tiptap/pm": "2.1.12",
"@tiptap/starter-kit": "2.1.12",
"async-mutex": "^0.4.0",
"clipboard-polyfill": "4.0.0",
"detect-indent": "^7.0.0",
"entities": "^4.5.0",
@@ -319,7 +320,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.9",
"react-native-mmkv-storage": "^0.10.0-alpha.11",
"react-native-modal-datetime-picker": "14.0.0",
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",
@@ -18479,9 +18480,9 @@
"integrity": "sha512-TbiO8AO055EPjaxIuKxrCEOB4r81uQCVDFbtcs+e4pEOHxf6z3JgDC0UU0poJElKfCh37b+TFRxQhg6DbsYJBA=="
},
"node_modules/react-native-mmkv-storage": {
"version": "0.10.0-alpha.9",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.10.0-alpha.9.tgz",
"integrity": "sha512-nv1A0t5dxhcAtMnhqqBodNp94U4QziW1RO352GB3BPpic03abPHj+0HjBJH72VeKKMfYIc0PR5pmo6BbsaPExw==",
"version": "0.10.0-alpha.11",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.10.0-alpha.11.tgz",
"integrity": "sha512-rr5i7v9aOgMHZ3a7LjpwCrHd2ULdCX2Q+n+zlDOEgfXV0P+4EloptE39Z4iOQhQPyr3b/s3q9gj+4sS1QCPkHw==",
"bin": {
"mmkv-link": "autolink/postlink/run.js"
},
@@ -26830,7 +26831,7 @@
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"@types/html-to-text": "^9.0.0",
"@types/katex": "^0.16.1",
"@types/prismjs": "^1.26.0",
@@ -26904,6 +26905,7 @@
"@types/react-dom": "17.0.2",
"@types/react-modal": "3.13.1",
"@types/tinycolor2": "^1.4.3",
"async-mutex": "^0.4.0",
"clipboard-polyfill": "4.0.0",
"detect-indent": "^7.0.0",
"entities": "^4.5.0",
@@ -27076,7 +27078,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.9",
"react-native-mmkv-storage": "^0.10.0-alpha.11",
"react-native-modal-datetime-picker": "14.0.0",
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",
@@ -36184,9 +36186,9 @@
"integrity": "sha512-TbiO8AO055EPjaxIuKxrCEOB4r81uQCVDFbtcs+e4pEOHxf6z3JgDC0UU0poJElKfCh37b+TFRxQhg6DbsYJBA=="
},
"react-native-mmkv-storage": {
"version": "0.10.0-alpha.9",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.10.0-alpha.9.tgz",
"integrity": "sha512-nv1A0t5dxhcAtMnhqqBodNp94U4QziW1RO352GB3BPpic03abPHj+0HjBJH72VeKKMfYIc0PR5pmo6BbsaPExw=="
"version": "0.10.0-alpha.11",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.10.0-alpha.11.tgz",
"integrity": "sha512-rr5i7v9aOgMHZ3a7LjpwCrHd2ULdCX2Q+n+zlDOEgfXV0P+4EloptE39Z4iOQhQPyr3b/s3q9gj+4sS1QCPkHw=="
},
"react-native-modal-datetime-picker": {
"version": "14.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "2.6.10",
"version": "2.6.15",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -1377,7 +1377,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "2.6.8",
"version": "2.6.14",
"license": "GPL-3.0-or-later",
"dependencies": {
"@aws-sdk/util-base64-browser": "^3.208.0",
@@ -1388,7 +1388,7 @@
"@emotion/react": "11.11.1",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@notesnook-importer/core": "^1.7.1",
"@notesnook-importer/core": "^2.0.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -1408,6 +1408,7 @@
"@theme-ui/core": "^0.14.7",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@zip.js/zip.js": "^2.7.32",
"allotment": "^1.19.0",
"axios": "^1.3.4",
"clipboard-polyfill": "4.0.0",

View File

@@ -171,7 +171,7 @@ export class EditorModel {
await this.tagInput.fill(tag);
await this.tagInput.press("Enter");
await this.tags
.locator(":scope", { hasText: new RegExp(`^${tag}$`) })
.locator("span", { hasText: new RegExp(`^${tag}$`) })
.waitFor();
}
}
@@ -181,7 +181,7 @@ export class EditorModel {
const count = await this.tags.count();
for (let i = 0; i < count; ++i) {
const item = this.tags.nth(i);
const tag = await item.textContent();
const tag = await item.locator("span").textContent();
if (tag) tags.push(tag);
}
return tags;

View File

@@ -8,8 +8,11 @@
content="This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1"
/>
<title>Test 1 - Notesnook</title>
<meta name="created-on" content="xxx" />
<meta name="last-edited-on" content="xxx" />
<meta name="created-at" content="22-12-2023 11:33 AM" />
<meta name="updated-at" content="22-12-2023 11:33 AM" />
<link rel="stylesheet" href="https://app.notesnook.com/assets/editor-styles.css?d=0">
@@ -25,6 +28,11 @@
color: #202124;
font-family: "Open Sans", "Noto Sans", Frutiger, Calibri, Myriad, Arial, Ubuntu, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif;
}
.math-block {
padding-top: 20px;
padding-bottom: 20px;
}
h1,
h2,

View File

@@ -2,6 +2,3 @@
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
---

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "2.6.9",
"version": "2.6.14",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -16,7 +16,7 @@
"@emotion/react": "11.11.1",
"@mdi/js": "^7.2.96",
"@mdi/react": "^1.6.1",
"@notesnook-importer/core": "^1.7.1",
"@notesnook-importer/core": "^2.0.0",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",
@@ -36,6 +36,7 @@
"@theme-ui/core": "^0.14.7",
"@trpc/client": "10.38.3",
"@trpc/react-query": "10.38.3",
"@zip.js/zip.js": "^2.7.32",
"allotment": "^1.19.0",
"axios": "^1.3.4",
"clipboard-polyfill": "4.0.0",

View File

@@ -17,13 +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 { db } from "./db";
import { TaskManager } from "./task-manager";
import { zip } from "../utils/zip";
import { saveAs } from "file-saver";
import { showToast } from "../utils/toast";
import { sanitizeFilename } from "@notesnook/common";
import Vault from "./vault";
import { ExportStream } from "../utils/streams/export-stream";
import { createZipStream } from "../utils/streams/zip-stream";
import { createWriteStream } from "../utils/stream-saver";
export async function exportToPDF(
title: string,
@@ -69,7 +66,7 @@ export async function exportToPDF(
}
export async function exportNotes(
format: "pdf" | "md" | "txt" | "html",
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
noteIds: string[]
): Promise<boolean> {
return await TaskManager.startTask({
@@ -77,75 +74,11 @@ export async function exportNotes(
title: "Exporting notes",
subtitle: "Please wait while your notes are exported.",
action: async (report) => {
let vaultUnlocked = false;
if (noteIds.length === 1 && db.notes?.note(noteIds[0])?.data.locked) {
vaultUnlocked = await Vault.unlockVault();
if (!vaultUnlocked) return false;
} else if (noteIds.length > 1 && (await db.vault?.exists())) {
vaultUnlocked = await Vault.unlockVault();
if (!vaultUnlocked)
showToast(
"error",
"Failed to unlock vault. Locked notes will be skipped."
);
}
const files = [];
let index = 0;
for (const noteId of noteIds) {
const note = db.notes?.note(noteId);
if (!note) continue;
if (!vaultUnlocked && note.data.locked) continue;
report({
current: ++index,
total: noteIds.length,
text: `Exporting "${note.title}"...`
});
const rawContent = await db.content?.raw(note.data.contentId);
const content = note.data.locked
? await db.vault?.decryptContent(rawContent)
: rawContent;
const exported = await note
.export(format === "pdf" ? "html" : format, content)
.catch((e: Error) => {
console.error(note.data, e);
showToast(
"error",
`Failed to export note "${note.title}": ${e.message}`
);
});
if (typeof exported !== "string") {
showToast("error", `Failed to export note "${note.title}"`);
continue;
}
if (format === "pdf") {
return await exportToPDF(note.title, exported);
}
files.push({
filename: note.title,
content: exported,
date: note.dateEdited
});
}
if (!files.length) return false;
if (files.length === 1) {
saveAs(
new Blob([Buffer.from(files[0].content, "utf-8")]),
`${sanitizeFilename(files[0].filename)}.${format}`
);
} else {
const zipped = await zip(files, format);
saveAs(new Blob([zipped.buffer]), "notes.zip");
}
await new ExportStream(noteIds, format, undefined, (c, text) =>
report({ total: noteIds.length, current: c, text })
)
.pipeThrough(createZipStream())
.pipeTo(await createWriteStream("notes.zip"));
return true;
}
});

View File

@@ -36,9 +36,9 @@ 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 { createZipStream } from "../utils/streams/zip-stream";
import { FeatureKeys } from "../dialogs/feature-dialog";
import { Entry, Reader } from "../utils/zip-reader";
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
export const CREATE_BUTTON_MAP = {
notes: {
@@ -113,7 +113,7 @@ export async function createBackup() {
controller.close();
}
})
.pipeThrough(new ZipStream())
.pipeThrough(createZipStream())
.pipeTo(writeStream);
}
});
@@ -138,8 +138,9 @@ export async function selectBackupFile() {
export async function importBackup() {
const backupFile = await selectBackupFile();
if (!backupFile) return;
if (!backupFile) return false;
await restoreBackupFile(backupFile);
return true;
}
export async function restoreBackupFile(backupFile: File) {
@@ -165,12 +166,12 @@ export async function restoreBackupFile(backupFile: File) {
type: "modal",
action: async (report) => {
let cachedPassword: string | undefined = undefined;
const { read, totalFiles } = await Reader(backupFile);
const entries: Entry[] = [];
// const { read, totalFiles } = await Reader(backupFile);
const entries: ZipEntry[] = [];
let filesProcessed = 0;
let isValid = false;
for await (const entry of read()) {
for await (const entry of createUnzipIterator(backupFile)) {
if (entry.name === ".nnbackup") {
isValid = true;
continue;
@@ -199,7 +200,6 @@ export async function restoreBackupFile(backupFile: File) {
}
report({
total: totalFiles,
text: `Processed ${entry.name}`,
current: filesProcessed++
});

View File

@@ -35,20 +35,11 @@ type Item = {
};
async function moveNotesToTrash(notes: Item[], confirm = true) {
const item = notes[0];
if (confirm && !(await showMultiDeleteConfirmation(notes.length))) return;
if (notes.length === 1) {
if (
item.locked &&
!(await Vault.unlockNote(item.id, "unlock_and_delete_note"))
)
return;
item.locked = false;
}
if (notes.some((n) => n.locked) && !(await Vault.unlockVault())) return;
const items = notes.map((item) => {
if (item.locked || db.monographs?.isPublished(item.id)) return 0;
if (db.monographs?.isPublished(item.id)) return 0;
return item.id;
});

View File

@@ -291,7 +291,9 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
case "link": {
const url = new URL(action.data);
const target =
url.origin === window.location.origin ? "_self" : "_blank";
url.origin === window.location.origin && !IS_DESKTOP_APP
? "_self"
: "_blank";
window.open(action.data, target, "noopener noreferrer");
break;
}

View File

@@ -164,7 +164,11 @@ export function Attachment({
<AttachmentError
color={"icon-error"}
size={16}
title={attachment.failed}
title={
typeof attachment.failed === "object"
? attachment.failed.toString()
: attachment.failed
}
/>
) : attachment.working ? (
<Loading size={16} />

View File

@@ -171,8 +171,10 @@ function AuthContainer(props) {
{testimonial.text} {" "}
<Link
sx={{ fontStyle: "italic", color: "paragraph-secondary" }}
href={testimonial.link}
>
href={testimonial.link}
target="_blank"
rel="noopener noreferrer"
>
source
</Link>
</Text>

View File

@@ -80,11 +80,10 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
border: 0,
zIndex: 999,
backgroundColor: "var(--backdrop)"
},
overlay: {
opacity: 1
}
// overlay: {
// zIndex: 999,
// background: "var(--backdrop)"
// }
}}
>
<ScopedThemeProvider

View File

@@ -430,13 +430,15 @@ export function Editor(props: EditorProps) {
onContentChange={onContentChange}
onChange={onEditorChange}
onDownloadAttachment={(attachment) => saveAttachment(attachment.hash)}
onPreviewAttachment={async ({ hash, dataurl }) => {
onPreviewAttachment={async (data) => {
const { hash } = data;
const attachment = db.attachments?.attachment(hash);
if (attachment && attachment.metadata.type.startsWith("image/")) {
const container = document.getElementById("dialogContainer");
if (!(container instanceof HTMLElement)) return;
dataurl = dataurl || (await downloadAttachment(hash, "base64"));
const dataurl =
data.bloburl || (await downloadAttachment(hash, "base64"));
if (!dataurl)
return showToast("error", "This image cannot be previewed.");
@@ -555,7 +557,7 @@ function EditorChrome(
<Box
id="editorToolbar"
sx={{
display: readonly ? "none" : "flex",
display: "flex",
bg: "background",
position: "sticky",
top: 0,

View File

@@ -137,6 +137,7 @@ export type Attachment = {
mime: string;
size: number;
dataurl?: string;
bloburl?: string;
};
type AddAttachmentOptions = {

View File

@@ -51,6 +51,7 @@ function Toolbar() {
const sessionId = useStore((store) => store.session.id);
const isDeleted = useStore((store) => store.session.isDeleted);
const isLocked = useStore((store) => store.session.locked);
const readonly = useStore((store) => store.session.readonly);
const [isFullscreen, setIsFullscreen] = useState(false);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const toggleFocusMode = useAppStore((store) => store.toggleFocusMode);
@@ -227,6 +228,7 @@ function Toolbar() {
}}
transition={{ duration: 0.5 }}
defaultValue={title}
readOnly={readonly}
onChange={(e) => {
AppEventManager.publish(AppEvents.changeNoteTitle, {
title: e.target.value,

View File

@@ -111,7 +111,8 @@ const sortByMenu: (options: GroupingMenuOptions) => MenuItem = (options) => ({
{
key: "dateCreated",
title: "Date created",
isHidden: options.groupingKey === "trash"
isHidden:
options.groupingKey === "trash" || options.groupingKey === "tags"
},
{
key: "dateEdited",

View File

@@ -305,6 +305,12 @@ const formats = [
icon: Markdown,
subtitle: "Can be opened in any plain-text or markdown editor."
},
{
type: "md-frontmatter",
title: "Markdown + Frontmatter",
icon: Markdown,
subtitle: "Can be opened in any plain-text or markdown editor."
},
{
type: "html",
title: "HTML",
@@ -466,7 +472,7 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
{
type: "button",
key: "copy-as-text",
tooltip: `Export as Text`,
tooltip: `Copy as Text`,
title: "Text",
icon: Plaintext.path,
onClick: () => copyNote(note.id, "txt")
@@ -474,7 +480,7 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
{
type: "button",
key: "copy-as-markdown",
tooltip: `Export as Markdown`,
tooltip: `Copy as Markdown`,
title: "Markdown",
icon: Markdown.path,
onClick: () => copyNote(note.id, "md")
@@ -533,7 +539,7 @@ const menuItems: (note: any, items?: any[]) => MenuItem[] = (
icon: Trash.path,
isDisabled:
items.length === 1
? db.monographs?.isPublished(note.id) || note.locked
? db.monographs?.isPublished(note.id)
: items.some((item) => !db.notes?.note(item.id).synced()),
onClick: () => Multiselect.moveNotesToTrash(items, items.length > 1),
multiSelect: true

View File

@@ -29,7 +29,7 @@ function SearchBox({ onSearch }) {
id="search"
name="search"
type="text"
sx={{ m: 0, mx: 2, mt: 1 }}
sx={{ m: 0, mx: 2, mt: 1, mb: 1 }}
placeholder="Type your query here"
onChange={debounce((e) => onSearch(e.target.value), 250)}
action={{

View File

@@ -34,7 +34,7 @@ import {
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useAppStore } from "../../stores/app-store";
import TimeAgo from "../time-ago";
import { hardNavigate, hashNavigate, navigate } from "../../navigation";
import { hardNavigate, hashNavigate } from "../../navigation";
import { useAutoUpdater, UpdateStatus } from "../../hooks/use-auto-updater";
import {
showIssueDialog,
@@ -67,40 +67,33 @@ function StatusBar() {
{isFocusMode ? (
<Flex />
) : (
<Flex>
<Flex sx={{ gap: 1 }}>
{isLoggedIn ? (
<>
<Button
onClick={() =>
user?.isEmailConfirmed
? navigate("/settings")
: hashNavigate("/email/verify")
}
variant="statusitem"
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex"
}}
>
<Circle
size={7}
color={
user?.isEmailConfirmed
? "var(--icon-success)"
: "var(--icon-error)"
}
/>
<Text
className="selectable"
variant="subBody"
ml={1}
sx={{ color: "paragraph" }}
{user?.isEmailConfirmed ? (
<Circle size={7} color={"var(--icon-success)"} />
) : (
<Button
onClick={() => hashNavigate("/email/verify")}
variant="statusitem"
sx={{
alignItems: "center",
justifyContent: "center",
display: "flex",
height: "100%"
}}
>
{user?.email}
{user?.isEmailConfirmed ? "" : " (not verified)"}
</Text>
</Button>
<Circle size={7} color={"var(--icon-error)"} />
<Text
className="selectable"
variant="subBody"
ml={1}
sx={{ color: "paragraph" }}
>
Email not confirmed
</Text>
</Button>
)}
<SyncStatus />
</>

View File

@@ -59,7 +59,9 @@ function TrashItem(props: TrashItemProps) {
menuItems={menuItems}
onClick={() => {
if (item.itemType === "note")
hashNavigate(`/notes/${item.id}/edit`, { replace: true });
!item.locked
? hashNavigate(`/notes/${item.id}/edit`, { replace: true })
: showToast("error", "Locked notes cannot be previewed in trash.");
}}
/>
);

View File

@@ -89,17 +89,7 @@ const features: Record<FeatureKeys, Feature> = {
)
}
]
: [
{
title: "Date time shortcuts in note title",
subtitle:
"You can now use date time shortcuts (/now, /date & /time) in note & task list titles."
},
{
title: "Restore backups without an account",
subtitle: "You can now restore backups without logging in."
}
],
: [],
cta: {
title: "Got it",
icon: Checkmark,

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Text, Flex, Button, Image, Box } from "@theme-ui/components";
import { Text, Flex, Button, Image, Box, Link } from "@theme-ui/components";
import Dialog from "../components/dialog";
import {
Pro,
@@ -417,9 +417,21 @@ function CrossPlatform() {
return (
<Flex my={4} sx={{ alignItems: "center" }}>
{isMacStoreApp() ? null : (
<Image src={GooglePlay} sx={{ flexShrink: 0, width: 135 }} />
<Link
href="https://play.google.com/store/apps/details?id=com.streetwriters.notesnook"
target="_blank"
rel="noopener noreferrer"
>
<Image src={GooglePlay} sx={{ flexShrink: 0, width: 135 }} />
</Link>
)}
<Image src={AppleStore} sx={{ flexShrink: 0, width: 110 }} />
<Link
href="https://apps.apple.com/us/app/notesnook-take-private-notes/id1544027013"
target="_blank"
rel="noopener noreferrer"
>
<Image src={AppleStore} sx={{ flexShrink: 0, width: 110 }} />
</Link>
</Flex>
);
}

View File

@@ -108,7 +108,8 @@ function PasswordDialog(props) {
sx={{
fontSize: "body",
fontFamily: "body",
alignItems: "center"
alignItems: "center",
color: "paragraph"
}}
>
<Checkbox id={check.key} name={check.key} />

View File

@@ -105,6 +105,7 @@ export default function ReminderPreviewDialog(
id: reminder.id,
snoozeUntil: Date.now() + time.interval
});
props.onClose(false);
}}
sx={{
borderRadius: 100,

View File

@@ -67,8 +67,9 @@ export const BackupExportSettings: SettingsGroup[] = [
type: "button",
title: "Restore",
action: async () => {
await importBackup();
await useAppStore.getState().refresh();
if (await importBackup()) {
await useAppStore.getState().refresh();
}
},
variant: "secondary"
}
@@ -179,6 +180,11 @@ export const BackupExportSettings: SettingsGroup[] = [
{ value: "-", title: "Select format" },
{ value: "txt", title: "Text" },
{ value: "md", title: "Markdown", premium: true },
{
value: "md-frontmatter",
title: "Markdown + Frontmatter",
premium: true
},
{ value: "html", title: "HTML", premium: true }
],
selectedOption: () => "-",
@@ -186,7 +192,7 @@ export const BackupExportSettings: SettingsGroup[] = [
if (!db.notes || value === "-") return;
if (await verifyAccount())
await exportNotes(
value as "txt" | "md" | "html",
value as "txt" | "md" | "html" | "md-frontmatter",
db.notes.all.map((n) => n.id)
);
}

View File

@@ -75,7 +75,7 @@ export function Importer() {
<>
<CheckCircleOutline color="accent" sx={{ mt: 150 }} />
<Text variant="body" my={2} sx={{ textAlign: "center" }}>
Import successful. {errors.length} errors occured.
Import completed. {errors.length} errors occured.
</Text>
<Button
variant="secondary"
@@ -127,6 +127,8 @@ export function Importer() {
<Link
href="https://help.notesnook.com/importing-notes/import-notes-from-evernote"
target="_blank"
rel="noopener noreferrer"
sx={{ color: "accent" }}
>
import guide
</Link>{" "}
@@ -134,23 +136,25 @@ export function Importer() {
</Text>
</Flex>
<Button
variant="accent"
onClick={async () => {
setIsDone(false);
setIsImporting(true);
await db.syncer?.acquireLock(async () => {
try {
for await (const {
count,
filesRead,
totalFiles
} of importFiles(files)) {
if (notesCounter.current)
notesCounter.current.innerText = `${count}`;
if (importProgress.current)
importProgress.current.style.width = `${
(filesRead / totalFiles) * 100
}%`;
for await (const message of importFiles(files)) {
switch (message.type) {
case "error":
setErrors((errors) => [...errors, message.error]);
break;
case "progress": {
const { count } = message;
if (notesCounter.current)
notesCounter.current.innerText = `${count}`;
break;
}
}
}
} catch (e) {
console.error(e);

View File

@@ -196,40 +196,27 @@ export function SubscriptionStatus() {
Request a refund
</Button>
</>
) : isBasic ? (
<>
<Button
variant="accent"
onClick={async () => {
showBuyDialog();
}}
>
Upgrade to Pro
</Button>
<Button
variant="secondary"
onClick={activateTrial}
sx={{ bg: "background" }}
>
{isActivatingTrial ? (
<Loading size={16} />
) : (
"Try free for 14 days"
)}
</Button>
</>
) : isTrial ? (
<>
<Button
variant="accent"
onClick={async () => {
showBuyDialog();
}}
>
Upgrade to Pro
</Button>
</>
) : null}
{!isPro && (
<>
<Button variant="accent" onClick={() => showBuyDialog()}>
{isProCancelled ? "Resubscribe" : "Upgrade to Pro"}
</Button>
{isBasic && (
<Button
variant="secondary"
onClick={activateTrial}
sx={{ bg: "background" }}
>
{isActivatingTrial ? (
<Loading size={16} />
) : (
"Try free for 14 days"
)}
</Button>
)}
</>
)}
</Flex>
</Flex>
{isBasic ? <Features /> : null}

View File

@@ -75,16 +75,17 @@ export const AboutSettings: SettingsGroup[] = [
{
key: "source-code",
title: "Source code",
description: "All the source code for Notesnook is available & open for everyone on GitHub.",
description:
"All the source code for Notesnook is available & open for everyone on GitHub.",
components: [
{
type: "button",
action: () => {
window.open(
"https://github.com/streetwriters/notesnook",
"_blank"
);
},
window.open(
"https://github.com/streetwriters/notesnook",
"_blank"
);
},
title: "View source code",
variant: "secondary"
}
@@ -234,6 +235,23 @@ export const LegalSettings: SettingsGroup[] = [
variant: "secondary"
}
]
},
{
key: "license",
title: "License",
description: "GNU GENERAL PUBLIC LICENSE Version 3",
components: [
{
type: "button",
action: () =>
void window.open(
"https://github.com/streetwriters/notesnook/blob/master/LICENSE",
"_blank"
),
title: "Read License",
variant: "secondary"
}
]
}
]
}

View File

@@ -128,7 +128,7 @@ export function useKeyboardListNavigation(
focusItemAt(nextIndex);
return true;
},
"Ctrl-a": () => {
"Mod-a": () => {
resetSelection();
for (let i = 0; i < length; ++i) select(i);
return true;

View File

@@ -226,8 +226,10 @@ type UploadAdditionalData = {
async function uploadFile(filename: string, requestOptions: RequestOptions) {
const fileHandle = await streamablefs.readFile(filename);
if (!fileHandle)
throw new Error(`File stream not found. (File hash: ${filename})`);
if (!fileHandle || !(await exists(filename)))
throw new Error(
`File is corrupt or missing data. Please upload the file again. (File hash: ${filename})`
);
try {
if (fileHandle.file.additionalData?.uploaded) {
await checkUpload(filename);
@@ -359,6 +361,7 @@ async function multiPartUploadFile(
);
};
onUploadProgress({ bytes: 0, loaded: 0 });
for (let i = uploadedChunks.length; i < TOTAL_PARTS; ++i) {
const blob = await fileHandle.readChunks(
i * UPLOAD_PART_REQUIRED_CHUNKS,
@@ -370,7 +373,7 @@ async function multiPartUploadFile(
.request({
url,
method: "PUT",
headers: { "Content-Type": "" },
headers: { "Content-Type": "", "X-Content-Length": data.byteLength },
signal,
data,
onUploadProgress
@@ -391,6 +394,8 @@ async function multiPartUploadFile(
});
await fileHandle.addAdditionalData("uploadedChunks", uploadedChunks);
await fileHandle.addAdditionalData("uploadedBytes", uploadedBytes);
onUploadProgress({ bytes: 0, loaded: blob.size });
}
await axios

View File

@@ -70,6 +70,10 @@ export class NNStorage {
return this.database.delete(key);
}
removeMulti(keys: string[]) {
return this.database.deleteMany(keys);
}
clear() {
return this.database.clear();
}

View File

@@ -150,5 +150,6 @@ export default hashroutes;
export type HashRoute = keyof typeof hashroutes;
function afterAction() {
window.location.hash = "";
hashNavigate("/", { replace: true, notify: false });
if (!history.state.replace) history.back();
}

View File

@@ -65,8 +65,8 @@ export function hashNavigate<TPath extends HashRoute>(
let url: string = path;
if (addNonce) url += `/${++lastNonce}`;
if (replace) window.history.replaceState(null, "", `#${url}`);
else window.history.pushState(null, "", `#${url}`);
if (replace) window.history.replaceState({ replace }, "", `#${url}`);
else window.history.pushState({ replace }, "", `#${url}`);
const event = new HashChangeEvent("hashchange");
(event as any).notify = notify;

View File

@@ -56,26 +56,23 @@ export function register(config: ServiceWorkerRegistrationConfig) {
return;
}
window.addEventListener("load", () => {
const swUrl = `${PUBLIC_URL}/service-worker.js`;
const swUrl = `${PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA"
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://cra.link/PWA"
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
}
}

View File

@@ -184,7 +184,7 @@ class AppStore extends BaseStore {
toggleRealtimeSync = () => {
const { isRealtimeSyncEnabled } = this.get();
Config.set("realtimeSyncEnabled", !isRealtimeSyncEnabled);
Config.set("isRealtimeSyncEnabled", !isRealtimeSyncEnabled);
this.set(
(state) => (state.isRealtimeSyncEnabled = !state.isRealtimeSyncEnabled)
);

View File

@@ -24,7 +24,7 @@ import { store as editorStore } from "./editor-store";
import { checkAttachment } from "../common/attachments";
import { showToast } from "../utils/toast";
import { AttachmentStream } from "../utils/streams/attachment-stream";
import { ZipStream } from "../utils/streams/zip-stream";
import { createZipStream } from "../utils/streams/zip-stream";
import { createWriteStream } from "../utils/stream-saver";
let abortController = undefined;
@@ -67,7 +67,7 @@ class AttachmentStore extends BaseStore {
}
);
await attachmentStream
.pipeThrough(new ZipStream())
.pipeThrough(createZipStream(abortController.signal))
.pipeTo(
await createWriteStream("attachments.zip", {
signal: abortController.signal

View File

@@ -30,6 +30,9 @@ export function showFilePicker({
input.setAttribute("type", "file");
input.setAttribute("accept", acceptedFileTypes);
input.dispatchEvent(new MouseEvent("click"));
input.oncancel = async function () {
resolve(undefined);
};
input.onchange = async function () {
if (!input.files) return resolve(undefined);
const file = input.files[0];

View File

@@ -18,28 +18,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import type { Note, Notebook } from "@notesnook-importer/core";
import {
Note,
Notebook,
ContentType
} from "@notesnook-importer/core/dist/src/models";
import {
ATTACHMENTS_DIRECTORY_NAME,
NOTE_DATA_FILENAME
} from "@notesnook-importer/core/dist/src/utils/note-stream";
import { Cipher } from "@notesnook/crypto/dist/src/types";
import { Reader, Entry } from "./zip-reader";
import { path } from "@notesnook-importer/core/dist/src/utils/path";
import { ZipEntry, createUnzipIterator } from "./streams/unzip-stream";
export async function* importFiles(zipFiles: File[]) {
const { default: FS } = await import("../interfaces/fs");
for (const zip of zipFiles) {
let count = 0;
let filesRead = 0;
const attachments: Record<
string,
Omit<Cipher, "cipher" | "format"> & { key: any; chunkSize: number }
> = {};
const { read, totalFiles } = await Reader(zip);
const attachments: Record<string, any> = {};
for await (const entry of read()) {
for await (const entry of createUnzipIterator(zip)) {
++filesRead;
const isAttachment = entry.name.includes(
@@ -47,52 +45,70 @@ export async function* importFiles(zipFiles: File[]) {
);
const isNote = !isAttachment && entry.name.endsWith(NOTE_DATA_FILENAME);
if (isAttachment) {
const name = path.basename(entry.name);
if (!name || attachments[name] || db.attachments?.exists(name))
continue;
const data = await entry.arrayBuffer();
const { hash } = await FS.hashBuffer(new Uint8Array(data));
if (hash !== name) {
throw new Error(`integrity check failed: ${name} !== ${hash}`);
try {
if (isAttachment) {
await processAttachment(entry, attachments);
} else if (isNote) {
await processNote(entry, attachments);
++count;
}
const file = new File([data], name, {
type: "application/octet-stream"
});
const key = await db.attachments?.generateKey();
const cipherData = await FS.writeEncryptedFile(file, key, name);
attachments[name] = { ...cipherData, key };
} else if (isNote) {
const note = await fileToJson<Note>(entry);
for (const attachment of note.attachments || []) {
const cipherData = attachments[attachment.hash];
if (!cipherData || db.attachments?.exists(attachment.hash)) continue;
await db.attachments?.add({
...cipherData,
hash: attachment.hash,
hashType: attachment.hashType,
filename: attachment.filename,
type: attachment.mime
});
}
await importNote(note);
++count;
} catch (e) {
if (e instanceof Error) yield { type: "error" as const, error: e };
}
yield {
type: "progress" as const,
count,
totalFiles,
filesRead
};
}
}
}
async function importNote(note: Note) {
async function processAttachment(
entry: ZipEntry,
attachments: Record<string, any>
) {
const name = path.basename(entry.name);
if (!name || attachments[name] || db.attachments?.exists(name)) return;
const { default: FS } = await import("../interfaces/fs");
const data = await entry.arrayBuffer();
const { hash } = await FS.hashBuffer(new Uint8Array(data));
if (hash !== name) {
throw new Error(`integrity check failed: ${name} !== ${hash}`);
}
const file = new File([data], name, {
type: "application/octet-stream"
});
const key = await db.attachments?.generateKey();
const cipherData = await FS.writeEncryptedFile(file, key, name);
attachments[name] = { ...cipherData, key };
}
async function processNote(entry: ZipEntry, attachments: Record<string, any>) {
const note = await fileToJson<Note>(entry);
for (const attachment of note.attachments || []) {
const cipherData = attachments[attachment.hash];
if (!cipherData || db.attachments?.exists(attachment.hash)) continue;
await db.attachments?.add({
...cipherData,
hash: attachment.hash,
hashType: attachment.hashType,
filename: attachment.filename,
type: attachment.mime
});
}
if (!note.content)
note.content = {
data: "<p></p>",
type: ContentType.HTML
};
if (note.content?.type === "html") (note.content.type as string) = "tiptap";
else throw new Error("Invalid content type: " + note.content?.type);
@@ -101,7 +117,7 @@ async function importNote(note: Note) {
const noteId = await db.notes?.add(note);
for (const nb of notebooks) {
const notebook = await importNotebook(nb);
const notebook = await importNotebook(nb).catch(() => ({ id: undefined }));
if (!notebook.id) continue;
await db.notes?.addToNotebook(
{ id: notebook.id, topic: notebook.topic },
@@ -110,7 +126,7 @@ async function importNote(note: Note) {
}
}
async function fileToJson<T>(file: Entry) {
async function fileToJson<T>(file: ZipEntry) {
const text = await file.text();
return JSON.parse(text) as T;
}
@@ -126,9 +142,10 @@ async function importNotebook(
title: notebook.notebook,
topics: notebook.topic ? [notebook.topic] : []
});
nb = db.notebooks?.notebook(nbId).data;
nb = db.notebooks?.notebook(nbId)?.data;
if (!nb) return {};
}
const topic = nb?.topics.find((t: any) => t.title === notebook.topic);
const topic = nb.topics.find((t: any) => t.title === notebook.topic);
return { id: nb ? nb.id : undefined, topic: topic ? topic.id : undefined };
}

View File

@@ -23,27 +23,36 @@ import {
logManager
} from "@notesnook/core/dist/logger";
import { LogMessage } from "@notesnook/logger";
import FileSaver from "file-saver";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { zip } from "./zip";
import { ZipFile, createZipStream } from "./streams/zip-stream";
import { createWriteStream } from "./stream-saver";
let logger: typeof _logger;
async function initalizeLogger(persistence: DatabasePersistence = "db") {
initalize(await NNStorage.createInstance("Logs", persistence));
initalize(await NNStorage.createInstance("Logs", persistence), false);
logger = _logger.scope("notesnook-web");
}
async function downloadLogs() {
if (!logManager) return;
const allLogs = await logManager.get();
const files = allLogs.map((log) => ({
filename: log.key,
content: (log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
}));
const archive = await zip(files, "log");
FileSaver.saveAs(new Blob([archive.buffer]), "notesnook-logs.zip");
const textEncoder = new TextEncoder();
await new ReadableStream<ZipFile>({
pull(controller) {
for (const log of allLogs) {
controller.enqueue({
path: log.key,
data: textEncoder.encode(
(log.logs as LogMessage[])
.map((line) => JSON.stringify(line))
.join("\n")
)
});
}
}
})
.pipeThrough(createZipStream())
.pipeTo(await createWriteStream("notesnook-logs.zip"));
}
async function clearLogs() {

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "../../common/db";
import { lazify } from "../lazify";
import { makeUniqueFilename } from "./utils";
import { ZipFile } from "./zip-stream";
export const METADATA_FILENAME = "metadata.json";
@@ -84,17 +85,3 @@ export class AttachmentStream extends ReadableStream<ZipFile> {
});
}
}
function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
filePath = filePath.toLowerCase();
counters[filePath] = (counters[filePath] || 0) + 1;
if (counters[filePath] === 1) return filePath;
const parts = filePath.split(".");
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${
parts[parts.length - 1]
}`;
}

View File

@@ -0,0 +1,115 @@
/*
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 { sanitizeFilename } from "@notesnook/common";
import { db } from "../../common/db";
import { exportToPDF } from "../../common/export";
import Vault from "../../common/vault";
import { showToast } from "../toast";
import { makeUniqueFilename } from "./utils";
import { ZipFile } from "./zip-stream";
const FORMAT_TO_EXT = {
pdf: "pdf",
md: "md",
txt: "txt",
html: "html",
"md-frontmatter": "md"
} as const;
export class ExportStream extends ReadableStream<ZipFile> {
constructor(
noteIds: string[],
format: "pdf" | "md" | "txt" | "html" | "md-frontmatter",
signal?: AbortSignal,
onProgress?: (current: number, text: string) => void
) {
const textEncoder = new TextEncoder();
let index = 0;
const counters: Record<string, number> = {};
let vaultUnlocked = false;
super({
async start() {
if (noteIds.length === 1 && db.notes?.note(noteIds[0])?.data.locked) {
vaultUnlocked = await Vault.unlockVault();
if (!vaultUnlocked) return false;
} else if (noteIds.length > 1 && (await db.vault?.exists())) {
vaultUnlocked = await Vault.unlockVault();
if (!vaultUnlocked)
showToast(
"error",
"Failed to unlock vault. Locked notes will be skipped."
);
}
},
async pull(controller) {
if (signal?.aborted) {
controller.close();
return;
}
const note = db.notes?.note(noteIds[index++]);
if (!note) return;
if (!vaultUnlocked && note.data.locked) return;
onProgress && onProgress(index, `Exporting "${note.title}"...`);
const rawContent = await db.content?.raw(note.data.contentId);
const content = note.data.locked
? await db.vault?.decryptContent(rawContent)
: rawContent;
const exported = await note
.export(format === "pdf" ? "html" : format, content)
.catch((e: Error) => {
console.error(note.data, e);
showToast(
"error",
`Failed to export note "${note.title}": ${e.message}`
);
});
if (typeof exported !== "string") {
showToast("error", `Failed to export note "${note.title}"`);
return;
}
if (format === "pdf") {
await exportToPDF(note.title, exported);
controller.close();
return;
}
const filename = sanitizeFilename(note.title, { replacement: "-" });
const ext = FORMAT_TO_EXT[format];
controller.enqueue({
path: makeUniqueFilename([filename, ext].join("."), counters),
data: textEncoder.encode(exported),
mtime: new Date(note.data.dateEdited),
ctime: new Date(note.data.dateCreated)
});
if (index === noteIds.length) {
controller.close();
}
}
});
}
}

View File

@@ -0,0 +1,101 @@
/*
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 { Deflate as FflateDeflate, Inflate as FflateInflate } from "fflate";
function initShimAsyncCodec(library, options = {}, registerDataHandler) {
return {
Deflate: createCodecClass(
library.Deflate,
options.deflate,
registerDataHandler
),
Inflate: createCodecClass(
library.Inflate,
options.inflate,
registerDataHandler
)
};
}
function objectHasOwn(object, propertyName) {
// eslint-disable-next-line no-prototype-builtins
return typeof Object.hasOwn === "function"
? Object.hasOwn(object, propertyName)
: // eslint-disable-next-line no-prototype-builtins
object.hasOwnProperty(propertyName);
}
function createCodecClass(
constructor,
constructorOptions,
registerDataHandler
) {
return class {
constructor(options) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const codecAdapter = this;
const onData = (data) => {
if (codecAdapter.pendingData) {
const previousPendingData = codecAdapter.pendingData;
codecAdapter.pendingData = new Uint8Array(
previousPendingData.length + data.length
);
const { pendingData } = codecAdapter;
pendingData.set(previousPendingData, 0);
pendingData.set(data, previousPendingData.length);
} else {
codecAdapter.pendingData = new Uint8Array(data);
}
};
if (objectHasOwn(options, "level") && options.level === undefined) {
delete options.level;
}
codecAdapter.codec = new constructor(
Object.assign({}, constructorOptions, options)
);
registerDataHandler(codecAdapter.codec, onData);
}
append(data) {
this.codec.push(data);
return getResponse(this);
}
flush() {
this.codec.push(new Uint8Array(), true);
return getResponse(this);
}
};
function getResponse(codec) {
if (codec.pendingData) {
const output = codec.pendingData;
codec.pendingData = null;
return output;
} else {
return new Uint8Array();
}
}
}
const { Deflate, Inflate } = initShimAsyncCodec(
{ Deflate: FflateDeflate, Inflate: FflateInflate },
undefined,
(codec, onData) => (codec.ondata = onData)
);
export { Deflate, Inflate };

View File

@@ -0,0 +1,77 @@
/*
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 { Deflate, Inflate } from "./fflate-shim";
import {
Entry,
TextWriter,
Uint8ArrayWriter,
ZipReader,
configure
} from "@zip.js/zip.js";
configure({ Deflate, Inflate });
export async function* createUnzipIterator(file: File) {
const reader = new ZipReader(file.stream());
for await (const entry of reader.getEntriesGenerator()) {
yield new ZipEntry(entry);
}
await reader.close();
}
export class ZipEntry extends Blob {
private ts = new TransformStream();
constructor(public readonly entry: Entry) {
super();
}
stream(): ReadableStream<Uint8Array> {
this.entry.getData?.(this.ts.writable);
return this.ts.readable;
}
async text(): Promise<string> {
const writer = new TextWriter("utf-8");
await this.entry.getData?.(writer);
return await writer.getData();
}
get size() {
return this.entry.uncompressedSize;
}
get name() {
return this.entry.filename;
}
async arrayBuffer(): Promise<ArrayBuffer> {
const writer = new Uint8ArrayWriter();
await this.entry.getData?.(writer);
return (await writer.getData()).buffer;
}
slice(
_start?: number | undefined,
_end?: number | undefined,
_contentType?: string | undefined
): Blob {
throw new Error("Slice is not supported.");
}
}

View File

@@ -0,0 +1,32 @@
/*
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/>.
*/
export function makeUniqueFilename(
filePath: string,
counters: Record<string, number>
) {
filePath = filePath.toLowerCase();
counters[filePath] = (counters[filePath] || 0) + 1;
if (counters[filePath] === 1) return filePath;
const parts = filePath.split(".");
return `${parts.slice(0, -1).join(".")}-${counters[filePath]}.${
parts[parts.length - 1]
}`;
}

View File

@@ -17,32 +17,51 @@ 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 { Zip, ZipDeflate } from "fflate";
import { Deflate, Inflate } from "./fflate-shim";
import { Uint8ArrayReader, ZipWriter, configure } from "@zip.js/zip.js";
export type ZipFile = { path: string; data: Uint8Array };
export class ZipStream extends TransformStream<ZipFile, Uint8Array> {
constructor() {
const zipper = new Zip();
super({
start(controller) {
zipper.ondata = (err, data) => {
if (err) {
controller.error(err);
} else controller.enqueue(data);
};
},
transform(chunk) {
const fileStream = new ZipDeflate(chunk.path, {
level: 5,
mem: 8
configure({ Deflate, Inflate });
export type ZipFile = {
path: string;
data: Uint8Array;
mtime?: Date;
ctime?: Date;
};
export function createZipStream(signal?: AbortSignal) {
const written = new Set<string>();
const ts = new TransformStream<Uint8Array, Uint8Array>();
const writer = new ZipWriter<Uint8Array>(ts.writable, {
zip64: true,
signal
});
const entryWriter = new WritableStream<ZipFile>({
start() {},
async write(chunk, c) {
// zip.js doesn't support overwriting files.
if (written.has(chunk.path)) return;
await writer
.add(chunk.path, new Uint8ArrayReader(chunk.data), {
creationDate: chunk.ctime,
lastModDate: chunk.mtime
})
.catch(async (e) => {
await ts.writable.abort(e);
await ts.readable.cancel(e);
c.error(e);
});
zipper.add(fileStream);
fileStream.push(chunk.data, true);
},
flush() {
zipper.end();
zipper.terminate();
}
});
}
written.add(chunk.path);
},
async close() {
await writer.close();
await ts.writable.close();
},
async abort(reason) {
await ts.writable.abort(reason);
await ts.readable.cancel(reason);
}
});
return { writable: entryWriter, readable: ts.readable };
}

View File

@@ -1,418 +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-underscore-dangle */
/**
* Conflux
* Read (and build) zip files with whatwg streams in the browser.
*
* @author Transcend Inc. <https://transcend.io>
* @license MIT
*/
import { Inflate } from "fflate";
const ERR_BAD_FORMAT = "File format is not recognized.";
const ZIP_COMMENT_MAX = 65536;
const EOCDR_MIN = 22;
const EOCDR_MAX = EOCDR_MIN + ZIP_COMMENT_MAX;
const MAX_VALUE_32BITS = 0xffffffff;
const decoder = new TextDecoder();
const uint16e = (b: Uint8Array, n: number) => b[n] | (b[n + 1] << 8);
export class Entry {
private dataView: DataView;
private _fileLike: File;
private _extraFields: Record<string, DataView>;
constructor(dataView: DataView, fileLike: File) {
if (dataView.getUint32(0) !== 0x504b0102) {
throw new Error("ERR_BAD_FORMAT");
}
const dv = dataView;
this.dataView = dv;
this._fileLike = fileLike;
this._extraFields = {};
for (let i = 46 + this.filenameLength; i < dv.byteLength; ) {
const id = dv.getUint16(i, true);
const len = dv.getUint16(i + 2, true);
const start = dv.byteOffset + i + 4;
this._extraFields[id] = new DataView(dv.buffer.slice(start, start + len));
i += len + 4;
}
}
get webkitRelativePath(): string {
return this.name;
}
get type(): string {
return "application/octet-stream";
}
get versionMadeBy() {
return this.dataView.getUint16(4, true);
}
get versionNeeded() {
return this.dataView.getUint16(6, true);
}
get bitFlag() {
return this.dataView.getUint16(8, true);
}
get encrypted() {
return (this.bitFlag & 0x0001) === 0x0001;
}
get compressionMethod() {
return this.dataView.getUint16(10, true);
}
get crc32() {
return this.dataView.getUint32(16, true);
}
get compressedSize() {
return this.dataView.getUint32(20, true);
}
get filenameLength() {
return this.dataView.getUint16(28, true);
}
get extraFieldLength() {
return this.dataView.getUint16(30, true);
}
get commentLength() {
return this.dataView.getUint16(32, true);
}
get diskNumberStart() {
return this.dataView.getUint16(34, true);
}
get internalFileAttributes() {
return this.dataView.getUint16(36, true);
}
get externalFileAttributes() {
return this.dataView.getUint32(38, true);
}
get directory() {
return !!(this.dataView.getUint8(38) & 16);
}
get offset() {
return this.dataView.getUint32(42, true);
}
get zip64() {
return this.dataView.getUint32(24, true) === MAX_VALUE_32BITS;
}
get comment() {
const dv = this.dataView;
const uint8 = new Uint8Array(
dv.buffer,
dv.byteOffset + this.filenameLength + this.extraFieldLength + 46,
this.commentLength
);
return decoder.decode(uint8);
}
// File like IDL methods
get lastModifiedDate() {
const t = this.dataView.getUint32(12, true);
return new Date(
// Date.UTC(
((t >> 25) & 0x7f) + 1980, // year
((t >> 21) & 0x0f) - 1, // month
(t >> 16) & 0x1f, // day
(t >> 11) & 0x1f, // hour
(t >> 5) & 0x3f, // minute
(t & 0x1f) << 1
// ),
);
}
get lastModified() {
return +this.lastModifiedDate;
}
get name() {
if (!this.bitFlag && this._extraFields && this._extraFields[0x7075]) {
return decoder.decode(this._extraFields[0x7075].buffer.slice(5));
}
const dv = this.dataView;
const uint8 = new Uint8Array(
dv.buffer,
dv.byteOffset + 46,
this.filenameLength
);
return decoder.decode(uint8);
}
get size() {
const size = this.dataView.getUint32(24, true);
return size === MAX_VALUE_32BITS ? this._extraFields[1].getUint8(0) : size;
}
stream() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
const crc = new Crc32();
let inflator: Inflate | undefined;
const onEnd = (ctrl: ReadableStreamController<Uint8Array>) =>
crc.get() === self.crc32
? ctrl.close()
: ctrl.error(new Error("The crc32 checksum don't match"));
return new ReadableStream<Uint8Array>({
async start(ctrl) {
// Need to read local header to get fileName + extraField length
// Since they are not always the same length as in central dir...
const ab = await self._fileLike
.slice(self.offset + 26, self.offset + 30)
.arrayBuffer();
const bytes = new Uint8Array(ab);
const localFileOffset = uint16e(bytes, 0) + uint16e(bytes, 2) + 30;
const start = self.offset + localFileOffset;
const end = start + self.compressedSize;
(this as any).reader = self._fileLike
.slice(start, end)
.stream()
.getReader();
if (self.compressionMethod) {
inflator = new Inflate();
inflator.ondata = (chunk, final) => {
crc.append(chunk);
ctrl.enqueue(chunk);
if (final) onEnd(ctrl);
};
}
},
async pull(ctrl) {
const v = await (this as any).reader.read();
if (inflator && !v.done) {
inflator.push(v.value);
} else if (v.done) {
onEnd(ctrl);
} else {
ctrl.enqueue(v.value);
crc.append(v.value);
}
}
});
}
arrayBuffer() {
return new Response(this.stream()).arrayBuffer().catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
text() {
return new Response(this.stream()).text().catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
file() {
return new Response(this.stream())
.blob()
.then(
(blob) =>
new File([blob], this.name, { lastModified: this.lastModified })
)
.catch((e) => {
throw new Error(`Failed to read Entry\n${e}`);
});
}
}
/**
* Get a BigInt 64 from a DataView
*
* @param {DataView} view a dataview
* @param {number} position the position
* @param {boolean} littleEndian whether this uses littleEndian encoding
* @returns BigInt
*/
function getBigInt64(view: DataView, position: number, littleEndian = false) {
if ("getBigInt64" in DataView.prototype) {
return view.getBigInt64(position, littleEndian);
}
let value = BigInt(0);
const isNegative =
(view.getUint8(position + (littleEndian ? 7 : 0)) & 0x80) > 0;
let carrying = true;
for (let i = 0; i < 8; i++) {
let byte = view.getUint8(position + (littleEndian ? i : 7 - i));
if (isNegative) {
if (carrying) {
if (byte !== 0x00) {
byte = ~(byte - 1) & 0xff;
carrying = false;
}
} else {
byte = ~byte & 0xff;
}
}
value = value + BigInt(byte) + BigInt(256) ** BigInt(i);
}
if (isNegative) {
value = -value;
}
return value;
}
export async function Reader(file: File) {
// Seek EOCDR - "End of central directory record" is the last part of a zip archive, and is at least 22 bytes long.
// Zip file comment is the last part of EOCDR and has max length of 64KB,
// so we only have to search the last 64K + 22 bytes of a archive for EOCDR signature (0x06054b50).
if (file.size < EOCDR_MIN) throw new Error(ERR_BAD_FORMAT);
// seek last length bytes of file for EOCDR
async function doSeek(length: number) {
const ab = await file.slice(file.size - length).arrayBuffer();
const bytes = new Uint8Array(ab);
for (let i = bytes.length - EOCDR_MIN; i >= 0; i--) {
if (
bytes[i] === 0x50 &&
bytes[i + 1] === 0x4b &&
bytes[i + 2] === 0x05 &&
bytes[i + 3] === 0x06
) {
return new DataView(bytes.buffer, i, EOCDR_MIN);
}
}
return null;
}
// In most cases, the EOCDR is EOCDR_MIN bytes long
let dv =
(await doSeek(EOCDR_MIN)) || (await doSeek(Math.min(EOCDR_MAX, file.size)));
if (!dv) throw new Error(ERR_BAD_FORMAT);
let fileslength = dv.getUint16(8, true);
let centralDirSize = dv.getUint32(12, true);
let centralDirOffset = dv.getUint32(16, true);
// const fileCommentLength = dv.getUint16(20, true);
const isZip64 = centralDirOffset === MAX_VALUE_32BITS;
if (isZip64) {
const l = -dv.byteLength - 20;
dv = new DataView(await file.slice(l, -dv.byteLength).arrayBuffer());
// const signature = dv.getUint32(0, true) // 4 bytes
// const diskWithZip64CentralDirStart = dv.getUint32(4, true) // 4 bytes
const relativeOffsetEndOfZip64CentralDir = Number(getBigInt64(dv, 8, true)); // 8 bytes
// const numberOfDisks = dv.getUint32(16, true) // 4 bytes
const zip64centralBlob = file.slice(relativeOffsetEndOfZip64CentralDir, l);
dv = new DataView(await zip64centralBlob.arrayBuffer());
// const zip64EndOfCentralSize = dv.getBigInt64(4, true)
// const diskNumber = dv.getUint32(16, true)
// const diskWithCentralDirStart = dv.getUint32(20, true)
// const centralDirRecordsOnThisDisk = dv.getBigInt64(24, true)
fileslength = Number(getBigInt64(dv, 32, true));
centralDirSize = Number(getBigInt64(dv, 40, true));
centralDirOffset = Number(getBigInt64(dv, 48, true));
}
if (centralDirOffset < 0 || centralDirOffset >= file.size) {
throw new Error(ERR_BAD_FORMAT);
}
const start = centralDirOffset;
const end = centralDirOffset + centralDirSize;
const blob = file.slice(start, end);
const bytes = new Uint8Array(await blob.arrayBuffer());
async function* read() {
for (let i = 0, index = 0; i < fileslength; i++) {
const size =
uint16e(bytes, index + 28) + // filenameLength
uint16e(bytes, index + 30) + // extraFieldLength
uint16e(bytes, index + 32) + // commentLength
46;
if (index + size > bytes.length) {
throw new Error("Invalid ZIP file.");
}
yield new Entry(new DataView(bytes.buffer, index, size), file);
index += size;
}
}
return { read, totalFiles: fileslength };
}
/* eslint-disable no-param-reassign */
class Crc32 {
crc: number;
constructor() {
this.crc = -1;
}
append(data: Uint8Array) {
let crc = this.crc | 0;
const { table } = this;
for (let offset = 0, len = data.length | 0; offset < len; offset++) {
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xff];
}
this.crc = crc;
}
get() {
return (this.crc ^ -1) >>> 0;
}
private table = ((table: number[], i: number, j: number, t: number) => {
for (i = 0; i < 256; i++) {
t = i;
for (j = 0; j < 8; j++) {
t = t & 1 ? (t >>> 1) ^ 0xedb88320 : t >>> 1;
}
table[i] = t;
}
return table;
})([], 0, 0, 0);
}

View File

@@ -1,48 +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 { AsyncZippable, zip as zipAsync } from "fflate";
import { sanitizeFilename } from "@notesnook/common";
const textEncoder = new TextEncoder();
type File = { filename: string; content: string; date: number };
async function zip(files: File[], format: string): Promise<Uint8Array> {
const obj: AsyncZippable = Object.create(null);
files.forEach((file) => {
const name = sanitizeFilename(file.filename, { replacement: "-" });
let counter = 0;
while (obj[makeFilename(name, format, counter)]) ++counter;
obj[makeFilename(name, format, counter)] = [
textEncoder.encode(file.content),
{ mtime: file.date }
];
});
return new Promise((resolve, reject) =>
zipAsync(obj, (err, data) => (err ? reject(err) : resolve(data)))
);
}
export { zip };
function makeFilename(filename: string, extension: string, counter: number) {
let final = filename;
if (counter) final += `-${counter}`;
final += `.${extension}`;
return final;
}

14
bugs.md
View File

@@ -1,14 +0,0 @@
Mobile:
1. Select all notes in a topic -> Click on add button
2. Edit button in a notebook doesn't work
3. Edit notebook sheet doesn't close after saving edits
4. Removing notes from a topic doesn't update the notes count in the list automatically
5. Notes inside a notebook/topic keep showing the notebook tag at the top
6.
Web:
1. When a note is in a single topic, its impossible to move it to its parent notebook
2. When a note is in a single notebook, its impossible to remove it
3. Disable sync -> Refresh app -> Enable sync & notice how the sync status is not updated nor does the sync run automatically

View File

@@ -1,81 +0,0 @@
workflows:
react-native-ios:
name: Notesnook iOS Build
max_build_duration: 120
instance_type: mac_mini
environment:
groups:
- appstore_credentials
- certificate_credentials
- provisioning_profile
vars:
XCODE_WORKSPACE: "Notesnook.xcworkspace" # <-- Put the name of your Xcode workspace here
XCODE_SCHEME: "Notesnook" # <-- Put the name of your Xcode scheme here
BUNDLE_ID: "org.streetwriters.notesnook" # <-- Put your Bundle Id here e.g com.domain.myapp
APP_STORE_APP_ID: 1544027013 # <-- Put the app id number here. This is found in App Store Connect > App > General > App Information
node: 16.13.2
xcode: latest
cocoapods: default
cache:
cache_paths:
- $HOME/Library/Caches/CocoaPods
scripts:
- name: Install npm dependencies
script: |
npm i
- name: Install CocoaPods dependencies
script: |
yarn prepare:ios
- name: Set up keychain to be used for codesigning using Codemagic CLI 'keychain' command
script: |
keychain initialize
- name: Set up Provisioning profiles from environment variables
script: |
PROFILES_HOME="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILES_HOME"
for profile in "${!FCI_PROVISIONING_PROFILE_@}"; do
PROFILE_PATH="$(mktemp "$HOME/Library/MobileDevice/Provisioning Profiles"/ios_$(uuidgen).mobileprovision)"
echo ${!profile} | base64 --decode > "$PROFILE_PATH"
echo "Saved provisioning profile $PROFILE_PATH"
done
- name: Set up signing certificate
script: |
echo $FCI_CERTIFICATE | base64 --decode > /tmp/certificate.p12
if [ -z ${FCI_CERTIFICATE_PASSWORD+x} ]; then
# when using a certificate that is not password-protected
keychain add-certificates --certificate /tmp/certificate.p12
else
# when using a password-protected certificate
keychain add-certificates --certificate /tmp/certificate.p12 --certificate-password $FCI_CERTIFICATE_PASSWORD
fi
- name: Set up code signing settings on Xcode project
script: xcode-project use-profiles
- name: Build packages
script: |
yarn build
- name: Build ipa for distribution
script: |
xcode-project build-ipa --workspace "$FCI_BUILD_DIR/apps/mobile/native/ios/$XCODE_WORKSPACE" --scheme "$XCODE_SCHEME"
artifacts:
- build/ios/ipa/*.ipa
- /tmp/xcodebuild_logs/*.log
- $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.app
- $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.dSYM
publishing:
# See the following link for details about email publishing - https://docs.codemagic.io/publishing-yaml/distribution/#email
email:
recipients:
- ammarahmed6506@gmail.com
notify:
success: true # To not receive a notification when a build succeeds
failure: false # To not receive a notification when a build fails
app_store_connect:
api_key: $APP_STORE_CONNECT_PRIVATE_KEY # Contents of the API key, can also reference environment variable such as $APP_STORE_CONNECT_PRIVATE_KEY
key_id: $APP_STORE_CONNECT_KEY_IDENTIFIER # Alphanumeric value that identifies the API key, can also reference environment variable such as $APP_STORE_CONNECT_KEY_IDENTIFIER
issuer_id: $APP_STORE_CONNECT_ISSUER_ID # Alphanumeric value that identifies who created the API key, can also reference environment variable such as $APP_STORE_CONNECT_ISSUER_ID
submit_to_testflight: false # Optional boolean, defaults to false. Whether or not to submit the uploaded build to TestFlight beta review. Required for distributing to beta groups. Note: This action is performed during post-processing.
# beta_groups: # Specify the names of beta tester groups that will get access to the build once it has passed beta review.
# - group name 1
# - group name 2

View File

@@ -210,7 +210,7 @@ In Notesnook, one note can belong to multiple notebooks & topics. This allows fo
1. Go to `Notebooks`
2. Press the ![Three dot button](/three-dot-button.png) button.
3. Press `Move to trash` to delete the topic.
3. Press `Move to trash` to delete the notebook.
4. Select whether you also want to delete the notes inside this notebook.
---

View File

@@ -306,7 +306,7 @@ export function Main() {
</Button>
))}
{clipData && !isClipping && (
{clipData && clipData.data && !isClipping && (
<Text
variant="body"
sx={{
@@ -322,7 +322,6 @@ export function Main() {
}
}}
onClick={async () => {
if (!clipData) return;
const winUrl = URL.createObjectURL(
new Blob(["\ufeff", clipData.data], { type: "text/html" })
);

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,5 @@
- Added support for markdown with frontmatter in exports
- Fixed a bug that would randomly cause the app to freeze on launch after using the app for some days.
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,6 @@
- Added support to check/uncheck all items in a checklist
- Now you can open pdfs/images in readonly mode
- Fixed a bug where users are unable to login when app lock is enabled
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -0,0 +1,3 @@
- Bug fixes and performance improvements
Thank you for using Notesnook!

View File

@@ -31,7 +31,7 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -147,7 +147,7 @@
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/crypto": "file:../crypto",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"@types/html-to-text": "^9.0.0",
"@types/katex": "^0.16.1",
"@types/prismjs": "^1.26.0",

View File

@@ -12,7 +12,7 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",
@@ -589,9 +589,9 @@
}
},
"node_modules/@streetwriters/showdown": {
"version": "3.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.1-alpha.2.tgz",
"integrity": "sha512-GkIDQlcvJVVMgp1J5H5upiRr2hZQJAJK6gnDzOjvJAflKO4gV96xCq4RFakwahLLhL7uQ8Yx10qtmrtBuJMsEw==",
"version": "3.0.2-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.2-alpha.tgz",
"integrity": "sha512-j6IuhMgCBKnOPruierM9Lbrd+Zgj9bc80K6jYXaKcqvxd9hFnNNvbzm24IMxfuEGxv+Tqv+ukr9gxOBZU+5WuA==",
"bin": {
"showdown": "bin/showdown.js"
},
@@ -3263,9 +3263,9 @@
}
},
"@streetwriters/showdown": {
"version": "3.0.1-alpha.2",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.1-alpha.2.tgz",
"integrity": "sha512-GkIDQlcvJVVMgp1J5H5upiRr2hZQJAJK6gnDzOjvJAflKO4gV96xCq4RFakwahLLhL7uQ8Yx10qtmrtBuJMsEw=="
"version": "3.0.2-alpha",
"resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.2-alpha.tgz",
"integrity": "sha512-j6IuhMgCBKnOPruierM9Lbrd+Zgj9bc80K6jYXaKcqvxd9hFnNNvbzm24IMxfuEGxv+Tqv+ukr9gxOBZU+5WuA=="
},
"@tootallnate/once": {
"version": "2.0.0",

View File

@@ -41,7 +41,7 @@
"@microsoft/signalr": "^7.0.10",
"@microsoft/signalr-protocol-msgpack": "^7.0.10",
"@notesnook/logger": "file:../logger",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@streetwriters/showdown": "^3.0.2-alpha",
"async-mutex": "^0.3.2",
"dayjs": "1.11.9",
"entities": "^4.3.1",

View File

@@ -160,7 +160,6 @@ class Database {
this.notes = await Notes.new(this, "notes");
await this.trash.init();
await this.relations.cleanup();
this.monographs.init().catch(console.error);
}

View File

@@ -136,19 +136,20 @@ function filterSyncableItems(items, lastSyncedTimestamp, isForceSync) {
// synced is a local only property
delete item.synced;
if (item.localOnly) {
if (isUnsynced && isSyncable) {
ids.push(item.id);
syncableItems.push(
JSON.stringify({
id: item.id,
deleted: true,
dateModified: item.dateModified,
deleteReason: "localOnly"
})
JSON.stringify(
item.localOnly
? {
id: item.id,
deleted: true,
dateModified: item.dateModified,
deleteReason: "localOnly"
}
: item
)
);
} else if (isUnsynced && isSyncable) {
ids.push(item.id);
syncableItems.push(JSON.stringify(item));
}
}
return { items: syncableItems, ids };

View File

@@ -210,7 +210,7 @@ export default class Vault {
return await this._lockNote(note, this._password);
}
async exists(vaultKey) {
async exists(vaultKey = undefined) {
if (!vaultKey) vaultKey = await this._getKey();
return vaultKey && vaultKey.cipher && vaultKey.iv;
}

View File

@@ -210,7 +210,12 @@ export default class Attachments extends Collection {
if (!localOnly && !(await this._canDetach(attachment)))
throw new Error("This attachment is inside a locked note.");
if (await this._db.fs.deleteFile(attachment.metadata.hash, localOnly)) {
if (
await this._db.fs.deleteFile(
attachment.metadata.hash,
localOnly || !attachment.dateUploaded
)
) {
if (!localOnly) {
await this.detach(attachment);
}

View File

@@ -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 { logger } from "../logger";
import { makeId } from "../utils/id";
import Collection from "./collection";
@@ -189,39 +188,6 @@ export default class Relations extends Collection {
}
return items;
}
async cleanup() {
const relationsLogger = logger.scope("Relations");
const relations = this._collection.getItems();
for (const relation of relations) {
const references = [relation.to, relation.from];
for (let reference of references) {
let exists = false;
switch (reference.type) {
case "reminder":
exists = this._db.reminders.exists(reference.id);
break;
case "note":
exists =
this._db.notes.exists(reference.id) ||
this._db.trash.exists(reference.id);
break;
case "notebook":
exists =
this._db.notebooks.exists(reference.id) ||
this._db.trash.exists(reference.id);
break;
}
if (!exists) {
relationsLogger.debug(
`Removing relation during cleanup`,
relation.id
);
await this.remove(relation.id);
}
}
}
}
}
/**

View File

@@ -276,6 +276,10 @@ export default class Backup {
if (attachment) {
item = {
...attachment,
metadata: {
...attachment.metadata,
type: item.metadata.type || attachment.metadata.type
},
noteIds: setManipulator.union(attachment.noteIds, item.noteIds)
};
} else {

View File

@@ -48,6 +48,10 @@ export default class Storage {
return this.storage.remove(key);
}
removeMulti(keys) {
return this.storage.removeMulti(keys);
}
getAllKeys() {
return this.storage.getAllKeys();
}

View File

@@ -59,8 +59,7 @@ class DatabaseLogWriter {
*/
constructor(storage) {
this.storage = storage;
this.key = new Date().toLocaleDateString();
this.queue = {};
this.queue = new Map();
this.hasCleared = false;
setInterval(() => {
setTimeout(() => {
@@ -74,34 +73,29 @@ class DatabaseLogWriter {
}
push(message) {
this.queue[`${this.key}:${message.timestamp}`] = message;
}
async read() {
const logKeys = await this.storage.getAllKeys();
const keys = [];
for (let key of logKeys) {
if (key.startsWith(this.key)) keys.push(key);
}
return Object.values(await this.storage.readMulti(keys));
const key = new Date(message.timestamp).toLocaleDateString();
this.queue.set(`${key}:${message.timestamp}`, message);
}
async flush() {
if (Object.keys(this.queue).length === 0) return;
const queueCopy = Object.entries(this.queue);
this.queue = {};
if (this.queue.size === 0) return;
const queueCopy = Array.from(this.queue.entries());
this.queue = new Map();
await this.storage.writeMulti(queueCopy);
}
async rotate() {
const logKeys = (await this.storage.getAllKeys()).sort();
const keysToRemove = [];
for (let key of logKeys) {
const keyParts = key.split(":");
if (keyParts.length === 1 || parseInt(keyParts[1]) < Date.now() - WEEK) {
await this.storage.remove(key);
keysToRemove.push(key);
}
}
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
}
}
@@ -116,42 +110,41 @@ class DatabaseLogManager {
async get() {
const logKeys = await this.storage.getAllKeys();
const logs = {};
const logs = await this.storage.readMulti(logKeys);
const logGroups = {};
for (const logKey of logKeys) {
const keyParts = logKey.split(":");
for (const [key, log] of logs) {
const keyParts = key.split(":");
if (keyParts.length === 1) continue;
const key = keyParts[0];
const log = await this.storage.read(logKey, true);
if (!logs[key]) logs[key] = [];
logs[key].push(log);
const groupKey = keyParts[0];
if (!logGroups[groupKey]) logGroups[groupKey] = [];
logGroups[groupKey].push(log);
}
return Object.keys(logs).map((key) => ({
key: key,
logs: logs[key]?.sort((a, b) => a.timestamp - b.timestamp)
}));
return Object.keys(logGroups)
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }))
.map((key) => ({
key,
logs: logGroups[key]?.sort((a, b) => a.timestamp - b.timestamp)
}));
}
async clear() {
const logKeys = await this.storage.getAllKeys();
for (const key of logKeys) {
await this.storage.remove(key);
}
await this.storage.removeMulti(logKeys);
}
async delete(key) {
const logKeys = await this.storage.getAllKeys();
const keysToRemove = [];
for (const logKey of logKeys) {
const keyParts = logKey.split(":");
if (keyParts.length === 1) continue;
const currKey = keyParts[0];
if (currKey === key) {
await this.storage.remove(logKey);
}
if (currKey === key) keysToRemove.push(logKey);
}
if (keysToRemove.length) await this.storage.removeMulti(keysToRemove);
}
}

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import MarkdownBuilder from "../utils/templates/markdown/builder";
import * as MarkdownBuilder from "../utils/templates/markdown/builder";
import HTMLBuilder from "../utils/templates/html/builder";
import TextBuilder from "../utils/templates/text/builder";
import { getContentFromData } from "../content-types";
@@ -79,7 +79,7 @@ export default class Note {
/**
*
* @param {"html"|"md"|"txt"} format - Format to export into
* @param {"html"|"md"|"md-frontmatter"|"txt"} format - Format to export into
* @param {string} [contentItem=undefined]
* @param {boolean} [template=true]
* @param {string} [rawHTML=undefined] rawHTML - Use this raw content instead of generating itself
@@ -88,6 +88,7 @@ export default class Note {
async export(to = "html", contentItem, template = true, rawHTML) {
if (to !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
return false;
if (!this.data) return false;
const templateData = {
metadata: this.data,
@@ -132,6 +133,11 @@ export default class Note {
return template
? MarkdownBuilder.buildMarkdown(templateData)
: templateData.content;
case "md-frontmatter":
templateData.content = rawHTML || content.toMD();
return template
? MarkdownBuilder.buildMarkdownWithFrontmatter(templateData)
: templateData.content;
default:
throw new Error("Export format not supported.");
}

Some files were not shown because too many files have changed in this diff Show More