Compare commits

...

128 Commits

Author SHA1 Message Date
Abdullah Atta
e6ff0a2430 editor: adjust font size and padding of inline code 2025-11-24 11:51:24 +05:00
01zulfi
eab0bdee8b editor: update text color of inline code
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-24 10:10:01 +05:00
01zulfi
0d94797bc1 editor: remove top&bottom padding from inline code
* it was cutting off a bit of text from top&bottom lines

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-24 09:36:24 +05:00
01zulfi
372c49d81e editor: improve inline code styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-21 15:46:24 +05:00
Abdullah Atta
97d21c818e Merge pull request #8858 from 01zulfi/editor/disable-collapsible-headings-in-table
editor: disable collapsible headings inside tables
2025-11-12 11:30:38 +05:00
Abdullah Atta
3beb60d8b6 Merge pull request #8918 from streetwriters/core/fix-keys-reencryption-on-update-password
core: fix user keys re-encryption when updating password
2025-11-12 11:29:03 +05:00
Ammar Ahmed
6343212d7e Merge pull request #8923 from streetwriters/mobile-release-3.3.9
mobile: release v3.3.9
2025-11-12 09:47:26 +05:00
01zulfi
f728ad8826 editor: disable collapsible headings inside tables
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-11 11:39:41 +05:00
Abdullah Atta
b96af6c4cf mobile: release v3.3.9 2025-11-05 23:35:27 +05:00
Abdullah Atta
6069a41fa4 Merge pull request #8909 from streetwriters/mobile-release-3.3.8
Mobile release 3.3.8
2025-11-05 23:28:48 +05:00
01zulfi
7190207e5c core: fix user keys re-encryption when updating password
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-05 14:02:48 +05:00
01zulfi
015453a40b Merge pull request #8914 from streetwriters/release/v3.3.5
web: bump version to 3.3.5
2025-11-05 13:07:01 +05:00
01zulfi
dad92a186c web: bump version to 3.3.5
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-05 12:08:04 +05:00
Abdullah Atta
7e5e19e72c Merge pull request #8913 from streetwriters/editor/fix-checklist-bulletlist
editor: fix checklist parsing as bulletlist
2025-11-05 12:03:55 +05:00
01zulfi
8e1484a029 editor: fix checklist parsing as bulletlist
* reduce list-item's parseHTML priority lower than check-list-item's parseHTML priority

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-05 11:01:52 +05:00
Ammar Ahmed
4a3b3a14ec mobile: release v3.3.8 2025-11-04 10:35:34 +05:00
Ammar Ahmed
758157cf78 mobile: update documentation link 2025-11-04 10:34:43 +05:00
Ammar Ahmed
a1a171d648 mobile: fix restore backup keeps loading 2025-11-04 10:24:22 +05:00
01zulfi
92ba4d7624 Merge pull request #8907 from streetwriters/release/v3.3.4
web: bump version to 3.3.4
2025-11-03 17:13:35 +05:00
01zulfi
d44ee5ca05 web: bump version to 3.3.4
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-03 16:18:57 +05:00
Abdullah Atta
d7dde74675 Merge pull request #8905 from streetwriters/editor/fix-images-in-lists
editor: fix old inline images in bullet,numbered,task&check lists
2025-11-03 16:13:02 +05:00
01zulfi
5fde1c0f07 editor: fix old inline images in bullet,numbered,task&check lists
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-03 16:10:26 +05:00
Abdullah Atta
ffddbe2d7a Merge pull request #8857 from 01zulfi/core/escape-paranthesis-in-search
core: escape paranthesis in notes search query
2025-11-03 12:37:39 +05:00
01zulfi
eb3998d72d core: remove unnecessary only in tests
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-03 12:21:11 +05:00
01zulfi
fca61afd7c core: escape paranthesis in notes search query
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-03 11:02:02 +05:00
Abdullah Atta
33559250d8 Merge pull request #8904 from streetwriters/editor/fix-heading-plugin
editor: fix crash in heading plugin when pasting markdown content
2025-11-03 10:27:53 +05:00
01zulfi
38daca0099 editor: fix crash in heading plugin when pasting markdown content
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-03 09:01:09 +05:00
Abdullah Atta
36c8b8cc25 Merge pull request #8896 from streetwriters/webclipper/release/0.4.0
Update Web Clipper to 0.4.0
2025-11-01 19:43:00 +05:00
Abdullah Atta
dd4ca817c7 clipper(web): bump version to 0.4.0 2025-11-01 19:41:01 +05:00
Ammar Ahmed
594ceb1f52 Merge pull request #8894 from streetwriters/mobile-release-3.3.7
Mobile release 3.3.7
2025-11-01 09:43:14 +05:00
Ammar Ahmed
fbc74ec359 mobile: update pods 2025-10-31 21:55:22 +05:00
Ammar Ahmed
0dfc1dfa52 mobile: release v3.3.7 2025-10-31 20:02:43 +05:00
Ammar Ahmed
b9c14ef9aa Merge pull request #8888 from streetwriters/fix-clipper-mobile
mobile: fix share extension not able to clip webpage
2025-10-31 18:38:59 +05:00
Ammar Ahmed
3fec2b397f Merge pull request #8883 from streetwriters/fix-16kb-alignment-x64
mobile: fix 16kb alignment on x86_64 platform
2025-10-31 18:38:34 +05:00
Ammar Ahmed
ec841caef7 Merge pull request #8866 from streetwriters/feat-add-clear-button-search
Feat add clear button search
2025-10-31 18:38:16 +05:00
Ammar Ahmed
c163fc3be4 Merge pull request #8865 from streetwriters/mobile-fix-crash-sub-status
mobile: fix crash in subscription-status settings item
2025-10-31 18:37:59 +05:00
Ammar Ahmed
1e04315596 Merge pull request #8853 from streetwriters/fix-export-notes-free-users
mobile: allow free users to export note in all formats
2025-10-31 18:37:18 +05:00
Ammar Ahmed
9aef28b03d Merge pull request #8856 from streetwriters/fix-table-cell-selection-mobile
Fix table cell selection mobile
2025-10-31 18:36:56 +05:00
01zulfi
88628c650f Merge pull request #8890 from streetwriters/release/v3.3.3
web: bump version to 3.3.3
2025-10-31 16:37:09 +05:00
01zulfi
9523fd52d8 web: bump version to 3.3.3
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-31 14:40:34 +05:00
Abdullah Atta
8cc6df92f4 Merge pull request #8889 from 01zulfi/editor/fix-heading-level-change
editor: fix changing level of collapsed heading
2025-10-31 14:05:09 +05:00
01zulfi
042656856a editor: fix changing level of collapsed heading
* preserve collasped state
* reset visibility of hidden nodes under changed heading

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-31 13:14:06 +05:00
Ammar Ahmed
5921c61e98 mobile: fix share extension not able to clip webpage 2025-10-31 10:01:59 +05:00
Abdullah Atta
c5cd214d97 Merge pull request #8882 from 01zulfi/editor/fix-collapsible-hidden-under
editor: fix disappearing nodes under collapsed headings if heading is edited
2025-10-30 13:29:05 +05:00
Abdullah Atta
b30c35e59d Merge pull request #8877 from 01zulfi/editor/fix-outline-images
editor: fix images from older version not rendering in outline list
2025-10-30 13:28:00 +05:00
01zulfi
ce4fa13758 editor: fix disappearing nodes under collapsed headings if heading is edited
* design flaw in using the hiddenUnder attribute on hidden nodes, a hidden attribute should be sufficient

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-30 12:59:50 +05:00
01zulfi
1f47df0f45 editor: add test case to assert migration of old inline images in outline list item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-30 12:53:21 +05:00
Ammar Ahmed
91109278b6 mobile: fix 16kb alignment on x86_64 platform 2025-10-30 11:16:23 +05:00
01zulfi
78e4672871 editor: fix images from older version not rendering in outline list
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-29 13:12:39 +05:00
Abdullah Atta
dbecd6a466 Merge pull request #8743 from 01zulfi/editor/restrict-attachment-for-notloggedin-users
editor: restrict attachment upload for not logged-in users
2025-10-29 12:35:53 +05:00
Ammar Ahmed
e690973107 mobile: restrict attachment upload by logged out users 2025-10-29 12:33:20 +05:00
Abdullah Atta
17b14e0ac6 Merge pull request #8875 from streetwriters/fix-remove-line-through-checklist
Fix remove line through checklist
2025-10-29 11:53:58 +05:00
Abdullah Atta
0f8da747f9 Merge pull request #8867 from streetwriters/fix/warn-arm64-translation
Warn users if app is running under arm64 translation
2025-10-29 11:53:19 +05:00
Ammar Ahmed
df7d91e4f3 mobile: fix resize input delay too long 2025-10-29 11:38:51 +05:00
Ammar Ahmed
7eefecd961 editor: do not strikethrough on checked item in checklist 2025-10-29 11:38:32 +05:00
Abdullah Atta
2ec764d862 clipper: update lockfile 2025-10-28 14:36:48 +05:00
Abdullah Atta
5a9b9da47e Merge pull request #8869 from 01zulfi/editor/fix-outline-list-collapse
editor: fix outline list item collapsing the parent item
2025-10-28 13:53:33 +05:00
01zulfi
c5a82bd779 editor: fix outline list item collapsing the parent item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-28 13:37:44 +05:00
Abdullah Atta
9c2a807025 desktop: show warning after app is ready 2025-10-28 13:29:57 +05:00
Abdullah Atta
4fa1ea4380 desktop: warn users if app is running under arm64 translation 2025-10-28 12:41:34 +05:00
01zulfi
351d412fdf editor: restrict attachment upload for not logged-in users
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-28 12:40:25 +05:00
Ammar Ahmed
38600bf2a5 mobile: add tests 2025-10-28 12:00:38 +05:00
Ammar Ahmed
4720456d63 mobile: add clear button to search on mobile 2025-10-28 11:50:25 +05:00
Ammar Ahmed
5a2221c5d3 mobile: fix crash in subscription-status settings item 2025-10-28 10:52:11 +05:00
Abdullah Atta
91ef98df4f Merge pull request #8814 from streetwriters/feature/direct-navigation-to-setting-sections
Add support for direct navigation (via url) to setting sections
2025-10-27 15:34:34 +05:00
Abdullah Atta
68b921887b Merge pull request #7294 from 01zulfi/webclipper/bookmark
webclipper: add bookmark option
2025-10-27 15:19:07 +05:00
Abdullah Atta
dea91c48a2 web: add hr after bookmark link 2025-10-27 15:18:16 +05:00
Abdullah Atta
f2e91d4a5c webclipper: fix bookmark not working in web clipper 2025-10-27 15:18:00 +05:00
01zulfi
47be00355d webclipper: add bookmark option
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-27 13:58:06 +05:00
Abdullah Atta
05c4051cc9 Merge pull request #8254 from 01zulfi/webclipper/start-clip-button
webclipper: improve clipping ux
2025-10-27 13:48:44 +05:00
01zulfi
71fe6bbebc webclipper: improve clipping ux
* start clip when button is clicked, not on load or selection click
* add discard button

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-27 13:43:53 +05:00
Abdullah Atta
74ed529f16 Merge pull request #8195 from 01zulfi/clipper/improve-clip-with-styles
clipper: improve clip with styles
2025-10-27 13:31:53 +05:00
Abdullah Atta
24a6d8c3f9 editor: fix really large images not loading 2025-10-27 13:16:38 +05:00
Ammar Ahmed
14c1567c07 editor: fix scrollLeft value on mobile when using scroll container 2025-10-27 12:50:16 +05:00
Ammar Ahmed
fb778f56ed editor: use default scroll container on mobile to fix selection bugs 2025-10-27 12:47:41 +05:00
Abdullah Atta
1eb0ef0cbe editor: fix web clipped screenshot not visible in editor 2025-10-27 12:23:19 +05:00
Abdullah Atta
a81c8d0064 clipper: fix error on inlining css background images 2025-10-27 11:29:52 +05:00
Abdullah Atta
36099c6b4d clipper: update lockfile 2025-10-25 15:28:31 +05:00
01zulfi
8f7a15e692 clipper: remove unnecessary packages
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:44 +05:00
Abdullah Atta
de4a660eb2 clipper: add svg screenshots for debugging 2025-10-25 15:12:31 +05:00
Abdullah Atta
c53fab603f clipper: ignore noscript elements 2025-10-25 15:12:31 +05:00
01zulfi
a3e11b62e0 clipper: append head for node selection clips
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:30 +05:00
01zulfi
5d1775151d clipper: remove invalid element attributes for screenshot clips
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:30 +05:00
Abdullah Atta
a0f5880e2c clipper: serialize styles using xml serializer 2025-10-25 15:12:30 +05:00
Abdullah Atta
3dc7e70890 clipper: fix svg -> canvas -> image logic 2025-10-25 15:12:30 +05:00
01zulfi
2fa1fc8662 clipper: fix screenshot clip
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:27 +05:00
01zulfi
879a65a41c clipper: skip style inlining if property not present
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
Abdullah Atta
d5b68bf0da clipper: fix tests 2025-10-25 15:12:08 +05:00
Abdullah Atta
fd460d3e90 clipper: bg images in css not clipped 2025-10-25 15:12:08 +05:00
01zulfi
a74bd4972c clipper: move inlining stylesheeet & css import resolution logic to addStylesToHead
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
0ed8839ea9 clipper: fix build error
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
Abdullah Atta
d7b0f2684a clipper: minor perf improvements 2025-10-25 15:12:08 +05:00
Abdullah Atta
b5be9b3abf clipper: fix image inlining in stylesheets 2025-10-25 15:12:08 +05:00
01zulfi
911105c409 clipper: inline background-images style for stylesheets appended to head
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
200c842671 clipper: ignore print stylesheets when adding to head
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
5e5930e9e8 clipper: inline styles if cssRules.length > 0
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
Abdullah Atta
fd64c1cf2d clipper: set downloaded stylesheet as html instead of text 2025-10-25 15:12:08 +05:00
01zulfi
48e2916874 clipper: update filtering in cloned node
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
a4b0e5bfef clipper: bring back node processing
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
0b56121a26 clipper: preserve root element attributes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:12:08 +05:00
01zulfi
f3f61d0e19 clipper: remove unneeded code & packages
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:10:21 +05:00
01zulfi
faaf1eeadf clipper: remove images from clip if images false
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:42 +05:00
01zulfi
4fbaecc24e clipper: remove clone related functions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:42 +05:00
01zulfi
4fb056095c clipper: create & use addStylesToHead()
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:42 +05:00
01zulfi
2d6142381b clipper: remove valid attributes check
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:41 +05:00
01zulfi
d97d14e830 clipper: improve clip with styles
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:41 +05:00
01zulfi
f2f3f7e393 clipper: (wip) improve clip with styles
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-25 15:07:41 +05:00
Ammar Ahmed
50176d2b3a mobile: allow free users to export note in all formats 2025-10-24 13:47:02 +05:00
Ammar Ahmed
ece7b29005 Merge pull request #8839 from streetwriters/mobile-release-3.3.6
Mobile release 3.3.6
2025-10-24 09:01:33 +05:00
Ammar Ahmed
8cd8e93487 mobile: fix styles in editor 2025-10-23 21:58:27 +05:00
Ammar Ahmed
8900a6b080 mobile: release v3.3.6 2025-10-23 21:39:20 +05:00
Ammar Ahmed
19d90fa221 mobile: allow users to open redeem code url from app 2025-10-23 21:38:55 +05:00
Ammar Ahmed
2e5ff859eb Merge pull request #8835 from streetwriters/fix-reminder-widget
mobile: fix reminders widget on android
2025-10-23 13:48:28 +05:00
Ammar Ahmed
06747575ee Merge pull request #8834 from streetwriters/fix-signup-navigation
mobile: fix navigation after signup complete broken
2025-10-23 13:48:07 +05:00
Ammar Ahmed
98bb8ca186 Merge pull request #8833 from streetwriters/fix-app-lock-free-user
mobile: fix app-lock disabled dialog does not show to free users
2025-10-23 13:47:46 +05:00
Ammar Ahmed
d5f68602b6 Merge pull request #8832 from streetwriters/fix-vault-biometrics-unlock
mobile: fix vault biometrics state incorrect on app launch
2025-10-23 13:47:09 +05:00
Ammar Ahmed
312999aa5b Merge pull request #8827 from streetwriters/fix-delete-note-locked-bulk
mobile: ask user for vault password before deleting locked notes in bulk
2025-10-23 13:46:09 +05:00
Ammar Ahmed
607a4d26f4 Merge pull request #8826 from streetwriters/fix-restore-button-mobile
mobile: fix restore button is hidden in history preview item
2025-10-23 13:44:09 +05:00
Ammar Ahmed
a88b6615e4 Merge pull request #8825 from streetwriters/fix-gesture-navigation
mobile: fix gesture navigation not working
2025-10-23 13:43:50 +05:00
Ammar Ahmed
f900098201 mobile: fix app-lock disabled dialog does not show to free users 2025-10-23 12:17:01 +05:00
Ammar Ahmed
d51aaa8eb1 mobile: fix vault biometrics state incorrect on app launch 2025-10-23 12:05:50 +05:00
Ammar Ahmed
e9a48ae028 mobile: fix reminders widget on android 2025-10-23 11:01:28 +05:00
Ammar Ahmed
90a0f619ec mobile: ask user for vault password before deleting locked notes in bulk 2025-10-22 19:27:43 +05:00
Ammar Ahmed
ed3662a636 mobile: fix restore button is hidden in history preview item 2025-10-22 14:54:47 +05:00
Ammar Ahmed
26eda3b105 mobile: fix gesture navigation not working 2025-10-22 14:40:56 +05:00
Ammar Ahmed
e239597eb1 Merge pull request #8821 from streetwriters/fix--disable-post-install-script
Fix fdroid build failing due to postinstall script using bun
2025-10-22 09:11:41 +05:00
Ammar Ahmed
bf1ef07d3a mobile: fix fdroid build failing due to postinstall script using bun 2025-10-22 09:09:32 +05:00
Abdullah Atta
7d2e1dab4c web: add support for direct navigation (via url) to setting sections 2025-10-21 14:42:37 +05:00
143 changed files with 2138 additions and 3882 deletions

View File

@@ -47,6 +47,11 @@ jobs:
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Check for typescript errors
run: |
cd apps/mobile
npx tsc --noEmit
- name: Detox build
run: |
yarn build:android

View File

@@ -73,6 +73,12 @@ jobs:
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build unsigned app bundle
run: yarn release:android:bundle

View File

@@ -73,6 +73,12 @@ jobs:
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build unsigned app bundle
run: yarn release:android:bundle

View File

@@ -64,6 +64,12 @@ jobs:
bundle install
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: CCache Stats Before Build
run: ccache -sv

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.2",
"version": "3.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.2",
"version": "3.3.5",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.2",
"version": "3.3.5",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./overrides";
import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
import { configureAutoUpdater } from "./utils/autoupdater";
@@ -182,6 +182,17 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (app.runningUnderARM64Translation) {
console.log("App is running under ARM64 translation");
dialog.showMessageBoxSync({
message:
"Notesnook detected that it is running under ARM64 translation. For the best performance, please download the ARM64 build of Notesnook from our website.",
type: "warning",
buttons: ["Okay"],
title: "Degraded Performance Warning"
});
}
if (config.customDns) enableCustomDns();
else disableCustomDns();

View File

@@ -94,7 +94,9 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKeyCipher: Cipher = CipherStorage.getMap(
DB_KEY_CIPHER
) as Cipher;
const databaseKey = (await decrypt(
{
password: appLockPassword
@@ -135,7 +137,9 @@ export async function clearAppLockVerificationCipher() {
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
const appLockCipher: Cipher = CipherStorage.getMap(
APPLOCK_CIPHER
) as Cipher;
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -159,7 +163,9 @@ export function clearDatabaseKey() {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKeyCipher: Cipher = CipherStorage.getMap(
"databaseKeyCipher"
) as Cipher;
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -293,7 +299,7 @@ export async function deriveCryptoKey(data: SerializedKey) {
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;

View File

@@ -136,6 +136,12 @@ export const Storage: IStorage = {
clear(): Promise<void> {
return DefaultStorage.clear();
},
generateCryptoKeyPair() {
throw new Error("Not implemented");
},
decryptAsymmetric() {
throw new Error("Not implemented");
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},

View File

@@ -99,7 +99,7 @@ export const useLogin = (
callback && callback(false);
} catch (e) {
callback && callback(false);
if (e.message === "invalid_grant") {
if ((e as Error).message === "invalid_grant") {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);

View File

@@ -35,29 +35,35 @@ import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import { useSettingStore } from "../../stores/use-setting-store";
const DialogProvider = () => {
const { colors } = useThemeColors();
const isAppLoading = useSettingStore((state) => state.isAppLoading);
return (
<>
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<AuthModal colors={colors} />
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<SheetProvider />
<SheetProvider context="sync_progress" />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
<Dialog context="global" />
<Progress />
{isAppLoading ? null : (
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
)}
</>
);
};

View File

@@ -41,7 +41,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
positiveText: strings.restore(),
negativeText: strings.delete(),
positivePress: async () => {
if ((await db.trash.restore(item.id)) === false) return;
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({

View File

@@ -117,7 +117,8 @@ export default function NotePreview({ session, content, note }) {
{!session?.locked && !locked ? (
<View
style={{
flex: 1
flex: 1,
backgroundColor: colors.primary.background
}}
>
<ReadonlyEditor

View File

@@ -44,6 +44,7 @@ import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { SafeAreaView } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import {
ANDROID_POLICE_SVG,

View File

@@ -39,9 +39,7 @@ import {
presentSheet
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -83,7 +81,6 @@ const ExportNotesSheet = ({
type: "pdf" | "txt" | "md" | "html" | "md-frontmatter"
) => {
if (exporting) return;
if (!PremiumService.get() && type !== "txt") return;
setExporting(true);
update?.({ disableClosing: true } as PresentSheetOptions);
setComplete(false);
@@ -145,8 +142,7 @@ const ExportNotesSheet = ({
await exportNoteAs("txt");
},
icon: "card-text",
id: notesnook.ids.dialogs.export.text,
pro: true
id: notesnook.ids.dialogs.export.text
},
{
title: "HTML",
@@ -194,8 +190,7 @@ const ExportNotesSheet = ({
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
justifyContent: "flex-start",
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.pro ? 1 : 0.5
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View

View File

@@ -29,6 +29,7 @@ import {
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";

View File

@@ -171,7 +171,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
eSendEvent(eOnLoadNote, { newNote: true });
fluidTabsRef.current?.goToPage("editor", false);
return;
} else if (url.startsWith("https://notesnook.com/open_note")) {
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const note = await db.notes.note(id);
@@ -182,13 +182,13 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
fluidTabsRef.current?.goToPage("editor", false);
}
}
} else if (url.startsWith("https://notesnook.com/open_reminder")) {
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
const id = new URL(url).searchParams.get("id");
if (id) {
const reminder = await db.reminders.reminder(id);
if (reminder) AddReminder.present(reminder);
}
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
} else if (url.startsWith("https://app.notesnook.com/new_reminder")) {
const reminderFeature = await isFeatureAvailable("activeReminders");
if (!reminderFeature.isAllowed) {
ToastManager.show({
@@ -780,13 +780,11 @@ export const useAppEvents = () => {
if (!isAppLoading && !appLocked) {
setTimeout(() => {
sub = AppState.addEventListener("change", onAppStateChanged);
if (
refValues.current.initialUrl &&
!refValues.current.initialUrl?.includes("open_note")
) {
if (refValues.current.initialUrl) {
onAppOpenedFromURL({
url: refValues.current.initialUrl!
});
refValues.current.initialUrl = undefined;
}
}, 1000);

View File

@@ -69,36 +69,6 @@ export default function useFeatureManager() {
db.settings.setDefaultTag(undefined);
}
}
const isAppLocked = useUserStore.getState().appLocked;
let unsub: () => void;
if (isAppLocked) {
unsub = useUserStore.subscribe((state) => {
if (!state.appLocked && !features?.appLock?.isAllowed) {
unsub();
SettingsService.setProperty("appLockEnabled", false);
setTimeout(() => {
presentDialog({
title: "App Lock Disabled",
paragraph: features?.appLock?.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
eSendEvent(eCloseSimpleDialog);
if (SettingsService.getProperty("serverUrls")) return;
Navigation.navigate("PayWall", {
context: "logged-in"
});
}
});
}, 1000);
}
});
}
return () => {
unsub?.();
};
}, [features, plan]);
return true;

View File

@@ -1,67 +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 { Platform } from "react-native";
import { Subscription } from "react-native-iap";
import PremiumService from "../services/premium";
import { db } from "../common/database";
import { Product } from "@notesnook/core";
const skuInfos: { [name: string]: Product | undefined } = {};
export const usePricing = (period: "monthly" | "yearly") => {
const [current, setCurrent] = useState<{
period: string;
info?: Product;
product?: Subscription;
}>();
const getDefaultSku = (period: "monthly" | "yearly") => {
return period === "monthly"
? "com.streetwriters.notesnook.sub.mo"
: "com.streetwriters.notesnook.sub.yr";
};
useEffect(() => {
(async () => {
const skuInfo =
skuInfos[period] ||
(await db.pricing?.sku(
Platform.OS === "android" ? "android" : "ios",
period
));
skuInfos[period] = skuInfo;
const products = (await (
await PremiumService.loadProductsAndSubs()
).subs) as Subscription[];
let product = products.find((p) => p.productId === skuInfo?.sku);
if (!product)
product = products.find((p) => p.productId === getDefaultSku(period));
setCurrent({
info: skuInfo,
period,
product
});
})();
}, [period]);
return current;
};

View File

@@ -21,6 +21,7 @@ import React, { useCallback, useEffect } from "react";
import { db } from "../common/database";
import BiometricService from "../services/biometrics";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import { useSettingStore } from "../stores/use-setting-store";
const VaultStatusDefaults = {
exists: false,
@@ -36,6 +37,7 @@ export type VaultStatusType = {
export const useVaultStatus = () => {
const [vaultStatus, setVaultStatus] = React.useState(VaultStatusDefaults);
const isAppLoading = useSettingStore((state) => state.isAppLoading);
const checkVaultStatus = useCallback(() => {
db.vault?.exists().then(async (exists) => {
@@ -50,12 +52,13 @@ export const useVaultStatus = () => {
}, []);
useEffect(() => {
if (isAppLoading) return;
checkVaultStatus();
eSubscribeEvent("vaultUpdated", () => checkVaultStatus());
return () => {
eUnSubscribeEvent("vaultUpdated", () => checkVaultStatus());
};
}, [checkVaultStatus]);
}, [checkVaultStatus, isAppLoading]);
return vaultStatus;
};

View File

@@ -49,6 +49,7 @@ import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../../components/sheets/paywall";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
const ReminderModes =
Platform.OS === "ios"
@@ -89,6 +90,7 @@ const ReminderNotificationModes = {
export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
useNavigationFocus(props.navigation, { focusOnInit: true });
const { colors, isDark } = useThemeColors();
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
reminder?.mode || "once"

View File

@@ -61,6 +61,7 @@ import {
import { fluidTabsRef } from "../../utils/global-refs";
import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { useVaultStatus } from "../../hooks/use-vault-status";
const style: ViewStyle = {
height: "100%",
@@ -197,6 +198,7 @@ const Editor = React.memo(
export default Editor;
const useLockedNoteHandler = () => {
const vaultStatus = useVaultStatus();
const tab = useTabStore((state) => state.getTab(state.currentTab));
const tabRef = useRef(tab);
tabRef.current = tab;
@@ -217,15 +219,13 @@ const useLockedNoteHandler = () => {
useEffect(() => {
(async () => {
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
biometryAvailable: !!biometry,
biometryEnrolled: !!fingerprint
biometryAvailable: !!vaultStatus.isBiometryAvailable,
biometryEnrolled: !!vaultStatus.biometryEnrolled
});
syncTabs("biometry");
})();
}, [tab?.id]);
}, [tab?.id, vaultStatus.biometryEnrolled, vaultStatus.isBiometryAvailable]);
useEffect(() => {
const unlockWithBiometrics = async () => {

View File

@@ -59,6 +59,7 @@ export type Settings = {
fontScale: number;
markdownShortcuts: boolean;
features: Record<any, any>;
loggedIn: boolean;
};
export type EditorProps = {

View File

@@ -82,6 +82,7 @@ import { editorState, openInternalLink } from "./utils";
import AddReminder from "../../add-reminder";
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import PaywallSheet from "../../../components/sheets/paywall";
import useNavigationStore from "../../../stores/use-navigation-store";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -164,6 +165,7 @@ export const useEditorEvents = (
state.timeFormat
]);
const handleBack = useRef<NativeEventSubscription>();
const loggedIn = useUserStore((state) => !!state.user);
const { fontScale } = useWindowDimensions();
const doubleSpacedLines = useSettingStore(
@@ -223,7 +225,8 @@ export const useEditorEvents = (
timeFormat: db.settings?.getTimeFormat(),
fontScale,
markdownShortcuts,
features
features,
loggedIn
});
}, [
fullscreen,
@@ -241,7 +244,8 @@ export const useEditorEvents = (
timeFormat,
loading,
fontScale,
markdownShortcuts
markdownShortcuts,
loggedIn
]);
const onBackPress = useCallback(async () => {
@@ -274,7 +278,15 @@ export const useEditorEvents = (
const onHardwareBackPress = useCallback(() => {
if (fluidTabsRef.current?.page() === "editor") {
onBackPress();
if (
useNavigationStore.getState().currentRoute === "ManageTags" ||
useNavigationStore.getState().currentRoute === "LinkNotebooks" ||
useNavigationStore.getState().currentRoute === "AddReminder"
) {
Navigation.goBack();
} else {
onBackPress();
}
return true;
}
}, [onBackPress]);
@@ -547,9 +559,17 @@ export const useEditorEvents = (
if (editor.state.current?.isFocused) {
editor.state.current.isFocused = true;
}
PaywallSheet.present(
await isFeatureAvailable(editorMessage.value.feature)
);
if (editorMessage.value.feature === "insertAttachment") {
ToastManager.show({
type: "info",
message: strings.loginRequired()
});
} else {
PaywallSheet.present(
await isFeatureAvailable(editorMessage.value.feature)
);
}
break;
case EditorEvents.monograph:
publishNote();

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef } from "react";
import React, { useRef, useState } from "react";
import { View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import { IconButton } from "../../components/ui/icon-button";
@@ -36,6 +36,7 @@ export const SearchBar = ({
onChangeText: (value: string) => void;
loading?: boolean;
}) => {
const [clearButton, setClearButton] = useState(false);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const isFocused = useNavigationStore(
(state) => state.focusedRouteId === "Search"
@@ -45,6 +46,7 @@ export const SearchBar = ({
const inputRef = useRef<TextInput>(null);
const _onChangeText = (value: string) => {
onChangeText(value);
setClearButton(!!value);
};
return selectionMode && isFocused ? null : (
@@ -100,6 +102,23 @@ export const SearchBar = ({
autoCorrect={false}
placeholderTextColor={colors.primary.placeholder}
/>
{clearButton ? (
<IconButton
name="close"
size={AppFontSize.xxl}
top={10}
testID="clear-search"
bottom={10}
onPress={() => {
inputRef.current?.clear();
onChangeText("");
setClearButton(false);
}}
color={colors.primary.paragraph}
type="plain"
/>
) : null}
</View>
</View>
);

View File

@@ -185,7 +185,7 @@ export default function DebugLogs() {
paragraph: strings.clearLogsConfirmation(currentLog.key),
negativeText: strings.cancel(),
positiveText: strings.clear(),
positivePress: () => {
positivePress: async () => {
const index = logs.findIndex((l) => (l.key = currentLog.key));
logManager?.delete(currentLog.key);
if (logs.length > 1) {

View File

@@ -2,7 +2,8 @@ import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useAsync } from "react-async-hook";
import {
ActivityIndicator,
Image,
@@ -13,6 +14,7 @@ import {
import { db } from "../../common/database";
import AppIcon from "../../components/ui/AppIcon";
import { Button } from "../../components/ui/button";
import { Notice } from "../../components/ui/notice";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
@@ -21,8 +23,8 @@ import PremiumService from "../../services/premium";
import { useUserStore } from "../../stores/use-user-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { useAsync } from "react-async-hook";
import { Notice } from "../../components/ui/notice";
import { openLinkInBrowser } from "../../utils/functions";
import { Pressable } from "../../components/ui/pressable";
export const NotesnookCircle = () => {
const user = useUserStore((state) => state.user);
@@ -156,32 +158,53 @@ const Partner = ({
}}
/>
) : (
<TouchableOpacity
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
alignItems: "center",
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
borderWidth: 0.5,
borderColor: colors.secondary.border,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
}}
activeOpacity={0.9}
onPress={() => {
Clipboard.setString(code);
}}
>
<Paragraph
size={AppFontSize.lg}
color={colors.secondary.paragraph}
<>
<TouchableOpacity
style={{
backgroundColor: colors.secondary.background,
borderRadius: defaultBorderRadius,
alignItems: "center",
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
borderWidth: 0.5,
borderColor: colors.secondary.border,
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
}}
activeOpacity={0.9}
onPress={() => {
Clipboard.setString(code);
}}
>
{code}
</Paragraph>
<Paragraph
size={AppFontSize.lg}
color={colors.secondary.paragraph}
>
{code}
</Paragraph>
<AppIcon name="content-copy" />
</TouchableOpacity>
<AppIcon name="content-copy" />
</TouchableOpacity>
{item.codeRedeemUrl ? (
<Pressable
onPress={() => {
if (item.codeRedeemUrl) {
openLinkInBrowser(
item.codeRedeemUrl.replace("{{code}}", code)
);
}
}}
>
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xxs}
>
{strings.clickToDirectlyClaimPromo()}
</Paragraph>
</Pressable>
) : null}
</>
)}
</>
) : null}

View File

@@ -322,6 +322,7 @@ export const RestoreBackup = () => {
});
setFiles(files);
setLoading(false);
BACKUP_FILES_CACHE.splice(0, BACKUP_FILES_CACHE.length, ...files);
} catch (e) {
e;
@@ -519,7 +520,11 @@ const BackupItem = ({
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<View>
<View
style={{
flexShrink: 1
}}
>
<Paragraph size={AppFontSize.sm}>{itemName}</Paragraph>
<Paragraph
size={AppFontSize.xs}

View File

@@ -26,18 +26,17 @@ import {
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, TextInput, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
//@ts-ignore
import { FeatureResult, useIsFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
//@ts-ignore
import ToggleSwitch from "toggle-switch-react-native";
import PaywallSheet from "../../components/sheets/paywall";
import AppIcon from "../../components/ui/AppIcon";
import { IconButton } from "../../components/ui/icon-button";
import Input from "../../components/ui/input";
import { Pressable } from "../../components/ui/pressable";
import Seperator from "../../components/ui/seperator";
import Heading from "../../components/ui/typography/heading";
import Paragraph from "../../components/ui/typography/paragraph";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { SettingStore, useSettingStore } from "../../stores/use-setting-store";
@@ -45,7 +44,6 @@ import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { components } from "./components";
import { RouteParams, SettingSection } from "./types";
import AppIcon from "../../components/ui/AppIcon";
const _SectionItem = ({ item }: { item: SettingSection }) => {
const { colors } = useThemeColors();

View File

@@ -32,6 +32,7 @@ import React from "react";
import { Appearance, Linking, Platform } from "react-native";
import { getVersion } from "react-native-device-info";
import * as RNIap from "react-native-iap";
//@ts-ignore
import { enabled } from "react-native-privacy-snapshot";
import ScreenGuardModule from "react-native-screenguard";
import { DatabaseLogger, db } from "../../common/database";
@@ -81,15 +82,22 @@ export const settingsGroups: SettingSection[] = [
{
id: "subscription-status",
useHook: () => useUserStore((state) => state.user),
hidden: (current) =>
!current ||
(current as User).subscription?.plan === SubscriptionPlan.FREE,
hidden: (current) => {
const user = current as User;
return (
!user ||
!user.subscription ||
user.subscription.provider === undefined ||
!strings.subscriptionProviderInfo[user?.subscription?.provider] ||
user.subscription?.plan === SubscriptionPlan.FREE
);
},
name: (current) => {
const user = (current as User) || useUserStore.getState().user;
return (
strings.subscriptionProviderInfo[
user?.subscription?.provider
].title() || "Unknown provider"
]?.title() || `Unknown provider id: ${user?.subscription?.provider}`
);
},
icon: "credit-card",
@@ -99,6 +107,8 @@ export const settingsGroups: SettingSection[] = [
const subscriptionProviderInfo =
strings.subscriptionProviderInfo[user?.subscription?.provider];
if (!subscriptionProviderInfo) return;
const isCurrentPlatform =
(user.subscription?.provider === SubscriptionProvider.APPLE &&
Platform.OS === "ios") ||
@@ -1426,7 +1436,7 @@ export const settingsGroups: SettingSection[] = [
id: "docs-link",
name: strings.documentation(),
modifer: async () => {
Linking.openURL("https://docs.notesnook.com");
Linking.openURL("https://help.notesnook.com/");
},
description: strings.documentationDesc(),
icon: "file-document"

View File

@@ -580,7 +580,7 @@ function openSettingsDialog(context: string) {
positivePress:
Platform.OS === "ios"
? undefined
: () => {
: async () => {
resolve(true);
},
onClose: () => {

View File

@@ -17,8 +17,15 @@ 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 { isFeatureAvailable } from "@notesnook/common";
import { Profile, User } from "@notesnook/core";
import create, { State } from "zustand";
import SettingsService from "../services/settings";
import { presentDialog } from "../components/dialog/functions";
import { strings } from "@notesnook/intl";
import { eSendEvent } from "../services/event-manager";
import { eCloseSimpleDialog } from "../utils/events";
import Navigation from "../services/navigation";
export enum SyncStatus {
Passed,
@@ -57,6 +64,28 @@ export const useUserStore = create<UserStore>((set) => ({
setLastSynced: (lastSynced) => set({ lastSynced: lastSynced }),
lockApp: (appLocked) => {
set({ appLocked });
if (!appLocked) {
isFeatureAvailable("appLock").then((feature) => {
if (!feature.isAllowed) {
SettingsService.setProperty("appLockEnabled", false);
setTimeout(() => {
presentDialog({
title: "App Lock Disabled",
paragraph: feature?.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
eSendEvent(eCloseSimpleDialog);
if (SettingsService.getProperty("serverUrls")) return;
Navigation.navigate("PayWall", {
context: "logged-in"
});
}
});
}, 1000);
}
});
}
},
lastSyncStatus: SyncStatus.Never,
disableAppLockRequests: false,

View File

@@ -33,6 +33,7 @@ import { useNotebookStore } from "../stores/use-notebook-store";
import { useRelationStore } from "../stores/use-relation-store";
import { useTagStore } from "../stores/use-tag-store";
import { eUpdateNoteInEditor } from "./events";
import { unlockVault } from "./unlock-vault";
export function getObfuscatedEmail(email: string) {
if (!email) return "";
@@ -56,7 +57,7 @@ function confirmDeleteAllNotes(
title: strings.doActions.delete.notebook(items.length),
positiveText: strings.delete(),
negativeText: strings.cancel(),
positivePress: (_inputValue, value) => {
positivePress: async (_inputValue, value) => {
setTimeout(() => {
resolve({ delete: true, deleteNotes: value });
});
@@ -98,6 +99,30 @@ export const deleteItems = async (
await db.reminders.remove(...itemIds);
useRelationStore.getState().update();
} else if (type === "note") {
let someNotesLocked = false;
for (const id of itemIds) {
if (
await db.vaults.itemExists({
id: id,
type: "note"
})
) {
someNotesLocked = true;
break;
}
}
if (someNotesLocked) {
const unlocked = await unlockVault({
title: strings.unlockVault(),
paragraph: strings.unlockVaultDesc(),
context: "global",
requirePassword: true
});
if (!unlocked) return;
}
for (const id of itemIds) {
if (db.monographs.isPublished(id)) {
ToastManager.show({
@@ -108,6 +133,7 @@ export const deleteItems = async (
});
continue;
}
await db.notes.moveToTrash(id);
eSendEvent(
@@ -157,7 +183,7 @@ export const deleteItems = async (
heading: message,
type: "success",
func: async () => {
if ((await db.trash.restore(...deletedIds)) === false) return;
await db.trash.restore(...deletedIds);
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
useMenuStore.getState().setColorNotes();

View File

@@ -27,18 +27,20 @@ let unlockPromise: Promise<any> | undefined = undefined;
export async function unlockVault({
context,
title,
paragraph
paragraph,
requirePassword
}: {
context?: string;
title: string;
paragraph: string;
requirePassword?: boolean;
}) {
if (unlockPromise) {
return unlockPromise;
}
unlockPromise = new Promise(async (resolve) => {
const result = await (async () => {
if (db.vault.unlocked) return true;
if (db.vault.unlocked && !requirePassword) return true;
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
if (biometry && fingerprint) {

View File

@@ -1,5 +1,5 @@
import { execSync } from "child_process";
//@ts-ignore
import { pathExists, ensureDir } from "fs-extra";
import { resolveConfig } from "detox/internals";

View File

@@ -28,6 +28,9 @@ describe("Search", () => {
.typeTextById("search-input", "Test")
.wait(1000)
.isVisibleByText("1")
.waitAndTapById("clear-search")
.wait(2000)
.isNotVisibleByText("1")
.run();
});
});

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expect as jestExpect } from "@jest/globals";
import { device as _device, expect } from "detox";
import { readFileSync } from "fs";
//@ts-ignore
import { toMatchImageSnapshot } from "jest-image-snapshot";
import type { RouteName } from "../../app/stores/use-navigation-store";
import { notesnook } from "../test.ids";

View File

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

View File

@@ -33,7 +33,7 @@ public class NotePreviewWidget extends AppWidgetProvider {
intent.putExtra(OpenNoteId, note.getId());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
intent.setData(Uri.parse("https://notesnook.com/open_note?id=" + note.getId()));
intent.setData(Uri.parse("https://app.notesnook.com/open_note?id=" + note.getId()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);

View File

@@ -75,7 +75,7 @@ class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactor
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
fillInIntent.setData(Uri.parse("https://notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);

View File

@@ -43,7 +43,7 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
new_reminder_intent.putExtra(NewReminder, NewReminder);
new_reminder_intent.setAction(Intent.ACTION_VIEW);
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
new_reminder_intent.setData(Uri.parse("https://notesnook.com/new_reminder"));
new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder"));
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);

View File

@@ -2,6 +2,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:id="@+id/reminder_item_btn">
<TextView

View File

@@ -2,6 +2,7 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:id="@+id/reminder_item_btn">
<TextView

View File

@@ -25,9 +25,9 @@
android:text="Upcoming Reminders"/>
<ImageButton
android:layout_width="30dp"
android:layout_width="35dp"
android:id="@+id/add_button"
android:layout_height="30dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:background="@drawable/ic_newnote" />
</RelativeLayout>

View File

@@ -1,5 +1,3 @@
- Collapsible Headings
- Notesnook Circle
- Minor bug fixes and improvements
- Bug fixes and minor improvements
Thank you for using Notesnook!

View File

@@ -654,6 +654,7 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Notesnook/Pods-Notesnook-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
@@ -665,6 +666,7 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
@@ -821,6 +823,7 @@
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Notesnook-NotesnookTests/Pods-Notesnook-NotesnookTests-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/RNImageCropPickerPrivacyInfo.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNImageCropPicker/QBImagePicker.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
@@ -832,6 +835,7 @@
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImageCropPickerPrivacyInfo.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/QBImagePicker.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
@@ -1091,7 +1095,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1165,7 +1169,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1196,7 +1200,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1270,7 +1274,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1429,7 +1433,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1441,7 +1445,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1472,7 +1476,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1485,7 +1489,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1515,7 +1519,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1596,7 +1600,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1627,7 +1631,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2153;
CURRENT_PROJECT_VERSION = 2157;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1709,7 +1713,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.5;
MARKETING_VERSION = 3.3.9;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1786,7 +1790,10 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -1851,7 +1858,10 @@
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = "$(inherited) ";
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;

View File

@@ -9,6 +9,7 @@
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>3B52.1</string>
<string>C617.1</string>
</array>
</dict>

View File

@@ -1273,11 +1273,11 @@ PODS:
- React-Core
- react-native-keep-awake (1.3.1):
- React-Core
- react-native-mmkv-storage (0.11.2):
- react-native-mmkv-storage (12.0.0):
- DoubleConversion
- glog
- hermes-engine
- MMKV (~> 1.3.9)
- MMKV (~> 1.3.14)
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
@@ -1372,7 +1372,7 @@ PODS:
- react-native-screenguard (1.0.0):
- React-Core
- SDWebImage (~> 5.11.1)
- react-native-share-extension (2.9.0):
- react-native-share-extension (2.9.5):
- React
- react-native-sodium (1.6.5):
- React
@@ -1770,15 +1770,53 @@ PODS:
- Yoga
- RNIap (12.16.2):
- React-Core
- RNImageCropPicker (0.40.2):
- RNImageCropPicker (0.51.1):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-RCTImage
- RNImageCropPicker/QBImagePickerController (= 0.40.2)
- TOCropViewController
- RNImageCropPicker/QBImagePickerController (0.40.2):
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNImageCropPicker/QBImagePickerController (= 0.51.1)
- TOCropViewController (~> 2.8.0)
- Yoga
- RNImageCropPicker/QBImagePickerController (0.51.1):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.11.18.00)
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-RCTImage
- TOCropViewController
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- TOCropViewController (~> 2.8.0)
- Yoga
- RNKeychain (4.0.5):
- React
- RNNotifee (7.4.9):
@@ -1975,7 +2013,7 @@ PODS:
- SwiftyRSA (1.7.0):
- SwiftyRSA/ObjC (= 1.7.0)
- SwiftyRSA/ObjC (1.7.0)
- TOCropViewController (2.7.4)
- TOCropViewController (2.8.0)
- toolbar-android (0.2.1):
- React
- Yoga (0.0.0)
@@ -2425,7 +2463,7 @@ SPEC CHECKSUMS:
react-native-image-resizer: 24c5d06fae2176dc0caed4b6396e02befb44064a
react-native-in-app-review: b3d1eed3d1596ebf6539804778272c4c65e4a400
react-native-keep-awake: 03b74eebe4f2bb5e8478fc8f420651a92463b6f8
react-native-mmkv-storage: 51c33c6f6678d67061059cce8189a57c3e47192e
react-native-mmkv-storage: 935d661dc3913a41be70d041aa3ff860d6aa4873
react-native-netinfo: 66c9ac0d0ae92a57a9ed99ab8a75429740700c73
react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068
react-native-orientation-locker: cc6f357b289a2e0dd2210fea0c52cb8e0727fdaa
@@ -2434,7 +2472,7 @@ SPEC CHECKSUMS:
react-native-quick-sqlite: 1bfc7f1e9acbe9a5aa5c4cc81712e9bde3ab7672
react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1
react-native-screenguard: 82437eeb0086a90b5e5d7e54130bb04fb406373e
react-native-share-extension: bcb7e466390a9e50c742f4b1019d6f181aedd7ad
react-native-share-extension: fdc6aaab51591a2d445df239c446aaa3a99658ec
react-native-sodium: 285eec063e4232cb67347ef6a434b85e588d38cb
react-native-theme-switch-animation: d90fe2de0d9e87a63cd6235d98cba6e7054e9a10
react-native-webview: 079eca50edf657503318b66687dadfb903731aa8
@@ -2479,7 +2517,7 @@ SPEC CHECKSUMS:
RNFlashList: ff5a0b3113c4cda0eaf4b94df8572ccad3c40fd5
RNGestureHandler: 92e89a04cd0d1c77f383a55d14c15e7f423f4c00
RNIap: f94647b3a3dbd5fa08ad7ed847eb588aa5fe95fe
RNImageCropPicker: 30d770b383d84e1067d82ea7b5ed4fff851bbbb2
RNImageCropPicker: aa81bda9f887e20542b569938cde5ce50a4ec404
RNKeychain: ffd0513e676445c637410b47249460cbf56bc9cb
RNNotifee: dabf3cdd7bfd9340bb84358cc78f635af4bc80e2
RNPrivacySnapshot: ccad3a548338c2f526bb7b1789af3fb0618b7d1d
@@ -2495,7 +2533,7 @@ SPEC CHECKSUMS:
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
TOCropViewController: 797deaf39c90e6e9ddd848d88817f6b9a8a09888
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 31a098f74c16780569aebd614a0f37a907de0189

File diff suppressed because one or more lines are too long

View File

@@ -21,6 +21,7 @@
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "6.6.2",
"@sayem314/react-native-keep-awake": "^1.3.1",
"react-native-image-crop-picker": "^0.51.1",
"react": "18.2.0",
"react-native": "0.77.2",
"react-native-actions-shortcuts": "^1.0.1",
@@ -48,7 +49,7 @@
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",
"@shopify/flash-list": "^1.8.0",
"react-native-mmkv-storage": "^0.11.2",
"react-native-mmkv-storage": "^12.0.0",
"react-native-quick-sqlite": "^8.2.7",
"react-native-svg": "^15.12.0",
"react-native-webview": "^13.13.5",
@@ -61,7 +62,6 @@
"react-native-zip-archive": "6.0.9",
"react-native-theme-switch-animation": "^0.6.0",
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"react-native-image-crop-picker": "^0.40.2",
"react-native-url-polyfill": "^2.0.0",
"react-native-screenguard": "^1.0.0",
"@formatjs/intl-locale": "4.0.0",
@@ -72,10 +72,10 @@
"react-native-begin-background-task": "github:blockfirm/react-native-begin-background-task",
"react-native-privacy-snapshot": "github:standardnotes/react-native-privacy-snapshot",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.9.0",
"react-native-pager-view": "^6.5.1",
"react-native-tab-view": "^4.0.2",
"react-native-orientation-locker": "^1.7.0"
"react-native-orientation-locker": "^1.7.0",
"@ammarahmed/react-native-share-extension": "^2.9.5"
},
"devDependencies": {
"@babel/core": "^7.27.1",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.3.5",
"version": "3.3.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.5",
"version": "3.3.7",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -28326,7 +28326,7 @@
"@ammarahmed/react-native-background-fetch": "^4.2.2",
"@ammarahmed/react-native-eventsource": "1.1.0",
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.0",
"@ammarahmed/react-native-share-extension": "^2.9.0",
"@ammarahmed/react-native-share-extension": "^2.9.5",
"@ammarahmed/react-native-sodium": "^1.6.5",
"@bam.tech/react-native-image-resizer": "3.0.11",
"@callstack/repack": "~5.1.2",
@@ -28361,10 +28361,10 @@
"react-native-gzip": "1.1.0",
"react-native-html-to-pdf-lite": "^0.9.1",
"react-native-iap": "12.16.2",
"react-native-image-crop-picker": "^0.40.2",
"react-native-image-crop-picker": "^0.51.1",
"react-native-in-app-review": "4.3.3",
"react-native-keychain": "4.0.5",
"react-native-mmkv-storage": "^0.11.2",
"react-native-mmkv-storage": "^12.0.0",
"react-native-modal-datetime-picker": "14.0.0",
"react-native-navigation-bar-color": "2.0.2",
"react-native-notification-sounds": "0.5.5",
@@ -28472,9 +28472,9 @@
}
},
"node_modules/@ammarahmed/react-native-share-extension": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.0.tgz",
"integrity": "sha512-iEkpJQFZL5/WDPh7iRCLZREANgaaAHthDsPaAYT6sI4wieQcHZMBAnfRMwBEnC0BZ0WVxbQZdzJYez+dmwpr2g==",
"version": "2.9.5",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-share-extension/-/react-native-share-extension-2.9.5.tgz",
"integrity": "sha512-UTE8TSxkx+RFNgSJCCfD5FSdQ5IuMOsZzuuy0BVy46emRrpPw1fedJTtDbqyKrU8fwtKEIbzJWeQtjjRZlHxjQ==",
"license": "MIT",
"dependencies": {
"react-native": "^0.63.1"
@@ -44519,10 +44519,13 @@
}
},
"node_modules/react-native-image-crop-picker": {
"version": "0.40.2",
"version": "0.51.1",
"resolved": "https://registry.npmjs.org/react-native-image-crop-picker/-/react-native-image-crop-picker-0.51.1.tgz",
"integrity": "sha512-GIFRyXJgv1dPceKd/hraK9q9V38v45rSg2ONR6RiSePcOJemkHpc/PMU86pq6lWPilDYHSxbZmea2pNMk85ayw==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.40.0"
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-image-pan-zoom": {
@@ -44590,9 +44593,9 @@
}
},
"node_modules/react-native-mmkv-storage": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.11.2.tgz",
"integrity": "sha512-/jbYNOUrwgVU09WyXDK6lFGXqBs+23oR9X37z3N68rwHNiXF5WDyXnT38dU2tF07ZlvmsobNHgdxgTu4kGQUKQ==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-12.0.0.tgz",
"integrity": "sha512-sssZInILQBquytDDfjosjEdvevwMPk3fqQQjdjfnH362IcsBsApvtT8yJS406Mz9aJ6VhqVsZw/awltsUuPSYg==",
"license": "MIT",
"bin": {
"mmkv-link": "autolink/postlink/run.js"

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.5",
"version": "3.3.9",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [
@@ -56,4 +56,4 @@
"react": "18.2.0",
"react-native": "0.77.2"
}
}
}

View File

@@ -1,27 +1,27 @@
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
index 5de0845..1b158d8 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
@@ -692,6 +692,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
image.putString("mime", options.outMimeType);
diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
index b9c494e..34ef800 100644
--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/ImageCropPicker.java
@@ -679,6 +679,7 @@ class ImageCropPicker implements ActivityEventListener {
image.putInt("size", (int) new File(compressedImagePath).length());
image.putString("modificationDate", String.valueOf(modificationDate));
image.putString("filename", new File(path).getName());
+ image.putString("sourceURL", path);
if (includeBase64) {
image.putString("data", getBase64StringFromFile(compressedImagePath));
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
index 9f20973..5e14da8 100644
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.m
@@ -595,8 +595,10 @@ - (void)qb_imagePickerController:
NSString *mimeType = [self determineMimeTypeFromImageData:imageData];
diff --git a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
index 354ad09..622f779 100644
--- a/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
+++ b/node_modules/react-native-image-crop-picker/ios/src/ImageCropPicker.mm
@@ -603,7 +603,10 @@ - (void)qb_imagePickerController:(QBImagePickerController *)imagePickerControlle
Boolean isKnownMimeType = [mimeType length] > 0;
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
+
ImageResult *imageResult = [[ImageResult alloc] init];
- if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg) {
+
+ Boolean isHeicOrHeif = [mimeType isEqualToString:@"image/heic"] || [mimeType isEqualToString:@"image/heif"];
+
+ if (isLossless && useOriginalWidth && useOriginalHeight && isKnownMimeType && !forceJpg && !isHeicOrHeif) {
// Use original, unmodified image
imageResult.data = imageData;

View File

@@ -0,0 +1,169 @@
diff --git a/node_modules/react-native-keychain/android/build.gradle b/node_modules/react-native-keychain/android/build.gradle
index bd2fe04..53ce4b1 100755
--- a/node_modules/react-native-keychain/android/build.gradle
+++ b/node_modules/react-native-keychain/android/build.gradle
@@ -51,5 +51,4 @@ dependencies {
//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
implementation 'androidx.annotation:annotation:1.1.0'
- implementation 'com.facebook.conceal:conceal:1.1.3@aar'
}
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
index 61d061e..be49f8a 100644
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
@@ -15,7 +15,6 @@ import com.oblador.keychain.PrefsStorage.ResultSet;
import com.oblador.keychain.cipherStorage.CipherStorage;
import com.oblador.keychain.cipherStorage.CipherStorage.DecryptionResult;
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
-import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAESCBC;
import com.oblador.keychain.exceptions.CryptoFailedException;
import com.oblador.keychain.exceptions.EmptyParameterException;
@@ -48,7 +47,6 @@ public class KeychainModule extends ReactContextBaseJavaModule {
super(reactContext);
prefsStorage = new PrefsStorage(reactContext);
- addCipherStorageToMap(new CipherStorageFacebookConceal(reactContext));
addCipherStorageToMap(new CipherStorageKeystoreAESCBC());
}
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
index 99448b1..792c24c 100644
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/PrefsStorage.java
@@ -7,7 +7,6 @@ import android.util.Base64;
import com.facebook.react.bridge.ReactApplicationContext;
import com.oblador.keychain.cipherStorage.CipherStorage.EncryptionResult;
-import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal;
public class PrefsStorage {
public static final String KEYCHAIN_DATA = "RN_KEYCHAIN";
@@ -35,10 +34,6 @@ public class PrefsStorage {
byte[] bytesForPassword = getBytesForPassword(service);
String cipherStorageName = getCipherStorageName(service);
if (bytesForUsername != null && bytesForPassword != null) {
- if (cipherStorageName == null) {
- // If the CipherStorage name is not found, we assume it is because the entry was written by an older version of this library. The older version used Facebook Conceal, so we default to that.
- cipherStorageName = CipherStorageFacebookConceal.CIPHER_STORAGE_NAME;
- }
return new ResultSet(cipherStorageName, bytesForUsername, bytesForPassword);
}
return null;
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java
deleted file mode 100644
index 3162f99..0000000
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.java
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.oblador.keychain.cipherStorage;
-
-import android.os.Build;
-import androidx.annotation.NonNull;
-
-import com.facebook.android.crypto.keychain.AndroidConceal;
-import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain;
-import com.facebook.crypto.Crypto;
-import com.facebook.crypto.CryptoConfig;
-import com.facebook.crypto.Entity;
-import com.facebook.crypto.keychain.KeyChain;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.oblador.keychain.SecurityLevel;
-import com.oblador.keychain.exceptions.CryptoFailedException;
-
-import java.nio.charset.Charset;
-
-public class CipherStorageFacebookConceal implements CipherStorage {
- public static final String CIPHER_STORAGE_NAME = "FacebookConceal";
- public static final String KEYCHAIN_DATA = "RN_KEYCHAIN";
- private final Crypto crypto;
-
- public CipherStorageFacebookConceal(ReactApplicationContext reactContext) {
- KeyChain keyChain = new SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256);
- this.crypto = AndroidConceal.get().createDefaultCrypto(keyChain);
- }
-
- @Override
- public String getCipherStorageName() {
- return CIPHER_STORAGE_NAME;
- }
-
- @Override
- public int getMinSupportedApiLevel() {
- return Build.VERSION_CODES.JELLY_BEAN;
- }
-
- @Override
- public SecurityLevel securityLevel() {
- return SecurityLevel.ANY;
- }
-
- @Override
- public boolean supportsSecureHardware() {
- return false;
- }
-
- @Override
- public EncryptionResult encrypt(@NonNull String service, @NonNull String username, @NonNull String password, SecurityLevel level) throws CryptoFailedException {
-
- if (!this.securityLevel().satisfiesSafetyThreshold(level)) {
- throw new CryptoFailedException(String.format("Insufficient security level (wants %s; got %s)", level, this.securityLevel()));
- }
-
- if (!crypto.isAvailable()) {
- throw new CryptoFailedException("Crypto is missing");
- }
- Entity usernameEntity = createUsernameEntity(service);
- Entity passwordEntity = createPasswordEntity(service);
-
- try {
- byte[] encryptedUsername = crypto.encrypt(username.getBytes(Charset.forName("UTF-8")), usernameEntity);
- byte[] encryptedPassword = crypto.encrypt(password.getBytes(Charset.forName("UTF-8")), passwordEntity);
-
- return new EncryptionResult(encryptedUsername, encryptedPassword, this);
- } catch (Exception e) {
- throw new CryptoFailedException("Encryption failed for service " + service, e);
- }
- }
-
- @Override
- public DecryptionResult decrypt(@NonNull String service, @NonNull byte[] username, @NonNull byte[] password) throws CryptoFailedException {
- if (!crypto.isAvailable()) {
- throw new CryptoFailedException("Crypto is missing");
- }
- Entity usernameEntity = createUsernameEntity(service);
- Entity passwordEntity = createPasswordEntity(service);
-
- try {
- byte[] decryptedUsername = crypto.decrypt(username, usernameEntity);
- byte[] decryptedPassword = crypto.decrypt(password, passwordEntity);
-
- return new DecryptionResult(
- new String(decryptedUsername, Charset.forName("UTF-8")),
- new String(decryptedPassword, Charset.forName("UTF-8")),
- SecurityLevel.ANY);
- } catch (Exception e) {
- throw new CryptoFailedException("Decryption failed for service " + service, e);
- }
- }
-
- @Override
- public void removeKey(@NonNull String service) {
- // Facebook Conceal stores only one key across all services, so we cannot delete the key (otherwise decryption will fail for encrypted data of other services).
- }
-
- private Entity createUsernameEntity(String service) {
- String prefix = getEntityPrefix(service);
- return Entity.create(prefix + "user");
- }
-
- private Entity createPasswordEntity(String service) {
- String prefix = getEntityPrefix(service);
- return Entity.create(prefix + "pass");
- }
-
- private String getEntityPrefix(String service) {
- return KEYCHAIN_DATA + ":" + service;
- }
-}

View File

@@ -1,14 +0,0 @@
diff --git a/node_modules/react-native-mmkv-storage/android/build.gradle b/node_modules/react-native-mmkv-storage/android/build.gradle
index abb696e..ba35baa 100644
--- a/node_modules/react-native-mmkv-storage/android/build.gradle
+++ b/node_modules/react-native-mmkv-storage/android/build.gradle
@@ -98,7 +98,8 @@ android {
cmake {
cppFlags "-O3 -frtti -fexceptions -Wall -fstack-protector-all"
arguments "-DANDROID_STL=c++_shared",
- "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}"
+ "-DREACT_NATIVE_MINOR_VERSION=${REACT_NATIVE_MINOR_VERSION}",
+ "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
abiFilters (*reactNativeArchitectures())
}
}

View File

@@ -28,7 +28,6 @@ import RNFetchBlob from "react-native-blob-util";
import WebView from "react-native-webview";
import { Config } from "./store";
import { db } from "../app/common/database";
import { SUBSCRIPTION_STATUS } from "../app/utils/constants";
export const fetchHandle = createRef();
export const HtmlLoadingWebViewAgent = React.memo(
@@ -40,6 +39,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
const webview = useRef();
const premium = useRef(false);
const corsProxy = Config.corsProxy;
const [isLoggedIn, setIsLoggedIn] = useState();
useImperativeHandle(
fetchHandle,
@@ -72,11 +72,8 @@ export const HtmlLoadingWebViewAgent = React.memo(
useEffect(() => {
(async () => {
const user = await db.user.getUser();
const subscriptionStatus =
user?.subscription?.type || SUBSCRIPTION_STATUS.BASIC;
premium.current =
user && subscriptionStatus !== SUBSCRIPTION_STATUS.BASIC;
setIsLoggedIn(!!user);
console.log("USER", !!user);
const clipperPath =
Platform.OS === "ios"
? RNFetchBlob.fs.dirs.MainBundleDir +
@@ -91,6 +88,7 @@ export const HtmlLoadingWebViewAgent = React.memo(
.catch((e) => console.log(e));
})();
}, []);
console.log(isLoggedIn);
return !source || !clipper ? null : (
<WebView
@@ -103,7 +101,8 @@ export const HtmlLoadingWebViewAgent = React.memo(
height: 100,
position: "absolute",
opacity: 0,
zIndex: -1
zIndex: -1,
pointerEvents: "none"
}}
useSharedProcessPool={false}
pointerEvents="none"
@@ -123,7 +122,11 @@ export const HtmlLoadingWebViewAgent = React.memo(
console.log("Error handling webview message", e);
}
}}
injectedJavaScriptBeforeContentLoaded={script(clipper, premium.current)}
injectedJavaScriptBeforeContentLoaded={script(
clipper,
corsProxy,
isLoggedIn
)}
onError={() => {
console.log("Error loading page");
loadHandler.current?.();
@@ -137,9 +140,11 @@ export const HtmlLoadingWebViewAgent = React.memo(
() => true
);
const script = (clipper, pro) => `
const script = (clipper, corsProxy, loggedIn) => `
globalThis.module = {};
${clipper}
function postMessage(type, value) {
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(
@@ -158,10 +163,10 @@ function postMessage(type, value) {
postMessage("error", globalThis.Clipper.clipPage);
} else {
globalThis.Clipper.clipPage(document,false, {
images: ${pro},
images: ${loggedIn ? "true" : "false"},
inlineImages: false,
styles: false,
corsProxy: undefined
corsProxy: ${corsProxy ? `"${corsProxy}"` : "undefined"}
}).then(result => {
postMessage("html", result);
}).catch(e => {

View File

@@ -267,7 +267,6 @@ const ShareView = () => {
);
const onLoad = useCallback(() => {
console.log(noteContent.current, "current...");
eSendEvent(eOnLoadNote + "shareEditor", {
id: null,
content: {
@@ -747,7 +746,7 @@ const ShareView = () => {
</Paragraph>
{rawData.value && isURL(rawData.value) ? (
<Button
type={mode === 2 ? "inverted" : "transparent"}
type={mode === 2 ? "inverted" : "plain"}
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
title={modes[2].title}
@@ -758,9 +757,9 @@ const ShareView = () => {
/>
) : null}
<Button
type={mode === 1 ? "inverted" : "transparent"}
type={mode === 1 ? "inverted" : "plain"}
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
onPress={() => changeMode(2)}
onPress={() => changeMode(1)}
title={modes[1].title}
height={30}
style={{

View File

@@ -68,11 +68,10 @@ export const useShareStore = create((set) => ({
let appendNote = MMKV.getString(StorageKeys.appendNote);
let selectedNotebooks = MMKV.getString(StorageKeys.selectedNotebooks);
let selectedTags = MMKV.getString(StorageKeys.selectedTag);
appendNote = JSON.parse(appendNote);
set({
appendNote: appendNote,
selectedNotebooks: selectedNotebooks ? JSON.parse(selectedNotebooks) : [],
selectedTag: selectedTags ? JSON.parse(selectedTags) : []
selectedTags: selectedTags ? JSON.parse(selectedTags) : []
});
},
selectedTags: [],

View File

@@ -15,10 +15,7 @@
"@notesnook/theme": ["../../packages/theme"],
"@types/react": ["./node_modules/@types/react"],
"react": ["./node_modules/react"]
},
"incremental": true,
"maxNodeModuleJsDepth": 5,
"downlevelIteration": true
}
},
"exclude": ["native", "e2e"]
}

File diff suppressed because one or more lines are too long

View File

@@ -141,7 +141,7 @@
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook-importer/core": "^2.2.2",
"@notesnook-importer/core": "^2.2.5",
"@notesnook/common": "file:../common",
"@notesnook/intl": "file:../intl",
"@notesnook/theme": "file:../theme",

View File

@@ -987,7 +987,7 @@
},
"../web": {
"name": "@notesnook/web",
"version": "3.3.1",
"version": "3.3.4",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -1001,7 +1001,7 @@
"@lingui/react": "5.1.2",
"@mdi/js": "7.4.47",
"@mdi/react": "1.6.1",
"@notesnook-importer/core": "^2.2.2",
"@notesnook-importer/core": "^2.2.5",
"@notesnook/common": "file:../../packages/common",
"@notesnook/core": "file:../../packages/core",
"@notesnook/crypto": "file:../../packages/crypto",

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": "3.3.2",
"version": "3.3.5",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -44,6 +44,7 @@ import {
useStore as useAppStore,
store as appstore
} from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { useStore as useSearchStore } from "../../stores/search-store";
import { AppEventManager, AppEvents } from "../../common/app-events";
import { FlexScrollContainer } from "../scroll-container";
@@ -77,6 +78,7 @@ import { Pane, SplitPane } from "../split-pane";
import { TITLE_BAR_HEIGHT } from "../title-bar";
import { isMobile } from "../../hooks/use-mobile";
import { isTablet } from "../../hooks/use-tablet";
import { ConfirmDialog } from "../../dialogs/confirm";
const PDFPreview = React.lazy(() => import("../pdf-preview"));
@@ -604,6 +606,15 @@ export function Editor(props: EditorProps) {
}
}}
onInsertAttachment={async (type) => {
if (!useUserStore.getState().isLoggedIn) {
ConfirmDialog.show({
title: strings.notLoggedIn(),
message: strings.loginToUploadAttachments(),
positiveButtonText: strings.okay()
});
return;
}
const mime = type === "file" ? "*/*" : "image/*";
const attachments = await insertAttachments(mime);
const editor = useEditorManager.getState().getEditor(id)?.editor;

View File

@@ -51,6 +51,7 @@ import {
import { IEditor, MAX_AUTO_SAVEABLE_WORDS } from "./types";
import { useEditorConfig, useToolbarConfig, useEditorManager } from "./manager";
import { useStore as useSettingsStore } from "../../stores/setting-store";
import { useStore as useUserStore } from "../../stores/user-store";
import { debounce, useAreFeaturesAvailable } from "@notesnook/common";
import { ScopedThemeProvider } from "../theme-provider";
import { useStore as useThemeStore } from "../../stores/theme-store";
@@ -65,6 +66,8 @@ import { EDITOR_ZOOM } from "./common";
import { ScrollContainer } from "@notesnook/ui";
import { showFeatureNotAllowedToast } from "../../common/toasts";
import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
import { ConfirmDialog } from "../../dialogs/confirm";
import { strings } from "@notesnook/intl";
export type OnChangeHandler = (
content: () => string,
@@ -185,7 +188,6 @@ function TipTap(props: TipTapProps) {
const autoSave = useRef(true);
const { toolbarConfig } = useToolbarConfig();
const features = useAreFeaturesAvailable([
"callout",
"outlineList",
@@ -196,9 +198,19 @@ function TipTap(props: TipTapProps) {
claims: {
callout: !!features?.callout?.isAllowed,
outlineList: !!features?.outlineList?.isAllowed,
taskList: !!features?.taskList?.isAllowed
taskList: !!features?.taskList?.isAllowed,
insertAttachment: !!useUserStore.getState().isLoggedIn
},
onPermissionDenied: (claim, silent) => {
if (claim === "insertAttachment") {
ConfirmDialog.show({
title: strings.notLoggedIn(),
message: strings.loginToUploadAttachments(),
positiveButtonText: strings.okay()
});
return;
}
if (silent) {
console.log(features, features?.[claim]);
if (features?.[claim]) showFeatureNotAllowedToast(features[claim]);

View File

@@ -82,6 +82,7 @@ import { mdToHtml } from "../../utils/md";
import { InboxSettings } from "./inbox-settings";
import { withFeatureCheck } from "../../common";
import { NotesnookCircleSettings } from "./notesnook-circle-settings";
import { hashNavigate } from "../../navigation";
type SettingsDialogProps = BaseDialogProps<false> & {
activeSection?: SectionKeys;
@@ -271,6 +272,12 @@ function SettingsSideBar(props: SettingsSideBarProps) {
const [route, setRoute] = useState<SectionKeys>(activeSection || "profile");
useUserStore((store) => store.isLoggedIn);
useEffect(() => {
hashNavigate(`/settings/${route}`, {
notify: false
});
}, [route, activeSection]);
return (
<FlexScrollContainer
id="settings-side-menu"

View File

@@ -20,29 +20,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureId } from "@notesnook/common";
import { Icon } from "../../components/icons";
export type SectionKeys =
| "profile"
| "auth"
| "subscription"
| "sync"
| "appearance"
| "behaviour"
| "desktop"
| "notifications"
| "servers"
| "editor"
| "backup-export"
| "export"
| "importer"
| "vault"
| "app-lock"
| "privacy"
| "support"
| "legal"
| "developer"
| "about"
| "inbox"
| "circle";
const SectionKeys = [
"profile",
"auth",
"subscription",
"sync",
"appearance",
"behaviour",
"desktop",
"notifications",
"servers",
"editor",
"backup-export",
"export",
"importer",
"vault",
"app-lock",
"privacy",
"support",
"legal",
"developer",
"about",
"inbox",
"circle"
] as const;
export type SectionKeys = (typeof SectionKeys)[number];
export type SectionGroupKeys =
| "account"
@@ -153,3 +156,7 @@ export type TextInputSettingComponent = BaseSettingComponent<"input"> & {
export type CustomSettingComponent = BaseSettingComponent<"custom"> & {
component: () => JSX.Element | null;
};
export function isSectionKey(key: string): key is SectionKeys {
return SectionKeys.includes(key as SectionKeys);
}

View File

@@ -33,6 +33,7 @@ import {
import { FeatureDialog } from "../dialogs/feature-dialog";
import { CreateTagDialog } from "../dialogs/item-dialog";
import { OnboardingDialog } from "../dialogs/onboarding-dialog";
import { isSectionKey, SectionKeys } from "../dialogs/settings/types";
const hashroutes = defineHashRoutes({
"/": () => {},
@@ -68,6 +69,11 @@ const hashroutes = defineHashRoutes({
},
"/settings": () => {
SettingsDialog.show({}).then(afterAction);
},
"/settings/:section": ({ section }) => {
SettingsDialog.show(
isSectionKey(section) ? { activeSection: section as SectionKeys } : {}
).then(afterAction);
}
});

View File

@@ -73,7 +73,11 @@ export class WebExtensionServer implements Server {
async saveClip(clip: Clip) {
let clipContent = "";
if (clip.mode === "simplified" || clip.mode === "screenshot") {
if (
clip.mode === "simplified" ||
clip.mode === "screenshot" ||
clip.mode === "bookmark"
) {
clipContent += clip.data;
} else {
const clippedFile = new File(
@@ -105,11 +109,17 @@ export class WebExtensionServer implements Server {
if (isCipher(content)) return;
content += clipContent;
content += h("div", [
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
]).innerHTML;
content +=
clip.mode === "bookmark"
? h("div", [
h("p", [`Date bookmarked: ${getFormattedDate(Date.now())}`]),
h("hr")
]).innerHTML
: h("div", [
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${getFormattedDate(Date.now())}`])
]).innerHTML;
const id = await db.notes.add({
id: note?.id,

View File

@@ -0,0 +1,8 @@
---
title: Login to upload attachments
description: We require users to be logged in to upload attachments.
---
# Login to upload attachments
We require users to be logged in to upload attachments. This is because attachments are encrypted using a sub-key derived from your database encryption key. Without a login, we cannot encrypt/upload/sync attachments.

View File

@@ -73,6 +73,10 @@ The `Selected nodes` mode allows you to select exactly which nodes you want to c
The clipping mode controls how the final clip should look.
### Bookmark
`Bookmark` mode saves only the URL of the page along with the title. It is best suited to save pages for later reading/reference.
### Simplified
`Simplified` mode doesn't include any styles. It is best suited for long-form content such as articles & blogs. All clips in `Simplified` mode are saved directly as is i.e. they do not appear as web clip embeds in the Notesnook editor.

View File

@@ -84,3 +84,4 @@ navigation:
children:
- path: faqs/what-are-merge-conflicts.md
- path: faqs/is-there-an-eta.md
- paht: faqs/login-to-upload-attachments.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/web-clipper",
"version": "0.3.4",
"version": "0.4.0",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
@@ -105,7 +105,7 @@
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.1",
"ts-loader": "^9.2.6",
"web-ext": "^7.6.2",
"web-ext": "^7.12.0",
"webpack": "5.88.2",
"webpack-cli": "5.1.4",
"webpack-dev-server": "^4.7.4"

View File

@@ -21,7 +21,7 @@ import { ThemeDefinition } from "@notesnook/theme";
export type ClipArea = "full-page" | "visible" | "selection" | "article";
export type ClipMode = "simplified" | "screenshot" | "complete";
export type ClipMode = "bookmark" | "simplified" | "screenshot" | "complete";
export type User = {
email?: string;

View File

@@ -57,6 +57,7 @@ export const Icons = {
visible: mdiViewDayOutline,
selection: mdiCursorDefaultClickOutline,
bookmark: mdiBookmarkOutline,
simplified: mdiTextBoxOutline,
screenshot: mdiFitToScreenOutline,
complete: mdiViewDashboardOutline,

View File

@@ -68,6 +68,11 @@ const clipAreas: { name: string; id: ClipArea; icon: string }[] = [
const clipModes: { name: string; id: ClipMode; icon: string; pro?: boolean }[] =
[
{
name: "Bookmark",
id: "bookmark",
icon: Icons.bookmark
},
{
name: "Simplified",
id: "simplified",
@@ -87,6 +92,20 @@ const clipModes: { name: string; id: ClipMode; icon: string; pro?: boolean }[] =
}
];
enum ClipperState {
Idle = "idle",
Clipping = "clipping",
Clipped = "clipped",
Error = "error"
}
const clipperButtonLabelMap: Record<ClipperState, string> = {
[ClipperState.Clipping]: "Clipping...",
[ClipperState.Clipped]: "Save clip",
[ClipperState.Error]: "Retry Clip",
[ClipperState.Idle]: "Start clip"
};
export function Main() {
const [error, setError] = useState<string>();
// const [colorMode, setColorMode] = useColorMode();
@@ -98,7 +117,6 @@ export function Main() {
const [title, setTitle] = useState<string>();
const [hasPermission, setHasPermission] = useState<boolean>(false);
const [url, setUrl] = useState<string>();
const [clipNonce, setClipNonce] = useState(0);
const [clipMode, setClipMode] = usePersistentState<ClipMode>(
"clipMode",
"simplified"
@@ -107,10 +125,12 @@ export function Main() {
"clipArea",
"article"
);
const [isClipping, setIsClipping] = useState(false);
const [note, setNote] = usePersistentState<ItemReference>("note");
const [refs, setRefs] = usePersistentState<SelectedReference[]>("refs", []);
const [clipData, setClipData] = useState<ClipData>();
const [clipperState, setClipperState] = useState<ClipperState>(
ClipperState.Idle
);
const pageTitle = useRef<string>();
useEffect(() => {
@@ -129,7 +149,6 @@ export function Main() {
useEffect(() => {
(async () => {
if (!clipArea || !clipMode) return;
if (
!isPremium &&
(clipMode === "complete" || clipMode === "screenshot")
@@ -137,25 +156,8 @@ export function Main() {
setClipMode("simplified");
return;
}
try {
setIsClipping(true);
setClipData(
await clip(clipArea, clipMode, {
...DEFAULT_SETTINGS,
...settings,
images: isPremium,
inlineImages: isPremium
})
);
} catch (e) {
console.error(e);
if (e instanceof Error) setError(e.message);
} finally {
setIsClipping(false);
}
})();
}, [isPremium, clipArea, clipMode, clipNonce]);
}, [isPremium, clipArea, clipMode]);
useEffect(() => {
(async () => {
@@ -168,6 +170,32 @@ export function Main() {
})();
}, [settings]);
async function startClip() {
if (!clipArea || !clipMode || clipMode === "bookmark") return;
try {
setError(undefined);
setClipperState(ClipperState.Clipping);
setClipData(
await clip(clipArea, clipMode, {
...DEFAULT_SETTINGS,
...settings,
images: isPremium,
inlineImages: isPremium
})
);
setClipperState(ClipperState.Clipped);
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
}
setClipperState(ClipperState.Error);
}
}
const isClipping = clipperState === ClipperState.Clipping;
if (!hasPermission && !!settings?.corsProxy) {
return (
<FlexScrollContainer style={{ maxHeight: 560 }}>
@@ -241,10 +269,15 @@ export function Main() {
key={item.id}
variant="icon"
onClick={() => {
setError(undefined);
setClipperState(ClipperState.Idle);
setClipArea(item.id);
setClipNonce((s) => ++s);
}}
disabled={isClipping}
disabled={
isClipping ||
clipperState === ClipperState.Clipped ||
clipMode === "bookmark"
}
sx={{
display: "flex",
borderRadius: "default",
@@ -283,10 +316,15 @@ export function Main() {
key={item.id}
variant="icon"
onClick={() => {
setError(undefined);
setClipperState(ClipperState.Idle);
setClipMode(item.id);
setClipNonce((s) => ++s);
}}
disabled={isClipping || (item.pro && !isPremium)}
disabled={
isClipping ||
clipperState === ClipperState.Clipped ||
(item.pro && !isPremium)
}
sx={{
display: "flex",
borderRadius: "default",
@@ -313,33 +351,58 @@ export function Main() {
</Button>
))}
{clipData && clipData.data && !isClipping && (
<Text
variant="body"
sx={{
mt: 1,
bg: "shade",
color: "accent",
p: 1,
border: "1px solid var(--accent)",
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
const winUrl = URL.createObjectURL(
new Blob(["\ufeff", clipData.data], { type: "text/html" })
);
await browser.windows.create({
url: winUrl
});
}}
>
Clip done. Click here to preview.
</Text>
)}
{clipMode !== "bookmark" &&
clipData &&
clipData.data &&
!isClipping && (
<Flex sx={{ gap: 1, justifyContent: "space-between" }}>
<Text
variant="body"
sx={{
flex: 1,
mt: 1,
bg: "shade",
color: "accent",
p: 1,
border: "1px solid var(--accent)",
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
const winUrl = URL.createObjectURL(
new Blob(["\ufeff", clipData.data], { type: "text/html" })
);
await browser.windows.create({
url: winUrl
});
}}
>
Clip done. Click here to preview.
</Text>
<Text
variant="body"
sx={{
mt: 1,
bg: "background-secondary",
p: 1,
borderRadius: "default",
cursor: "pointer",
":hover": {
filter: "brightness(80%)"
}
}}
onClick={async () => {
setClipData(undefined);
setClipperState(ClipperState.Idle);
}}
>
Discard
</Text>
</Flex>
)}
{error && (
<Text
@@ -357,7 +420,7 @@ export function Main() {
}
}}
onClick={async () => {
setClipNonce((s) => ++s);
await startClip();
}}
>
{ERROR_MAP[error] || error}
@@ -400,9 +463,27 @@ export function Main() {
<Button
variant="accent"
sx={{ mt: 1 }}
disabled={!clipData}
disabled={isClipping}
onClick={async () => {
if (!clipData || !title || !clipArea || !clipMode || !url) return;
if (
clipMode !== "bookmark" &&
(clipperState === ClipperState.Idle ||
clipperState === ClipperState.Error)
) {
await startClip();
return;
}
if (!title || !clipArea || !clipMode || !url) return;
const data =
clipMode === "bookmark"
? {
data: createBookmark(url, title)
}
: clipData;
if (!data) return;
const notesnook = await connectApi(false);
if (!notesnook) {
@@ -417,7 +498,7 @@ export function Main() {
note,
refs,
pageTitle: pageTitle.current,
...clipData
...data
});
setClipData(undefined);
@@ -433,7 +514,9 @@ export function Main() {
window.close();
}}
>
Save clip
{clipMode === "bookmark"
? "Save bookmark"
: clipperButtonLabelMap[clipperState]}
</Button>
<Flex
@@ -508,3 +591,11 @@ export async function clip(
settings
});
}
function createBookmark(url: string, title: string) {
const a = document.createElement("a");
a.setAttribute("href", url);
a.setAttribute("title", title);
a.innerText = title;
return a.outerHTML;
}

View File

@@ -138,7 +138,8 @@ var options = {
plugins: [
new CleanWebpackPlugin({
verbose: false,
dangerouslyAllowCleanPatternsOutsideProject: true
dangerouslyAllowCleanPatternsOutsideProject: true,
dry: false
}),
new webpack.ProgressPlugin(),
// expose and write the allowed env vars on the compiled bundle

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -104,6 +104,10 @@
"run-ios",
"build-ios",
"build-android",
"build-android-debug",
"e2e-android-debug",
"e2e-android",
"e2e-ios",
"release-android",
"release-android-bundle",
"release-ios",

View File

@@ -60,8 +60,6 @@ for (const website of Websites) {
await page.addScriptTag({ content: output, type: "text/javascript" });
await page.waitForLoadState("networkidle");
// const originalScreenshot = await page.screenshot({
// fullPage: true,
// type: "jpeg"
@@ -73,7 +71,12 @@ for (const website of Websites) {
// });
const result = await page.evaluate(async () => {
const html = await window.clipper.clipPage(window.document, true, false);
const html = await window.clipper.clipPage(window.document, false, {
corsProxy: "https://cors.notesnook.com",
images: true,
inlineImages: true,
styles: true
});
if (html) {
return `\ufeff${html}`;
}
@@ -101,97 +104,6 @@ for (const website of Websites) {
maxDiffPixelRatio: 0.1
});
rmSync(tempFilePath, { force: true });
});
}
for (const website of Websites) {
const domain = new URL(website.url).hostname;
test(`clip as image ${domain} (${website.title})`, async ({ page }, info) => {
info.setTimeout(0);
await page.goto(website.url);
await page.addScriptTag({ content: output, type: "text/javascript" });
await page.waitForLoadState("networkidle");
// const originalScreenshot = await page.screenshot({
// fullPage: true,
// type: "jpeg"
// });
// expect(originalScreenshot).toMatchSnapshot({
// name: `${slugify(website.title)}.jpg`,
// maxDiffPixelRatio: 0.1
// });
const result = await page.evaluate(async () => {
const data = await window.clipper.clipScreenshot(undefined, "raw");
if (data) {
return base64ArrayBuffer(await data.arrayBuffer());
}
function base64ArrayBuffer(arrayBuffer) {
let base64 = "";
const encodings =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const bytes = new Uint8Array(arrayBuffer);
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + "==";
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + "=";
}
return base64;
}
return null;
});
if (!result) throw new Error("Failed to clip page.");
expect(Buffer.from(result, "base64")).toMatchSnapshot({
name: `${slugify(website.title)}-image.png`,
maxDiffPixelRatio: 0.1
});
// rmSync(tempFilePath, { force: true });
});
}

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/clipper",
"version": "2.1.3",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
@@ -11,9 +11,7 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@mozilla/readability": "^0.4.2",
"css-what": "6.1.0",
"hyperapp": "^2.0.22",
"specificity": "^0.4.1"
"hyperapp": "^2.0.22"
},
"devDependencies": {
"@playwright/test": "1.48.2",
@@ -22,9 +20,9 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz",
"integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -34,9 +32,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz",
"integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -115,17 +113,18 @@
}
},
"node_modules/@mozilla/readability": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.2.tgz",
"integrity": "sha512-48MJXzi4Dhy2fJ3lGjmwdEJKoMmn3oiYew9n/1OW6cZy78hAzRIyDJDBCGrg4PBFDyY4xos+H4LCFn5QVRDcfw==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.4.tgz",
"integrity": "sha512-MCgZyANpJ6msfvVMi6+A0UAsvZj//4OHREYUB9f2087uXHVoU+H+SWhuihvb1beKpM323bReQPRio0WNk2+V6g==",
"license": "Apache-2.0",
"engines": {
"node": ">=10.0.0"
"node": ">=14.0.0"
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.6.tgz",
"integrity": "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz",
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -140,6 +139,7 @@
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.48.2"
},
@@ -151,17 +151,17 @@
}
},
"node_modules/@rsbuild/core": {
"version": "1.5.13",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.13.tgz",
"integrity": "sha512-P+TCvZCVpBYZ3GDdnzR/tZKicE41khJIqIRlJYnEc9dwUfX1/eqRf8lA8yrsbB5iZbSfj1iOoH1N25cCQ3hhuA==",
"version": "1.5.17",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.17.tgz",
"integrity": "sha512-tHa4puv+pEooQvSewu/K5sm270nkVPcP07Ioz1c+fbFCrFpiZWV5XumgznilS80097glUrieN+9xTbIHGXjThQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "1.5.8",
"@rspack/lite-tapable": "~1.0.1",
"@swc/helpers": "^0.5.17",
"core-js": "~3.45.1",
"jiti": "^2.6.0"
"core-js": "~3.46.0",
"jiti": "^2.6.1"
},
"bin": {
"rsbuild": "bin/rsbuild.js"
@@ -384,9 +384,9 @@
}
},
"node_modules/core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"version": "3.46.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.46.0.tgz",
"integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -395,23 +395,13 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -423,7 +413,8 @@
"node_modules/hyperapp": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.22.tgz",
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA=="
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA==",
"license": "MIT"
},
"node_modules/jiti": {
"version": "2.6.1",
@@ -440,6 +431,7 @@
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.48.2"
},
@@ -458,6 +450,7 @@
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -470,18 +463,11 @@
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/specificity": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==",
"bin": {
"specificity": "bin/specificity"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -489,319 +475,5 @@
"dev": true,
"license": "0BSD"
}
},
"dependencies": {
"@emnapi/core": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"dev": true,
"optional": true,
"requires": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@emnapi/wasi-threads": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"@module-federation/error-codes": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz",
"integrity": "sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==",
"dev": true
},
"@module-federation/runtime": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.0.tgz",
"integrity": "sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==",
"dev": true,
"requires": {
"@module-federation/error-codes": "0.18.0",
"@module-federation/runtime-core": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"@module-federation/runtime-core": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.0.tgz",
"integrity": "sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==",
"dev": true,
"requires": {
"@module-federation/error-codes": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"@module-federation/runtime-tools": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.0.tgz",
"integrity": "sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==",
"dev": true,
"requires": {
"@module-federation/runtime": "0.18.0",
"@module-federation/webpack-bundler-runtime": "0.18.0"
}
},
"@module-federation/sdk": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.0.tgz",
"integrity": "sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==",
"dev": true
},
"@module-federation/webpack-bundler-runtime": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.0.tgz",
"integrity": "sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==",
"dev": true,
"requires": {
"@module-federation/runtime": "0.18.0",
"@module-federation/sdk": "0.18.0"
}
},
"@mozilla/readability": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.4.2.tgz",
"integrity": "sha512-48MJXzi4Dhy2fJ3lGjmwdEJKoMmn3oiYew9n/1OW6cZy78hAzRIyDJDBCGrg4PBFDyY4xos+H4LCFn5QVRDcfw=="
},
"@napi-rs/wasm-runtime": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.6.tgz",
"integrity": "sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==",
"dev": true,
"optional": true,
"requires": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1"
}
},
"@playwright/test": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
"dev": true,
"requires": {
"playwright": "1.48.2"
}
},
"@rsbuild/core": {
"version": "1.5.13",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-1.5.13.tgz",
"integrity": "sha512-P+TCvZCVpBYZ3GDdnzR/tZKicE41khJIqIRlJYnEc9dwUfX1/eqRf8lA8yrsbB5iZbSfj1iOoH1N25cCQ3hhuA==",
"dev": true,
"requires": {
"@rspack/core": "1.5.8",
"@rspack/lite-tapable": "~1.0.1",
"@swc/helpers": "^0.5.17",
"core-js": "~3.45.1",
"jiti": "^2.6.0"
}
},
"@rspack/binding": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.5.8.tgz",
"integrity": "sha512-/91CzhRl9r5BIQCgGsS7jA6MDbw1I2BQpbfcUUdkdKl2P79K3Zo/Mw/TvKzS86catwLaUQEgkGRmYawOfPg7ow==",
"dev": true,
"requires": {
"@rspack/binding-darwin-arm64": "1.5.8",
"@rspack/binding-darwin-x64": "1.5.8",
"@rspack/binding-linux-arm64-gnu": "1.5.8",
"@rspack/binding-linux-arm64-musl": "1.5.8",
"@rspack/binding-linux-x64-gnu": "1.5.8",
"@rspack/binding-linux-x64-musl": "1.5.8",
"@rspack/binding-wasm32-wasi": "1.5.8",
"@rspack/binding-win32-arm64-msvc": "1.5.8",
"@rspack/binding-win32-ia32-msvc": "1.5.8",
"@rspack/binding-win32-x64-msvc": "1.5.8"
}
},
"@rspack/binding-darwin-arm64": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.5.8.tgz",
"integrity": "sha512-spJfpOSN3f7V90ic45/ET2NKB2ujAViCNmqb0iGurMNQtFRq+7Kd+jvVKKGXKBHBbsQrFhidSWbbqy2PBPGK8g==",
"dev": true,
"optional": true
},
"@rspack/binding-darwin-x64": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.5.8.tgz",
"integrity": "sha512-YFOzeL1IBknBcri8vjUp43dfUBylCeQnD+9O9p0wZmLAw7DtpN5JEOe2AkGo8kdTqJjYKI+cczJPKIw6lu1LWw==",
"dev": true,
"optional": true
},
"@rspack/binding-linux-arm64-gnu": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.5.8.tgz",
"integrity": "sha512-UAWCsOnpkvy8eAVRo0uipbHXDhnoDq5zmqWTMhpga0/a3yzCp2e+fnjZb/qnFNYb5MeL0O1mwMOYgn1M3oHILQ==",
"dev": true,
"optional": true
},
"@rspack/binding-linux-arm64-musl": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.5.8.tgz",
"integrity": "sha512-GnSvGT4GjokPSD45cTtE+g7LgghuxSP1MRmvd+Vp/I8pnxTVSTsebRod4TAqyiv+l11nuS8yqNveK9qiOkBLWw==",
"dev": true,
"optional": true
},
"@rspack/binding-linux-x64-gnu": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.5.8.tgz",
"integrity": "sha512-XLxh5n/pzUfxsugz/8rVBv+Tx2nqEM+9rharK69kfooDsQNKyz7PANllBQ/v4svJ+W0BRHnDL4qXSGdteZeEjA==",
"dev": true,
"optional": true
},
"@rspack/binding-linux-x64-musl": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.5.8.tgz",
"integrity": "sha512-gE0+MZmwF+01p9/svpEESkzkLpBkVUG2o03YMpwXYC/maeRRhWvF8BJ7R3i/Ls/jFGSE87dKX5NbRLVzqksq/w==",
"dev": true,
"optional": true
},
"@rspack/binding-wasm32-wasi": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.5.8.tgz",
"integrity": "sha512-cfg3niNHeJuxuml1Vy9VvaJrI/5TakzoaZvKX2g5S24wfzR50Eyy4JAsZ+L2voWQQp1yMJbmPYPmnTCTxdJQBQ==",
"dev": true,
"optional": true,
"requires": {
"@napi-rs/wasm-runtime": "^1.0.5"
}
},
"@rspack/binding-win32-arm64-msvc": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.5.8.tgz",
"integrity": "sha512-7i3ZTHFXKfU/9Jm9XhpMkrdkxO7lfeYMNVEGkuU5dyBfRMQj69dRgPL7zJwc2plXiqu9LUOl+TwDNTjap7Q36g==",
"dev": true,
"optional": true
},
"@rspack/binding-win32-ia32-msvc": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.5.8.tgz",
"integrity": "sha512-7ZPPWO11J+soea1+mnfaPpQt7GIodBM7A86dx6PbXgVEoZmetcWPrCF2NBfXxQWOKJ9L3RYltC4z+ZyXRgMOrw==",
"dev": true,
"optional": true
},
"@rspack/binding-win32-x64-msvc": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.5.8.tgz",
"integrity": "sha512-N/zXQgzIxME3YUzXT8qnyzxjqcnXudWOeDh8CAG9zqTCnCiy16SFfQ/cQgEoLlD9geQntV6jx2GbDDI5kpDGMQ==",
"dev": true,
"optional": true
},
"@rspack/core": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.5.8.tgz",
"integrity": "sha512-sUd2LfiDhqYVfvknuoz0+/c+wSpn693xotnG5g1CSWKZArbtwiYzBIVnNlcHGmuoBRsnj/TkSq8dTQ7gwfBroQ==",
"dev": true,
"requires": {
"@module-federation/runtime-tools": "0.18.0",
"@rspack/binding": "1.5.8",
"@rspack/lite-tapable": "1.0.1"
}
},
"@rspack/lite-tapable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz",
"integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==",
"dev": true
},
"@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"dev": true,
"requires": {
"tslib": "^2.8.0"
}
},
"@tybys/wasm-util": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"optional": true,
"requires": {
"tslib": "^2.4.0"
}
},
"core-js": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz",
"integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==",
"dev": true
},
"css-what": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"hyperapp": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.22.tgz",
"integrity": "sha512-3uf9HjnjrhbfykowFNEObZewBEo4DXJIM+9FnGkiR9E4H2eh2f921SzMCMS69X5nN3A7KFfmZc9KCKh/7TQBFA=="
},
"jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true
},
"playwright": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.48.2"
}
},
"playwright-core": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
"dev": true
},
"slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"dev": true
},
"specificity": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz",
"integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg=="
},
"tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true
}
}
}

View File

@@ -33,8 +33,6 @@
},
"dependencies": {
"@mozilla/readability": "^0.4.2",
"css-what": "6.1.0",
"hyperapp": "^2.0.22",
"specificity": "^0.4.1"
"hyperapp": "^2.0.22"
}
}

View File

@@ -16,331 +16,29 @@ 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 { createImage, FetchOptions } from "./fetch.js";
import { Filter } from "./types.js";
import { uid } from "./utils.js";
const SVGElements = [
"altGlyph",
"altGlyphDef",
"altGlyphItem",
"animate",
"animateColor",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"color-profile",
"cursor",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"font-face",
"font-face-format",
"font-face-name",
"font-face-src",
"font-face-uri",
"foreignObject",
"g",
"glyph",
"glyphRef",
"hkern",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"missing-glyph",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"set",
"stop",
"svg",
"switch",
"symbol",
"text",
"textPath",
"title",
"tref",
"tspan",
"use",
"view",
"vkern"
].map((a) => a.toLowerCase());
const INVALID_ELEMENTS = ["script", "noscript"];
const INVALID_ELEMENTS = ["script"].map((a) => a.toLowerCase());
type CloneProps = {
filter?: Filter;
root: boolean;
vector: boolean;
styles?: boolean;
getElementStyles?: (element: HTMLElement) => CSSStyleDeclaration | undefined;
getPseudoElementStyles?: (
element: HTMLElement,
pseudoElement: string
) => CSSStyleDeclaration | undefined;
fetchOptions?: FetchOptions;
type CloneNodeOptions = {
images?: boolean;
styles?: boolean;
};
export async function cloneNode(node: HTMLElement, options: CloneProps) {
const { root, filter } = options;
if (!root && filter && !filter(node)) return null;
let clone = await makeNodeCopy(node, options);
if (!clone) return null;
clone = await cloneChildren(node, clone, options);
const processed = processClone(node, clone, options);
return processed;
}
function makeNodeCopy(original: HTMLElement, options?: CloneProps) {
try {
if (original instanceof HTMLCanvasElement && options?.images)
return createImage(original.toDataURL(), options?.fetchOptions);
if (!options?.images && original instanceof HTMLImageElement) return null;
if (
!options?.styles &&
(original instanceof HTMLButtonElement ||
original instanceof HTMLFormElement ||
original instanceof HTMLSelectElement ||
original instanceof HTMLInputElement ||
original instanceof HTMLTextAreaElement)
)
return null;
if (original.nodeType === Node.COMMENT_NODE) return null;
if (isInvalidElement(original)) return null;
if (original.nodeType !== Node.TEXT_NODE && !isSVGElement(original)) {
const { display, width, height } = window.getComputedStyle(original);
if (display === "none" || (width === "0px" && height === "0px"))
return null;
if (isCustomElement(original)) {
const isInline = display.includes("inline");
const element = document.createElement(isInline ? "span" : "div");
for (const attribute of original.attributes) {
element.setAttribute(attribute.name, attribute.value);
}
return element;
}
}
return original.cloneNode(false) as HTMLElement;
} catch (e) {
console.error("Failed to clone element", e);
return null;
}
}
function isCustomElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return (
!SVGElements.includes(element.tagName.toLowerCase()) &&
element.tagName.includes("-")
);
}
export function isSVGElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return SVGElements.includes(element.tagName.toLowerCase());
}
function isInvalidElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return INVALID_ELEMENTS.includes(element.tagName.toLowerCase());
}
async function cloneChildren(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
const children = original.childNodes;
if (children.length === 0) return clone;
await cloneChildrenInOrder(clone, children, options);
return clone;
}
async function cloneChildrenInOrder(
parent: HTMLElement,
childs: NodeListOf<ChildNode>,
options: CloneProps
) {
for (const node of childs) {
const childClone = await cloneNode(node as HTMLElement, {
...options,
root: false
});
if (childClone) parent.appendChild(childClone);
}
}
function processClone(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
if (!(clone instanceof Element)) return clone;
// if (clone instanceof HTMLElement) removeAttributes(clone);
if (options.styles) {
copyStyle(original, clone, options);
clonePseudoElements(original, clone, options);
}
fixRelativeUrl(clone);
copyUserInput(original, clone);
fixSvg(clone);
return clone;
}
function fixRelativeUrl(node: HTMLElement) {
const attributes = ["href", "src"];
const baseUrl = window.location.href;
for (const attribute of attributes) {
const url = node.getAttribute(attribute);
const relativeUrl = url?.startsWith("http") ? undefined : url;
if (relativeUrl) {
const absoluteUrl = new URL(relativeUrl, baseUrl).href;
node.setAttribute(attribute, absoluteUrl);
}
}
}
function copyFont(source: CSSStyleDeclaration, target: CSSStyleDeclaration) {
target.font = source.font;
target.fontFamily = source.fontFamily;
target.fontFeatureSettings = source.fontFeatureSettings;
target.fontKerning = source.fontKerning;
target.fontSize = source.fontSize;
target.fontStretch = source.fontStretch;
target.fontStyle = source.fontStyle;
target.fontVariant = source.fontVariant;
target.fontVariantCaps = source.fontVariantCaps;
target.fontVariantEastAsian = source.fontVariantEastAsian;
target.fontVariantLigatures = source.fontVariantLigatures;
target.fontVariantNumeric = source.fontVariantNumeric;
target.fontVariationSettings = source.fontVariationSettings;
target.fontWeight = source.fontWeight;
}
function copyStyle(
sourceElement: HTMLElement,
targetElement: HTMLElement,
options: CloneProps
) {
const { getElementStyles } = options;
const sourceComputedStyles =
getElementStyles && getElementStyles(sourceElement);
if (!sourceComputedStyles) return;
targetElement.style.cssText = sourceComputedStyles.cssText;
if (sourceElement.tagName.toLowerCase() === "body") {
copyFont(getComputedStyle(sourceElement), targetElement.style);
export function cloneNode(node: HTMLElement, options: CloneNodeOptions) {
node = node.cloneNode(true) as HTMLElement;
if (!options.images) {
const images = node.getElementsByTagName("img");
for (const image of images) image.remove();
}
const styles = targetElement.getAttribute("style");
if (styles) targetElement.setAttribute("style", minifyStyles(styles));
}
function clonePseudoElements(
original: HTMLElement,
clone: HTMLElement,
options: CloneProps
) {
const { getPseudoElementStyles } = options;
let hasPseudoElements = false;
const styleElement = document.createElement("style");
const className = `pseudo--${uid()}`;
for (const element of [":before", ":after"]) {
const style =
(getPseudoElementStyles && getPseudoElementStyles(original, element)) ||
getComputedStyle(original, element);
if (!style.cssText) continue;
const selector = `.${className}:${element} {
${style.cssText}
}`;
styleElement.appendChild(document.createTextNode(selector));
hasPseudoElements = true;
if (!options.styles) {
const elements = node.querySelectorAll(
`button, form, select, input, textarea`
);
for (const element of elements) element.remove();
}
if (hasPseudoElements) {
clone.className = className;
clone.appendChild(styleElement);
}
return hasPseudoElements;
}
function copyUserInput(original: HTMLElement, clone: HTMLElement) {
if (
original instanceof HTMLInputElement ||
original instanceof HTMLTextAreaElement
)
clone.setAttribute("value", original.value);
}
function fixSvg(clone: Element) {
if (!(clone instanceof SVGElement)) return;
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
// if (!(clone instanceof SVGRectElement)) return;
["width", "height"].forEach(function (attribute) {
const value = clone.getAttribute(attribute);
if (!value || !!clone.style.getPropertyValue(attribute)) return;
clone.style.setProperty(attribute, value);
});
}
function minifyStyles(text: string) {
return text.replace(/(:?[:;])(:? +)/gm, (_full, sep) => {
return sep;
});
const invalidElements = node.querySelectorAll(INVALID_ELEMENTS.join(","));
for (const element of invalidElements) element.remove();
return node;
}

View File

@@ -1,176 +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/>.
*/
const from = String.fromCharCode;
function trim(value: string): string {
return value.trim();
}
function charat(value: string, index: number): number {
return value.charCodeAt(index) | 0;
}
function strlen(value: string): number {
return value.length;
}
function substr(value: string, begin: number, end: number): string {
return value.slice(begin, end);
}
function append<T>(value: T, array: T[]): T {
array.push(value);
return value;
}
let line = 1;
let column = 1;
let length = 0;
let position = 0;
let character = 0;
let characters = "";
function next(): number {
character = position < length ? charat(characters, position++) : 0;
if ((column++, character === 10)) (column = 1), line++;
return character;
}
function peek(): number {
return charat(characters, position);
}
function slice(begin: number, end: number): string {
return substr(characters, begin, end);
}
function token(type: number): number {
switch (type) {
// \0 \t \n \r \s whitespace token
case 0:
case 9:
case 10:
case 13:
case 32:
return 5;
// ! + , / > @ ~ isolate token
case 33:
case 42:
case 43:
case 44:
case 47:
case 62:
case 64:
case 126:
case 59: /* ; { } breakpoint token */
case 123:
case 125:
return 4;
// : accompanied token
case 58:
return 3;
// " ' ( [ opening delimit token
case 34:
case 39:
case 40:
case 91:
return 2;
// ) ] closing delimit token
case 41:
case 93:
return 1;
}
return 0;
}
function alloc(value: string): [] {
line = column = 1;
length = strlen((characters = value));
position = 0;
return [];
}
function dealloc<T>(value: T): T {
characters = "";
return value;
}
function delimit(type: number): string {
return trim(
slice(
position - 1,
delimiter(type === 91 ? type + 2 : type === 40 ? type + 1 : type)
)
);
}
export function tokenize(value: string): string[] {
return dealloc(tokenizer(alloc(value)));
}
function tokenizer(children: string[]): string[] {
while (next())
switch (token(character)) {
case 0:
append(identifier(position - 1), children);
break;
case 2:
append(delimit(character), children);
break;
default:
append(from(character), children);
}
return children;
}
function delimiter(type: number): number {
while (next())
switch (character) {
// ] ) " '
case type:
return position;
// " '
case 34:
case 39:
if (type !== 34 && type !== 39) delimiter(character);
break;
// (
case 40:
if (type === 41) delimiter(type);
break;
// \
case 92:
next();
break;
}
return position;
}
function identifier(index: number): string {
while (!token(peek())) next();
return slice(index, position);
}

View File

@@ -16,13 +16,12 @@ 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 { cloneNode, isSVGElement } from "./clone.js";
import { createImage, FetchOptions } from "./fetch.js";
import { resolveAll } from "./fontfaces.js";
import { inlineAllImages } from "./images.js";
import { Options } from "./types.js";
import { canvasToBlob, delay, escapeXhtml, height, width } from "./utils.js";
import { cacheStylesheets, inlineStylesheets } from "./styles.js";
import { canvasToBlob, delay, height, width, isSVGElement } from "./utils.js";
import { cloneNode } from "./clone.js";
// Default impl options
const defaultOptions: Options = {
@@ -33,23 +32,9 @@ async function getInlinedNode(node: HTMLElement, options: Options) {
const { fonts, images, stylesheets, inlineImages } =
options.inlineOptions || {};
if (stylesheets) await inlineStylesheets(options.fetchOptions);
const documentStyles = getComputedStyle(document.documentElement);
const styleCache = stylesheets
? await cacheStylesheets(documentStyles)
: undefined;
let clone = await cloneNode(node, {
styles: options.styles,
filter: options.filter,
root: true,
vector: !options.raster,
fetchOptions: options.fetchOptions,
getElementStyles: styleCache?.get,
getPseudoElementStyles: styleCache?.getPseudo,
images: images
let clone = cloneNode(node, {
images,
styles: stylesheets
});
if (!clone || clone instanceof Text) return;
@@ -62,86 +47,72 @@ async function getInlinedNode(node: HTMLElement, options: Options) {
return clone;
}
async function toSvg(node: HTMLElement, options: Options) {
options.inlineOptions = {
fonts: true,
images: true,
stylesheets: true,
...options.inlineOptions
};
let clone = await getInlinedNode(node, options);
if (!clone) return;
clone = applyOptions(clone, options);
return makeSvgDataUri(
clone,
options.width || width(node),
options.height || height(node)
);
}
function applyOptions(clone: HTMLElement, options: Options) {
if (options.backgroundColor)
clone.style.backgroundColor = options.backgroundColor;
if (options.width) clone.style.width = options.width + "px";
if (options.height) clone.style.height = options.height + "px";
return clone;
}
function toPixelData(node: HTMLElement, options: Options) {
options = options || {};
function toPng(body: HTMLElement, head: HTMLHeadElement, options: Options) {
options.raster = true;
return draw(node, options).then(function (canvas) {
return canvas
?.getContext("2d")
?.getImageData(0, 0, width(node), height(node)).data;
});
}
function toPng(node: HTMLElement, options: Options) {
options.raster = true;
return draw(node, options).then(function (canvas) {
return draw(body, head, options).then(function (canvas) {
return canvas?.toDataURL();
});
}
function toJpeg(node: HTMLElement, options: Options) {
async function toJpeg(
body: HTMLElement,
head: HTMLHeadElement,
options: Options
) {
options.raster = true;
return draw(node, options).then(function (canvas) {
return canvas?.toDataURL("image/jpeg", options.quality || 1.0);
});
return draw(body, head, options).then((canvas) =>
canvas?.toDataURL("image/jpeg", options.quality || 1.0)
);
}
function toBlob(node: HTMLElement, options: Options) {
function toBlob(body: HTMLElement, head: HTMLHeadElement, options: Options) {
options.raster = true;
return draw(node, options).then((canvas) => canvas && canvasToBlob(canvas));
return draw(body, head, options).then(
(canvas) => canvas && canvasToBlob(canvas)
);
}
function toCanvas(node: HTMLElement, options: Options) {
options.raster = true;
return draw(node, options);
function toSvg(body: HTMLElement, head: HTMLHeadElement, options: Options) {
return makeSvg(
body,
head,
options.width || width(body),
options.height || height(body)
);
}
function draw(domNode: HTMLElement, options: Options) {
async function draw(
body: HTMLElement,
head: HTMLHeadElement,
options: Options
) {
options = { ...defaultOptions, ...options };
return toSvg(domNode, options)
.then((uri) => (uri ? createImage(uri, options.fetchOptions) : null))
const uri = makeSvgDataUri(
makeSvg(
body,
head,
options.width || width(body),
options.height || height(body)
)
);
return createImage(uri, options.fetchOptions)
.then(delay(0))
.then(function (image) {
if (!image) return null;
image.setAttribute("crossorigin", "anonymous");
const scale = typeof options.scale !== "number" ? 1 : options.scale;
const canvas = newCanvas(domNode, scale, options);
const canvas = newCanvas(body, scale, options);
const ctx = canvas?.getContext("2d");
if (!ctx) return null;
// ctx.mozImageSmoothingEnabled = false;
// ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;
if (image) {
ctx.scale(scale, scale);
ctx.drawImage(image, 0, 0);
}
ctx.scale(scale, scale);
ctx.drawImage(image as HTMLImageElement, 0, 0);
return canvas;
});
}
@@ -170,52 +141,60 @@ function embedFonts(node: HTMLElement, options?: FetchOptions) {
});
}
function makeSvgDataUri(node: HTMLElement, width: number, height: number) {
node.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
const xhtml = escapeXhtml(new XMLSerializer().serializeToString(node));
function makeSvg(
body: HTMLElement,
head: HTMLHeadElement,
width: number,
height: number
) {
body.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
/**
* We're removing all attributes that contain non-word characters
* Sometimes a webpage could have invalid html and that causes attribute names to break
* HTML is resilient to this but SVG is not and will throw an error, so we remove these attributes altogether
*/
for (const element of body.querySelectorAll("img, svg, video, iframe")) {
const attributes = element.getAttributeNames();
for (const attribute of attributes) {
if (attribute.match(/\W/)) {
element.removeAttribute(attribute);
}
}
}
const xhtml = new XMLSerializer().serializeToString(body);
const xstyles = Array.from(head.getElementsByTagName("style"))
.map((s) => new XMLSerializer().serializeToString(s))
.join("\n");
const foreignObject =
'<foreignObject x="0" y="0" width="100%" height="100%">' +
xhtml +
"</foreignObject>";
const svgStr =
return (
'<svg xmlns="http://www.w3.org/2000/svg" width="' +
width +
'" height="' +
height +
'">' +
xstyles +
foreignObject +
"</svg>";
return "data:image/svg+xml;charset=utf-8," + svgStr;
"</svg>"
);
}
export { toJpeg, toBlob, toCanvas, toPixelData, toPng, toSvg, getInlinedNode };
function makeSvgDataUri(str: string) {
return "data:image/svg+xml; charset=utf8, " + encodeURIComponent(str);
}
const VALID_ATTRIBUTES = [
"src",
"href",
"title",
"style",
"srcset",
"sizes",
"width",
"height",
"target",
"rel"
];
export { toJpeg, toBlob, toPng, toSvg, getInlinedNode };
function finalize(root: HTMLElement) {
for (const element of root.querySelectorAll("*")) {
if (!(element instanceof HTMLElement) || isSVGElement(element)) continue;
for (const attribute of Array.from(element.attributes)) {
if (attribute.name === "class" && element.className.includes("pseudo--"))
continue;
if (!VALID_ATTRIBUTES.includes(attribute.name)) {
element.removeAttribute(attribute.name);
}
}
if (element instanceof HTMLAnchorElement) {
element.href = element.href.startsWith("http")

View File

@@ -39,7 +39,10 @@ export async function fetchResource(url: string, options?: FetchOptions) {
});
}
export function createImage(url: string, options?: FetchOptions) {
export function createImage(
url: string,
options?: FetchOptions
): Promise<HTMLImageElement | null> {
if (url === "data:,") return Promise.resolve(null);
return new Promise<HTMLImageElement>(function (resolve, reject) {
const image = new Image();
@@ -47,7 +50,7 @@ export function createImage(url: string, options?: FetchOptions) {
image.onload = function () {
resolve(image);
};
image.onerror = reject;
image.onerror = () => reject(new Error("Failed to render image."));
image.src = constructUrl(url, options);
});
}

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 { FetchOptions, fetchResource } from "./fetch.js";
import { inlineAll } from "./inliner.js";
import { isDataUrl } from "./utils.js";
async function inlineAllImages(root: HTMLElement, options?: FetchOptions) {
@@ -28,15 +27,7 @@ async function inlineAllImages(root: HTMLElement, options?: FetchOptions) {
promises.push(inlineImage(image, options));
}
const backgroundImageNodes = root.querySelectorAll(
`[style*="background-image:"],[style*="background:"]`
);
for (let i = 0; i < backgroundImageNodes.length; ++i) {
const image = backgroundImageNodes[i];
promises.push(inlineBackground(image as HTMLElement, options));
}
await Promise.all(promises).catch((e) => console.error(e));
await Promise.allSettled(promises).catch((e) => console.error(e));
}
export { inlineAllImages };
@@ -54,26 +45,10 @@ async function inlineImage(element: HTMLImageElement, options?: FetchOptions) {
return element;
}
return new Promise<HTMLImageElement | null>(function (resolve, reject) {
if (element.parentElement?.tagName === "PICTURE") {
element.parentElement?.replaceWith(element);
}
if (element.parentElement?.tagName === "PICTURE") {
element.parentElement?.replaceWith(element);
}
element.onload = () => resolve(element);
// for any image with invalid src(such as <img src />), just ignore it
element.onerror = (e) => reject(e);
element.src = dataURL;
element.removeAttribute("srcset");
});
}
async function inlineBackground(
backgroundNode: HTMLElement,
options?: FetchOptions
) {
const background = backgroundNode.style.getPropertyValue("background-image");
if (!background) return backgroundNode;
const inlined = await inlineAll(background, options);
backgroundNode.style.setProperty("background-image", inlined);
return backgroundNode;
element.src = dataURL;
element.removeAttribute("srcset");
}

View File

@@ -26,7 +26,7 @@ declare global {
};
}
global.Clipper = {
globalThis.Clipper = {
clipArticle,
clipPage
};

View File

@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Readability } from "@mozilla/readability";
import { injectCss } from "./utils.js";
import { app, h, text } from "hyperapp";
import { getInlinedNode, toBlob, toJpeg, toPng } from "./domtoimage.js";
import { getInlinedNode, toBlob, toJpeg, toPng, toSvg } from "./domtoimage.js";
import { Config, InlineOptions } from "./types.js";
import { FetchOptions } from "./fetch.js";
import { addStylesToHead } from "./styles.js";
type ReadabilityEnhanced = Readability<string> & {
PRESENTATIONAL_ATTRIBUTES: string[];
@@ -84,36 +85,45 @@ async function clipArticle(
}
async function clipScreenshot<
TOutputFormat extends "jpeg" | "png" | "raw",
TOutput extends TOutputFormat extends "jpeg"
? string
: TOutputFormat extends "png"
TOutputFormat extends "jpeg" | "png" | "raw" | "svg",
TOutput extends TOutputFormat extends "jpeg" | "png" | "svg"
? string
: Blob | undefined
>(
target?: HTMLElement,
output: TOutputFormat = "jpeg" as TOutputFormat,
config?: Config
): Promise<TOutput> {
const screenshotTarget = target || document.body;
): Promise<TOutput | null> {
const fetchOptions = resolveFetchOptions(config);
const func = output === "jpeg" ? toJpeg : output === "png" ? toPng : toBlob;
const screenshot = await func(screenshotTarget, {
const { body, head } = await getPage(document, config, false);
if (!body || !head) return null;
const func =
output === "jpeg"
? toJpeg
: output === "png"
? toPng
: output === "svg"
? toSvg
: toBlob;
const screenshot = await func(body, head, {
quality: 1,
backgroundColor: "white",
width: document.body.scrollWidth,
height: document.body.scrollHeight,
fetchOptions: resolveFetchOptions(config),
fetchOptions,
inlineOptions: {
inlineImages: true,
fonts: true,
images: true,
stylesheets: true
},
styles: true
}
});
if (output === "jpeg" || output === "png")
return `<img width="${document.body.scrollWidth}px" height="${document.body.scrollHeight}px" src="${screenshot}" />` as TOutput;
else if (output === "svg") return screenshot as TOutput;
else return screenshot as TOutput;
}
@@ -192,7 +202,11 @@ function enterNodeSelectionMode(doc: Document, config?: Config) {
`.${CLASSES.nodeSelected}`
);
const div = document.createElement("div");
const { head } = await getPage(document, config, false);
const html = document.createElement("html");
html.append(head!);
const body = document.createElement("body");
html.append(body);
for (const node of selectedNodes) {
node.classList.remove(CLASSES.nodeSelected);
const inlined = await getInlinedNode(node as HTMLElement, {
@@ -205,9 +219,9 @@ function enterNodeSelectionMode(doc: Document, config?: Config) {
}
});
if (!inlined) continue;
div.appendChild(inlined);
body.appendChild(inlined);
}
resolve(div?.outerHTML);
resolve(html?.outerHTML);
},
() => reject("Cancelled.")
);
@@ -454,16 +468,16 @@ async function getPage(
config?: Config,
onlyVisible = false
) {
const fetchOptions = resolveFetchOptions(config);
const body = await getInlinedNode(document.body, {
raster: true,
fetchOptions: resolveFetchOptions(config),
fetchOptions,
inlineOptions: {
fonts: false,
inlineImages: config?.inlineImages,
images: config?.images,
stylesheets: config?.styles
},
styles: config?.styles,
filter: (node) => {
return !onlyVisible || isElementInViewport(node);
}
@@ -476,6 +490,16 @@ async function getPage(
title.innerText = document.title;
head.appendChild(title);
if (config?.styles) {
await addStylesToHead(head, fetchOptions);
}
for (const [name, value] of Object.entries(
toAttributes(document.documentElement)
)) {
body.setAttribute(name, value);
}
return {
body,
head
@@ -492,3 +516,13 @@ function resolveFetchOptions(config?: Config): FetchOptions | undefined {
}
: undefined;
}
function toAttributes(element: HTMLElement) {
const attributes: Record<string, string> = {};
for (const { name } of element.attributes) {
const value = element.getAttribute(name);
if (!value) continue;
attributes[name] = value;
}
return attributes;
}

View File

@@ -16,7 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { FetchOptions, fetchResource } from "./fetch.js";
import { constructUrl, FetchOptions, fetchResource } from "./fetch.js";
import { isDataUrl, resolveUrl, escape } from "./utils.js";
const URL_REGEX = /url\(['"]?([^'"]+?)['"]?\)/g;
@@ -40,32 +40,37 @@ async function inline(
string: string,
url: string,
options?: FetchOptions,
baseUrl?: string
baseUrl?: string,
onlyResolve?: boolean
) {
url = baseUrl ? resolveUrl(url, baseUrl) : url;
const dataUrl = await fetchResource(url, options);
// const dataUrl = dataAsUrl(data, mimeType(url));
return string.replace(urlAsRegex(url), "$1" + dataUrl + "$3");
}
function urlAsRegex(urlValue: string) {
return new RegExp("(url\\(['\"]?)(" + escape(urlValue) + ")(['\"]?\\))", "g");
const resolvedUrl = baseUrl ? resolveUrl(url, baseUrl) : url;
const dataUrl = onlyResolve
? constructUrl(resolvedUrl, options)
: await fetchResource(resolvedUrl, options).catch(() =>
constructUrl(resolvedUrl, options)
);
return string.replace(url, dataUrl || resolvedUrl);
}
async function inlineAll(
string: string,
options?: FetchOptions,
baseUrl?: string
baseUrl?: string,
onlyResolve?: boolean
) {
if (!shouldProcess(string)) return string;
const urls = readUrls(string);
let prefix = string;
for (const url of urls) {
prefix = await inline(prefix, url, options, baseUrl);
string = await inline(string, url, options, baseUrl, onlyResolve).catch(
(e) => {
console.error(e);
return string;
}
);
}
return prefix;
return string;
}
export { shouldProcess, inlineAll, readUrls };

View File

@@ -17,64 +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 { constructUrl, FetchOptions } from "./fetch.js";
import { compare, calculate, SpecificityArray } from "specificity";
import { tokenize } from "./css-tokenizer.js";
import { stringify, parse, SelectorType } from "css-what";
import { safeQuerySelectorAll } from "./utils.js";
const SHORTHANDS = [
"animation",
"background",
"border",
"border-block-end",
"border-block-start",
"border-bottom",
"border-color",
"border-image",
"border-inline-end",
"border-inline-start",
"border-left",
"border-radius",
"border-right",
"border-style",
"border-top",
"border-width",
"column-rule",
"columns",
"contain-intrinsic-size",
"flex",
"flex-flow",
"font",
"gap",
"grid",
"grid-area",
"grid-column",
"grid-row",
"grid-template",
"grid-gap",
"list-style",
"margin",
"mask",
"offset",
"outline",
"overflow",
"padding",
"place-content",
"place-items",
"place-self",
"scroll-margin",
"scroll-padding",
"text-decoration",
"text-emphasis",
"transition"
];
export async function inlineStylesheets(options?: FetchOptions) {
for (const sheet of document.styleSheets) {
if (await skipStyleSheet(sheet, options)) continue;
}
await resolveImports(options);
}
import { inlineAll } from "./inliner.js";
async function resolveImports(options?: FetchOptions) {
for (const sheet of document.styleSheets) {
@@ -96,7 +39,9 @@ async function resolveImports(options?: FetchOptions) {
}
}
for (const ruleIndex of rulesToDelete) sheet.deleteRule(ruleIndex);
if (sheet.cssRules.length !== 0) {
for (const ruleIndex of rulesToDelete) sheet.deleteRule(ruleIndex);
}
}
}
@@ -105,7 +50,7 @@ async function downloadStylesheet(href: string, options?: FetchOptions) {
const style = document.createElement("style");
const response = await fetch(constructUrl(href, options));
if (!response.ok) return false;
style.innerText = await response.text();
style.innerHTML = await response.text();
style.setAttribute("href", href);
return style;
} catch (e) {
@@ -113,318 +58,129 @@ async function downloadStylesheet(href: string, options?: FetchOptions) {
}
}
type StyleableElement = HTMLElement | SVGElement;
type BaseStyle = {
rule: CSSStyleDeclaration;
href: URL | null;
};
type SpecifiedStyle = BaseStyle & {
specificity: SpecificityArray;
};
type PseudoElementStyle = BaseStyle & {
pseudoElement: string;
};
type CSSStyledElements = Map<StyleableElement, SpecifiedStyle[]>;
type CSSPseudoElements = Map<StyleableElement, PseudoElementStyle[]>;
export async function cacheStylesheets(documentStyles: CSSStyleDeclaration) {
const styledElements: CSSStyledElements = new Map();
const styledPseudoElements: CSSPseudoElements = new Map();
for (const sheet of document.styleSheets) {
if (await skipStyleSheet(sheet)) continue;
let href = sheet.href || undefined;
if (!href && sheet.ownerNode instanceof HTMLElement)
href = sheet.ownerNode.getAttribute("href") || undefined;
walkRules(
sheet.cssRules,
documentStyles,
styledElements,
styledPseudoElements,
href
);
}
return {
getPseudo(element: StyleableElement, pseudoElement: string) {
const styles = styledPseudoElements
.get(element)
?.filter((s) => s.pseudoElement.includes(pseudoElement));
if (!styles || !styles.length) return;
return getElementStyles(element, styles, documentStyles);
},
get(element: StyleableElement) {
const styles = styledElements.get(element);
if (!styles) return;
const allStyles = styles.sort((a, b) =>
compare(a.specificity, b.specificity)
);
allStyles.push({
rule: element.style,
specificity: [0, 0, 0, 0],
href: null
});
return getElementStyles(element, allStyles, documentStyles);
}
};
}
function walkRules(
cssRules: CSSRuleList,
documentStyles: CSSStyleDeclaration,
styled: CSSStyledElements,
pseudoElements: CSSPseudoElements,
href?: string
) {
for (const rule of cssRules) {
if (rule instanceof CSSStyleRule) {
if (isPseudoSelector(rule.selectorText)) {
const selectors = parsePseudoSelector(rule.selectorText);
for (const selector of selectors) {
if (!selector || !selector.selector.trim()) continue;
const elements = safeQuerySelectorAll(
document,
selector.selector
) as NodeListOf<StyleableElement>;
for (const element of elements) {
if (
!(element instanceof HTMLElement) &&
!(element instanceof SVGElement)
)
continue;
const styles: PseudoElementStyle[] =
pseudoElements.get(element) || [];
pseudoElements.set(element, styles);
styles.push({
rule: rule.style,
href: getBaseUrl(href),
pseudoElement: selector.pseudoElement
});
}
}
}
const elements = safeQuerySelectorAll(
document,
rule.selectorText
) as NodeListOf<StyleableElement>;
for (const element of elements) {
if (
!(element instanceof HTMLElement) &&
!(element instanceof SVGElement)
)
continue;
const parts = rule.selectorText.split(",");
const styles: SpecifiedStyle[] = styled.get(element) || [];
styled.set(element, styles);
for (const part of parts) {
try {
const specificity = calculate(part)[0];
styles.push({
specificity: specificity.specificityArray,
rule: rule.style,
href: getBaseUrl(href)
});
break;
} catch (e) {
console.error(e, href && getBaseUrl(href));
// ignore
}
}
}
} else if (
rule instanceof CSSMediaRule &&
window.matchMedia(rule.conditionText).matches
) {
walkRules(rule.cssRules, documentStyles, styled, pseudoElements, href);
} else if (
rule instanceof CSSSupportsRule &&
CSS.supports(rule.conditionText)
) {
walkRules(rule.cssRules, documentStyles, styled, pseudoElements, href);
}
}
}
function getElementStyles(
element: StyleableElement,
styles: BaseStyle[],
documentStyles: CSSStyleDeclaration
) {
const newStyles = newStyleDeclaration();
const computedStyle = lazyComputedStyle(element);
const overrides = ["display"];
for (const style of styles) {
for (const property of [...style.rule, ...SHORTHANDS]) {
let value = style.rule.getPropertyValue(property);
if (overrides.includes(property))
value = computedStyle.style.getPropertyValue(property);
if (value.trim()) {
setStyle(
newStyles,
property,
value,
(variable) => {
return (
computedStyle.style.getPropertyValue(variable) ||
documentStyles.getPropertyValue(variable)
);
},
(url) => {
console.log("resolving url", url, style.href);
if (url.startsWith("data:") || !style.href) return url;
console.log("resolving url", url, style.href.href);
if (url.startsWith("/"))
return new URL(`${style.href.origin}${url}`).href;
return new URL(`${style.href.href}${url}`).href;
},
style.rule.getPropertyPriority(property)
);
}
}
}
return newStyles;
}
function setStyle(
target: CSSStyleDeclaration,
property: string,
value: string,
get: (variable: string) => string,
resolveUrl: (variable: string) => string,
priority?: string
) {
value = resolveCssVariables(value, get);
value = resolveCssUrl(value, resolveUrl);
target.setProperty(property, value, priority);
}
function newStyleDeclaration() {
const sheet = new CSSStyleSheet();
sheet.insertRule(".dummy{}");
return (sheet.cssRules[0] as CSSStyleRule).style;
}
function lazyComputedStyle(element: StyleableElement) {
let computedStyle: CSSStyleDeclaration | undefined;
return Object.defineProperty({}, "style", {
get: () => {
if (!computedStyle) computedStyle = getComputedStyle(element);
return computedStyle;
}
}) as { style: CSSStyleDeclaration };
}
async function skipStyleSheet(sheet: CSSStyleSheet, options?: FetchOptions) {
try {
sheet.cssRules.length;
} catch (_e) {
const node = sheet.ownerNode;
if (sheet.href && node instanceof HTMLLinkElement) {
if (isStylesheetForPrint(node)) return true;
const styleNode = await downloadStylesheet(node.href, options);
if (styleNode) node.replaceWith(styleNode);
}
return true;
}
return sheet.media.mediaText
return isStylesheetForPrint(sheet);
}
function isStylesheetForPrint(sheet: CSSStyleSheet | HTMLLinkElement) {
const mediaText =
typeof sheet.media === "string" ? sheet.media : sheet.media.mediaText;
return mediaText
.split(",")
.map((t) => t.trim())
.includes("print");
}
function resolveCssVariables(css: string, get: (variable: string) => string) {
const tokens = tokenize(css);
const finalTokens: string[] = [];
for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];
if (token === "var") {
const args = tokenize(tokens[++i].slice(1, -1));
const [variable, operator, space, ...restArgs] = args;
export async function addStylesToHead(
head: HTMLHeadElement,
options?: FetchOptions
) {
await resolveImports(options);
const value = get(variable);
if (value) {
finalTokens.push(value);
} else if (operator && restArgs.length <= 1) {
finalTokens.push(restArgs[0] || space);
} else if (operator && restArgs.length === 2) {
finalTokens.push(resolveCssVariables(restArgs.join(""), get));
for (const sheet of document.styleSheets) {
if (isStylesheetForPrint(sheet)) continue;
const href =
sheet.href && sheet.ownerNode instanceof HTMLLinkElement
? sheet.ownerNode.href
: sheet.ownerNode instanceof HTMLStyleElement
? sheet.ownerNode.getAttribute("href")
: null;
if (href) {
const result = await downloadStylesheet(href, options);
if (!result) continue;
const cssStylesheet = new CSSStyleSheet();
await cssStylesheet.replace(result.innerHTML);
await inlineBackgroundImages(cssStylesheet, options);
const toAppend = rulesToStyleNode(cssStylesheet.cssRules);
head.appendChild(toAppend);
continue;
}
if (sheet.cssRules.length > 0) {
await inlineBackgroundImages(sheet, options);
const styleNode = rulesToStyleNode(sheet.cssRules);
head.appendChild(styleNode);
continue;
}
if (sheet.ownerNode instanceof HTMLStyleElement) {
head.appendChild(sheet.ownerNode.cloneNode(true));
continue;
}
}
}
function rulesToStyleNode(cssRules: CSSRuleList) {
const cssText = Array.from(cssRules)
.map((r) => r.cssText)
.reduce((acc, text) => acc + text, "");
const style = document.createElement("style");
style.innerHTML = cssText;
return style;
}
async function inlineBackgroundImages(
sheet: CSSStyleSheet,
options?: FetchOptions
) {
const promises: Promise<void>[] = [];
for (const rule of sheet.cssRules) {
if (rule.type === CSSRule.STYLE_RULE) {
promises.push(processStyleRule(sheet, rule as CSSStyleRule, options));
} else if (rule.type === CSSRule.MEDIA_RULE) {
const mediaRule = rule as CSSMediaRule;
const mediaMatches = window.matchMedia(mediaRule.media.mediaText).matches;
for (const innerRule of mediaRule.cssRules) {
if (innerRule && innerRule.type === CSSRule.STYLE_RULE) {
promises.push(
processStyleRule(
sheet,
innerRule as CSSStyleRule,
options,
mediaMatches
)
);
}
}
} else if (token.startsWith("(") && token.endsWith(")")) {
finalTokens.push("(", resolveCssVariables(token.slice(1, -1), get), ")");
} else finalTokens.push(token);
}
return finalTokens.join("");
}
function resolveCssUrl(css: string, get: (url: string) => string) {
const tokens = tokenize(css);
const finalTokens: string[] = [];
for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];
if (token === "url" && !tokens[i + 1].startsWith("(data")) {
const url = tokens[++i].slice(2, -2);
const resolvedUrl = get(url);
if (resolvedUrl) {
finalTokens.push(token);
finalTokens.push('("');
finalTokens.push(resolvedUrl);
finalTokens.push('")');
} else if (rule.type === CSSRule.SUPPORTS_RULE) {
const supportsRule = rule as CSSSupportsRule;
for (const innerRule of supportsRule.cssRules) {
if (innerRule && innerRule.type === CSSRule.STYLE_RULE) {
promises.push(
processStyleRule(sheet, innerRule as CSSStyleRule, options, false)
);
}
}
} else finalTokens.push(token);
}
}
return finalTokens.join("");
await Promise.allSettled(promises);
}
function getBaseUrl(href?: string | null) {
if (!href) return null;
if (href.startsWith("/")) href = `${document.location.origin}${href}`;
const url = new URL(href);
const basepath = url.pathname.split("/").slice(0, -1).join("/");
return new URL(`${url.origin}${basepath}/`);
}
function isPseudoSelector(text: string) {
return (
text.includes(":before") ||
text.includes(":after") ||
text.includes("::after") ||
text.includes("::before")
);
}
function parsePseudoSelector(selector: string) {
const output = [];
const selectors = parse(selector);
for (const part of selectors) {
const pseduoElementIndex = part.findIndex(
(s) =>
(s.type === SelectorType.Pseudo ||
s.type === SelectorType.PseudoElement) &&
(s.name === "after" || s.name === "before")
);
if (pseduoElementIndex <= -1) continue;
output.push({
selector: stringify([part.slice(0, pseduoElementIndex)]),
pseudoElement: stringify([part.slice(pseduoElementIndex)])
});
async function processStyleRule(
sheet: CSSStyleSheet,
rule: CSSStyleRule,
options?: FetchOptions,
inline = true
) {
const baseUrl = sheet.href || document.location.href;
for (const property of rule.style) {
const oldValue = rule.style.getPropertyValue(property);
if (!oldValue) continue;
const resolved = await inlineAll(oldValue, options, baseUrl, !inline);
rule.style.setProperty(property, resolved);
}
return output;
}

View File

@@ -48,7 +48,6 @@ export type Options = {
scale?: number;
fetchOptions?: FetchOptions;
inlineOptions?: InlineOptions;
styles?: boolean;
};
export type Config = {

View File

@@ -24,6 +24,85 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
const WOFF = "application/font-woff";
const JPEG = "image/jpeg";
const SVGElements = [
"altGlyph",
"altGlyphDef",
"altGlyphItem",
"animate",
"animateColor",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"color-profile",
"cursor",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"font-face",
"font-face-format",
"font-face-name",
"font-face-src",
"font-face-uri",
"foreignObject",
"g",
"glyph",
"glyphRef",
"hkern",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"missing-glyph",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"set",
"stop",
"svg",
"switch",
"symbol",
"text",
"textPath",
"title",
"tref",
"tspan",
"use",
"view",
"vkern"
].map((a) => a.toLowerCase());
const mimes = {
woff: WOFF,
woff2: WOFF,
@@ -165,12 +244,9 @@ function getRootStylesheet() {
return null;
}
function safeQuerySelectorAll(root: Node, selector: string) {
try {
return (root as HTMLElement).querySelectorAll(selector);
} catch (e) {
return new NodeList();
}
function isSVGElement(element: HTMLElement) {
if (!element || !element.tagName) return false;
return SVGElements.includes(element.tagName.toLowerCase());
}
export {
@@ -188,5 +264,5 @@ export {
escapeXhtml,
width,
height,
safeQuerySelectorAll
isSVGElement
};

View File

@@ -139,3 +139,18 @@ test("search reminders", () =>
const descriptionSearch = await db.lookup.reminders("please do").ids();
expect(descriptionSearch).toHaveLength(1);
}));
describe("notesWithHighlighting", () => {
test("search notes with parentheses in query should load the item", () =>
noteTest({
title: "(with parantheses)"
}).then(async ({ db }) => {
await db.notes.add(TEST_NOTE);
const filtered = await db.lookup.notesWithHighlighting(
"(with parantheses)",
db.notes.all
);
const item = await filtered.item(0);
expect(item.item).toBeDefined();
}));
});

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