1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

305 Commits

Author SHA1 Message Date
Laurent Cozic
3ff720463b Android 3.1.8 2024-11-09 13:04:50 +00:00
Laurent Cozic
d58126484a Desktop release v3.1.24 2024-11-09 12:56:27 +00:00
Henry Heino
360ece6f88 Desktop: Fix title rendering in GotoAnything search results (#11356) 2024-11-09 12:54:29 +00:00
Henry Heino
a08ebb9ce5 Android: Fixes #11324: Fix sharing to Joplin causes back navigation to get stuck (#11355) 2024-11-09 12:54:09 +00:00
Henry Heino
0876086caa Desktop: Fixes #11335: Markdown editor: Auto-close backticks (#11351) 2024-11-09 12:53:25 +00:00
Henry Heino
1fca7eddcb Mobile: Fixes #11325: Fix error on creating new notes if the user is a share recipient (#11326) 2024-11-08 15:34:39 +00:00
Henry Heino
9fb104ec7c Mobile: Fixes #11276: Fix new note button is pushed offscreen on certain Android devices (#11323) 2024-11-08 13:01:29 +00:00
Laurent Cozic
b3ea127848 Desktop release v3.1.23 2024-11-05 17:54:37 +00:00
Henry Heino
cbe55055e9 Desktop: Fixes #11313: Don't completely left-align the editor with the toolbar (#11316) 2024-11-05 17:53:35 +00:00
Laurent Cozic
da10297572 Desktop release v3.1.22 2024-11-04 20:42:34 +00:00
Laurent Cozic
9866c27156 Chore: Fixed macOS CI 2024-11-04 20:42:19 +00:00
Laurent Cozic
6875fd271c Android 3.1.7 2024-11-04 20:28:06 +00:00
Laurent Cozic
a5c14c8d10 Desktop release v3.1.21 2024-11-04 20:21:49 +00:00
Henry Heino
ddd18551eb Mobile: Fixes #11197: Fix search result note hidden after powering on device (#11297) 2024-10-31 08:18:13 +00:00
Henry Heino
612d72d765 Desktop: Fixes #11274: Fix content dropped into the Markdown editor is missing a cursor preview or dropped at the wrong location (#11289) 2024-10-30 21:09:59 +00:00
Henry Heino
2974465882 Desktop: Resolves #11279: Remove left/right edge margin around editor content when disabled in settings (#11290) 2024-10-30 21:09:37 +00:00
Henry Heino
f7f4a50d35 Desktop: Custom CSS: Add cm-listItem class to lines with list items, don't add region start/end markers for items that are always single-line (#11291) 2024-10-30 21:09:23 +00:00
Henry Heino
f1e5ab8255 Desktop: Re-enable media with local file URLs in the note viewer (#11244) 2024-10-26 21:08:51 +01:00
Laurent Cozic
81993628ab Desktop release v3.1.20 2024-10-22 11:51:50 +01:00
Laurent Cozic
0b3f6a268e iOS 13.1.6 2024-10-17 23:17:57 +01:00
Laurent Cozic
a2069df3e0 Android 3.1.6 2024-10-17 23:14:16 +01:00
Laurent Cozic
1ad150c1bf Desktop release v3.1.19 2024-10-17 23:05:46 +01:00
Henry Heino
41b251d67a Linux: Move keychain support behind an off-by-default feature flag (#11227) 2024-10-17 22:58:03 +01:00
Henry Heino
2c40cec639 Chore: Desktop: Fix incorrect log tag (#11215) 2024-10-17 22:49:50 +01:00
Henry Heino
efb58c5f40 Desktop: Fix error screen shown on opening settings when an incompatible plugin is installed (#11223) 2024-10-17 22:49:29 +01:00
Henry Heino
9d8cd1d707 Desktop: Security: Open more target="_blank" links in a browser (#11212) 2024-10-15 16:38:33 +01:00
Henry Heino
591c458a4f Desktop: Security: Improve KaTeX error handling (#11207) 2024-10-15 16:37:15 +01:00
Laurent Cozic
f9b1a32ae7 Tools: Update script to test plugins 2024-10-14 17:52:31 +01:00
Henry Heino
1a195e23dd Desktop: Security: Improve Markdown viewer link handling (#11201) 2024-10-14 17:51:28 +01:00
Joplin Bot
26ae3f853e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-12 00:47:59 +00:00
Laurent Cozic
e84e9a58e1 iOS 13.1.5 2024-10-11 23:29:52 +01:00
Laurent Cozic
3b8da5023d Tools: Always run pod install 2024-10-11 23:28:45 +01:00
Laurent Cozic
548d41d0d4 lock files 2024-10-11 23:28:29 +01:00
Laurent Cozic
d6c921249f iOS 13.1.4 2024-10-11 23:21:27 +01:00
Laurent Cozic
e044c50b03 Android 3.1.5 2024-10-11 23:16:57 +01:00
Laurent Cozic
beec74d792 Desktop release v3.1.18 2024-10-11 23:05:38 +01:00
pedr
8b4e163b28 Server: Fixes #10532: Fix PostgreSQL version check failing on Windows Server because wrong regex (#11038) 2024-10-11 22:26:01 +01:00
Henry Heino
b61467097d Mobile: Fixes #11134: Fix automatic resource download mode (#11144) 2024-10-11 22:14:18 +01:00
Matthew Moore
447e4638d1 add typecript information regarding turndown-plugin-gfm (#11153)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
2024-10-11 22:14:05 +01:00
ScriptInfra
b831525b20 Update faq.md (#11169)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-10-11 22:12:05 +01:00
Henry Heino
e05be832d5 Desktop, Mobile: Downgrade CodeMirror packages to fix various Android regressions (#11170) 2024-10-11 22:08:17 +01:00
Henry Heino
64c9c3179f Chore: Update KaTeX asset files (#11172) 2024-10-11 22:08:07 +01:00
Henry Heino
0ea61f26eb Desktop: Accessibility: Fix context menu button doesn't open the note list context menu (regression) (#11175) 2024-10-11 22:07:56 +01:00
Henry Heino
349fa426ea Mobile: Fixes #11183: Fix new note/edit buttons only work if pressed quickly (#11185) 2024-10-11 22:04:29 +01:00
Henry Heino
e3d5f0c9cf Chore: Desktop: Use SCSS instead of styled-components for plugin dialogs and toolbar buttons (#11189) 2024-10-11 22:03:41 +01:00
Joplin Bot
e63d545ed8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-11 18:20:26 +00:00
Laurent Cozic
ab3058612d Doc: Add sponsor 2024-10-11 19:00:43 +01:00
Laurent Cozic
715abcce32 Plugins: Add support for joplin.settings.values and deprecate joplin.settings.value 2024-10-11 18:56:04 +01:00
renovate[bot]
f165b3f870 Update dependency @types/serviceworker to v0.0.89 (#11191) 2024-10-11 03:01:01 +00:00
renovate[bot]
8895d745e7 Update dependency glob to v10.4.5 (#11192) 2024-10-11 03:01:00 +00:00
renovate[bot]
33a9b96a31 Update dependency pm2 to v5.4.2 (#11193) 2024-10-11 03:00:52 +00:00
github-actions[bot]
d1ac3d415e @moorage has signed the CLA in laurent22/joplin#11153 2024-10-05 22:34:48 +00:00
Henry Heino
432fac8fda Chore: Fix CI (#11173) 2024-10-05 12:22:44 -07:00
renovate[bot]
0f23882d47 Update dependency nodemon to v3.1.7 (#11162) 2024-10-01 02:49:36 +00:00
renovate[bot]
693c0f22c8 Update eslint (#11163) 2024-10-01 02:49:34 +00:00
renovate[bot]
e2db7a6b61 Update types (#11164) 2024-10-01 02:49:27 +00:00
renovate[bot]
2a74f60812 Update dependency katex to v0.16.11 (#11159) 2024-10-01 02:19:59 +00:00
renovate[bot]
2419291976 Update dependency nodemon to v3.1.4 (#11160) 2024-10-01 02:19:57 +00:00
renovate[bot]
733845eb95 Update eslint (#11161) 2024-10-01 02:19:50 +00:00
Joplin Bot
b3315aeb03 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-10-01 00:52:32 +00:00
renovate[bot]
d88c522d96 Update dependency @react-native/babel-preset to v0.74.85 (#11156) 2024-09-30 02:46:33 +00:00
renovate[bot]
c0cefc30f4 Update dependency @react-native/metro-config to v0.74.85 (#11157) 2024-09-30 02:17:39 +00:00
renovate[bot]
0dc3589661 Update dependency nodemon to v3.1.2 (#11140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 16:24:50 +01:00
Henry Heino
f64c3d5484 Desktop: Remove Math Mode from the list of plugins incompatible with the new editor (#11143) 2024-09-28 16:21:59 +01:00
Henry Heino
5fceb5a3c9 Chore: Reduce mobile note screen test flakiness (#11145) 2024-09-28 16:20:46 +01:00
renovate[bot]
916b3f6f69 Update dependency rate-limiter-flexible to v5.0.3 (#11148) 2024-09-28 11:15:33 +00:00
renovate[bot]
0c4e8eeafc Update dependency react-native-document-picker to v9.3.0 (#11141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 11:30:30 +01:00
renovate[bot]
b27e0ff1f4 Update dependency rate-limiter-flexible to v5 (#11147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-28 11:29:30 +01:00
Laurent Cozic
59ffb0f265 Update renovate.json5 2024-09-28 11:29:10 +01:00
renovate[bot]
20b4fd85c1 Update dependency react-native-safe-area-context to v4.10.7 (#11138) 2024-09-27 20:52:29 +00:00
renovate[bot]
fc2da05ba6 Update dependency stream to v0.0.3 (#11139) 2024-09-27 20:52:22 +00:00
Henry Heino
948ca605b0 Mobile,Desktop: Fixes #11135: Fix incorrect list switching behavior (#11137) 2024-09-27 21:28:56 +01:00
Henry Heino
eda2c69334 Desktop: Fixes #11129: Improve performance by allowing note list background timers to be cancelled (#11133) 2024-09-27 15:25:55 +01:00
Henry Heino
42ab9ecd95 Mobile: Fixes #11130: Fix regression: Search screen not hidden when cached for search result navigation (#11131) 2024-09-27 15:23:31 +01:00
Henry Heino
5935c9c147 Chore: Mobile: Improve note screen tests and fix CI warning (#11126) 2024-09-27 15:23:02 +01:00
Joplin Bot
90640e590e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-26 12:25:12 +00:00
Laurent Cozic
75b8caf816 Desktop, Mobile: Plugins: Name webview root attribute so that it can be styled 2024-09-26 11:40:13 +01:00
Laurent Cozic
3ea403d004 Desktop release v3.1.17 2024-09-26 11:36:01 +01:00
Laurent Cozic
058a559de4 Desktop: Enable again auto-updates 2024-09-26 11:35:42 +01:00
Laurent Cozic
ac43c62ce8 Chore: Disable custom protocol debug logging 2024-09-26 11:35:42 +01:00
Henry Heino
c4a7749f2a Desktop: Fixes #11105: Plugin API: Save changes made with editor.setText (#11117) 2024-09-26 11:35:32 +01:00
Joplin Bot
e6c09da639 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-24 18:20:50 +00:00
Laurent Cozic
2d05b5f43e Android 3.1.4 2024-09-24 15:23:16 +01:00
Laurent Cozic
63d0855a59 Desktop release v3.1.16 2024-09-24 15:15:43 +01:00
Henry Heino
3d42485315 Mobile: Show loading indicator while loading search results (#11104) 2024-09-24 15:12:02 +01:00
renovate[bot]
f772cc500c Update dependency jsdom to v24.1.0 (#11114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-23 20:16:50 +01:00
renovate[bot]
ad8bcacbca Update dependency glob to v10.4.2 (#11112) 2024-09-23 17:59:26 +00:00
renovate[bot]
fbab549a1c Update dependency pm2 to v5.4.1 (#11113) 2024-09-23 17:59:19 +00:00
pedr
817f3bc121 Cli: Fixes #10992: Disable deprecation warning when running Joplin from CLI (#11074) 2024-09-23 18:20:13 +01:00
pedr
e3576683b0 Desktop,Mobile,Cli: Fixes #10608: WebDAV synchronisation not working because of URL encoding differences (#11076) 2024-09-23 18:19:51 +01:00
renovate[bot]
85c2eb43dd Update dependency turndown to v7.2.0 (#11103)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:45:21 +01:00
renovate[bot]
0f2b2b1e7b Update dependency pm2 to v5.4.0 (#11102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:45:05 +01:00
renovate[bot]
8fd2eeaea5 Update dependency glob to v10.4.1 (#11101)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-22 01:44:56 +01:00
renovate[bot]
b97a14c559 Update dependency nodemailer to v6.9.14 (#11098) 2024-09-21 17:43:23 +00:00
renovate[bot]
bbb97bcb02 Update dependency react-native-webview to v13.10.4 (#11099) 2024-09-21 17:43:21 +00:00
renovate[bot]
8a51ed892a Update dependency sass to v1.77.6 (#11100) 2024-09-21 17:43:14 +00:00
Henry Heino
0cac69c2fa Chore: Migrate back-button.js to TypeScript (#11087) 2024-09-21 18:28:41 +01:00
Henry Heino
feb946acfb Chore: Mobile: Search screen: Use stronger types, try to prevent multiple concurrent attempts to update the result list (#11075) 2024-09-21 18:28:33 +01:00
Henry Heino
220f867814 Mobile: Resolves #10763: Support permanent note deletion on mobile (#10786) 2024-09-21 13:05:27 +01:00
Henry Heino
050a896c8b Android,Web: Fix scroll issues and incorrect main content height (#11071) 2024-09-21 13:05:17 +01:00
Henry Heino
d13e7b32c3 Desktop,Mobile,Cli: Fixes #11017: Delete revisions on the sync target when deleted locally (#11035) 2024-09-21 13:04:54 +01:00
Alice
a56f104fe8 Desktop: Seamless-Updates: triggering updates (#11079) 2024-09-21 13:02:22 +01:00
Henry Heino
99696637b9 Chore: Desktop: Add extra check to try to prevent duplicate setting key warning (#11084) 2024-09-21 13:01:08 +01:00
Henry Heino
be5a6c189a Desktop: New Markdown editor: Fix horizontal rule button when cursor is not on a new line (#11085) 2024-09-21 13:00:43 +01:00
Henry Heino
a01f519131 Mobile: Resolves #11082: Make pressing "back" navigate to the previous note after following a link (#11086) 2024-09-21 13:00:31 +01:00
Dave Goldberg
a71ee1d0b8 Doc: Add Backblaze to S3 Providers (#11088)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-09-21 12:59:38 +01:00
Henry Heino
a40bb77feb Mobile: Use fade animation for edit link dialog (#11090) 2024-09-21 12:58:20 +01:00
Henry Heino
5c23765458 Mobile: Scroll dropdown to selected value when first opened (#11091) 2024-09-21 12:58:01 +01:00
Henry Heino
d023ce592c Desktop: Fix ctrl/cmd-n can create new notes while the trash folder is selected (#11092) 2024-09-21 12:57:48 +01:00
Henry Heino
8c4bf057d6 Chore: Mobile: Improve Notes screen type safety (#11093) 2024-09-21 12:57:38 +01:00
Henry Heino
b9dc226031 Chore: Mobile: Migrate NoteItem and Checkbox to TypeScript (#11094) 2024-09-21 12:57:26 +01:00
Henry Heino
a81c1ff663 Chore: Mobile: Convert side-menu-content-note.js to a TypeScript function component (#11095) 2024-09-21 12:57:08 +01:00
Jonatan
c909d85acc Update Swedish translation (#11096) 2024-09-21 12:56:55 +01:00
Henry Heino
0965c6d257 Desktop,Mobile: Fixes #11065: Improve performance when there are many selected items (#11067) 2024-09-21 12:53:16 +01:00
Laurent Cozic
5beb80bf61 Desktop, Mobile: Automatically detect and use operating system theme by default 2024-09-20 19:31:35 +01:00
renovate[bot]
1b2f5e5cd8 Update dependency @types/node to v18.19.39 (#11089) 2024-09-20 17:52:37 +00:00
github-actions[bot]
2db82ac732 @dhgoldberg has signed the CLA in laurent22/joplin#11088 2024-09-20 14:29:03 +00:00
renovate[bot]
3f1ec682b9 Update dependency @types/node to v18.19.38 (#11080) 2024-09-19 05:27:44 +00:00
renovate[bot]
59b3030e45 Update dependency @types/node to v18.19.37 (#11078) 2024-09-18 11:52:31 +00:00
Joplin Bot
54d223a721 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-17 12:24:41 +00:00
Laurent Cozic
e5771a36bb Desktop release v3.1.15 2024-09-17 09:37:02 +01:00
Laurent Cozic
31a5ee20df Tools: Trying to fix autoupdate CI script 2024-09-17 09:35:39 +01:00
Laurent Cozic
efd9ada977 Desktop release v3.1.14 2024-09-17 07:37:06 +01:00
Alice
b4450ae4ef Desktop: Seamless-Updates: used axios for download function (#11068) 2024-09-17 07:36:53 +01:00
github-actions[bot]
73076bd4b7 @mrjo118 has signed the CLA in laurent22/joplin#11069 2024-09-17 00:13:07 +00:00
Laurent Cozic
0ba0550baf Desktop release v3.1.13 2024-09-16 22:24:40 +01:00
Laurent Cozic
41b03f9356 macOS: Fixed shortcut for permanent note deletion 2024-09-16 22:22:27 +01:00
et al.
95f1992b8a Update Simplified Chinese translation (#11061) 2024-09-16 22:22:08 +01:00
Henry Heino
11c1c0638d Desktop: Resolves #11063: Improve the performance of GoToAnything (#11064) 2024-09-16 22:20:44 +01:00
Henry Heino
e0daf807a6 Mobile: Fixes #11028: Accessibility: Fix sidebar broken in right-to-left mode, improve screen reader accessibility (#11056) 2024-09-16 22:17:12 +01:00
Alice
2594c1edb1 Desktop: Seamless-Updates: used download function from tool-utils (#11066) 2024-09-16 22:13:24 +01:00
github-actions[bot]
e80bede7b7 @howfool has signed the CLA in laurent22/joplin#11061 2024-09-16 13:37:59 +00:00
renovate[bot]
1eb721c717 Update dependency @types/node to v18.19.36 (#11057) 2024-09-16 03:03:06 +00:00
renovate[bot]
38b6484f12 Update dependency @types/node to v18.19.35 (#11055) 2024-09-15 19:42:31 +00:00
Laurent Cozic
a0163ba793 Desktop release v3.1.12 2024-09-15 20:17:57 +01:00
Alice
e2e589e907 Desktop: Seamless-Updates: added debugging logs for createReleaseAssets (#11053) 2024-09-15 20:17:44 +01:00
renovate[bot]
93f96c03b1 Update dependency react-native-safe-area-context to v4.10.5 (#11051) 2024-09-15 14:52:44 +00:00
Laurent Cozic
77f09a4408 Desktop release v3.1.11 2024-09-15 15:32:08 +01:00
Alice
faf30306da Desktop: Seamless-Updates: updated asset name locally after update call to GitHub API (#11050) 2024-09-15 15:31:41 +01:00
Laurent Cozic
c1c02204fa Desktop release v3.1.10 2024-09-15 11:02:44 +01:00
Alice
017480eb45 Desktop: Seamless-Updates: used url instead of browser url and added api heade… (#11049) 2024-09-15 11:02:02 +01:00
Laurent Cozic
8931a68ec8 Desktop release v3.1.9 2024-09-14 22:24:02 +01:00
Henry Heino
3c6a419cad Desktop: Accessibility: Add "Move to" context menu action for notebooks (#11039) 2024-09-14 22:18:07 +01:00
renovate[bot]
dce4c715e3 Update dependency markdown-it-sup to v2 (#11048)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 22:16:57 +01:00
Alice
5763de3b26 Desktop: Seamless-Updates: generated and uploaded latest-mac-arm64.yml to GitHub Releases (#11042) 2024-09-14 22:16:42 +01:00
renovate[bot]
4fa61e443f Update dependency markdown-it-ins to v4 (#11045)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 18:39:39 +01:00
renovate[bot]
84e312563a Update dependency react-native-device-info to v10.14.0 (#11044)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-14 18:38:33 +01:00
renovate[bot]
707c21a2fe Update dependency sass to v1.77.5 (#11043) 2024-09-14 15:22:02 +00:00
renovate[bot]
d0057ae838 Update dependency lint-staged to v15.2.7 (#11041) 2024-09-14 14:52:40 +00:00
pedr
8d3ac630c5 Desktop: Fixes #10560: Fix table column and rows not being resizable on RTE (#11037) 2024-09-13 18:07:52 +01:00
pedr
b5f06b6958 Desktop: Fixes #10561: Table options not visible on dark theme (#11036) 2024-09-13 18:07:03 +01:00
Henry Heino
5a07b795d3 Desktop, Mobile: Upgrade CodeMirror packages (#11034) 2024-09-13 13:24:37 +01:00
Alice
bfab4426ca Seamless-Updates: Auto Updater Service is architecture dependent (#11015) 2024-09-13 11:03:14 +01:00
Henry Heino
bcb5218e1a Desktop: Fix editor/viewer loses focus when visible panels are changed with ctrl-l (#11029) 2024-09-12 17:54:10 +01:00
Henry Heino
c897cc1582 Desktop: Update plugin compatibility layer to allow more legacy plugins (e.g. Markdown Prettier) to run (#11033) 2024-09-12 17:51:38 +01:00
Henry Heino
ea61bfc498 Mobile: Fixes #10253: Move accessibility focus to the first note action menu item on open (#11031) 2024-09-12 09:04:23 +01:00
Henry Heino
ca5d35339f Desktop: Accessibility: Improve text read by screen readers when focusing the note viewer (#11030) 2024-09-12 09:00:35 +01:00
Laurent Cozic
5c00ea93c2 Doc: Move next release 3.1 dates by 10 days 2024-09-11 20:59:05 +01:00
Henry Heino
f005977ce0 Desktop: Encryption screen: Fix "invalid password" border shown for some correct passwords (#11027) 2024-09-11 19:02:31 +01:00
Henry Heino
79773dab95 Mobile,Desktop: Fix unable to change incorrect decryption password if the same as the master password (#11026) 2024-09-11 19:01:57 +01:00
Henry Heino
69168f1ec2 Desktop: Fixes #11020: Fix clicking on most non-media resource links opens them inline (#11022) 2024-09-11 16:49:35 +01:00
Henry Heino
147a66d64e Desktop: Accessibility: Fix multi-note selection menu not tab-focusable (#11018) 2024-09-10 19:29:17 +01:00
Joplin Bot
ec36847de0 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-09 00:48:14 +00:00
renovate[bot]
d7bef7e923 Update dependency jsdom to v24 (#11007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 22:56:53 +01:00
Laurent Cozic
55faab25b5 Desktop release v3.1.8 2024-09-08 20:50:06 +01:00
Alice
4da8060e62 Desktop: Seamless-Updates - rename latest-mac.yml to latest-mac-arm64.yml (#10985) 2024-09-08 20:49:29 +01:00
Laurent Cozic
821cfc5bd8 Update renovate.json5 2024-09-08 16:27:44 +01:00
renovate[bot]
9956caea1b Update dependency react-native-share to v10.2.1 (#11003) 2024-09-08 12:09:18 +00:00
renovate[bot]
f95b663f28 Update dependency react-native-webview to v13.10.3 (#11004) 2024-09-08 12:09:17 +00:00
renovate[bot]
dd990e7cf6 Update dependency sass to v1.77.4 (#11005) 2024-09-08 12:09:09 +00:00
davidsmoot
23dec124dd Doc: Update nextcloud.md (#10987)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-09-08 12:49:39 +01:00
renovate[bot]
2b6cb908fa Update dependency react-native-webview to v13.10.0 (#11000)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 03:57:35 +01:00
renovate[bot]
40475d60fb Update dependency react-native-share to v10.2.0 (#10999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 03:57:19 +01:00
renovate[bot]
48e96a055f Update dependency sass to v1.77.1 (#11001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 02:50:44 +01:00
renovate[bot]
3dbc9a5723 Update dependency tesseract.js to v5.1.0 (#11002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-08 02:50:24 +01:00
renovate[bot]
9832af0d3a Update dependency react-native-localize to v3.1.0 (#10997)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 21:22:17 +01:00
renovate[bot]
26caf2a4c6 Update dependency @playwright/test to v1.44.1 (#10998) 2024-09-07 19:23:22 +00:00
renovate[bot]
29d7804ffd Update dependency react-native-document-picker to v9.2.0 (#10996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 20:16:35 +01:00
renovate[bot]
6fe0104483 Update dependency @playwright/test to v1.44.0 (#10995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-07 20:15:19 +01:00
Henry Heino
04f5433839 Mobile: Drawing: Fix clicking "cancel" after starting a new drawing in editing mode creates an empty resource (#10986) 2024-09-07 15:11:08 +01:00
Henry Heino
0bfa28d795 Mobile,Desktop,CLI: Fixes #10856: Decrypt master keys only as needed (#10990) 2024-09-07 11:56:13 +01:00
Laurent Cozic
ac2258769a Desktop: Fixes #10865: Removed unneeded electron-log dependency 2024-09-06 18:06:04 +01:00
github-actions[bot]
7cd0ed1714 @davidsmoot has signed the CLA in laurent22/joplin#10987 2024-09-05 11:49:13 +00:00
Laurent Cozic
803d508c69 Desktop release v3.1.7 2024-09-04 12:14:55 +01:00
Henry Heino
3c13568107 Chore: Mobile tests: Fix CodeMirror-related warnings (#10978) 2024-09-04 12:14:23 +01:00
Henry Heino
e41394b57f Chore: Tests: Improve Playwright test reliability (#10981) 2024-09-04 12:14:12 +01:00
Henry Heino
0b13dbddd8 Chore: Mobile tests: Fix warning: "A worker process ... has been force exited" (#10980) 2024-09-04 12:14:05 +01:00
Henry Heino
2a2dd96c02 Chore: Mobile tests: Hide debug logging (#10977) 2024-09-04 12:13:48 +01:00
renovate[bot]
2f7b2fb948 Update dependency css-loader to v6.11.0 (#10976)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-04 12:13:42 +01:00
Alice
4e8316a6ee Desktop: Seamless-Updates - generated latest-mac-arm64.yml (#10982) 2024-09-04 12:11:17 +01:00
renovate[bot]
01412b4500 Update dependency @rollup/plugin-replace to v5.0.7 (#10983) 2024-09-04 02:35:06 +00:00
renovate[bot]
2df8137281 Update dependency js-draw to v1.20.3 (#10973) 2024-09-03 03:43:16 +00:00
renovate[bot]
f24e229a4e Update dependency pg to v8.11.6 (#10974) 2024-09-03 03:43:14 +00:00
renovate[bot]
fa6060d6d2 Update dependency react-native-safe-area-context to v4.10.4 (#10975) 2024-09-03 03:43:07 +00:00
renovate[bot]
2d6796db16 Update dependency @react-native/babel-preset to v0.74.84 (#10970) 2024-09-03 03:07:48 +00:00
renovate[bot]
82be4f566a Update dependency @js-draw/material-icons to v1.20.3 (#10969) 2024-09-03 03:00:56 +00:00
renovate[bot]
f353686166 Update dependency @react-native/metro-config to v0.74.84 (#10971) 2024-09-03 03:00:20 +00:00
renovate[bot]
a6dbe4b67a Update dependency @rollup/plugin-replace to v5.0.6 (#10972) 2024-09-03 03:00:13 +00:00
Joplin Bot
b597d5f9d1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-02 18:20:26 +00:00
Laurent Cozic
f0496bd348 Android 3.1.3 2024-09-02 13:18:23 +01:00
Laurent Cozic
a746c02d4a Desktop release v3.1.6 2024-09-02 13:05:22 +01:00
Laurent Cozic
02def5900e lock file 2024-09-02 13:04:32 +01:00
Henry Heino
fd06c18cf0 Desktop: Windows portable: Fix keychain-backed storage incorrectly enabled (#10942) 2024-09-02 12:26:43 +01:00
Alice
6163364b26 Desktop: Seamless-Updates - added tests for autoUpdaterService (#10935)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-09-02 12:24:19 +01:00
renovate[bot]
7e9c7d7d23 Update dependency @types/node to v18.19.34 (#10966) 2024-09-02 02:37:00 +00:00
renovate[bot]
88d02b04dd Update dependency ldapts to v7.0.12 (#10951) 2024-09-01 20:33:07 +00:00
renovate[bot]
42e39b7789 Update dependency re-resizable to v6.9.17 (#10953) 2024-09-01 20:33:05 +00:00
renovate[bot]
9b3ce555cb Update dependency react-native-webview to v13.8.7 (#10956) 2024-09-01 20:33:04 +00:00
renovate[bot]
bae311b2a9 Update dependency sharp to v0.33.4 (#10958) 2024-09-01 20:32:56 +00:00
Laurent Cozic
f8a09e5540 Update renovate.json5 2024-09-01 21:14:35 +01:00
renovate[bot]
461ba1762a Update dependency sass to v1.76.0 (#10962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:11:34 +01:00
renovate[bot]
82a4b8915b Update dependency async-mutex to v0.5.0 (#10961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:11:21 +01:00
renovate[bot]
e8593e7ae5 Update dependency @react-native-clipboard/clipboard to v1.14.1 (#10960)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:11:13 +01:00
renovate[bot]
8f1c7bf61c Update dependency @playwright/test to v1.43.1 (#10959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:11:04 +01:00
renovate[bot]
23c08f77e0 Update dependency react-native-safe-area-context to v4.10.3 (#10955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:10:28 +01:00
renovate[bot]
e1fa7d0e83 Update dependency react-native-device-info to v10.13.2 (#10954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:10:15 +01:00
renovate[bot]
fc3c59bad5 Update dependency lint-staged to v15.2.5 (#10952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:10:01 +01:00
renovate[bot]
8588e36373 Update dependency koa to v2.15.3 (#10950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:09:44 +01:00
renovate[bot]
1f43b199d5 Update dependency glob to v10.3.16 (#10949)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-01 21:09:37 +01:00
Joplin Bot
e2fd48eecd Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-09-01 00:53:18 +00:00
renovate[bot]
d0e6368867 Update dependency @types/react to v18.3.3 (#10964) 2024-08-31 18:04:01 +00:00
renovate[bot]
4f98aad24f Update dependency dayjs to v1.11.11 (#10948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-31 17:59:17 +00:00
renovate[bot]
fae313785d Update dependency react-native-zip-archive to v6.1.2 (#10957) 2024-08-31 15:29:19 +00:00
renovate[bot]
db5bbeecd4 Update react monorepo (#10963) 2024-08-31 15:28:44 +00:00
Henry Heino
d2b7d64f4f Desktop: Accessibility: Improve note list keyboard and screen reader accessibility (#10940) 2024-08-31 16:05:01 +01:00
Henry Heino
4f2d0c8e5d Desktop: Add left/right arrow keys as expand/collapse shortcuts for notebooks (#10944) 2024-08-31 15:44:12 +01:00
Alice
3bca12fd4a Desktop: Seamless-Updates: added latest-mac-arm64.yml (#10941) 2024-08-31 15:42:07 +01:00
Henry Heino
4ccc46e4db Docs: Fix typo: Change "Thee" to "The" (#10945) 2024-08-31 15:40:28 +01:00
renovate[bot]
5062cd1616 Update dependency @bam.tech/react-native-image-resizer to v3.0.10 (#10938) 2024-08-28 20:52:24 +00:00
renovate[bot]
1710155a6f Update dependency @types/jsdom to v21.1.7 (#10939) 2024-08-28 20:52:17 +00:00
Joplin Bot
4cb693b4d6 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-28 00:45:16 +00:00
Joplin Bot
e607a7376f Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-27 18:20:33 +00:00
Laurent Cozic
d1403ba2cb Desktop release v3.1.4 2024-08-27 18:06:16 +01:00
Henry Heino
74be949d33 Desktop: Accessibility: Improve note title focus handling (#10932) 2024-08-27 18:05:48 +01:00
Alice
2afc2ca369 Desktop: Seamless-Updates: implemented flow for prereleases (#10892) 2024-08-27 18:04:18 +01:00
Laurent Cozic
b617a84696 All: Resolves #10407: Added feature flag to disable sync lock support (#10925) 2024-08-23 12:16:19 +01:00
Henry Heino
c691fedc12 Mac: Fixes #10921: Markdown editor: Fix custom arrow-key shortcuts fail to register (#10922) 2024-08-23 11:49:30 +01:00
Henry Heino
12a26023dd Chore: CI: Fix .eslintignore and .gitignore CI check passes when it should fail (#10923) 2024-08-23 11:49:17 +01:00
Laurent Cozic
700ffa2529 Desktop, Mobile, Cli: Make feature flags advanced settings by default 2024-08-23 11:39:40 +01:00
Laurent Cozic
5fdd088504 Desktop, Mobile: Fixed italic support in Fountain documents 2024-08-23 10:05:50 +01:00
Laurent Cozic
fe89b1cf21 Merge branch 'release-3.0' into dev 2024-08-23 09:52:04 +01:00
Henry Heino
3d1bbf3c4d Chore: Web: Fix build fails on Windows (#10919) 2024-08-22 21:53:52 +01:00
Henry Heino
580d2914e7 Mobile,Desktop: Fixes #10914: Fix BMP image rendering in the Markdown viewer (#10915) 2024-08-22 21:53:44 +01:00
Henry Heino
33599324d6 Desktop: Fixes #10737: Fix Fix editing notes in "Conflicts" causes them to temporarily vanish (#10913) 2024-08-22 21:53:39 +01:00
Henry Heino
02bdb7a79c Docs: Include dependency overview (#10911)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-08-22 21:53:27 +01:00
Henry Heino
a1c64eeefc Web: Fix renderer plugin assets fail to load on secondary profiles (#10903) 2024-08-22 21:52:42 +01:00
Henry Heino
78015028ec Desktop, Mobile: Fixes #10891: Markdown editor: Fix toggling bulleted lists when items start with asterisks (#10902) 2024-08-22 21:52:36 +01:00
Henry Heino
beff45e195 Desktop: Fix viewer and panel plugins that check for the presence of exports fail to load (#10900) 2024-08-22 21:52:29 +01:00
Henry Heino
6a0dd4e20d Desktop: Fixes #10895: Fix alt+up/alt+down fails to re-order multiple lines (#10899) 2024-08-22 21:52:22 +01:00
Henry Heino
ea420967c4 Chore: Remove recently-added .gitignored files (#10898) 2024-08-22 21:52:17 +01:00
Henry Heino
c19d0773b3 Mobile,Desktop,Cli: Fixes #10896: Fix "Enable auto-updates" enabled by default and visible on unsupported platforms (#10897) 2024-08-22 21:51:57 +01:00
Henry Heino
59a8c2115c Chore: Fix CI: Add Famegear to cspell dictionary (#10912) 2024-08-21 20:57:11 +01:00
Joplin Bot
88cae93fc4 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-21 18:20:16 +00:00
Laurent Cozic
4eb0fb542e Doc: Update sponsors 2024-08-21 18:26:58 +01:00
Henry Heino
60a1617b22 Chore: Fix compilation errors due to test resources with .ts extensions (#10909) 2024-08-21 18:24:33 +01:00
Henry Heino
e05d7197bd Docs: Improve explanation for using custom OCR language data (#10910) 2024-08-21 18:24:05 +01:00
Joplin Bot
9dc29d034b Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-21 12:23:24 +00:00
renovate[bot]
32fe122e60 Update dependency @fortawesome/react-fontawesome to v0.2.2 (#10905) 2024-08-21 08:45:31 +00:00
renovate[bot]
8fcb377162 Update dependency @rollup/plugin-commonjs to v25.0.8 (#10906) 2024-08-21 08:45:24 +00:00
Laurent Cozic
598677b207 Desktop release v3.0.15 2024-08-21 09:36:44 +01:00
renovate[bot]
b3437fa4cd Update dependency @js-draw/material-icons to v1.20.2 (#10894) 2024-08-19 20:52:32 +00:00
Joplin Bot
0d82584777 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-18 12:23:44 +00:00
Laurent Cozic
e627fb7fad Doc: Update sponsors 2024-08-18 10:41:09 +01:00
Joplin Bot
b60acf038b Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-17 18:18:50 +00:00
renovate[bot]
111820c0cf Update dependency @js-draw/material-icons to v1.20.1 (#10890) 2024-08-17 11:38:01 +00:00
Laurent Cozic
263a7ef076 Desktop release v3.1.3 2024-08-17 12:23:14 +01:00
Henry Heino
255d3e737d Chore: Windows: Fix building the desktop app clears the list of supported Rich Text Editor locales (#10885) 2024-08-17 12:22:52 +01:00
Henry Heino
9cb13af7b0 Chore: Windows: Fix mobile plugin assets change when rebuilding the mobile app (#10886) 2024-08-17 12:22:35 +01:00
Henry Heino
7bc81880e1 Windows: Fix handleCustomProtocols test (#10884) 2024-08-17 12:22:13 +01:00
Henry Heino
eb53c7e3b9 Windows: Fix PDF, video, and audio rendering (#10881) 2024-08-17 12:22:03 +01:00
Henry Heino
b94cf5a107 Desktop: Resolves #5762: Rich Text Editor: Add eight spaces when pressing tab (#10880) 2024-08-17 12:21:43 +01:00
Alice
72163018b4 Desktop: Seamless-Updates - added flow for automatic updates for releases (#10857)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-08-17 12:19:05 +01:00
github-actions[bot]
0cbf8d8ebc @maresmar has signed the CLA in laurent22/joplin#10882 2024-08-16 18:34:22 +00:00
Joplin Bot
bde14f9ddc Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-16 12:23:54 +00:00
Henry Heino
b220413a9b Desktop: Security: Fix HTML parsing bug (#10876) 2024-08-15 19:37:58 +01:00
Henry Heino
72472623cc Merge remote-tracking branch 'origin/release-3.0' into dev 2024-08-15 08:34:56 -07:00
Laurent Cozic
7e599c3a3e Desktop release v3.1.2 2024-08-15 16:09:57 +01:00
Henry Heino
74ba386ae3 Docs: Mobile plugins: Document using the web app to run mobile plugins (#10831) 2024-08-15 16:09:11 +01:00
Henry Heino
06f05d3823 Docs: Add example transformers.js-based plugin (#10840) 2024-08-15 16:08:57 +01:00
Henry Heino
6ada0a0103 Docs: Explain purpose of main Cloud Teams account (#10841)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-08-15 16:08:43 +01:00
Henry Heino
39bd7d3aef Mobile: Fixes #10867: Fix toolbar overflow menu is invisible (#10871) 2024-08-15 16:02:05 +01:00
Henry Heino
b5313067cd Desktop: Allow searching when only the note viewer is visible and sync search with editor (#10866) 2024-08-15 16:01:52 +01:00
Henry Heino
1edef99811 Desktop: Upgrade electron to v29.4.5 (#10847)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-08-15 16:00:46 +01:00
Alice
6518c17679 Desktop: Seamless-Updates: generate latest-mac.yml file (#10869) 2024-08-15 15:59:18 +01:00
Laurent Cozic
3ef07ac39a Clipper release v3.1.1 2024-08-15 10:51:18 +01:00
Laurent Cozic
bfcf3815a4 Chore: Fixing clipper dependencies 2024-08-15 10:50:14 +01:00
renovate[bot]
292479ac28 Update dependency @fortawesome/react-fontawesome to v0.2.1 (#10875) 2024-08-15 05:35:10 +00:00
Laurent Cozic
786e58ed54 Chore: Remove jcenter() dependency, now discontinued (#10868) 2024-08-13 14:40:45 +01:00
Naoki Kawamukai
b88630855f All: Translation: Update ja_JP.po (#10864) 2024-08-12 16:40:07 -04:00
cedecode
b956a21dae All: Translation: Update de_DE.po (#10863) 2024-08-12 16:39:11 -04:00
Henry Heino
c4ebb64fb2 Chore: Mobile,Testing: Fix keychain warning (#10861) 2024-08-12 15:34:36 +01:00
renovate[bot]
fb181cf935 Update dependency @react-native-community/datetimepicker to v8.0.1 (#10862) 2024-08-12 02:56:05 +00:00
Joplin Bot
f6fdc9fe66 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-10 18:18:17 +00:00
Laurent Cozic
23f521de74 iOS 13.1.3 2024-08-10 14:08:45 +01:00
Joplin Bot
88ed7c934f Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-10 12:21:37 +00:00
renovate[bot]
4c7a1f0f88 Update dependency @react-native-community/netinfo to v11.3.2 (#10855) 2024-08-10 12:05:01 +00:00
Laurent Cozic
98b7354373 iOS 13.1.2 2024-08-10 12:51:29 +01:00
Laurent Cozic
8d8cca06eb Android 3.1.2 2024-08-10 12:50:34 +01:00
Henry Heino
45b61e1f49 iOS: Fix crash on opening the sidebar (#10852) 2024-08-09 20:49:10 +01:00
Henry Heino
f58d29a6a6 Mobile: Fixes #10848: Fix WebDAV sync on mobile (#10849) 2024-08-09 20:49:01 +01:00
Joplin Bot
4caca4934e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-09 12:23:13 +00:00
Laurent Cozic
a8613d1444 lock file 2024-08-09 12:16:58 +01:00
Laurent Cozic
c6377eafbf iOS 13.1.1 2024-08-09 12:15:35 +01:00
pedr
a6cf0a3a81 Desktop: Fixes #10740: Improve the reliability of fetching resources (#10826) 2024-08-05 19:36:54 +01:00
Henry Heino
60e347a782 Android: Fix crash on opening settings on old versions of Android (#10793) 2024-07-29 22:18:16 +01:00
373 changed files with 92240 additions and 11285 deletions

View File

@@ -209,7 +209,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
@@ -228,7 +227,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
packages/app-desktop/gui/MainScreen/commands/print.js
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
packages/app-desktop/gui/MainScreen/commands/renameTag.js
@@ -287,6 +285,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
@@ -297,8 +296,10 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
@@ -340,8 +341,10 @@ packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
packages/app-desktop/gui/NoteList/utils/useItemCss.js
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
@@ -362,6 +365,7 @@ packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteElementIdFromJoplinId.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
@@ -370,6 +374,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@@ -437,7 +442,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@@ -456,6 +460,7 @@ packages/app-desktop/gui/style/StyledLink.js
packages/app-desktop/gui/style/StyledMessage.js
packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/announceForAccessibility.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/loadScript.js
@@ -466,15 +471,18 @@ packages/app-desktop/integration-tests/markdownEditor.spec.js
packages/app-desktop/integration-tests/models/GoToAnything.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
packages/app-desktop/integration-tests/simpleBackup.spec.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/extendedExpect.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
@@ -483,6 +491,7 @@ packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -508,8 +517,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
@@ -536,6 +547,7 @@ packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
@@ -599,10 +611,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
@@ -610,6 +624,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -698,13 +713,14 @@ packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
@@ -714,6 +730,7 @@ packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
@@ -748,6 +765,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -769,6 +787,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -796,8 +815,6 @@ packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/editorCommands/swapLine.test.js
packages/editor/CodeMirror/editorCommands/swapLine.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
@@ -841,9 +858,12 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
@@ -873,6 +893,7 @@ packages/lib/ArrayUtils.js
packages/lib/AsyncActionQueue.test.js
packages/lib/AsyncActionQueue.js
packages/lib/BaseApplication.js
packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/ClipperServer.js
@@ -902,11 +923,14 @@ packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/config/config-shared.js
@@ -932,6 +956,7 @@ packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
packages/lib/file-api-driver-webdav.test.js
packages/lib/file-api-driver.test.js
packages/lib/file-api.test.js
packages/lib/file-api.js
@@ -945,6 +970,8 @@ packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/hooks/useQueuedAsyncEffect.test.js
packages/lib/hooks/useQueuedAsyncEffect.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
packages/lib/htmlUtils2.test.js
@@ -1425,7 +1452,23 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenseChecker.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
packages/tools/licenses/licenseOverrides/fontIsoOverride/index.js
packages/tools/licenses/licenseOverrides/index.js
packages/tools/licenses/licenseOverrides/materialCommunityIconsOverride/index.js
packages/tools/licenses/licenseOverrides/materialIconsOverride/index.js
packages/tools/licenses/licenseOverrides/types.js
packages/tools/licenses/licenseStatementBuilder.js
packages/tools/licenses/licenseText/agplv3.js
packages/tools/licenses/licenseText/apache2.js
packages/tools/licenses/licenseText/mit.js
packages/tools/licenses/utils/equalIgnoringSpacing.js
packages/tools/licenses/utils/matchApache2.test.js
packages/tools/licenses/utils/matchApache2.js
packages/tools/licenses/utils/matchMit.test.js
packages/tools/licenses/utils/matchMit.js
packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js

View File

@@ -180,6 +180,10 @@ fi
if [ "$IS_LINUX" == "1" ]; then
echo "Step: Checking for files that should have been ignored..."
# .gitignore and .eslintignore can be modified during yarn install. Reset them
# so that checkIgnoredFiles works.
git restore .gitignore .eslintignore
node packages/tools/checkIgnoredFiles.js
testResult=$?
if [ $testResult -ne 0 ]; then

View File

@@ -44,6 +44,14 @@ jobs:
with:
python-version: '3.11'
- name: Set Publish Flag
run: |
if [[ $GIT_TAG_NAME = v* ]]; then
echo "PUBLISH_ENABLED=true" >> $GITHUB_ENV
else
echo "PUBLISH_ENABLED=false" >> $GITHUB_ENV
fi
- name: Build macOS M1 app
env:
APPLE_ASC_PROVIDER: ${{ secrets.APPLE_ASC_PROVIDER }}
@@ -56,20 +64,23 @@ jobs:
GH_REPO: ${{ github.repository }}
IS_CONTINUOUS_INTEGRATION: 1
BUILD_SEQUENCIAL: 1
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
run: |
export npm_config_arch=arm64
export npm_config_target_arch=arm64
yarn install
cd packages/app-desktop
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
npm pkg set 'build.mac.target.target'='dmg'
npm pkg set 'build.mac.target.arch[0]'='arm64'
npm pkg set 'build.mac.target[0].target'='dmg'
npm pkg set 'build.mac.target[0].arch[0]'='arm64'
npm pkg set 'build.mac.target[1].target'='zip'
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
if [[ $GIT_TAG_NAME = v* ]]; then
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
echo "Building and publishing desktop application..."
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
yarn renameReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
else
echo "Building but *not* publishing desktop application..."
@@ -81,4 +92,4 @@ jobs:
npm pkg set 'build.mac.identity'=null --json
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
fi
fi

View File

@@ -23,7 +23,7 @@ jobs:
matrix:
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
# https://github.com/actions/runner-images/issues/6709
os: [macos-12, ubuntu-20.04, windows-2019]
os: [macos-13, ubuntu-20.04, windows-2019]
steps:
# Trying to fix random networking issues on Windows

59
.gitignore vendored
View File

@@ -186,7 +186,6 @@ packages/app-desktop/gui/MainScreen/MainScreen.js
packages/app-desktop/gui/MainScreen/commands/addProfile.js
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
packages/app-desktop/gui/MainScreen/commands/deleteFolder.js
packages/app-desktop/gui/MainScreen/commands/deleteNote.js
packages/app-desktop/gui/MainScreen/commands/duplicateNote.js
packages/app-desktop/gui/MainScreen/commands/editAlarm.js
packages/app-desktop/gui/MainScreen/commands/exportPdf.js
@@ -205,7 +204,6 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js
packages/app-desktop/gui/MainScreen/commands/openNote.js
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
packages/app-desktop/gui/MainScreen/commands/openTag.js
packages/app-desktop/gui/MainScreen/commands/permanentlyDeleteNote.js
packages/app-desktop/gui/MainScreen/commands/print.js
packages/app-desktop/gui/MainScreen/commands/renameFolder.js
packages/app-desktop/gui/MainScreen/commands/renameTag.js
@@ -264,6 +262,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
@@ -274,8 +273,10 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js
@@ -317,8 +318,10 @@ packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
packages/app-desktop/gui/NoteList/commands/index.js
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
packages/app-desktop/gui/NoteList/utils/types.js
packages/app-desktop/gui/NoteList/utils/useActiveDescendantId.js
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
packages/app-desktop/gui/NoteList/utils/useFocusVisible.js
packages/app-desktop/gui/NoteList/utils/useItemCss.js
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
@@ -339,6 +342,7 @@ packages/app-desktop/gui/NoteListHeader/utils/useContextMenu.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.test.js
packages/app-desktop/gui/NoteListHeader/utils/validateColumns.js
packages/app-desktop/gui/NoteListItem/NoteListItem.js
packages/app-desktop/gui/NoteListItem/utils/getNoteElementIdFromJoplinId.js
packages/app-desktop/gui/NoteListItem/utils/getNoteTitleHtml.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.test.js
packages/app-desktop/gui/NoteListItem/utils/prepareViewProps.js
@@ -347,6 +351,7 @@ packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
packages/app-desktop/gui/NoteListItem/utils/useRenderedNote.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.test.js
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
packages/app-desktop/gui/NotePropertiesDialog.js
@@ -414,7 +419,6 @@ packages/app-desktop/gui/ToggleEditorsButton/ToggleEditorsButton.js
packages/app-desktop/gui/ToggleEditorsButton/styles/index.js
packages/app-desktop/gui/ToolbarBase.js
packages/app-desktop/gui/ToolbarButton/ToolbarButton.js
packages/app-desktop/gui/ToolbarButton/styles/index.js
packages/app-desktop/gui/ToolbarSpace.js
packages/app-desktop/gui/TrashNotification/TrashNotification.js
packages/app-desktop/gui/UpdateNotification/UpdateNotification.js
@@ -433,6 +437,7 @@ packages/app-desktop/gui/style/StyledLink.js
packages/app-desktop/gui/style/StyledMessage.js
packages/app-desktop/gui/style/StyledTextInput.js
packages/app-desktop/gui/utils/NoteListUtils.js
packages/app-desktop/gui/utils/announceForAccessibility.js
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/loadScript.js
@@ -443,15 +448,18 @@ packages/app-desktop/integration-tests/markdownEditor.spec.js
packages/app-desktop/integration-tests/models/GoToAnything.js
packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/NoteList.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.spec.js
packages/app-desktop/integration-tests/pluginApi.spec.js
packages/app-desktop/integration-tests/richTextEditor.spec.js
packages/app-desktop/integration-tests/settings.spec.js
packages/app-desktop/integration-tests/sidebar.spec.js
packages/app-desktop/integration-tests/simpleBackup.spec.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/extendedExpect.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
@@ -460,6 +468,7 @@ packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/integration-tests/util/waitForNextOpenPath.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.test.js
packages/app-desktop/services/autoUpdater/AutoUpdaterService.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
@@ -485,8 +494,10 @@ packages/app-desktop/services/sortOrder/notesSortOrderUtils.test.js
packages/app-desktop/services/sortOrder/notesSortOrderUtils.js
packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
packages/app-desktop/tools/copy7Zip.js
packages/app-desktop/tools/generateLatestArm64Yml.js
packages/app-desktop/tools/githubReleasesUtils.js
packages/app-desktop/tools/modifyReleaseAssets.js
packages/app-desktop/tools/notarizeMacApp.js
packages/app-desktop/tools/renameReleaseAssets.js
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
@@ -513,6 +524,7 @@ packages/app-mobile/commands/util/showResource.js
packages/app-mobile/components/BackButtonDialogBox.js
packages/app-mobile/components/BetaChip.js
packages/app-mobile/components/CameraView.js
packages/app-mobile/components/Checkbox.js
packages/app-mobile/components/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
@@ -576,10 +588,12 @@ packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js
packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js
packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js
packages/app-mobile/components/NoteEditor/types.js
packages/app-mobile/components/NoteItem.js
packages/app-mobile/components/NoteList.js
packages/app-mobile/components/ProfileSwitcher/ProfileEditor.js
packages/app-mobile/components/ProfileSwitcher/ProfileSwitcher.js
packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/Menu.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
@@ -587,6 +601,7 @@ packages/app-mobile/components/ScreenHeader/WebBetaButton.js
packages/app-mobile/components/ScreenHeader/index.js
packages/app-mobile/components/SelectDateTimeDialog.js
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenuContentNote.js
packages/app-mobile/components/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
@@ -675,13 +690,14 @@ packages/app-mobile/components/screens/Note.test.js
packages/app-mobile/components/screens/Note.js
packages/app-mobile/components/screens/NoteTagsDialog.js
packages/app-mobile/components/screens/Notes.js
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
packages/app-mobile/components/screens/SearchScreen/index.js
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
packages/app-mobile/components/screens/ShareManager/IncomingShareItem.js
packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/search.js
packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
@@ -691,6 +707,7 @@ packages/app-mobile/root.js
packages/app-mobile/services/AlarmServiceDriver.android.js
packages/app-mobile/services/AlarmServiceDriver.ios.js
packages/app-mobile/services/AlarmServiceDriver.web.js
packages/app-mobile/services/BackButtonService.js
packages/app-mobile/services/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
@@ -725,6 +742,7 @@ packages/app-mobile/utils/fs-driver/testUtil/createFilesFromPathRecord.js
packages/app-mobile/utils/fs-driver/testUtil/verifyDirectoryMatches.js
packages/app-mobile/utils/getPackageInfo.js
packages/app-mobile/utils/getVersionInfoText.js
packages/app-mobile/utils/hooks/useReduceMotionEnabled.js
packages/app-mobile/utils/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
@@ -746,6 +764,7 @@ packages/app-mobile/utils/shim-init-react/injectedJs.js
packages/app-mobile/utils/shim-init-react/shimInitShared.js
packages/app-mobile/utils/testing/createMockReduxStore.js
packages/app-mobile/utils/testing/getWebViewDomById.js
packages/app-mobile/utils/testing/getWebViewWindowById.js
packages/app-mobile/utils/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
@@ -773,8 +792,6 @@ packages/editor/CodeMirror/editorCommands/insertLineAfter.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/editorCommands/swapLine.test.js
packages/editor/CodeMirror/editorCommands/swapLine.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
@@ -818,9 +835,12 @@ packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.js
packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.js
packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.js
packages/editor/CodeMirror/utils/formatting/types.js
packages/editor/CodeMirror/utils/getSearchState.js
packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
packages/editor/events.js
@@ -850,6 +870,7 @@ packages/lib/ArrayUtils.js
packages/lib/AsyncActionQueue.test.js
packages/lib/AsyncActionQueue.js
packages/lib/BaseApplication.js
packages/lib/BaseModel.test.js
packages/lib/BaseModel.js
packages/lib/BaseSyncTarget.js
packages/lib/ClipperServer.js
@@ -879,11 +900,14 @@ packages/lib/array.js
packages/lib/callbackUrlUtils.test.js
packages/lib/callbackUrlUtils.js
packages/lib/clipperUtils.js
packages/lib/commands/deleteNote.js
packages/lib/commands/historyBackward.js
packages/lib/commands/historyForward.js
packages/lib/commands/index.js
packages/lib/commands/openMasterPasswordDialog.js
packages/lib/commands/permanentlyDeleteNote.js
packages/lib/commands/synchronize.js
packages/lib/components/EncryptionConfigScreen/utils.test.js
packages/lib/components/EncryptionConfigScreen/utils.js
packages/lib/components/shared/NoteList/getEmptyFolderMessage.js
packages/lib/components/shared/config/config-shared.js
@@ -909,6 +933,7 @@ packages/lib/eventManager.js
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-local.js
packages/lib/file-api-driver-memory.js
packages/lib/file-api-driver-webdav.test.js
packages/lib/file-api-driver.test.js
packages/lib/file-api.test.js
packages/lib/file-api.js
@@ -922,6 +947,8 @@ packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/hooks/useQueuedAsyncEffect.test.js
packages/lib/hooks/useQueuedAsyncEffect.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
packages/lib/htmlUtils2.test.js
@@ -1402,7 +1429,23 @@ packages/tools/generate-database-types.js
packages/tools/generate-images.js
packages/tools/git-changelog.test.js
packages/tools/git-changelog.js
packages/tools/licenseChecker.js
packages/tools/licenses/getLicenses.js
packages/tools/licenses/licenseChecker.js
packages/tools/licenses/licenseOverrides/fontAwesomeOverride/index.js
packages/tools/licenses/licenseOverrides/fontIsoOverride/index.js
packages/tools/licenses/licenseOverrides/index.js
packages/tools/licenses/licenseOverrides/materialCommunityIconsOverride/index.js
packages/tools/licenses/licenseOverrides/materialIconsOverride/index.js
packages/tools/licenses/licenseOverrides/types.js
packages/tools/licenses/licenseStatementBuilder.js
packages/tools/licenses/licenseText/agplv3.js
packages/tools/licenses/licenseText/apache2.js
packages/tools/licenses/licenseText/mit.js
packages/tools/licenses/utils/equalIgnoringSpacing.js
packages/tools/licenses/utils/matchApache2.test.js
packages/tools/licenses/utils/matchApache2.js
packages/tools/licenses/utils/matchMit.test.js
packages/tools/licenses/utils/matchMit.js
packages/tools/packageJsonLint.js
packages/tools/postPreReleasesToForum.js
packages/tools/release-android.js

File diff suppressed because one or more lines are too long

875
.yarn/releases/yarn-3.8.3.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.6.4.cjs
yarnPath: .yarn/releases/yarn-3.8.3.cjs
logFilters:

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -31,7 +31,7 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
# Sponsors
<!-- SPONSORS-ORG -->
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://grundstueckspreise.info/"><img title="SP Software GmbH" width="256" src="https://joplinapp.org/images/sponsors/Grundstueckspreise.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://www.famegear.com"><img title="Famegear" width="256" src="https://joplinapp.org/images/sponsors/Famegear.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a>
<!-- SPONSORS-ORG -->
* * *

View File

@@ -61,6 +61,7 @@
"/packages/turndown/config",
"/readme/_i18n",
"/readme/about/changelog/desktop.md",
"/readme/licenses.md",
"/readme/i18n",
"cspell.json",
"node_modules"

View File

@@ -72,38 +72,38 @@
"@crowdin/cli": "3",
"@joplin/utils": "~2.12",
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"cspell": "5.21.2",
"eslint": "8.52.0",
"eslint": "8.57.0",
"eslint-interactive": "10.8.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-jest": "27.4.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-promise": "6.2.0",
"eslint-plugin-react": "7.34.3",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "10.3.12",
"glob": "10.4.5",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
"lint-staged": "15.2.2",
"lint-staged": "15.2.7",
"madge": "6.1.0",
"npm-package-json-lint": "7.1.0",
"typescript": "5.2.2"
"typescript": "5.4.5"
},
"dependencies": {
"@types/fs-extra": "11.0.4",
"eslint-plugin-github": "4.10.1",
"eslint-plugin-github": "4.10.2",
"http-server": "14.1.1",
"node-gyp": "9.4.1",
"nodemon": "3.0.3"
"nodemon": "3.1.7"
},
"packageManager": "yarn@3.6.4",
"packageManager": "yarn@3.8.3",
"resolutions": {
"react-native-camera@4.2.1": "patch:react-native-camera@npm%3A4.2.1#./.yarn/patches/react-native-camera-npm-4.2.1-24b2600a7e.patch",
"react-native-vosk@0.1.12": "patch:react-native-vosk@npm%3A0.1.12#./.yarn/patches/react-native-vosk-npm-0.1.12-76b1caaae8.patch",
"eslint": "patch:eslint@8.52.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"eslint": "patch:eslint@8.57.0#./.yarn/patches/eslint-npm-8.39.0-d92bace04d.patch",
"app-builder-lib@24.4.0": "patch:app-builder-lib@npm%3A24.4.0#./.yarn/patches/app-builder-lib-npm-24.4.0-05322ff057.patch",
"nanoid": "patch:nanoid@npm%3A3.3.7#./.yarn/patches/nanoid-npm-3.3.7-98824ba130.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A3.11.174#./.yarn/patches/pdfjs-dist-npm-3.11.174-67f2fee6d6.patch",

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env -S NODE_OPTIONS=--no-deprecation node
// Use njstrace to find out what Node.js might be spending time on
// var njstrace = require('njstrace').inject();

View File

@@ -57,7 +57,7 @@
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
"sharp": "0.33.3",
"sharp": "0.33.4",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
@@ -72,12 +72,12 @@
"devDependencies": {
"@joplin/tools": "~3.1",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.8",
"@types/node": "18.19.33",
"@types/jest": "29.5.12",
"@types/node": "18.19.39",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",
"temp": "0.9.4",
"typescript": "5.2.2"
"typescript": "5.4.5"
}
}

View File

@@ -1,4 +1,4 @@
import MdToHtml from '@joplin/renderer/MdToHtml';
import MdToHtml, { LinkRenderingType } from '@joplin/renderer/MdToHtml';
const { filename } = require('@joplin/lib/path-utils');
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import shim from '@joplin/lib/shim';
@@ -218,6 +218,9 @@ describe('MdToHtml', () => {
const mdToHtmlLinkifyOn = newTestMdToHtml({
pluginOptions: {
linkify: { enabled: true },
link_open: {
linkRenderingType: LinkRenderingType.HrefHandler,
},
},
});
@@ -227,29 +230,52 @@ describe('MdToHtml', () => {
},
});
const renderOptions = {
bodyOnly: true,
plainResourceRendering: true,
linkRenderingType: LinkRenderingType.HrefHandler,
};
for (const testCase of testCases) {
const [input, expectedLinkifyOff, expectedLinkifyOn] = testCase;
{
const actual = await mdToHtmlLinkifyOn.render(input, null, {
bodyOnly: true,
plainResourceRendering: true,
});
const actual = await mdToHtmlLinkifyOn.render(input, null, renderOptions);
expect(actual.html).toBe(expectedLinkifyOn);
}
{
const actual = await mdToHtmlLinkifyOff.render(input, null, {
bodyOnly: true,
plainResourceRendering: true,
});
const actual = await mdToHtmlLinkifyOff.render(input, null, renderOptions);
expect(actual.html).toBe(expectedLinkifyOff);
}
}
}));
it.each([
'[test](http://example.com/)',
'[test](mailto:test@example.com)',
])('should add onclick handlers to links when linkRenderingType is JavaScriptHandler (%j)', async (markdown) => {
const mdToHtml = newTestMdToHtml();
const renderWithoutOnClickOptions = {
bodyOnly: true,
linkRenderingType: LinkRenderingType.HrefHandler,
};
expect(
(await mdToHtml.render(markdown, undefined, renderWithoutOnClickOptions)).html,
).not.toContain('onclick');
const renderWithOnClickOptions = {
bodyOnly: true,
linkRenderingType: LinkRenderingType.JavaScriptHandler,
};
expect(
(await mdToHtml.render(markdown, undefined, renderWithOnClickOptions)).html,
).toMatch(/<a data-from-md .*onclick=['"].*['"].*>/);
});
it('should return attributes of line numbers', (async () => {
const mdToHtml = newTestMdToHtml();
@@ -326,4 +352,12 @@ describe('MdToHtml', () => {
expect(html).toContain('Inline</span>');
expect(html).toContain('Block</span>');
});
it('should sanitize KaTeX errors', async () => {
const markdown = '$\\a<svg>$';
const renderResult = await newTestMdToHtml().render(markdown, null, { bodyOnly: true });
// Should not contain the HTML in unsanitized form
expect(renderResult.html).not.toContain('<svg>');
});
});

View File

@@ -0,0 +1,2 @@
<div class="jop-noMdConv">
&lt;.a

View File

@@ -0,0 +1,2 @@
<div>
<.a<iframe src="http://example.com/" >

View File

@@ -0,0 +1,3 @@
dist/
node_modules/
publish/

View File

@@ -0,0 +1,8 @@
*.md
!README.md
/*.jpl
/api
/src
/dist
tsconfig.json
webpack.config.js

View File

@@ -0,0 +1,78 @@
# Plugin development
This documentation describes how to create a plugin, and how to work with the plugin builder framework and API.
## Installation
First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)).
```bash
npm install -g yo
npm install -g generator-joplin
```
Then generate your new project:
```bash
yo --node-package-manager npm joplin
```
## Structure
The main two files you will want to look at are:
- `/src/index.ts`, which contains the entry point for the plugin source code.
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
## Building the plugin
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
To build the plugin, simply run `npm run dist`.
The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript.
## Updating the manifest version number
You can run `npm run updateVersion` to bump the patch part of the version number, so for example 1.0.3 will become 1.0.4. This script will update both the package.json and manifest.json version numbers so as to keep them in sync.
## Publishing the plugin
To publish the plugin, add it to npmjs.com by running `npm publish`. Later on, a script will pick up your plugin and add it automatically to the Joplin plugin repository as long as the package satisfies these conditions:
- In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc".
- In `package.json`, the keywords include "joplin-plugin".
- In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`)
In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions.
## Updating the plugin framework
To update the plugin framework, run `npm run update`.
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
## External script files
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
## More information
- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)
## License
MIT © Laurent Cozic

View File

@@ -0,0 +1,10 @@
# Worker example
This plugin demonstrates how to create a `Worker` and, within it, use `transformers.js` for text classification.
At present, this plugin only works on desktop. However, it should also be possible to load the `Worker` from a plugin panel or dialog on mobile (but not web).
**Notes**:
- `plugin.config.json` was updated to target **web**. This allows the plugin to load `transformers.js` from the worker without depending on `node-loader`.
- This simplifies the build process and should make it easier to add mobile and web support to this plugin.
- WASM files and other assets are copied by `tools/copyAssets.js`.

View File

@@ -0,0 +1,14 @@
import Plugin from '../Plugin';
import Joplin from './Joplin';
/**
* @ignore
*/
/**
* @ignore
*/
export default class Global {
private joplin_;
constructor(implementation: any, plugin: Plugin, store: any);
get joplin(): Joplin;
get process(): any;
}

View File

@@ -0,0 +1,74 @@
import Plugin from '../Plugin';
import JoplinData from './JoplinData';
import JoplinPlugins from './JoplinPlugins';
import JoplinWorkspace from './JoplinWorkspace';
import JoplinFilters from './JoplinFilters';
import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import JoplinContentScripts from './JoplinContentScripts';
import JoplinClipboard from './JoplinClipboard';
import JoplinWindow from './JoplinWindow';
import BasePlatformImplementation from '../BasePlatformImplementation';
import JoplinImaging from './JoplinImaging';
/**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
*
* The API is now relatively stable and in general maintaining backward compatibility is a top priority, so you shouldn't except much breakages.
*
* If a breaking change ever becomes needed, best effort will be done to:
*
* - Deprecate features instead of removing them, so as to give you time to fix the issue;
* - Document breaking changes in the changelog;
*
* So if you are developing a plugin, please keep an eye on the changelog as everything will be in there with information about how to update your code.
*/
export default class Joplin {
private data_;
private plugins_;
private imaging_;
private workspace_;
private filters_;
private commands_;
private views_;
private interop_;
private settings_;
private contentScripts_;
private clipboard_;
private window_;
private implementation_;
constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: any);
get data(): JoplinData;
get clipboard(): JoplinClipboard;
get imaging(): JoplinImaging;
get window(): JoplinWindow;
get plugins(): JoplinPlugins;
get workspace(): JoplinWorkspace;
get contentScripts(): JoplinContentScripts;
/**
* @ignore
*
* Not sure if it's the best way to hook into the app
* so for now disable filters.
*/
get filters(): JoplinFilters;
get commands(): JoplinCommands;
get views(): JoplinViews;
get interop(): JoplinInterop;
get settings(): JoplinSettings;
/**
* It is not possible to bundle native packages with a plugin, because they
* need to work cross-platforms. Instead access to certain useful native
* packages is provided using this function.
*
* Currently these packages are available:
*
* - [sqlite3](https://www.npmjs.com/package/sqlite3)
* - [fs-extra](https://www.npmjs.com/package/fs-extra)
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/nativeModule)
*/
require(_path: string): any;
versionInfo(): Promise<import("./types").VersionInfo>;
}

View File

@@ -0,0 +1,23 @@
export default class JoplinClipboard {
private electronClipboard_;
private electronNativeImage_;
constructor(electronClipboard: any, electronNativeImage: any);
readText(): Promise<string>;
writeText(text: string): Promise<void>;
readHtml(): Promise<string>;
writeHtml(html: string): Promise<void>;
/**
* Returns the image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.
*/
readImage(): Promise<string>;
/**
* Takes an image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.
*/
writeImage(dataUrl: string): Promise<void>;
/**
* Returns the list available formats (mime types).
*
* For example [ 'text/plain', 'text/html' ]
*/
availableFormats(): Promise<string[]>;
}

View File

@@ -0,0 +1,89 @@
import { Command } from './types';
/**
* This class allows executing or registering new Joplin commands. Commands
* can be executed or associated with
* {@link JoplinViewsToolbarButtons | toolbar buttons} or
* {@link JoplinViewsMenuItems | menu items}.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*
* ## Executing Joplin's internal commands
*
* It is also possible to execute internal Joplin's commands which, as of
* now, are not well documented. You can find the list directly on GitHub
* though at the following locations:
*
* * [Main screen commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/MainScreen/commands)
* * [Global commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/commands)
* * [Editor commands](https://github.com/laurent22/joplin/tree/dev/packages/app-desktop/gui/NoteEditor/editorCommandDeclarations.ts)
*
* To view what arguments are supported, you can open any of these files
* and look at the `execute()` command.
*
* ## Executing editor commands
*
* There might be a situation where you want to invoke editor commands
* without using a {@link JoplinContentScripts | contentScript}. For this
* reason Joplin provides the built in `editor.execCommand` command.
*
* `editor.execCommand` should work with any core command in both the
* [CodeMirror](https://codemirror.net/doc/manual.html#execCommand) and
* [TinyMCE](https://www.tiny.cloud/docs/api/tinymce/tinymce.editorcommands/#execcommand) editors,
* as well as most functions calls directly on a CodeMirror editor object (extensions).
*
* * [CodeMirror commands](https://codemirror.net/doc/manual.html#commands)
* * [TinyMCE core editor commands](https://www.tiny.cloud/docs/advanced/editor-command-identifiers/#coreeditorcommands)
*
* `editor.execCommand` supports adding arguments for the commands.
*
* ```typescript
* await joplin.commands.execute('editor.execCommand', {
* name: 'madeUpCommand', // CodeMirror and TinyMCE
* args: [], // CodeMirror and TinyMCE
* ui: false, // TinyMCE only
* value: '', // TinyMCE only
* });
* ```
*
* [View the example using the CodeMirror editor](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/codemirror_content_script/src/index.ts)
*
*/
export default class JoplinCommands {
/**
* <span class="platform-desktop">desktop</span> Executes the given
* command.
*
* The command can take any number of arguments, and the supported
* arguments will vary based on the command. For custom commands, this
* is the `args` passed to the `execute()` function. For built-in
* commands, you can find the supported arguments by checking the links
* above.
*
* ```typescript
* // Create a new note in the current notebook:
* await joplin.commands.execute('newNote');
*
* // Create a new sub-notebook under the provided notebook
* // Note: internally, notebooks are called "folders".
* await joplin.commands.execute('newFolder', "SOME_FOLDER_ID");
* ```
*/
execute(commandName: string, ...args: any[]): Promise<any | void>;
/**
* <span class="platform-desktop">desktop</span> Registers a new command.
*
* ```typescript
* // Register a new commmand called "testCommand1"
*
* await joplin.commands.register({
* name: 'testCommand1',
* label: 'My Test Command 1',
* iconName: 'fas fa-music',
* execute: () => {
* alert('Testing plugin command 1');
* },
* });
* ```
*/
register(command: Command): Promise<void>;
}

View File

@@ -0,0 +1,40 @@
import Plugin from '../Plugin';
import { ContentScriptType } from './types';
export default class JoplinContentScripts {
private plugin;
constructor(plugin: Plugin);
/**
* Registers a new content script. Unlike regular plugin code, which runs in
* a separate process, content scripts run within the main process code and
* thus allow improved performances and more customisations in specific
* cases. It can be used for example to load a Markdown or editor plugin.
*
* Note that registering a content script in itself will do nothing - it
* will only be loaded in specific cases by the relevant app modules (eg.
* the Markdown renderer or the code editor). So it is not a way to inject
* and run arbitrary code in the app, which for safety and performance
* reasons is not supported.
*
* The plugin generator provides a way to build any content script you might
* want to package as well as its dependencies. See the [Plugin Generator
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
* for more information.
*
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
*
* See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
* @param id A unique ID for the content script.
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
*/
register(type: ContentScriptType, id: string, scriptPath: string): Promise<void>;
/**
* Listens to a messages sent from the content script using postMessage().
* See {@link ContentScriptType} for more information as well as the
* [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*/
onMessage(contentScriptId: string, callback: any): Promise<void>;
}

View File

@@ -0,0 +1,73 @@
import { ModelType } from '../../../BaseModel';
import Plugin from '../Plugin';
import { Path } from './types';
/**
* This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api
* This is the main way to retrieve data, such as notes, notebooks, tags, etc.
* or to update them or delete them.
*
* This is also what you would use to search notes, via the `search` endpoint.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/simple)
*
* In general you would use the methods in this class as if you were using a REST API. There are four methods that map to GET, POST, PUT and DELETE calls.
* And each method takes these parameters:
*
* * `path`: This is an array that represents the path to the resource in the form `["resouceName", "resourceId", "resourceLink"]` (eg. ["tags", ":id", "notes"]). The "resources" segment is the name of the resources you want to access (eg. "notes", "folders", etc.). If not followed by anything, it will refer to all the resources in that collection. The optional "resourceId" points to a particular resources within the collection. Finally, an optional "link" can be present, which links the resource to a collection of resources. This can be used in the API for example to retrieve all the notes associated with a tag.
* * `query`: (Optional) The query parameters. In a URL, this is the part after the question mark "?". In this case, it should be an object with key/value pairs.
* * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder.
* * `files`: (Optional) Used to create new resources and associate them with files.
*
* Please refer to the [Joplin API documentation](https://joplinapp.org/help/api/references/rest_api) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API.
*
* For example:
*
* ```typescript
* // Get a note ID, title and body
* const noteId = 'some_note_id';
* const note = await joplin.data.get(['notes', noteId], { fields: ['id', 'title', 'body'] });
*
* // Get all folders
* const folders = await joplin.data.get(['folders']);
*
* // Set the note body
* await joplin.data.put(['notes', noteId], null, { body: "New note body" });
*
* // Create a new note under one of the folders
* await joplin.data.post(['notes'], null, { body: "my new note", title: "some title", parent_id: folders[0].id });
* ```
*/
export default class JoplinData {
private api_;
private pathSegmentRegex_;
private plugin;
constructor(plugin: Plugin);
private serializeApiBody;
private pathToString;
get(path: Path, query?: any): Promise<any>;
post(path: Path, query?: any, body?: any, files?: any[]): Promise<any>;
put(path: Path, query?: any, body?: any, files?: any[]): Promise<any>;
delete(path: Path, query?: any): Promise<any>;
itemType(itemId: string): Promise<ModelType>;
resourcePath(resourceId: string): Promise<string>;
/**
* Gets an item user data. User data are key/value pairs. The `key` can be any
* arbitrary string, while the `value` can be of any type supported by
* [JSON.stringify](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#description)
*
* User data is synchronised across devices, and each value wil be merged based on their timestamp:
*
* - If value is modified by client 1, then modified by client 2, it will take the value from client 2
* - If value is modified by client 1, then deleted by client 2, the value will be deleted after merge
* - If value is deleted by client 1, then updated by client 2, the value will be restored and set to the value from client 2 after merge
*/
userDataGet<T>(itemType: ModelType, itemId: string, key: string): Promise<T>;
/**
* Sets a note user data. See {@link JoplinData.userDataGet} for more details.
*/
userDataSet<T>(itemType: ModelType, itemId: string, key: string, value: T): Promise<void>;
/**
* Deletes a note user data. See {@link JoplinData.userDataGet} for more details.
*/
userDataDelete(itemType: ModelType, itemId: string, key: string): Promise<void>;
}

View File

@@ -0,0 +1,10 @@
/**
* @ignore
*
* Not sure if it's the best way to hook into the app
* so for now disable filters.
*/
export default class JoplinFilters {
on(name: string, callback: Function): Promise<void>;
off(name: string, callback: Function): Promise<void>;
}

View File

@@ -0,0 +1,61 @@
import { Rectangle } from './types';
export interface Implementation {
nativeImage: any;
}
export interface CreateFromBufferOptions {
width?: number;
height?: number;
scaleFactor?: number;
}
export interface ResizeOptions {
width?: number;
height?: number;
quality?: 'good' | 'better' | 'best';
}
export type Handle = string;
/**
* Provides imaging functions to resize or process images. You create an image
* using one of the `createFrom` functions, then use the other functions to
* process the image.
*
* Images are associated with a handle which is what will be available to the
* plugin. Once you are done with an image, free it using the `free()` function.
*
* [View the
* example](https://github.com/laurent22/joplin/blob/dev/packages/app-cli/tests/support/plugins/imaging/src/index.ts)
*
*/
export default class JoplinImaging {
private implementation_;
private images_;
constructor(implementation: Implementation);
private createImageHandle;
private imageByHandle;
private cacheImage;
createFromPath(filePath: string): Promise<Handle>;
createFromResource(resourceId: string): Promise<Handle>;
getSize(handle: Handle): Promise<any>;
resize(handle: Handle, options?: ResizeOptions): Promise<string>;
crop(handle: Handle, rectange: Rectangle): Promise<string>;
toPngFile(handle: Handle, filePath: string): Promise<void>;
/**
* Quality is between 0 and 100
*/
toJpgFile(handle: Handle, filePath: string, quality?: number): Promise<void>;
private tempFilePath;
/**
* Creates a new Joplin resource from the image data. The image will be
* first converted to a JPEG.
*/
toJpgResource(handle: Handle, resourceProps: any, quality?: number): Promise<import("../../database/types").ResourceEntity>;
/**
* Creates a new Joplin resource from the image data. The image will be
* first converted to a PNG.
*/
toPngResource(handle: Handle, resourceProps: any): Promise<import("../../database/types").ResourceEntity>;
/**
* Image data is not automatically deleted by Joplin so make sure you call
* this method on the handle once you are done.
*/
free(handle: Handle): Promise<void>;
}

View File

@@ -0,0 +1,17 @@
import { ExportModule, ImportModule } from './types';
/**
* Provides a way to create modules to import external data into Joplin or to export notes into any arbitrary format.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export)
*
* To implement an import or export module, you would simply define an object with various event handlers that are called
* by the application during the import/export process.
*
* See the documentation of the [[ExportModule]] and [[ImportModule]] for more information.
*
* You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/help/api/references/rest_api
*/
export default class JoplinInterop {
registerExportModule(module: ExportModule): Promise<void>;
registerImportModule(module: ImportModule): Promise<void>;
}

View File

@@ -0,0 +1,43 @@
import Plugin from '../Plugin';
import { ContentScriptType, Script } from './types';
/**
* This class provides access to plugin-related features.
*/
export default class JoplinPlugins {
private plugin;
constructor(plugin: Plugin);
/**
* Registers a new plugin. This is the entry point when creating a plugin. You should pass a simple object with an `onStart` method to it.
* That `onStart` method will be executed as soon as the plugin is loaded.
*
* ```typescript
* joplin.plugins.register({
* onStart: async function() {
* // Run your plugin code here
* }
* });
* ```
*/
register(script: Script): Promise<void>;
/**
* @deprecated Use joplin.contentScripts.register()
*/
registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise<void>;
/**
* Gets the plugin own data directory path. Use this to store any
* plugin-related data. Unlike [[installationDir]], any data stored here
* will be persisted.
*/
dataDir(): Promise<string>;
/**
* Gets the plugin installation directory. This can be used to access any
* asset that was packaged with the plugin. This directory should be
* considered read-only because any data you store here might be deleted or
* re-created at any time. To store new persistent data, use [[dataDir]].
*/
installationDir(): Promise<string>;
/**
* @deprecated Use joplin.require()
*/
require(_path: string): any;
}

View File

@@ -0,0 +1,63 @@
import Plugin from '../Plugin';
import { SettingItem, SettingSection } from './types';
export interface ChangeEvent {
/**
* Setting keys that have been changed
*/
keys: string[];
}
export type ChangeHandler = (event: ChangeEvent) => void;
export declare const namespacedKey: (pluginId: string, key: string) => string;
/**
* This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user.
*
* Settings are essentially key/value pairs.
*
* Note: Currently this API does **not** provide access to Joplin's built-in settings. This is by design as plugins that modify user settings could give unexpected results
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/settings)
*/
export default class JoplinSettings {
private plugin_;
constructor(plugin: Plugin);
/**
* Registers new settings.
* Note that registering a setting item is dynamic and will be gone next time Joplin starts.
* What it means is that you need to register the setting every time the plugin starts (for example in the onStart event).
* The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some
* reason the plugin fails to start at some point.
*/
registerSettings(settings: Record<string, SettingItem>): Promise<void>;
/**
* @deprecated Use joplin.settings.registerSettings()
*
* Registers a new setting.
*/
registerSetting(key: string, settingItem: SettingItem): Promise<void>;
/**
* Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts.
*/
registerSection(name: string, section: SettingSection): Promise<void>;
/**
* Gets a setting value (only applies to setting you registered from your plugin)
*/
value(key: string): Promise<any>;
/**
* Sets a setting value (only applies to setting you registered from your plugin)
*/
setValue(key: string, value: any): Promise<void>;
/**
* Gets a global setting value, including app-specific settings and those set by other plugins.
*
* The list of available settings is not documented yet, but can be found by looking at the source code:
*
* https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142
*/
globalValue(key: string): Promise<any>;
/**
* Called when one or multiple settings of your plugin have been changed.
* - For performance reasons, this event is triggered with a delay.
* - You will only get events for your own plugin settings.
*/
onChange(handler: ChangeHandler): Promise<void>;
}

View File

@@ -0,0 +1,31 @@
import Plugin from '../Plugin';
import JoplinViewsDialogs from './JoplinViewsDialogs';
import JoplinViewsMenuItems from './JoplinViewsMenuItems';
import JoplinViewsMenus from './JoplinViewsMenus';
import JoplinViewsToolbarButtons from './JoplinViewsToolbarButtons';
import JoplinViewsPanels from './JoplinViewsPanels';
import JoplinViewsNoteList from './JoplinViewsNoteList';
/**
* This namespace provides access to view-related services.
*
* All view services provide a `create()` method which you would use to create the view object, whether it's a dialog, a toolbar button or a menu item.
* In some cases, the `create()` method will return a [[ViewHandle]], which you would use to act on the view, for example to set certain properties or call some methods.
*/
export default class JoplinViews {
private store;
private plugin;
private panels_;
private menuItems_;
private menus_;
private toolbarButtons_;
private dialogs_;
private noteList_;
private implementation_;
constructor(implementation: any, plugin: Plugin, store: any);
get dialogs(): JoplinViewsDialogs;
get panels(): JoplinViewsPanels;
get menuItems(): JoplinViewsMenuItems;
get menus(): JoplinViewsMenus;
get toolbarButtons(): JoplinViewsToolbarButtons;
get noteList(): JoplinViewsNoteList;
}

View File

@@ -0,0 +1,74 @@
import Plugin from '../Plugin';
import { ButtonSpec, ViewHandle, DialogResult } from './types';
/**
* Allows creating and managing dialogs. A dialog is modal window that
* contains a webview and a row of buttons. You can update the
* webview using the `setHtml` method. Dialogs are hidden by default and
* you need to call `open()` to open them. Once the user clicks on a
* button, the `open` call will return an object indicating what button was
* clicked on.
*
* ## Retrieving form values
*
* If your HTML content included one or more forms, a `formData` object
* will also be included with the key/value for each form.
*
* ## Special button IDs
*
* The following buttons IDs have a special meaning:
*
* - `ok`, `yes`, `submit`, `confirm`: They are considered "submit" buttons
* - `cancel`, `no`, `reject`: They are considered "dismiss" buttons
*
* This information is used by the application to determine what action
* should be done when the user presses "Enter" or "Escape" within the
* dialog. If they press "Enter", the first "submit" button will be
* automatically clicked. If they press "Escape" the first "dismiss" button
* will be automatically clicked.
*
* [View the demo
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/dialog)
*/
export default class JoplinViewsDialogs {
private store;
private plugin;
private implementation_;
constructor(implementation: any, plugin: Plugin, store: any);
private controller;
/**
* Creates a new dialog
*/
create(id: string): Promise<ViewHandle>;
/**
* Displays a message box with OK/Cancel buttons. Returns the button index that was clicked - "0" for OK and "1" for "Cancel"
*/
showMessageBox(message: string): Promise<number>;
/**
* Displays a dialog to select a file or a directory. Same options and
* output as
* https://www.electronjs.org/docs/latest/api/dialog#dialogshowopendialogbrowserwindow-options
*/
showOpenDialog(options: any): Promise<any>;
/**
* Sets the dialog HTML content
*/
setHtml(handle: ViewHandle, html: string): Promise<string>;
/**
* Adds and loads a new JS or CSS files into the dialog.
*/
addScript(handle: ViewHandle, scriptPath: string): Promise<void>;
/**
* Sets the dialog buttons.
*/
setButtons(handle: ViewHandle, buttons: ButtonSpec[]): Promise<ButtonSpec[]>;
/**
* Opens the dialog
*/
open(handle: ViewHandle): Promise<DialogResult>;
/**
* Toggle on whether to fit the dialog size to the content or not.
* When set to false, the dialog is set to 90vw and 80vh
* @default true
*/
setFitToContent(handle: ViewHandle, status: boolean): Promise<boolean>;
}

View File

@@ -0,0 +1,16 @@
import { CreateMenuItemOptions, MenuItemLocation } from './types';
import Plugin from '../Plugin';
/**
* Allows creating and managing menu items.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*/
export default class JoplinViewsMenuItems {
private store;
private plugin;
constructor(plugin: Plugin, store: any);
/**
* Creates a new menu item and associate it with the given command. You can specify under which menu the item should appear using the `location` parameter.
*/
create(id: string, commandName: string, location?: MenuItemLocation, options?: CreateMenuItemOptions): Promise<void>;
}

View File

@@ -0,0 +1,18 @@
import { MenuItem, MenuItemLocation } from './types';
import Plugin from '../Plugin';
/**
* Allows creating menus.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/menu)
*/
export default class JoplinViewsMenus {
private store;
private plugin;
constructor(plugin: Plugin, store: any);
private registerCommandAccelerators;
/**
* Creates a new menu from the provided menu items and place it at the given location. As of now, it is only possible to place the
* menu as a sub-menu of the application build-in menus.
*/
create(id: string, label: string, menuItems: MenuItem[], location?: MenuItemLocation): Promise<void>;
}

View File

@@ -0,0 +1,24 @@
import { Store } from 'redux';
import Plugin from '../Plugin';
import { ListRenderer } from './noteListType';
/**
* This API allows you to customise how each note in the note list is rendered.
* The renderer you implement follows a unidirectional data flow.
*
* The app provides the required dependencies whenever a note is updated - you
* process these dependencies, and return some props, which are then passed to
* your template and rendered. See [[[ListRenderer]]] for a detailed description
* of each property of the renderer.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer)
*
* The default list renderer is implemented using the same API, so it worth checking it too:
*
* [Default list renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts)
*/
export default class JoplinViewsNoteList {
private plugin_;
private store_;
constructor(plugin: Plugin, store: Store);
registerRenderer(renderer: ListRenderer): Promise<void>;
}

View File

@@ -0,0 +1,78 @@
import Plugin from '../Plugin';
import { ViewHandle } from './types';
/**
* Allows creating and managing view panels. View panels currently are
* displayed at the right of the sidebar and allows displaying any HTML
* content (within a webview) and update it in real-time. For example it
* could be used to display a table of content for the active note, or
* display various metadata or graph.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/toc)
*/
export default class JoplinViewsPanels {
private store;
private plugin;
constructor(plugin: Plugin, store: any);
private controller;
/**
* Creates a new panel
*/
create(id: string): Promise<ViewHandle>;
/**
* Sets the panel webview HTML
*/
setHtml(handle: ViewHandle, html: string): Promise<string>;
/**
* Adds and loads a new JS or CSS files into the panel.
*/
addScript(handle: ViewHandle, scriptPath: string): Promise<void>;
/**
* Called when a message is sent from the webview (using postMessage).
*
* To post a message from the webview to the plugin use:
*
* ```javascript
* const response = await webviewApi.postMessage(message);
* ```
*
* - `message` can be any JavaScript object, string or number
* - `response` is whatever was returned by the `onMessage` handler
*
* Using this mechanism, you can have two-way communication between the
* plugin and webview.
*
* See the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details.
*
*/
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
/**
* Sends a message to the webview.
*
* The webview must have registered a message handler prior, otherwise the message is ignored. Use;
*
* ```javascript
* webviewApi.onMessage((message) => { ... });
* ```
*
* - `message` can be any JavaScript object, string or number
*
* The view API may have only one onMessage handler defined.
* This method is fire and forget so no response is returned.
*
* It is particularly useful when the webview needs to react to events emitted by the plugin or the joplin api.
*/
postMessage(handle: ViewHandle, message: any): void;
/**
* Shows the panel
*/
show(handle: ViewHandle, show?: boolean): Promise<void>;
/**
* Hides the panel
*/
hide(handle: ViewHandle): Promise<void>;
/**
* Tells whether the panel is visible or not
*/
visible(handle: ViewHandle): Promise<boolean>;
}

View File

@@ -0,0 +1,16 @@
import { ToolbarButtonLocation } from './types';
import Plugin from '../Plugin';
/**
* Allows creating and managing toolbar buttons.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/register_command)
*/
export default class JoplinViewsToolbarButtons {
private store;
private plugin;
constructor(plugin: Plugin, store: any);
/**
* Creates a new toolbar button and associate it with the given command.
*/
create(id: string, commandName: string, location: ToolbarButtonLocation): Promise<void>;
}

View File

@@ -0,0 +1,24 @@
import Plugin from '../Plugin';
export interface Implementation {
injectCustomStyles(elementId: string, cssFilePath: string): Promise<void>;
}
export default class JoplinWindow {
private plugin_;
private store_;
private implementation_;
constructor(implementation: Implementation, plugin: Plugin, store: any);
/**
* Loads a chrome CSS file. It will apply to the window UI elements, except
* for the note viewer. It is the same as the "Custom stylesheet for
* Joplin-wide app styles" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css)
* for an example.
*/
loadChromeCssFile(filePath: string): Promise<void>;
/**
* Loads a note CSS file. It will apply to the note viewer, as well as any
* exported or printed note. It is the same as the "Custom stylesheet for
* rendered Markdown" setting. See the [Load CSS Demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/load_css)
* for an example.
*/
loadNoteCssFile(filePath: string): Promise<void>;
}

View File

@@ -0,0 +1,87 @@
import { FolderEntity } from '../../database/types';
import { Disposable, MenuItem } from './types';
export interface EditContextMenuFilterObject {
items: MenuItem[];
}
type FilterHandler<T> = (object: T) => Promise<void>;
declare enum ItemChangeEventType {
Create = 1,
Update = 2,
Delete = 3
}
interface ItemChangeEvent {
id: string;
event: ItemChangeEventType;
}
interface SyncStartEvent {
withErrors: boolean;
}
interface ResourceChangeEvent {
id: string;
}
type ItemChangeHandler = (event: ItemChangeEvent) => void;
type SyncStartHandler = (event: SyncStartEvent) => void;
type ResourceChangeHandler = (event: ResourceChangeEvent) => void;
/**
* The workspace service provides access to all the parts of Joplin that
* are being worked on - i.e. the currently selected notes or notebooks as
* well as various related events, such as when a new note is selected, or
* when the note content changes.
*
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins)
*/
export default class JoplinWorkspace {
private store;
constructor(store: any);
/**
* Called when a new note or notes are selected.
*/
onNoteSelectionChange(callback: Function): Promise<Disposable>;
/**
* Called when the content of a note changes.
* @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes.
*/
onNoteContentChange(callback: Function): Promise<void>;
/**
* Called when the content of the current note changes.
*/
onNoteChange(handler: ItemChangeHandler): Promise<Disposable>;
/**
* Called when a resource is changed. Currently this handled will not be
* called when a resource is added or deleted.
*/
onResourceChange(handler: ResourceChangeHandler): Promise<void>;
/**
* Called when an alarm associated with a to-do is triggered.
*/
onNoteAlarmTrigger(handler: Function): Promise<Disposable>;
/**
* Called when the synchronisation process is starting.
*/
onSyncStart(handler: SyncStartHandler): Promise<Disposable>;
/**
* Called when the synchronisation process has finished.
*/
onSyncComplete(callback: Function): Promise<Disposable>;
/**
* Called just before the editor context menu is about to open. Allows
* adding items to it.
*/
filterEditorContextMenu(handler: FilterHandler<EditContextMenuFilterObject>): void;
/**
* Gets the currently selected note
*/
selectedNote(): Promise<any>;
/**
* Gets the currently selected folder. In some cases, for example during
* search or when viewing a tag, no folder is actually selected in the user
* interface. In that case, that function would return the last selected
* folder.
*/
selectedFolder(): Promise<FolderEntity>;
/**
* Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes.
*/
selectedNoteIds(): Promise<string[]>;
}
export {};

View File

@@ -0,0 +1,5 @@
import type Joplin from './Joplin';
declare const joplin: Joplin;
export default joplin;

View File

@@ -0,0 +1,156 @@
import { Size } from './types';
type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_';
export declare enum ItemFlow {
TopToBottom = "topToBottom",
LeftToRight = "leftToRight"
}
export type RenderNoteView = Record<string, any>;
export interface OnChangeEvent {
elementId: string;
value: any;
noteId: string;
}
export type OnRenderNoteHandler = (props: any) => Promise<RenderNoteView>;
export type OnChangeHandler = (event: OnChangeEvent) => Promise<void>;
/**
* Most of these are the built-in note properties, such as `note.title`,
* `note.todo_completed`, etc.
*
* Additionally, the `item.*` properties are specific to the rendered item. The
* most important being `item.selected`, which you can use to display the
* selected note in a different way.
*
* Finally some special properties are provided to make it easier to render
* notes. In particular, if possible prefer `note.titleHtml` to `note.title`
* since some important processing has already been done on the string, such as
* handling the search highlighter and escaping. Since it's HTML and already
* escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax,
* which disables escaping).
*
* `notes.tag` gives you the list of tags associated with the note.
*
* `note.isWatched` tells you if the note is currently opened in an external
* editor. In which case you would generally display some indicator.
*/
export type ListRendererDepependency = ListRendererDatabaseDependency | 'item.size.width' | 'item.size.height' | 'item.selected' | 'note.titleHtml' | 'note.isWatched' | 'note.tags';
export interface ListRenderer {
/**
* It must be unique to your plugin.
*/
id: string;
/**
* Can be top to bottom or left to right. Left to right gives you more
* option to set the size of the items since you set both its width and
* height.
*/
flow: ItemFlow;
/**
* The size of each item must be specified in advance for performance
* reasons, and cannot be changed afterwards. If the item flow is top to
* bottom, you only need to specificy the item height (the width will be
* ignored).
*/
itemSize: Size;
/**
* The CSS is relative to the list item container. What will appear in the
* page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use
* child combinator with guarantee it will only apply to your own items. In
* this example, the styling will apply to `.note-list-item > .content`:
*
* ```css
* > .content {
* padding: 10px;
* }
* ```
*
* In order to get syntax highlighting working here, it's recommended
* installing an editor extension such as [es6-string-html VSCode
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
*/
itemCss?: string;
/**
* List the dependencies that your plugin needs to render the note list
* items. Only these will be passed to your `onRenderNote` handler. Ensure
* that you do not add more than what you need since there is a performance
* penalty for each property.
*/
dependencies: ListRendererDepependency[];
/**
* This is the HTML template that will be used to render the note list item.
* This is a [Mustache template](https://github.com/janl/mustache.js) and it
* will receive the variable you return from `onRenderNote` as tags. For
* example, if you return a property named `formattedDate` from
* `onRenderNote`, you can insert it in the template using `Created date:
* {{formattedDate}}`.
*
* In order to get syntax highlighting working here, it's recommended
* installing an editor extension such as [es6-string-html VSCode
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
*/
itemTemplate: string;
/**
* This user-facing text is used for example in the View menu, so that your
* renderer can be selected.
*/
label: () => Promise<string>;
/**
* This is where most of the real-time processing will happen. When a note
* is rendered for the first time and every time it changes, this handler
* receives the properties specified in the `dependencies` property. You can
* then process them, load any additional data you need, and once done you
* need to return the properties that are needed in the `itemTemplate` HTML.
* Again, to use the formatted date example, you could have such a renderer:
*
* ```typescript
* dependencies: [
* 'note.title',
* 'note.created_time',
* ],
*
* itemTemplate: // html
* `
* <div>
* Title: {{note.title}}<br/>
* Date: {{formattedDate}}
* </div>
* `,
*
* onRenderNote: async (props: any) => {
* const formattedDate = dayjs(props.note.created_time).format();
* return {
* // Also return the props, so that note.title is available from the
* // template
* ...props,
* formattedDate,
* }
* },
* ```
*/
onRenderNote: OnRenderNoteHandler;
/**
* This handler allows adding some interacivity to the note renderer -
* whenever an input element within the item is changed (for example, when a
* checkbox is clicked, or a text input is changed), this `onChange` handler
* is going to be called.
*
* You can inspect `event.elementId` to know which element had some changes,
* and `event.value` to know the new value. `event.noteId` also tells you
* what note is affected, so that you can potentially apply changes to it.
*
* You specify the element ID, by setting a `data-id` attribute on the
* input.
*
* For example, if you have such a template:
*
* ```html
* <div>
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
* </div>
* ```
*
* The event handler will receive an event with `elementId` set to
* `noteTitleInput`.
*/
onChange?: OnChangeHandler;
}
export {};

View File

@@ -0,0 +1,181 @@
/* eslint-disable multiline-comment-style */
import { Size } from './types';
// AUTO-GENERATED by generate-database-type
type ListRendererDatabaseDependency = 'folder.created_time' | 'folder.encryption_applied' | 'folder.encryption_cipher_text' | 'folder.icon' | 'folder.id' | 'folder.is_shared' | 'folder.master_key_id' | 'folder.parent_id' | 'folder.share_id' | 'folder.title' | 'folder.updated_time' | 'folder.user_created_time' | 'folder.user_data' | 'folder.user_updated_time' | 'folder.type_' | 'note.altitude' | 'note.application_data' | 'note.author' | 'note.body' | 'note.conflict_original_id' | 'note.created_time' | 'note.encryption_applied' | 'note.encryption_cipher_text' | 'note.id' | 'note.is_conflict' | 'note.is_shared' | 'note.is_todo' | 'note.latitude' | 'note.longitude' | 'note.markup_language' | 'note.master_key_id' | 'note.order' | 'note.parent_id' | 'note.share_id' | 'note.source' | 'note.source_application' | 'note.source_url' | 'note.title' | 'note.todo_completed' | 'note.todo_due' | 'note.updated_time' | 'note.user_created_time' | 'note.user_data' | 'note.user_updated_time' | 'note.type_';
// AUTO-GENERATED by generate-database-type
export enum ItemFlow {
TopToBottom = 'topToBottom',
LeftToRight = 'leftToRight',
}
export type RenderNoteView = Record<string, any>;
export interface OnChangeEvent {
elementId: string;
value: any;
noteId: string;
}
export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>;
export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>;
/**
* Most of these are the built-in note properties, such as `note.title`,
* `note.todo_completed`, etc.
*
* Additionally, the `item.*` properties are specific to the rendered item. The
* most important being `item.selected`, which you can use to display the
* selected note in a different way.
*
* Finally some special properties are provided to make it easier to render
* notes. In particular, if possible prefer `note.titleHtml` to `note.title`
* since some important processing has already been done on the string, such as
* handling the search highlighter and escaping. Since it's HTML and already
* escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax,
* which disables escaping).
*
* `notes.tag` gives you the list of tags associated with the note.
*
* `note.isWatched` tells you if the note is currently opened in an external
* editor. In which case you would generally display some indicator.
*/
export type ListRendererDepependency =
ListRendererDatabaseDependency |
'item.size.width' |
'item.size.height' |
'item.selected' |
'note.titleHtml' |
'note.isWatched' |
'note.tags';
export interface ListRenderer {
/**
* It must be unique to your plugin.
*/
id: string;
/**
* Can be top to bottom or left to right. Left to right gives you more
* option to set the size of the items since you set both its width and
* height.
*/
flow: ItemFlow;
/**
* The size of each item must be specified in advance for performance
* reasons, and cannot be changed afterwards. If the item flow is top to
* bottom, you only need to specificy the item height (the width will be
* ignored).
*/
itemSize: Size;
/**
* The CSS is relative to the list item container. What will appear in the
* page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use
* child combinator with guarantee it will only apply to your own items. In
* this example, the styling will apply to `.note-list-item > .content`:
*
* ```css
* > .content {
* padding: 10px;
* }
* ```
*
* In order to get syntax highlighting working here, it's recommended
* installing an editor extension such as [es6-string-html VSCode
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
*/
itemCss?: string;
/**
* List the dependencies that your plugin needs to render the note list
* items. Only these will be passed to your `onRenderNote` handler. Ensure
* that you do not add more than what you need since there is a performance
* penalty for each property.
*/
dependencies: ListRendererDepependency[];
/**
* This is the HTML template that will be used to render the note list item.
* This is a [Mustache template](https://github.com/janl/mustache.js) and it
* will receive the variable you return from `onRenderNote` as tags. For
* example, if you return a property named `formattedDate` from
* `onRenderNote`, you can insert it in the template using `Created date:
* {{formattedDate}}`.
*
* In order to get syntax highlighting working here, it's recommended
* installing an editor extension such as [es6-string-html VSCode
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
*/
itemTemplate: string;
/**
* This user-facing text is used for example in the View menu, so that your
* renderer can be selected.
*/
label: ()=> Promise<string>;
/**
* This is where most of the real-time processing will happen. When a note
* is rendered for the first time and every time it changes, this handler
* receives the properties specified in the `dependencies` property. You can
* then process them, load any additional data you need, and once done you
* need to return the properties that are needed in the `itemTemplate` HTML.
* Again, to use the formatted date example, you could have such a renderer:
*
* ```typescript
* dependencies: [
* 'note.title',
* 'note.created_time',
* ],
*
* itemTemplate: // html
* `
* <div>
* Title: {{note.title}}<br/>
* Date: {{formattedDate}}
* </div>
* `,
*
* onRenderNote: async (props: any) => {
* const formattedDate = dayjs(props.note.created_time).format();
* return {
* // Also return the props, so that note.title is available from the
* // template
* ...props,
* formattedDate,
* }
* },
* ```
*/
onRenderNote: OnRenderNoteHandler;
/**
* This handler allows adding some interacivity to the note renderer -
* whenever an input element within the item is changed (for example, when a
* checkbox is clicked, or a text input is changed), this `onChange` handler
* is going to be called.
*
* You can inspect `event.elementId` to know which element had some changes,
* and `event.value` to know the new value. `event.noteId` also tells you
* what note is affected, so that you can potentially apply changes to it.
*
* You specify the element ID, by setting a `data-id` attribute on the
* input.
*
* For example, if you have such a template:
*
* ```html
* <div>
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
* </div>
* ```
*
* The event handler will receive an event with `elementId` set to
* `noteTitleInput`.
*/
onChange?: OnChangeHandler;
}

View File

@@ -0,0 +1,748 @@
/* eslint-disable multiline-comment-style */
// =================================================================
// Command API types
// =================================================================
export interface Command {
/**
* Name of command - must be globally unique
*/
name: string;
/**
* Label to be displayed on menu items or keyboard shortcut editor for example.
* If it is missing, it's assumed it's a private command, to be called programmatically only.
* In that case the command will not appear in the shortcut editor or command panel, and logically
* should not be used as a menu item.
*/
label?: string;
/**
* Icon to be used on toolbar buttons for example
*/
iconName?: string;
/**
* Code to be ran when the command is executed. It may return a result.
*/
execute(...args: any[]): Promise<any | void>;
/**
* Defines whether the command should be enabled or disabled, which in turns
* affects the enabled state of any associated button or menu item.
*
* The condition should be expressed as a "when-clause" (as in Visual Studio
* Code). It's a simple boolean expression that evaluates to `true` or
* `false`. It supports the following operators:
*
* Operator | Symbol | Example
* -- | -- | --
* Equality | == | "editorType == markdown"
* Inequality | != | "currentScreen != config"
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder"
*
* Joplin, unlike VSCode, also supports parentheses, which allows creating
* more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
* level of parentheses is possible (nested ones aren't supported).
*
* Currently the supported context variables aren't documented, but you can
* find the list below:
*
* - [Global When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts)
* - [Desktop app When Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts)
*
* Note: Commands are enabled by default unless you use this property.
*/
enabledCondition?: string;
}
// =================================================================
// Interop API types
// =================================================================
export enum FileSystemItem {
File = 'file',
Directory = 'directory',
}
export enum ImportModuleOutputFormat {
Markdown = 'md',
Html = 'html',
}
/**
* Used to implement a module to export data from Joplin. [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/json_export) for an example.
*
* In general, all the event handlers you'll need to implement take a `context` object as a first argument. This object will contain the export or import path as well as various optional properties, such as which notes or notebooks need to be exported.
*
* To get a better sense of what it will contain it can be useful to print it using `console.info(context)`.
*/
export interface ExportModule {
/**
* The format to be exported, eg "enex", "jex", "json", etc.
*/
format: string;
/**
* The description that will appear in the UI, for example in the menu item.
*/
description: string;
/**
* Whether the module will export a single file or multiple files in a directory. It affects the open dialog that will be presented to the user when using your exporter.
*/
target: FileSystemItem;
/**
* Only applies to single file exporters or importers
* It tells whether the format can package multiple notes into one file.
* For example JEX or ENEX can, but HTML cannot.
*/
isNoteArchive: boolean;
/**
* The extensions of the files exported by your module. For example, it is `["htm", "html"]` for the HTML module, and just `["jex"]` for the JEX module.
*/
fileExtensions?: string[];
/**
* Called when the export process starts.
*/
onInit(context: ExportContext): Promise<void>;
/**
* Called when an item needs to be processed. An "item" can be any Joplin object, such as a note, a folder, a notebook, etc.
*/
onProcessItem(context: ExportContext, itemType: number, item: any): Promise<void>;
/**
* Called when a resource file needs to be exported.
*/
onProcessResource(context: ExportContext, resource: any, filePath: string): Promise<void>;
/**
* Called when the export process is done.
*/
onClose(context: ExportContext): Promise<void>;
}
export interface ImportModule {
/**
* The format to be exported, eg "enex", "jex", "json", etc.
*/
format: string;
/**
* The description that will appear in the UI, for example in the menu item.
*/
description: string;
/**
* Only applies to single file exporters or importers
* It tells whether the format can package multiple notes into one file.
* For example JEX or ENEX can, but HTML cannot.
*/
isNoteArchive: boolean;
/**
* The type of sources that are supported by the module. Tells whether the module can import files or directories or both.
*/
sources: FileSystemItem[];
/**
* Tells the file extensions of the exported files.
*/
fileExtensions?: string[];
/**
* Tells the type of notes that will be generated, either HTML or Markdown (default).
*/
outputFormat?: ImportModuleOutputFormat;
/**
* Called when the import process starts. There is only one event handler within which you should import the complete data.
*/
onExec(context: ImportContext): Promise<void>;
}
export interface ExportOptions {
format?: string;
path?: string;
sourceFolderIds?: string[];
sourceNoteIds?: string[];
// modulePath?: string;
target?: FileSystemItem;
}
export interface ExportContext {
destPath: string;
options: ExportOptions;
/**
* You can attach your own custom data using this propery - it will then be passed to each event handler, allowing you to keep state from one event to the next.
*/
userData?: any;
}
export interface ImportContext {
sourcePath: string;
options: any;
warnings: string[];
}
// =================================================================
// Misc types
// =================================================================
export interface Script {
onStart?(event: any): Promise<void>;
}
export interface Disposable {
// dispose():void;
}
export enum ModelType {
Note = 1,
Folder = 2,
Setting = 3,
Resource = 4,
Tag = 5,
NoteTag = 6,
Search = 7,
Alarm = 8,
MasterKey = 9,
ItemChange = 10,
NoteResource = 11,
ResourceLocalState = 12,
Revision = 13,
Migration = 14,
SmartFilter = 15,
Command = 16,
}
export interface VersionInfo {
version: string;
profileVersion: number;
syncVersion: number;
}
// =================================================================
// Menu types
// =================================================================
export interface CreateMenuItemOptions {
accelerator: string;
}
export enum MenuItemLocation {
File = 'file',
Edit = 'edit',
View = 'view',
Note = 'note',
Tools = 'tools',
Help = 'help',
/**
* @deprecated Do not use - same as NoteListContextMenu
*/
Context = 'context',
// If adding an item here, don't forget to update isContextMenuItemLocation()
/**
* When a command is called from the note list context menu, the
* command will receive the following arguments:
*
* - `noteIds:string[]`: IDs of the notes that were right-clicked on.
*/
NoteListContextMenu = 'noteListContextMenu',
EditorContextMenu = 'editorContextMenu',
/**
* When a command is called from a folder context menu, the
* command will receive the following arguments:
*
* - `folderId:string`: ID of the folder that was right-clicked on
*/
FolderContextMenu = 'folderContextMenu',
/**
* When a command is called from a tag context menu, the
* command will receive the following arguments:
*
* - `tagId:string`: ID of the tag that was right-clicked on
*/
TagContextMenu = 'tagContextMenu',
}
export function isContextMenuItemLocation(location: MenuItemLocation): boolean {
return [
MenuItemLocation.Context,
MenuItemLocation.NoteListContextMenu,
MenuItemLocation.EditorContextMenu,
MenuItemLocation.FolderContextMenu,
MenuItemLocation.TagContextMenu,
].includes(location);
}
export interface MenuItem {
/**
* Command that should be associated with the menu item. All menu item should
* have a command associated with them unless they are a sub-menu.
*/
commandName?: string;
/**
* Arguments that should be passed to the command. They will be as rest
* parameters.
*/
commandArgs?: any[];
/**
* Set to "separator" to create a divider line
*/
type?: ('normal' | 'separator' | 'submenu' | 'checkbox' | 'radio');
/**
* Accelerator associated with the menu item
*/
accelerator?: string;
/**
* Menu items that should appear below this menu item. Allows creating a menu tree.
*/
submenu?: MenuItem[];
/**
* Menu item label. If not specified, the command label will be used instead.
*/
label?: string;
}
// =================================================================
// View API types
// =================================================================
export interface ButtonSpec {
id: ButtonId;
title?: string;
onClick?(): void;
}
export type ButtonId = string;
export enum ToolbarButtonLocation {
/**
* This toolbar in the top right corner of the application. It applies to the note as a whole, including its metadata.
*/
NoteToolbar = 'noteToolbar',
/**
* This toolbar is right above the text editor. It applies to the note body only.
*/
EditorToolbar = 'editorToolbar',
}
export type ViewHandle = string;
export interface EditorCommand {
name: string;
value?: any;
}
export interface DialogResult {
id: ButtonId;
formData?: any;
}
export interface Size {
width?: number;
height?: number;
}
export interface Rectangle {
x?: number;
y?: number;
width?: number;
height?: number;
}
// =================================================================
// Settings types
// =================================================================
export enum SettingItemType {
Int = 1,
String = 2,
Bool = 3,
Array = 4,
Object = 5,
Button = 6,
}
export enum SettingItemSubType {
FilePathAndArgs = 'file_path_and_args',
FilePath = 'file_path', // Not supported on mobile!
DirectoryPath = 'directory_path', // Not supported on mobile!
}
export enum AppType {
Desktop = 'desktop',
Mobile = 'mobile',
Cli = 'cli',
}
export enum SettingStorage {
Database = 1,
File = 2,
}
// Redefine a simplified interface to mask internal details
// and to remove function calls as they would have to be async.
export interface SettingItem {
value: any;
type: SettingItemType;
/**
* Currently only used to display a file or directory selector. Always set
* `type` to `SettingItemType.String` when using this property.
*/
subType?: SettingItemSubType;
label: string;
description?: string;
/**
* A public setting will appear in the Configuration screen and will be
* modifiable by the user. A private setting however will not appear there,
* and can only be changed programmatically. You may use this to store some
* values that you do not want to directly expose.
*/
public: boolean;
/**
* You would usually set this to a section you would have created
* specifically for the plugin.
*/
section?: string;
/**
* To create a setting with multiple options, set this property to `true`.
* That setting will render as a dropdown list in the configuration screen.
*/
isEnum?: boolean;
/**
* This property is required when `isEnum` is `true`. In which case, it
* should contain a map of value => label.
*/
options?: Record<any, any>;
/**
* Reserved property. Not used at the moment.
*/
appTypes?: AppType[];
/**
* Set this to `true` to store secure data, such as passwords. Any such
* setting will be stored in the system keychain if one is available.
*/
secure?: boolean;
/**
* An advanced setting will be moved under the "Advanced" button in the
* config screen.
*/
advanced?: boolean;
/**
* Set the min, max and step values if you want to restrict an int setting
* to a particular range.
*/
minimum?: number;
maximum?: number;
step?: number;
/**
* Either store the setting in the database or in settings.json. Defaults to database.
*/
storage?: SettingStorage;
}
export interface SettingSection {
label: string;
iconName?: string;
description?: string;
name?: string;
}
// =================================================================
// Data API types
// =================================================================
/**
* An array of at least one element and at most three elements.
*
* - **[0]**: Resource name (eg. "notes", "folders", "tags", etc.)
* - **[1]**: (Optional) Resource ID.
* - **[2]**: (Optional) Resource link.
*/
export type Path = string[];
// =================================================================
// Content Script types
// =================================================================
export type PostMessageHandler = (message: any)=> Promise<any>;
/**
* When a content script is initialised, it receives a `context` object.
*/
export interface ContentScriptContext {
/**
* The plugin ID that registered this content script
*/
pluginId: string;
/**
* The content script ID, which may be necessary to post messages
*/
contentScriptId: string;
/**
* Can be used by CodeMirror content scripts to post a message to the plugin
*/
postMessage: PostMessageHandler;
}
export interface ContentScriptModuleLoadedEvent {
userData?: any;
}
export interface ContentScriptModule {
onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
plugin: ()=> any;
assets?: ()=> void;
}
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (markdownIt: any, options: any)=> any;
}
type EditorCommandCallback = (...args: any[])=> any;
export interface CodeMirrorControl {
/** Points to a CodeMirror 6 EditorView instance. */
editor: any;
cm6: any;
/** `extension` should be a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension). */
addExtension(extension: any|any[]): void;
supportsCommand(name: string): boolean;
execCommand(name: string, ...args: any[]): any;
registerCommand(name: string, callback: EditorCommandCallback): void;
joplinExtensions: {
/**
* Returns a [CodeMirror 6 extension](https://codemirror.net/docs/ref/#state.Extension) that
* registers the given [CompletionSource](https://codemirror.net/docs/ref/#autocomplete.CompletionSource).
*
* Use this extension rather than the built-in CodeMirror [`autocompletion`](https://codemirror.net/docs/ref/#autocomplete.autocompletion)
* if you don't want to use [langaugeData-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override).
*
* Using `autocompletion({ override: [ ... ]})` causes errors when done by multiple plugins.
*/
completionSource(completionSource: any): any;
/**
* Creates an extension that enables or disables [`languageData`-based autocompletion](https://codemirror.net/docs/ref/#autocomplete.autocompletion^config.override).
*/
enableLanguageDataAutocomplete: { of: (enabled: boolean)=> any };
};
}
export interface MarkdownEditorContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (editorControl: CodeMirrorControl)=> void;
}
export enum ContentScriptType {
/**
* Registers a new Markdown-It plugin, which should follow the template
* below.
*
* ```javascript
* module.exports = {
* default: function(context) {
* return {
* plugin: function(markdownIt, pluginOptions) {
* // ...
* },
* assets: {
* // ...
* },
* }
* }
* }
* ```
*
* See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* for a simple Markdown-it plugin example.
*
* ## Exported members
*
* - The `context` argument is currently unused but could be used later on
* to provide access to your own plugin so that the content script and
* plugin can communicate.
*
* - The **required** `plugin` key is the actual Markdown-It plugin - check
* the [official doc](https://github.com/markdown-it/markdown-it) for more
* information.
*
* - Using the **optional** `assets` key you may specify assets such as JS
* or CSS that should be loaded in the rendered HTML document. Check for
* example the Joplin [Mermaid
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured.
*
* ## Getting the settings from the renderer
*
* You can access your plugin settings from the renderer by calling
* `pluginOptions.settingValue("your-setting-key')`.
*
* ## Posting messages from the content script to your plugin
*
* The application provides the following function to allow executing
* commands from the rendered HTML code:
*
* ```javascript
* const response = await webviewApi.postMessage(contentScriptId, message);
* ```
*
* - `contentScriptId` is the ID you've defined when you registered the
* content script. You can retrieve it from the
* {@link ContentScriptContext | context}.
* - `message` can be any basic JavaScript type (number, string, plain
* object), but it cannot be a function or class instance.
*
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
*
* ## Registering an existing Markdown-it plugin
*
* To include a regular Markdown-It plugin, that doesn't make use of any
* Joplin-specific features, you would simply create a file such as this:
*
* ```javascript
* module.exports = {
* default: function(context) {
* return {
* plugin: require('markdown-it-toc-done-right');
* }
* }
* }
* ```
*/
MarkdownItPlugin = 'markdownItPlugin',
/**
* Registers a new CodeMirror plugin, which should follow the template
* below.
*
* ```javascript
* module.exports = {
* default: function(context) {
* return {
* plugin: function(CodeMirror) {
* // ...
* },
* codeMirrorResources: [],
* codeMirrorOptions: {
* // ...
* },
* assets: {
* // ...
* },
* }
* }
* }
* ```
*
* - The `context` argument is currently unused but could be used later on
* to provide access to your own plugin so that the content script and
* plugin can communicate.
*
* - The `plugin` key is your CodeMirror plugin. This is where you can
* register new commands with CodeMirror or interact with the CodeMirror
* instance as needed.
*
* - The `codeMirrorResources` key is an array of CodeMirror resources that
* will be loaded and attached to the CodeMirror module. These are made up
* of addons, keymaps, and modes. For example, for a plugin that want's to
* enable clojure highlighting in code blocks. `codeMirrorResources` would
* be set to `['mode/clojure/clojure']`.
*
* - The `codeMirrorOptions` key contains all the
* [CodeMirror](https://codemirror.net/doc/manual.html#config) options
* that will be set or changed by this plugin. New options can alse be
* declared via
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
* and then have their value set here. For example, a plugin that enables
* line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
*
* - Using the **optional** `assets` key you may specify **only** CSS assets
* that should be loaded in the rendered HTML document. Check for example
* the Joplin [Mermaid
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured.
*
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
* must be provided for the plugin to be valid. Having multiple or all
* provided is also okay.
*
* See also the [demo
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
* for an example of all these keys being used in one plugin.
*
* ## Posting messages from the content script to your plugin
*
* In order to post messages to the plugin, you can use the postMessage
* function passed to the {@link ContentScriptContext | context}.
*
* ```javascript
* const response = await context.postMessage('messageFromCodeMirrorContentScript');
* ```
*
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
*
*/
CodeMirrorPlugin = 'codeMirrorPlugin',
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
{
"name": "joplin-plugin-worker-example",
"version": "1.0.0",
"scripts": {
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && npm run copyAssets && webpack --env joplin-plugin-config=createArchive",
"copyAssets": "node tools/copyAssets.js",
"prepare": "npm run dist",
"updateVersion": "webpack --env joplin-plugin-config=updateVersion",
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force"
},
"license": "MIT",
"keywords": [
"joplin-plugin"
],
"files": [
"publish"
],
"devDependencies": {
"@types/node": "^18.7.13",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^11.0.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"tar": "^6.1.11",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"overrides": {
"@xenova/transformers": {
"sharp": "0.33.4"
}
},
"dependencies": {
"@xenova/transformers": "^2.17.2"
}
}

View File

@@ -0,0 +1,6 @@
{
"extraScripts": [ "worker.ts" ],
"webpackOverrides": {
"target": "web"
}
}

View File

@@ -0,0 +1,39 @@
import joplin from 'api';
import { ToolbarButtonLocation } from 'api/types';
joplin.plugins.register({
onStart: async function() {
const worker = new Worker(`${await joplin.plugins.installationDir()}/worker.js`);
worker.onmessage = event => {
if ('response' in event.data) {
alert(
JSON.stringify(event.data.response)
);
}
};
joplin.commands.register({
name: 'classifySelection',
label: 'Run sentiment analysis on selected text',
iconName: 'fas fa-robot',
execute: async () => {
const selectedText = await joplin.commands.execute('selectedText');
if (!selectedText) {
alert('No text selected!');
return;
}
worker.postMessage({
type: 'classify',
text: selectedText,
});
},
});
await joplin.views.toolbarButtons.create(
'classifySelectionButton',
'classifySelection',
ToolbarButtonLocation.EditorToolbar,
);
},
});

View File

@@ -0,0 +1,15 @@
{
"manifest_version": 1,
"id": "com.example.worker",
"app_min_version": "3.0",
"version": "1.0.0",
"name": "Web Worker",
"description": "Shows how to load a web worker from a plugin.",
"author": "",
"homepage_url": "",
"repository_url": "",
"keywords": [],
"categories": [],
"screenshots": [],
"icons": {}
}

View File

@@ -0,0 +1,30 @@
// @ts-ignore -- no types available for transformers.js
import { pipeline, env } from '@xenova/transformers';
// Use code similar to the following to use local models
// See https://github.com/xenova/transformers.js?tab=readme-ov-file#settings
// env.localModelPath = './models/';
// env.allowRemoteModels = false;
// Don't fetch ONNX WASM from the internet (see tools/copyModels.js).
env.backends.onnx.wasm.wasmPaths = './onnx-dist/';
let pipe: (text: string)=>Promise<unknown>;
const classify = async (text: string) => {
pipe ??= await pipeline('sentiment-analysis');
const classification = await pipe(text);
console.debug('classified', text, 'as', classification);
return classification;
};
self.addEventListener('message', async (event) => {
console.debug('got message', event.data);
if (event.data.type === 'classify') {
postMessage({
response: await classify(event.data.text),
});
}
});

View File

@@ -0,0 +1,12 @@
const fs = require('fs-extra');
const path = require('path');
// Copy WASM files
// See https://github.com/xenova/transformers.js/issues/367#issuecomment-1777743129
const onnxDistFiles = path.dirname(require.resolve('onnxruntime-web'));
const baseDir = path.dirname(__dirname);
fs.copy(onnxDistFiles, path.join(baseDir, 'dist', 'onnx-dist'));
// Predownload model files -- TODO.
// See https://huggingface.co/docs/transformers.js/custom_usage#use-custom-models

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"outDir": "./dist/",
"module": "commonjs",
"target": "es2015",
"jsx": "react",
"allowJs": true,
"baseUrl": "."
}
}

View File

@@ -0,0 +1,403 @@
// -----------------------------------------------------------------------------
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
// is recommended not to edit this file as it would be overwritten when updating
// the plugin framework. If you do make some changes, consider using an external
// JS file and requiring it here to minimize the changes. That way when you
// update, you can easily restore the functionality you've added.
// -----------------------------------------------------------------------------
/* eslint-disable no-console */
const path = require('path');
const crypto = require('crypto');
const fs = require('fs-extra');
const chalk = require('chalk');
const CopyPlugin = require('copy-webpack-plugin');
const tar = require('tar');
const glob = require('glob');
const execSync = require('child_process').execSync;
// AUTO-GENERATED by updateCategories
const allPossibleCategories = [{ 'name': 'appearance' }, { 'name': 'developer tools' }, { 'name': 'productivity' }, { 'name': 'themes' }, { 'name': 'integrations' }, { 'name': 'viewer' }, { 'name': 'search' }, { 'name': 'tags' }, { 'name': 'editor' }, { 'name': 'files' }, { 'name': 'personal knowledge management' }];
// AUTO-GENERATED by updateCategories
const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish');
const userConfig = { extraScripts: [], ...(fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {}) };
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const allPossibleScreenshotsType = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
const { builtinModules } = require('node:module');
// Webpack5 doesn't polyfill by default and displays a warning when attempting to require() built-in
// node modules. Set these to false to prevent Webpack from warning about not polyfilling these modules.
// We don't need to polyfill because the plugins run in Electron's Node environment.
const moduleFallback = {};
for (const moduleName of builtinModules) {
moduleFallback[moduleName] = false;
}
const getPackageJson = () => {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
};
function validatePackageJson() {
const content = getPackageJson();
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
console.warn(chalk.yellow(`WARNING: To publish the plugin, the package name should start with "joplin-plugin-" (found "${content.name}") in ${packageJsonPath}`));
}
if (!content.keywords || content.keywords.indexOf('joplin-plugin') < 0) {
console.warn(chalk.yellow(`WARNING: To publish the plugin, the package keywords should include "joplin-plugin" (found "${JSON.stringify(content.keywords)}") in ${packageJsonPath}`));
}
if (content.scripts && content.scripts.postinstall) {
console.warn(chalk.yellow(`WARNING: package.json contains a "postinstall" script. It is recommended to use a "prepare" script instead so that it is executed before publish. In ${packageJsonPath}`));
}
}
function fileSha256(filePath) {
const content = fs.readFileSync(filePath);
return crypto.createHash('sha256').update(content).digest('hex');
}
function currentGitInfo() {
try {
let branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
const commit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim();
if (branch === 'HEAD') branch = 'master';
return `${branch}:${commit}`;
} catch (error) {
const messages = error.message ? error.message.split('\n') : [''];
console.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim()));
console.info(chalk.cyan('Git information will not be stored in plugin info file'));
return '';
}
}
function validateCategories(categories) {
if (!categories) return null;
if ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
categories.forEach(category => {
if (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \n${allPossibleCategories.map(category => { return category.name; })}\n`);
});
}
function validateScreenshots(screenshots) {
if (!screenshots) return null;
for (const screenshot of screenshots) {
if (!screenshot.src) throw new Error('You must specify a src for each screenshot');
// Avoid attempting to download and verify URL screenshots.
if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) {
continue;
}
const screenshotType = screenshot.src.split('.').pop();
if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`);
const screenshotPath = path.resolve(rootDir, screenshot.src);
// Max file size is 1MB
const fileMaxSize = 1024;
const fileSize = fs.statSync(screenshotPath).size / 1024;
if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`);
}
}
function readManifest(manifestPath) {
const content = fs.readFileSync(manifestPath, 'utf8');
const output = JSON.parse(content);
if (!output.id) throw new Error(`Manifest plugin ID is not set in ${manifestPath}`);
validateCategories(output.categories);
validateScreenshots(output.screenshots);
return output;
}
function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true })
.map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
fs.removeSync(destPath);
tar.create(
{
strict: true,
portable: true,
file: destPath,
cwd: sourceDir,
sync: true,
},
distFiles,
);
console.info(chalk.cyan(`Plugin archive has been created in ${destPath}`));
}
const writeManifest = (manifestPath, content) => {
fs.writeFileSync(manifestPath, JSON.stringify(content, null, '\t'), 'utf8');
};
function createPluginInfo(manifestPath, destPath, jplFilePath) {
const contentText = fs.readFileSync(manifestPath, 'utf8');
const content = JSON.parse(contentText);
content._publish_hash = `sha256:${fileSha256(jplFilePath)}`;
content._publish_commit = currentGitInfo();
writeManifest(destPath, content);
}
function onBuildCompleted() {
try {
fs.removeSync(path.resolve(publishDir, 'index.js'));
createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
}
const baseConfig = {
mode: 'production',
target: 'node',
stats: 'errors-only',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
...userConfig.webpackOverrides,
};
const pluginConfig = { ...baseConfig, entry: './src/index.ts',
resolve: {
alias: {
api: path.resolve(__dirname, 'api'),
},
fallback: moduleFallback,
// JSON files can also be required from scripts so we include this.
// https://github.com/joplin/plugin-bibtex/pull/2
extensions: ['.js', '.tsx', '.ts', '.json'],
},
output: {
filename: 'index.js',
path: distDir,
},
plugins: [
new CopyPlugin({
patterns: [
{
from: '**/*',
context: path.resolve(__dirname, 'src'),
to: path.resolve(__dirname, 'dist'),
globOptions: {
ignore: [
// All TypeScript files are compiled to JS and
// already copied into /dist so we don't copy them.
'**/*.ts',
'**/*.tsx',
],
},
},
],
}),
] };
// These libraries can be included with require(...) or
// joplin.require(...) from content scripts.
const externalContentScriptLibraries = [
'@codemirror/view',
'@codemirror/state',
'@codemirror/search',
'@codemirror/language',
'@codemirror/autocomplete',
'@codemirror/commands',
'@codemirror/highlight',
'@codemirror/lint',
'@codemirror/lang-html',
'@codemirror/lang-markdown',
'@codemirror/language-data',
'@lezer/common',
'@lezer/markdown',
'@lezer/highlight',
];
const extraScriptExternals = {};
for (const library of externalContentScriptLibraries) {
extraScriptExternals[library] = { commonjs: library };
}
const extraScriptConfig = {
...baseConfig,
resolve: {
alias: {
api: path.resolve(__dirname, 'api'),
},
fallback: moduleFallback,
extensions: ['.js', '.tsx', '.ts', '.json'],
},
// We support requiring @codemirror/... libraries through require('@codemirror/...')
externalsType: 'commonjs',
externals: extraScriptExternals,
};
const createArchiveConfig = {
stats: 'errors-only',
entry: './dist/index.js',
resolve: {
fallback: moduleFallback,
},
output: {
filename: 'index.js',
path: publishDir,
},
plugins: [{
apply(compiler) {
compiler.hooks.done.tap('archiveOnBuildListener', onBuildCompleted);
},
}],
};
function resolveExtraScriptPath(name) {
const relativePath = `./src/${name}`;
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
return {
entry: relativePath,
output: {
filename: `${nameNoExt}.js`,
path: distDir,
library: 'default',
libraryTarget: 'commonjs',
libraryExport: 'default',
},
};
}
function buildExtraScriptConfigs(userConfig) {
if (!userConfig.extraScripts.length) return [];
const output = [];
for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveExtraScriptPath(scriptName);
output.push({ ...extraScriptConfig, entry: scriptPaths.entry,
output: scriptPaths.output });
}
return output;
}
const increaseVersion = version => {
try {
const s = version.split('.');
const d = Number(s[s.length - 1]) + 1;
s[s.length - 1] = `${d}`;
return s.join('.');
} catch (error) {
error.message = `Could not parse version number: ${version}: ${error.message}`;
throw error;
}
};
const updateVersion = () => {
const packageJson = getPackageJson();
packageJson.version = increaseVersion(packageJson.version);
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
const manifest = readManifest(manifestPath);
manifest.version = increaseVersion(manifest.version);
writeManifest(manifestPath, manifest);
if (packageJson.version !== manifest.version) {
console.warn(chalk.yellow(`Version numbers have been updated but they do not match: package.json (${packageJson.version}), manifest.json (${manifest.version}). Set them to the required values to get them in sync.`));
}
};
function main(environ) {
const configName = environ['joplin-plugin-config'];
if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
// Webpack configurations run in parallel, while we need them to run in
// sequence, and to do that it seems the only way is to run webpack multiple
// times, with different config each time.
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
// overwritten here by the compiled version. This is by design. The
// result is that JS files that don't need compilation, are simply
// copied to /dist, while those that do need it are correctly compiled.
buildExtraScripts: buildExtraScriptConfigs(userConfig),
// Ths config is for creating the .jpl, which is done via the plugin, so
// it doesn't actually need an entry and output, however webpack won't
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
// directories.
if (configName === 'buildMain') {
fs.removeSync(distDir);
fs.removeSync(publishDir);
fs.mkdirpSync(publishDir);
}
if (configName === 'updateVersion') {
updateVersion();
return [];
}
return configs[configName];
}
module.exports = (env) => {
let exportedConfigs = [];
try {
exportedConfigs = main(env);
} catch (error) {
console.error(error.message);
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
return exportedConfigs;
};

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Joplin Web Clipper [DEV]",
"version": "3.1.0",
"version": "3.1.1",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": {

View File

@@ -18,6 +18,7 @@
"@babel/preset-react": "7.23.3",
"babel-loader": "9.1.3",
"css-loader": "6.8.1",
"fs-extra": "11.2.0",
"html-webpack-plugin": "5.6.0",
"react-redux": "9.0.4",
"redux": "5.0.1",
@@ -2980,6 +2981,20 @@
"flat": "cli.js"
}
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3279,6 +3294,18 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -4217,6 +4244,15 @@
"node": ">=4"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@@ -6579,6 +6615,17 @@
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
"dev": true
},
"fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6780,6 +6827,16 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@@ -7456,6 +7513,12 @@
"integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
"dev": true
},
"universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true
},
"update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",

View File

@@ -18,6 +18,7 @@
"@babel/preset-react": "7.23.3",
"babel-loader": "9.1.3",
"css-loader": "6.8.1",
"fs-extra": "11.2.0",
"html-webpack-plugin": "5.6.0",
"react-redux": "9.0.4",
"redux": "5.0.1",

View File

@@ -24,3 +24,4 @@ build/defaultPlugins/
build/7zip/7za
build/7zip/7za.exe
sentry.properties
downloads/

View File

@@ -1,10 +1,11 @@
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
import { PluginMessage } from './services/plugins/PluginRunner';
// import AutoUpdaterService from './services/autoUpdater/AutoUpdaterService';
import shim from '@joplin/lib/shim';
import AutoUpdaterService, { defaultUpdateInterval, initialUpdateStartup } from './services/autoUpdater/AutoUpdaterService';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { BrowserWindow, Tray, screen } from 'electron';
import { BrowserWindow, Tray, WebContents, screen } from 'electron';
import bridge from './bridge';
const url = require('url');
const path = require('path');
@@ -42,8 +43,9 @@ export default class ElectronAppWrapper {
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
// private updaterService_: AutoUpdaterService = null;
private updaterService_: AutoUpdaterService = null;
private customProtocolHandler_: CustomProtocolHandler = null;
private updatePollInterval_: ReturnType<typeof setTimeout>|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public constructor(electronApp: any, env: string, profilePath: string|null, isDebugMode: boolean, initialCallbackUrl: string) {
@@ -230,14 +232,35 @@ export default class ElectronAppWrapper {
}, 3000);
}
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
this.win_.webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
const addWindowEventHandlers = (webContents: WebContents) => {
// will-frame-navigate is fired by clicking on a link within the BrowserWindow.
webContents.on('will-frame-navigate', event => {
// If the link changes the URL of the browser window,
if (event.isMainFrame) {
event.preventDefault();
void bridge().openExternal(event.url);
}
});
// Override calls to window.open and links with target="_blank": Open most in a browser instead
// of Electron:
webContents.setWindowOpenHandler((event) => {
if (event.url === 'about:blank') {
// Script-controlled pages: Used for opening notes in new windows
return {
action: 'allow',
};
} else if (event.url.match(/^https?:\/\//)) {
void bridge().openExternal(event.url);
}
return { action: 'deny' };
});
webContents.on('did-create-window', (event) => {
addWindowEventHandlers(event.webContents);
});
};
addWindowEventHandlers(this.win_.webContents);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
this.win_.on('close', (event: any) => {
@@ -326,6 +349,14 @@ export default class ElectronAppWrapper {
}
});
ipcMain.on('apply-update-now', () => {
this.updaterService_.updateApp();
});
ipcMain.on('check-for-updates', () => {
void this.updaterService_.checkForUpdates(true);
});
// Let us register listeners on the window, so we can update the state
// automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state
@@ -358,6 +389,7 @@ export default class ElectronAppWrapper {
}
public quit() {
this.stopPeriodicUpdateCheck();
this.electronApp_.quit();
}
@@ -462,6 +494,32 @@ export default class ElectronAppWrapper {
this.customProtocolHandler_ ??= handleCustomProtocols(logger);
}
// Electron's autoUpdater has to be init from the main process
public initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
if (shim.isWindows() || shim.isMac()) {
if (!this.updaterService_) {
this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases);
this.startPeriodicUpdateCheck();
}
}
}
private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => {
this.stopPeriodicUpdateCheck();
this.updatePollInterval_ = setInterval(() => {
void this.updaterService_.checkForUpdates(false);
}, updateInterval);
setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup);
};
private stopPeriodicUpdateCheck = (): void => {
if (this.updatePollInterval_) {
clearInterval(this.updatePollInterval_);
this.updatePollInterval_ = null;
this.updaterService_ = null;
}
};
public getCustomProtocolHandler() {
return this.customProtocolHandler_;
}
@@ -476,13 +534,6 @@ export default class ElectronAppWrapper {
this.createWindow();
// TODO: Disabled for now - needs to be behind a feature flag
// if (!shim.isLinux()) {
// this.updaterService_ = new AutoUpdaterService();
// this.updaterService_.startPeriodicUpdateCheck();
// }
this.electronApp_.on('before-quit', () => {
this.willQuitApp_ = true;
});

View File

@@ -2,13 +2,14 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
const sha512 = require('js-sha512');
const distDirName = 'dist';
const distPath = path.join(__dirname, distDirName);
const generateChecksumFile = () => {
if (os.platform() !== 'linux') {
return []; // SHA-512 is only for AppImage
return; // SHA-512 is only for AppImage
}
const distDirName = 'dist';
const distPath = path.join(__dirname, distDirName);
let appImageName = '';
const files = fs.readdirSync(distPath);
for (const key in files) {
@@ -27,7 +28,13 @@ const generateChecksumFile = () => {
const sha512FileName = `${appImageName}.sha512`;
const sha512FilePath = path.join(distPath, sha512FileName);
fs.writeFileSync(sha512FilePath, checksum);
return [sha512FilePath];
return sha512FilePath;
};
exports.default = generateChecksumFile;
const mainHook = () => {
const sha512FilePath = generateChecksumFile();
const outputFiles = [sha512FilePath].filter(item => item);
return outputFiles;
};
exports.default = mainHook;

View File

@@ -5,7 +5,8 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import resourceEditWatcherReducer, { defaultState as resourceEditWatcherDefaultState } from '@joplin/lib/services/ResourceEditWatcher/reducer';
import PluginRunner from './services/plugins/PluginRunner';
import PlatformImplementation from './services/plugins/PlatformImplementation';
import shim from '@joplin/lib/shim';
import type ShimType from '@joplin/lib/shim';
const shim: typeof ShimType = require('@joplin/lib/shim').default;
import AlarmService from '@joplin/lib/services/AlarmService';
import AlarmServiceDriverNode from '@joplin/lib/services/AlarmServiceDriverNode';
import Logger, { TargetType } from '@joplin/utils/Logger';
@@ -126,6 +127,12 @@ class Application extends BaseApplication {
bridge().setLocale(Setting.value('locale'));
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'renderer.fileUrls' || action.type === 'SETTING_UPDATE_ALL') {
bridge().electronApp().getCustomProtocolHandler().setMediaAccessEnabled(
Setting.value('renderer.fileUrls'),
);
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'showTrayIcon' || action.type === 'SETTING_UPDATE_ALL') {
this.updateTray();
}
@@ -403,6 +410,16 @@ class Application extends BaseApplication {
eventManager.on(EventName.ResourceChange, handleResourceChange);
}
private setupAutoUpdaterService() {
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
bridge().electronApp().initializeAutoUpdaterService(
Logger.create('AutoUpdaterService'),
Setting.value('env') === 'dev',
Setting.value('autoUpdate.includePreReleases'),
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public async start(argv: string[], startOptions: StartOptions = null): Promise<any> {
// If running inside a package, the command line, instead of being "node.exe <path> <flags>" is "joplin.exe <flags>" so
@@ -438,6 +455,8 @@ class Application extends BaseApplication {
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
this.setupAutoUpdaterService();
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
AlarmService.setLogger(reg.logger());
@@ -569,17 +588,19 @@ class Application extends BaseApplication {
// Note: Auto-update is a misnomer in the code.
// The code below only checks, if a new version is available.
// We only allow Windows and macOS users to automatically check for updates
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
};
if (!Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
};
// Initial check on startup
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
// Then every x hours
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
// Initial check on startup
shim.setTimeout(() => { runAutoUpdateCheck(); }, 5000);
// Then every x hours
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
}
}
initializeUserFetcher();

View File

@@ -4,14 +4,18 @@ export const declaration: CommandDeclaration = {
name: 'focusElement',
};
export interface FocusElementOptions {
moveCursorToStart: boolean;
}
export const runtime = (): CommandRuntime => {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
execute: async (_context: any, target: string) => {
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody');
execute: async (_context: any, target: string, options?: FocusElementOptions) => {
if (target === 'noteBody') return CommandService.instance().execute('focusElementNoteBody', options);
if (target === 'noteList') return CommandService.instance().execute('focusElementNoteList');
if (target === 'sideBar') return CommandService.instance().execute('focusElementSideBar');
if (target === 'noteTitle') return CommandService.instance().execute('focusElementNoteTitle');
if (target === 'noteTitle') return CommandService.instance().execute('focusElementNoteTitle', options);
throw new Error(`Invalid focus target: ${target}`);
},
};

View File

@@ -234,7 +234,7 @@ export default function(props: Props) {
return (
<CellFooter>
<NeedUpgradeMessage>
{PluginService.instance().describeIncompatibility(props.manifest)}
{PluginService.instance().describeIncompatibility(item.manifest)}
</NeedUpgradeMessage>
</CellFooter>
);

View File

@@ -116,7 +116,7 @@ const EncryptionConfigScreen = (props: Props) => {
);
} else {
return (
<td style={missingPasswordCellStyle}>
<td style={passwordChecks[masterKeyId] ? theme.textStyle : missingPasswordCellStyle}>
<input
type="password"
placeholder={_('Enter password')}

View File

@@ -2,7 +2,6 @@
import * as addProfile from './addProfile';
import * as commandPalette from './commandPalette';
import * as deleteFolder from './deleteFolder';
import * as deleteNote from './deleteNote';
import * as duplicateNote from './duplicateNote';
import * as editAlarm from './editAlarm';
import * as exportPdf from './exportPdf';
@@ -20,7 +19,6 @@ import * as openItem from './openItem';
import * as openNote from './openNote';
import * as openPdfViewer from './openPdfViewer';
import * as openTag from './openTag';
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
import * as print from './print';
import * as renameFolder from './renameFolder';
import * as renameTag from './renameTag';
@@ -52,7 +50,6 @@ const index: any[] = [
addProfile,
commandPalette,
deleteFolder,
deleteNote,
duplicateNote,
editAlarm,
exportPdf,
@@ -70,7 +67,6 @@ const index: any[] = [
openNote,
openPdfViewer,
openTag,
permanentlyDeleteNote,
print,
renameFolder,
renameTag,

View File

@@ -1,7 +1,13 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import Folder from '@joplin/lib/models/Folder';
import Folder, { FolderEntityWithChildren } from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem';
import { ModelType } from '@joplin/lib/BaseModel';
import Logger from '@joplin/utils/Logger';
import shim from '@joplin/lib/shim';
const logger = Logger.create('commands/moveToFolder');
export const declaration: CommandDeclaration = {
name: 'moveToFolder',
@@ -11,19 +17,44 @@ export const declaration: CommandDeclaration = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async (context: CommandContext, noteIds: string[] = null) => {
noteIds = noteIds || context.state.selectedNoteIds;
execute: async (context: CommandContext, itemIds: string[] = null) => {
itemIds = itemIds || context.state.selectedNoteIds;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const folders: any[] = await Folder.sortFolderTree();
let allAreFolders = true;
const itemIdToType = new Map<string, ModelType>();
for (const id of itemIds) {
const item = await BaseItem.loadItemById(id);
itemIdToType.set(id, item.type_);
if (item.type_ !== ModelType.Folder) {
allAreFolders = false;
}
}
const folders = await Folder.sortFolderTree();
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const startFolders: any[] = [];
const maxDepth = 15;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const addOptions = (folders: any[], depth: number) => {
// It's okay for folders (but not notes) to have no parent folder:
if (allAreFolders) {
startFolders.push({
key: '',
value: '',
label: _('None'),
indentDepth: 0,
});
}
const addOptions = (folders: FolderEntityWithChildren[], depth: number) => {
for (let i = 0; i < folders.length; i++) {
const folder = folders[i];
// Disallow making a folder a subfolder of itself.
if (itemIdToType.has(folder.id)) {
continue;
}
startFolders.push({ key: folder.id, value: folder.id, label: folder.title, indentDepth: depth });
if (folder.children) addOptions(folder.children, (depth + 1) < maxDepth ? depth + 1 : maxDepth);
}
@@ -40,8 +71,25 @@ export const runtime = (comp: any): CommandRuntime => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onClose: async (answer: any) => {
if (answer) {
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], answer.value);
try {
const targetFolderId = answer.value;
for (const id of itemIds) {
if (id === targetFolderId) {
continue;
}
const itemType = itemIdToType.get(id);
if (itemType === ModelType.Note) {
await Note.moveToFolder(id, targetFolderId);
} else if (itemType === ModelType.Folder) {
await Folder.moveToFolder(id, targetFolderId);
} else {
throw new Error(`Cannot move item with type ${itemType}`);
}
}
} catch (error) {
logger.error('Error moving items', error);
void shim.showMessageBox(`Error: ${error}`);
}
}
comp.setState({ promptOptions: null });

View File

@@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { EventName } from '@joplin/lib/eventManager';
import { ipcRenderer } from 'electron';
const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;
@@ -575,7 +576,12 @@ function useMenu(props: Props) {
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
function _checkForUpdates() {
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
ipcRenderer.send('check-for-updates');
} else {
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
}
}
function _showAbout() {

View File

@@ -6,7 +6,7 @@ describe('normalizeAccelerator', () => {
['Z', { v6: 'z', v5: 'Z' }],
['Alt+A', { v6: 'Alt-a', v5: 'Alt-A' }],
['Shift+A', { v6: 'Shift-a', v5: 'Shift-A' }],
['Shift+Up', { v6: 'Shift-Up', v5: 'Shift-Up' }],
['Shift+Up', { v6: 'Shift-ArrowUp', v5: 'Shift-Up' }],
])(
'should convert single-letter key names to lowercase for CM6, keep case unchanged for CM5 (%j)',
(original, expected) => {

View File

@@ -12,11 +12,17 @@ const normalizeAccelerator = (accelerator: string, editorVersion: CodeMirrorVers
const parts = command.split(/-(?!$)/);
let name = parts[parts.length - 1];
// In CodeMirror 6, an uppercase single-letter key name makes the editor
// require the shift key to activate the shortcut. If a key name like `Up`,
// however, `.toLowerCase` breaks the shortcut.
if (editorVersion === CodeMirrorVersion.CodeMirror6 && name.length === 1) {
name = name.toLowerCase();
if (editorVersion === CodeMirrorVersion.CodeMirror6) {
// In CodeMirror 6, an uppercase single-letter key name makes the editor
// require the shift key to activate the shortcut. If a key name like `Up`,
// however, `.toLowerCase` breaks the shortcut.
if (name.length === 1) {
name = name.toLowerCase();
}
if (['Up', 'Down', 'Left', 'Right'].includes(name)) {
name = `Arrow${name}`;
}
}
let alt, ctrl, shift, cmd;

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import CodeMirror5Emulation from '@joplin/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation';
@@ -20,16 +20,15 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
const overlayTimeoutRef = useRef(null);
overlayTimeoutRef.current = overlayTimeout;
function clearMarkers() {
const clearMarkers = useCallback(() => {
for (let i = 0; i < markers.length; i++) {
markers[i].clear();
}
setMarkers([]);
}
}, [markers]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function clearOverlay(cm: any) {
const clearOverlay = useCallback((cm: CodeMirror5Emulation) => {
if (overlay) cm.removeOverlay(overlay);
if (scrollbarMarks) {
try {
@@ -47,10 +46,10 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
setOverlay(null);
setScrollbarMarks(null);
setOverlayTimeout(null);
}
}, [scrollbarMarks, overlay, overlayTimeout]);
// Modified from codemirror/addons/search/search.js
function searchOverlay(query: RegExp) {
const searchOverlay = useCallback((query: RegExp) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return { token: function(stream: any) {
query.lastIndex = stream.pos;
@@ -65,13 +64,13 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
}
return null;
} };
}
}, []);
// Highlights the currently active found work
// It's possible to get tricky with this functions and just use findNext/findPrev
// but this is fast enough and works more naturally with the current search logic
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function highlightSearch(cm: any, searchTerm: RegExp, index: number, scrollTo: boolean, withSelection: boolean) {
function highlightSearch(cm: CodeMirror5Emulation, searchTerm: RegExp, index: number, scrollTo: boolean, withSelection: boolean) {
const cursor = cm.getSearchCursor(searchTerm);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -122,6 +121,12 @@ export default function useEditorSearchExtension(CodeMirror: CodeMirror5Emulatio
options = { selectedIndex: 0, searchTimestamp: 0 };
}
if (options.showEditorMarkers === false) {
clearMarkers();
clearOverlay(this);
return;
}
clearMarkers();
// HIGHLIGHT KEYWORDS

View File

@@ -1,6 +1,7 @@
import { RefObject, useEffect } from 'react';
import { RefObject, useEffect, useMemo, useRef } from 'react';
import usePrevious from '../../../../hooks/usePrevious';
import { RenderedBody } from './types';
import { SearchMarkers } from '../../../utils/useSearchMarkers';
const debounce = require('debounce');
interface Props {
@@ -14,14 +15,31 @@ interface Props {
noteContent: string;
renderedBody: RenderedBody;
showEditorMarkers: boolean;
}
const useEditorSearchHandler = (props: Props) => {
const { webviewRef, editorRef, renderedBody, noteContent, searchMarkers } = props;
const {
webviewRef, editorRef, renderedBody, noteContent, searchMarkers, showEditorMarkers,
} = props;
const previousContent = usePrevious(noteContent);
const previousRenderedBody = usePrevious(renderedBody);
const previousSearchMarkers = usePrevious(searchMarkers);
const showEditorMarkersRef = useRef(showEditorMarkers);
showEditorMarkersRef.current = showEditorMarkers;
// Fixes https://github.com/laurent22/joplin/issues/7565
const debouncedMarkers = useMemo(() => debounce((searchMarkers: SearchMarkers) => {
if (!editorRef.current) return;
if (showEditorMarkersRef.current) {
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
props.setLocalSearchResultCount(matches);
} else {
editorRef.current.setMarkers(searchMarkers.keywords, { ...searchMarkers.options, showEditorMarkers: false });
}
}, 50), [editorRef, props.setLocalSearchResultCount]);
useEffect(() => {
if (!searchMarkers) return () => {};
@@ -37,19 +55,7 @@ const useEditorSearchHandler = (props: Props) => {
if (webviewRef.current && (searchMarkers !== previousSearchMarkers || textChanged)) {
webviewRef.current.send('setMarkers', searchMarkers.keywords, searchMarkers.options);
if (editorRef.current) {
// Fixes https://github.com/laurent22/joplin/issues/7565
const debouncedMarkers = debounce(() => {
const matches = editorRef.current.setMarkers(searchMarkers.keywords, searchMarkers.options);
props.setLocalSearchResultCount(matches);
}, 50);
debouncedMarkers();
return () => {
debouncedMarkers.clear();
};
}
debouncedMarkers(searchMarkers);
}
return () => {};
}, [
@@ -62,6 +68,7 @@ const useEditorSearchHandler = (props: Props) => {
previousContent,
previousRenderedBody,
renderedBody,
debouncedMarkers,
]);
};

View File

@@ -686,6 +686,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorRef,
noteContent: props.content,
renderedBody,
showEditorMarkers: true,
});
const cellEditorStyle = useMemo(() => {

View File

@@ -16,7 +16,7 @@ import { MarkupToHtml } from '@joplin/renderer';
const { clipboard } = require('electron');
import { reg } from '@joplin/lib/registry';
import ErrorBoundary from '../../../../ErrorBoundary';
import { EditorKeymap, EditorLanguageType, EditorSettings, UserEventSource } from '@joplin/editor/types';
import { EditorKeymap, EditorLanguageType, EditorSettings, SearchState, UserEventSource } from '@joplin/editor/types';
import useStyles from '../utils/useStyles';
import { EditorEvent, EditorEventType } from '@joplin/editor/events';
import useScrollHandler from '../utils/useScrollHandler';
@@ -27,6 +27,8 @@ import useContextMenu from '../utils/useContextMenu';
import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import Toolbar from '../Toolbar';
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import CommandService from '@joplin/lib/services/CommandService';
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
const logger = Logger.create('CodeMirror6');
const logDebug = (message: string) => logger.debug(message);
@@ -175,6 +177,9 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
}
},
supportsCommand: (name: string) => {
if (name === 'search' && !props.visiblePanes.includes('editor')) {
return false;
}
return name in commands || editorRef.current.supportsCommand(name);
},
execCommand: async (cmd: EditorCommand) => {
@@ -197,7 +202,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return commandOutput;
},
};
}, [props.content, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
}, [props.content, props.visiblePanes, commands, resetScroll, setEditorPercentScroll, setViewerPercentScroll]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
@@ -314,6 +319,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return output;
}, [styles.cellViewer, props.visiblePanes]);
useRefocusOnVisiblePaneChange({ editorRef, webviewRef, visiblePanes: props.visiblePanes });
useEditorSearchHandler({
setLocalSearchResultCount: props.setLocalSearchResultCount,
searchMarkers: props.searchMarkers,
@@ -321,6 +328,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorRef,
noteContent: props.content,
renderedBody,
showEditorMarkers: !props.useLocalSearch,
});
useContextMenu({
@@ -330,6 +338,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
editorClassName: 'cm-editor',
});
const lastSearchState = useRef<SearchState|null>(null);
const onEditorEvent = useCallback((event: EditorEvent) => {
if (event.kind === EditorEventType.Scroll) {
editor_scroll();
@@ -337,8 +346,21 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
codeMirror_change(event.value);
} else if (event.kind === EditorEventType.SelectionRangeChange) {
setSelectionRange({ from: event.from, to: event.to });
} else if (event.kind === EditorEventType.UpdateSearchDialog) {
if (lastSearchState.current?.searchText !== event.searchState.searchText) {
props.setLocalSearch(event.searchState.searchText);
}
if (lastSearchState.current?.dialogVisible !== event.searchState.dialogVisible) {
props.setShowLocalSearch(event.searchState.dialogVisible);
}
lastSearchState.current = event.searchState;
}
}, [editor_scroll, codeMirror_change]);
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
const onSelectPastBeginning = useCallback(() => {
void CommandService.instance().execute('focusElement', 'noteTitle');
}, []);
const editorSettings = useMemo((): EditorSettings => {
const isHTMLNote = props.contentMarkupLanguage === MarkupToHtml.MARKUP_LANGUAGE_HTML;
@@ -356,6 +378,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
katexEnabled: Setting.value('markdown.plugin.katex'),
themeData: {
...styles.globalTheme,
marginLeft: 0,
marginRight: 0,
monospaceFont: Setting.value('style.editor.monospaceFontFamily'),
},
automatchBraces: Setting.value('editor.autoMatchingBraces'),
@@ -389,6 +413,9 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
onEvent={onEditorEvent}
onLogMessage={logDebug}
onEditorPaste={onEditorPaste}
onSelectPastBeginning={onSelectPastBeginning}
externalSearch={props.searchMarkers}
useLocalSearch={props.useLocalSearch}
/>
</div>
);

View File

@@ -13,12 +13,15 @@ import { dirname } from 'path';
import useKeymap from './utils/useKeymap';
import useEditorSearch from '../utils/useEditorSearchExtension';
import CommandService from '@joplin/lib/services/CommandService';
import { SearchMarkers } from '../../../utils/useSearchMarkers';
interface Props extends EditorProps {
style: React.CSSProperties;
pluginStates: PluginStates;
onEditorPaste: (event: Event)=> void;
externalSearch: SearchMarkers;
useLocalSearch: boolean;
}
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
@@ -117,6 +120,25 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Should run just once
}, []);
useEffect(() => {
if (!editor) {
return;
}
const searchState = editor.getSearchState();
const externalSearchText = props.externalSearch.keywords.map(k => k.value).join(' ') || searchState.searchText;
if (externalSearchText === searchState.searchText && searchState.dialogVisible === props.useLocalSearch) {
return;
}
editor.setSearchState({
...searchState,
dialogVisible: props.useLocalSearch,
searchText: externalSearchText,
});
}, [editor, props.externalSearch, props.useLocalSearch]);
const theme = props.settings.themeData;
useEffect(() => {
if (!editor) return () => {};

View File

@@ -1,14 +1,15 @@
import { RefObject, useMemo } from 'react';
import { CommandValue } from '../../../utils/types';
import { CommandValue, DropCommandValue } from '../../../utils/types';
import { commandAttachFileToBody } from '../../../utils/resourceHandling';
import { _ } from '@joplin/lib/locale';
import dialogs from '../../../../dialogs';
import { EditorCommandType } from '@joplin/editor/types';
import { EditorCommandType, UserEventSource } from '@joplin/editor/types';
import Logger from '@joplin/utils/Logger';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupLanguage } from '@joplin/renderer';
import { focus } from '@joplin/lib/utils/focusHandler';
import { FocusElementOptions } from '../../../../../commands/focusElement';
const logger = Logger.create('CodeMirror 6 commands');
@@ -37,13 +38,22 @@ const useEditorCommands = (props: Props) => {
};
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
dropItems: async (cmd: any) => {
dropItems: async (cmd: DropCommandValue) => {
let pos = cmd.pos && editorRef.current.editor.posAtCoords({ x: cmd.pos.clientX, y: cmd.pos.clientY });
if (cmd.type === 'notes') {
editorRef.current.insertText(cmd.markdownTags.join('\n'));
const text = cmd.markdownTags.join('\n');
if ((pos ?? null) !== null) {
editorRef.current.select(pos, pos);
}
editorRef.current.insertText(text, UserEventSource.Drop);
} else if (cmd.type === 'files') {
const pos = props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, { createFileURL: !!cmd.createFileURL, position: pos, markupLanguage: props.contentMarkupLanguage });
pos ??= props.selectionRange.from;
const newBody = await commandAttachFileToBody(props.editorContent, cmd.paths, {
createFileURL: !!cmd.createFileURL,
position: pos,
markupLanguage: props.contentMarkupLanguage,
});
editorRef.current.updateBody(newBody);
} else {
logger.warn('CodeMirror: unsupported drop item: ', cmd);
@@ -87,7 +97,7 @@ const useEditorCommands = (props: Props) => {
editorRef.current.updateBody(newBody);
}
},
textHorizontalRule: () => editorRef.current.insertText('* * *'),
textHorizontalRule: () => editorRef.current.execCommand(EditorCommandType.InsertHorizontalRule),
'editor.execCommand': (value: CommandValue) => {
if (!('args' in value)) value.args = [];
@@ -103,9 +113,15 @@ const useEditorCommands = (props: Props) => {
logger.warn('CodeMirror execCommand: unsupported command: ', value.name);
}
},
'editor.focus': () => {
'editor.focus': (options?: FocusElementOptions) => {
if (props.visiblePanes.indexOf('editor') >= 0) {
focus('useEditorCommands::editor.focus', editorRef.current.editor);
if (options?.moveCursorToStart) {
editorRef.current.editor.dispatch({
selection: { anchor: 0 },
scrollIntoView: true,
});
}
} else {
// If we just call focus() then the iframe is focused,
// but not its content, such that scrolling up / down

View File

@@ -27,6 +27,10 @@ const useKeymap = (editorControl: CodeMirrorControl) => {
binding.accelerator, CodeMirrorVersion.CodeMirror6,
),
run: () => {
if (!CommandService.instance().isEnabled(binding.command)) {
return false;
}
void CommandService.instance().execute(binding.command);
return true;
},

View File

@@ -0,0 +1,44 @@
import { RefObject, useRef, useEffect } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
import NoteTextViewer from '../../../../../NoteTextViewer';
interface Props {
editorRef: RefObject<CodeMirrorControl>;
webviewRef: RefObject<NoteTextViewer>;
visiblePanes: string[];
}
const useRefocusOnVisiblePaneChange = ({ editorRef, webviewRef, visiblePanes }: Props) => {
const lastVisiblePanes = useRef(visiblePanes);
useEffect(() => {
const editorHasFocus = editorRef.current?.cm6?.dom?.contains(document.activeElement);
const viewerHasFocus = webviewRef.current?.hasFocus();
const lastHadViewer = lastVisiblePanes.current.includes('viewer');
const hasViewer = visiblePanes.includes('viewer');
const lastHadEditor = lastVisiblePanes.current.includes('editor');
const hasEditor = visiblePanes.includes('editor');
const viewerJustHidden = lastHadViewer && !hasViewer;
if (viewerJustHidden && viewerHasFocus) {
focus('CodeMirror/refocusEditor1', editorRef.current);
}
// Jump focus to the editor just after showing it -- this assumes that the user
// shows the editor to start editing the note.
const editorJustShown = !lastHadEditor && hasEditor;
if (editorJustShown && viewerHasFocus) {
focus('CodeMirror/refocusEditor2', editorRef.current);
}
const editorJustHidden = lastHadEditor && !hasEditor;
if (editorJustHidden && editorHasFocus) {
focus('CodeMirror/refocusViewer', webviewRef.current);
}
lastVisiblePanes.current = visiblePanes;
}, [visiblePanes, editorRef, webviewRef]);
};
export default useRefocusOnVisiblePaneChange;

View File

@@ -38,6 +38,8 @@ const md5 = require('md5');
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
import { hasProtocol } from '@joplin/utils/url';
import useTabIndenter from './utils/useTabIndenter';
import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
const logger = Logger.create('TinyMCE');
@@ -128,6 +130,8 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
usePluginServiceRegistration(ref);
useContextMenu(editor, props.plugins, props.dispatch, props.htmlToMarkdown, props.markupToHtml);
useTabIndenter(editor);
useKeyboardRefocusHandler(editor);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const dispatchDidUpdate = (editor: any) => {
@@ -216,6 +220,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
editor.insertContent(result.html);
} else if (cmd.name === 'editor.focus') {
focus('TinyMCE::editor.focus', editor);
if (cmd.value?.moveCursorToStart) {
editor.selection.placeCaretAt(0, 0);
editor.selection.setCursorLocation(
editor.dom.root,
0,
);
}
} else if (cmd.name === 'editor.execCommand') {
if (!('ui' in cmd.value)) cmd.value.ui = false;
if (!('value' in cmd.value)) cmd.value.value = null;
@@ -400,6 +411,20 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
color: ${theme.color};
}
.tox .tox-dialog__body-nav-item {
color: ${theme.color};
}
.tox .tox-dialog__body-nav-item[aria-selected=true] {
color: ${theme.color3};
border-color: ${theme.color3};
background-color: ${theme.backgroundColor3};
}
.tox .tox-checkbox__icons .tox-checkbox-icon__unchecked svg {
fill: ${theme.color};
}
.tox .tox-collection--list .tox-collection__item--active {
color: ${theme.backgroundColor};
}
@@ -629,6 +654,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
icons_url: 'gui/NoteEditor/NoteBody/TinyMCE/icons.js',
plugins: 'noneditable link joplinLists hr searchreplace codesample table',
noneditable_noneditable_class: 'joplin-editable', // Can be a regex too
iframe_aria_text: _('Rich Text editor. Press Escape then Tab to escape focus.'),
// #p: Pad empty paragraphs with &nbsp; to prevent them from being removed.
// *[*]: Allow all elements and attributes -- we already filter in sanitize_html
@@ -643,7 +669,6 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// Handle the first table row as table header.
// https://www.tiny.cloud/docs/plugins/table/#table_header_type
table_header_type: 'sectionCells',
table_resize_bars: false,
language_url: ['en_US', 'en_GB'].includes(language) ? undefined : `${bridge().vendorDir()}/lib/tinymce/langs/${language}`,
toolbar: toolbar.join(' '),
localization_function: _,

View File

@@ -0,0 +1,43 @@
import CommandService from '@joplin/lib/services/CommandService';
import { useEffect } from 'react';
import type { Editor, EditorEvent } from 'tinymce';
const useKeyboardRefocusHandler = (editor: Editor) => {
useEffect(() => {
if (!editor) return () => {};
const isAtBeginningOf = (element: Node, parent: HTMLElement) => {
if (!parent.contains(element)) return false;
while (
element &&
element.parentNode !== parent &&
element.parentNode?.firstChild === element
) {
element = element.parentNode;
}
return !!element && element.parentNode?.firstChild === element;
};
const eventHandler = (event: EditorEvent<KeyboardEvent>) => {
if (!event.isDefaultPrevented() && event.key === 'ArrowUp') {
const selection = editor.selection.getRng();
if (selection.startOffset === 0 &&
selection.collapsed &&
isAtBeginningOf(selection.startContainer, editor.dom.getRoot())
) {
event.preventDefault();
void CommandService.instance().execute('focusElement', 'noteTitle');
}
}
};
editor.on('keydown', eventHandler);
return () => {
editor.off('keydown', eventHandler);
};
}, [editor]);
};
export default useKeyboardRefocusHandler;

View File

@@ -0,0 +1,76 @@
import { useEffect } from 'react';
import type { Editor, EditorEvent } from 'tinymce';
const useTabIndenter = (editor: Editor) => {
useEffect(() => {
if (!editor) return () => {};
const canChangeIndentation = () => {
const selectionElement = editor.selection.getNode();
// List items and tables have their own tab key handlers.
return !selectionElement.closest('li, table') && !editor.readonly;
};
const getSpacesBeforeSelectionRange = (maxLength: number) => {
const selectionRange = editor.selection.getRng();
let rangeStart = selectionRange.startOffset;
let outputRange = selectionRange.cloneRange();
while (rangeStart >= 0) {
rangeStart--;
const lastRange = outputRange.cloneRange();
outputRange.setStart(outputRange.startContainer, Math.max(rangeStart, 0));
const rangeContent = outputRange.toString();
const isWhitespace = rangeContent.match(/^\s*$/);
if (!isWhitespace || rangeContent.length > maxLength) {
outputRange = lastRange;
break;
}
}
return outputRange;
};
const indentLengthChars = 8;
let indentHtml = '';
for (let i = 0; i < indentLengthChars; i++) {
indentHtml += '&nbsp;';
}
let lastKeyWasEscape = false;
const eventHandler = (event: EditorEvent<KeyboardEvent>) => {
if (!event.isDefaultPrevented() && event.key === 'Tab' && canChangeIndentation() && !lastKeyWasEscape) {
if (!event.shiftKey) {
editor.execCommand('mceInsertContent', false, indentHtml);
event.preventDefault();
} else {
const selectionRange = editor.selection.getRng();
if (selectionRange.collapsed) {
const spacesRange = getSpacesBeforeSelectionRange(indentLengthChars);
const hasAtLeastOneSpace = spacesRange.toString().match(/^\s+$/);
if (hasAtLeastOneSpace) {
editor.selection.setRng(spacesRange);
editor.execCommand('Delete', false);
event.preventDefault();
}
}
}
} else if (event.key === 'Escape' && !event.shiftKey && !event.altKey && !event.metaKey && !event.ctrlKey) {
// For accessibility, let Escape followed by tab escape the focus trap.
lastKeyWasEscape = true;
} else if (event.key !== 'Shift') { // Allows Esc->Shift+Tab to escape the focus trap.
lastKeyWasEscape = false;
}
};
editor.on('keydown', eventHandler);
return () => {
editor.off('keydown', eventHandler);
};
}, [editor]);
};
export default useTabIndenter;

View File

@@ -218,15 +218,6 @@ function NoteEditor(props: NoteEditorProps) {
}
}, [handleProvisionalFlag, formNote, setFormNote, isNewNote, titleHasBeenManuallyChanged, scheduleNoteListResort, scheduleSaveNote]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
setFormNote,
});
const onDrop = useDropHandler({ editorRef });
const onBodyChange = useCallback((event: OnChangeEvent) => onFieldChange('body', event.content, event.changeId), [onFieldChange]);
@@ -234,6 +225,15 @@ function NoteEditor(props: NoteEditorProps) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleChange = useCallback((event: any) => onFieldChange('title', event.target.value), [onFieldChange]);
useWindowCommandHandler({
dispatch: props.dispatch,
setShowLocalSearch,
noteSearchBarRef,
editorRef,
titleInputRef,
onBodyChange,
});
// const onTitleKeydown = useCallback((event:any) => {
// const keyCode = event.keyCode;
@@ -411,6 +411,9 @@ function NoteEditor(props: NoteEditorProps) {
noteToolbar: null,
onScroll: onScroll,
setLocalSearchResultCount: setLocalSearchResultCount,
setLocalSearch: localSearch_change,
setShowLocalSearch,
useLocalSearch: showLocalSearch,
searchMarkers: searchMarkers,
visiblePanes: props.noteVisiblePanes || ['editor', 'viewer'],
keyboardMode: Setting.value('editor.keyboardMode'),
@@ -506,6 +509,7 @@ function NoteEditor(props: NoteEditorProps) {
onPrevious={localSearch_previous}
onClose={localSearch_close}
visiblePanes={props.noteVisiblePanes}
editorType={props.bodyEditor}
/>
);
}

View File

@@ -1,34 +1,10 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
import { ChangeEvent, useCallback } from 'react';
import { ChangeEvent, useCallback, useRef } from 'react';
import NoteToolbar from '../../NoteToolbar/NoteToolbar';
import { buildStyle } from '@joplin/lib/theme';
import time from '@joplin/lib/time';
import styled from 'styled-components';
const StyledRoot = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${props => props.theme.editorPaddingLeft}px;
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
`;
const InfoGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid ${props => props.theme.dividerColor};
width: 100%;
}
`;
interface Props {
themeId: number;
@@ -75,24 +51,48 @@ function styles_(props: Props) {
});
}
export default function NoteTitleBar(props: Props) {
const styles = styles_(props);
const useReselectHandlers = () => {
const lastTitleFocus = useRef([0, 0]);
const lastTitleValue = useRef('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTitleKeydown = useCallback((event: any) => {
const keyCode = event.keyCode;
const onTitleBlur: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
lastTitleFocus.current = [titleElement.selectionStart, titleElement.selectionEnd];
lastTitleValue.current = titleElement.value;
}, []);
if (keyCode === 9) { // TAB
event.preventDefault();
if (event.shiftKey) {
void CommandService.instance().execute('focusElement', 'noteList');
const onTitleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
// By default, focusing the note title bar can cause its content to become selected. We override
// this with a more reasonable default:
if (titleElement.selectionStart === 0 && titleElement.selectionEnd === titleElement.value.length) {
if (lastTitleValue.current !== titleElement.value) {
titleElement.selectionStart = titleElement.value.length;
} else {
void CommandService.instance().execute('focusElement', 'noteBody');
titleElement.selectionStart = lastTitleFocus.current[0];
titleElement.selectionEnd = lastTitleFocus.current[1];
}
}
}, []);
return { onTitleBlur, onTitleFocus };
};
export default function NoteTitleBar(props: Props) {
const styles = styles_(props);
const onTitleKeydown: React.KeyboardEventHandler<HTMLInputElement> = useCallback((event) => {
const titleElement = event.currentTarget;
const selectionAtEnd = titleElement.selectionEnd === titleElement.value.length;
if ((event.key === 'ArrowDown' && selectionAtEnd) || event.key === 'Enter') {
event.preventDefault();
const moveCursorToStart = event.key === 'ArrowDown';
void CommandService.instance().execute('focusElement', 'noteBody', { moveCursorToStart });
}
}, []);
const { onTitleFocus, onTitleBlur } = useReselectHandlers();
function renderTitleBarDate() {
return <span className="updated-time-label" style={styles.titleDate}>{time.formatMsToLocal(props.noteUserUpdatedTime)}</span>;
}
@@ -106,7 +106,7 @@ export default function NoteTitleBar(props: Props) {
}
return (
<StyledRoot>
<div className='note-title-wrapper'>
<input
className="title-input"
type="text"
@@ -116,12 +116,14 @@ export default function NoteTitleBar(props: Props) {
readOnly={props.disabled}
onChange={props.onTitleChange}
onKeyDown={onTitleKeydown}
onFocus={onTitleFocus}
onBlur={onTitleBlur}
value={props.noteTitle}
/>
<InfoGroup>
<div className='note-title-info-group'>
{renderTitleBarDate()}
{renderNoteToolbar()}
</InfoGroup>
</StyledRoot>
</div>
</div>
);
}

View File

@@ -38,7 +38,6 @@ const incompatiblePluginIds = [
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'plugin.calebjohn.MathMode',
'com.ckant.joplin-plugin-better-code-blocks',
// cSpell:enable
];

View File

@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import { FocusElementOptions } from '../../../commands/focusElement';
export const declaration: CommandDeclaration = {
name: 'focusElementNoteBody',
@@ -10,8 +11,8 @@ export const declaration: CommandDeclaration = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export const runtime = (comp: any): CommandRuntime => {
return {
execute: async () => {
comp.editorRef.current.execCommand({ name: 'editor.focus' });
execute: async (_context: unknown, options?: FocusElementOptions) => {
comp.editorRef.current.execCommand({ name: 'editor.focus', value: options });
},
enabledCondition: 'oneNoteSelected',
};

View File

@@ -1,3 +1,5 @@
@use "./styles/warning-banner.scss";
@use "./styles/warning-banner-link.scss";
@use "./styles/note-title-info-group.scss";
@use "./styles/note-title-wrapper.scss";

View File

@@ -0,0 +1,11 @@
.note-title-info-group {
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 800px) {
border-top: 1px solid var(--joplin-divider-color);
width: 100%;
}
}

View File

@@ -0,0 +1,12 @@
.note-title-wrapper {
display: flex;
flex-direction: row;
align-items: center;
padding-left: var(--joplin-editor-padding-left);
@media (max-width: 800px) {
flex-direction: column;
align-items: flex-start;
}
}

View File

@@ -2,6 +2,7 @@ import { FormNote } from './types';
import HtmlToMd from '@joplin/lib/HtmlToMd';
import Note from '@joplin/lib/models/Note';
import { NoteEntity } from '@joplin/lib/services/database/types';
const { MarkupToHtml } = require('@joplin/renderer');
export async function htmlToMarkdown(markupLanguage: number, html: string, originalCss: string): Promise<string> {
@@ -23,8 +24,7 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
return newBody;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export async function formNoteToNote(formNote: FormNote): Promise<any> {
export async function formNoteToNote(formNote: FormNote): Promise<NoteEntity> {
return {
id: formNote.id,
// Should also include parent_id and deleted_time so that the reducer
@@ -32,6 +32,7 @@ export async function formNoteToNote(formNote: FormNote): Promise<any> {
// https://discourse.joplinapp.org/t/experimental-wysiwyg-editor-in-joplin/6915/57?u=laurent
parent_id: formNote.parent_id,
deleted_time: formNote.deleted_time,
is_conflict: formNote.is_conflict,
title: formNote.title,
body: formNote.body,
};

View File

@@ -1,3 +1,4 @@
import { LinkRenderingType } from '@joplin/renderer/MdToHtml';
import { MarkupToHtmlOptions } from './types';
export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
@@ -7,7 +8,7 @@ export default (override: MarkupToHtmlOptions = null): MarkupToHtmlOptions => {
checkboxRenderingType: 2,
},
link_open: {
linkRenderingType: 2,
linkRenderingType: LinkRenderingType.HrefHandler,
},
},
replaceResourceInternalToExternalLinks: true,

View File

@@ -6,6 +6,7 @@ import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
import { Dispatch } from 'redux';
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
import { DropHandler } from './useDropHandler';
import { SearchMarkers } from './useSearchMarkers';
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
@@ -119,8 +120,11 @@ export interface NoteBodyEditorProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
noteToolbar: any;
setLocalSearchResultCount(count: number): void;
setLocalSearch(search: string): void;
setShowLocalSearch(show: boolean): void;
useLocalSearch: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
searchMarkers: any;
searchMarkers: SearchMarkers;
visiblePanes: string[];
keyboardMode: string;
resourceInfos: ResourceInfos;
@@ -144,6 +148,7 @@ export interface FormNote {
body: string;
parent_id: string;
is_todo: number;
is_conflict?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
bodyEditorContent?: any;
markup_language: number;
@@ -247,3 +252,19 @@ export interface CommandValue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
value?: any; // For TinyMCE only
}
type DropCommandBase = {
pos: {
clientX: number;
clientY: number;
}|undefined;
};
export type DropCommandValue = ({
type: 'notes';
markdownTags: string[];
}|{
type: 'files';
paths: string[];
createFileURL: boolean;
}) & DropCommandBase;

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import Note from '@joplin/lib/models/Note';
import { DragEvent as ReactDragEvent } from 'react';
import { DropCommandValue } from './types';
interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -19,6 +20,11 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
const dt = event.dataTransfer;
const createFileURL = event.altKey;
const eventPosition = {
clientX: event.clientX,
clientY: event.clientY,
};
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids'));
@@ -29,12 +35,15 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
noteMarkdownTags.push(Note.markdownTag(note));
}
const props: DropCommandValue = {
type: 'notes',
pos: eventPosition,
markdownTags: noteMarkdownTags,
};
editorRef.current.execCommand({
name: 'dropItems',
value: {
type: 'notes',
markdownTags: noteMarkdownTags,
},
value: props,
});
};
void dropNotes();
@@ -51,13 +60,16 @@ export default function useDropHandler(dependencies: HookDependencies): DropHand
paths.push(file.path);
}
const props: DropCommandValue = {
type: 'files',
pos: eventPosition,
paths: paths,
createFileURL: createFileURL,
};
editorRef.current.execCommand({
name: 'dropItems',
value: {
type: 'files',
paths: paths,
createFileURL: createFileURL,
},
value: props,
});
return true;
}

View File

@@ -1,9 +1,11 @@
import Note from '@joplin/lib/models/Note';
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
import { setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import useFormNote, { HookDependencies } from './useFormNote';
import shim from '@joplin/lib/shim';
import Resource from '@joplin/lib/models/Resource';
import { join } from 'path';
import { formNoteToNote } from '.';
const defaultFormNoteProps: HookDependencies = {
syncStarted: false,
@@ -81,6 +83,39 @@ describe('useFormNote', () => {
formNote.unmount();
});
// Lacking is_conflict has previously caused UI issues. See https://github.com/laurent22/joplin/pull/10913
// for details.
it('should preserve value of is_conflict on save', async () => {
const testNote = await Note.save({ title: 'Test Note!', is_conflict: 1 });
const makeFormNoteProps = (): HookDependencies => {
return {
...defaultFormNoteProps,
noteId: testNote.id,
};
};
const formNote = renderHook(props => useFormNote(props), {
initialProps: makeFormNoteProps(),
});
await formNote.waitFor(() => {
expect(formNote.result.current.formNote).toMatchObject({
is_conflict: 1,
title: testNote.title,
});
});
// Should preserve is_conflict after save.
expect(await formNoteToNote(formNote.result.current.formNote)).toMatchObject({
is_conflict: 1,
deleted_time: 0,
title: testNote.title,
});
formNote.unmount();
});
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
// it('should reload the note when it is changed outside of the editor', async () => {
@@ -115,7 +150,7 @@ describe('useFormNote', () => {
test('should refresh resource infos when changed outside the editor', async () => {
let note = await Note.save({});
note = await shim.attachFileToNote(note, __filename);
note = await shim.attachFileToNote(note, join(supportDir, 'sample.txt'));
const resourceIds = Note.linkedItemIds(note.body);
const resource = await Resource.load(resourceIds[0]);
@@ -141,12 +176,12 @@ describe('useFormNote', () => {
});
await act(async () => {
await Resource.save({ ...resource, filename: 'test.ts' });
await Resource.save({ ...resource, filename: 'test.txt' });
});
await formNote.waitFor(() => {
const resourceInfo = formNote.result.current.resourceInfos[resource.id];
expect(resourceInfo.item).toMatchObject({
id: resource.id, filename: 'test.ts',
id: resource.id, filename: 'test.txt',
});
});

View File

@@ -101,6 +101,7 @@ export default function useFormNote(dependencies: HookDependencies) {
is_todo: n.is_todo,
parent_id: n.parent_id,
deleted_time: n.deleted_time,
is_conflict: n.is_conflict,
bodyWillChangeId: 0,
bodyChangeId: 0,
markup_language: n.markup_language,

View File

@@ -6,7 +6,8 @@ import shim from '@joplin/lib/shim';
const { themeStyle } = require('@joplin/lib/theme');
import Note from '@joplin/lib/models/Note';
import { MarkupToHtmlOptions } from './types';
import { MarkupToHtmlOptions, ResourceInfos } from './types';
import { resourceFullPath } from '@joplin/lib/models/utils/resourceUtils';
interface HookDependencies {
themeId: number;
@@ -20,12 +21,16 @@ interface HookDependencies {
export default function useMarkupToHtml(deps: HookDependencies) {
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
const resourceBaseUrl = useMemo(() => {
return `joplin-content://note-viewer/${Setting.value('resourceDir')}/`;
}, []);
const markupToHtml = useMemo(() => {
return markupLanguageUtils.newMarkupToHtml(plugins, {
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
resourceBaseUrl,
customCss: customCss || '',
});
}, [plugins, customCss]);
}, [plugins, customCss, resourceBaseUrl]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions = null): Promise<any> => {
@@ -39,7 +44,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
md = md || '';
const theme = themeStyle(themeId);
let resources = {};
let resources: ResourceInfos = {};
if (options.replaceResourceInternalToExternalLinks) {
md = await Note.replaceResourceInternalToExternalLinks(md, { useAbsolutePaths: true });
@@ -58,9 +63,16 @@ export default function useMarkupToHtml(deps: HookDependencies) {
codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
whiteBackgroundNoteRendering,
itemIdToUrl: (id: string, urlParameters = '') => {
if (!(id in resources) || !resources[id]) {
return null;
}
return resourceFullPath(resources[id].item, resourceBaseUrl) + urlParameters;
},
...options,
});
return result;
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, deps.settingValue]);
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, resourceBaseUrl, deps.settingValue]);
}

View File

@@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { FormNote, NoteBodyEditorRef, ScrollOptionTypes } from './types';
import { NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
@@ -12,7 +12,7 @@ const commandsWithDependencies = [
require('../commands/pasteAsText'),
];
type SetFormNoteCallback = (callback: (prev: FormNote)=> FormNote)=> void;
type OnBodyChange = (event: OnChangeEvent)=> void;
interface HookDependencies {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
@@ -23,13 +23,13 @@ interface HookDependencies {
noteSearchBarRef: any;
editorRef: RefObject<NoteBodyEditorRef>;
titleInputRef: RefObject<HTMLInputElement>;
setFormNote: SetFormNoteCallback;
onBodyChange: OnBodyChange;
}
function editorCommandRuntime(
declaration: CommandDeclaration,
editorRef: RefObject<NoteBodyEditorRef>,
setFormNote: SetFormNoteCallback,
onBodyChange: OnBodyChange,
): CommandRuntime {
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -55,9 +55,7 @@ function editorCommandRuntime(
value: args[0],
});
} else if (declaration.name === 'editor.setText') {
setFormNote((prev: FormNote) => {
return { ...prev, body: args[0] };
});
onBodyChange({ content: args[0], changeId: 0 });
} else {
return editorRef.current.execCommand({
name: declaration.name,
@@ -78,11 +76,11 @@ function editorCommandRuntime(
}
export default function useWindowCommandHandler(dependencies: HookDependencies) {
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, setFormNote } = dependencies;
const { setShowLocalSearch, noteSearchBarRef, editorRef, titleInputRef, onBodyChange } = dependencies;
useEffect(() => {
for (const declaration of editorCommandDeclarations) {
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, setFormNote));
CommandService.instance().registerRuntime(declaration.name, editorCommandRuntime(declaration, editorRef, onBodyChange));
}
const dependencies = {
@@ -105,5 +103,5 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
CommandService.instance().unregisterRuntime(command.declaration.name);
}
};
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, setFormNote]);
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef, onBodyChange]);
}

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useMemo, useRef, useEffect } from 'react';
import { useMemo, useRef, useEffect, useCallback } from 'react';
import { AppState } from '../../app.reducer';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import { Props } from './utils/types';
@@ -23,6 +23,10 @@ import useDragAndDrop from './utils/useDragAndDrop';
import { itemIsInTrash } from '@joplin/lib/services/trash';
import getEmptyFolderMessage from '@joplin/lib/components/shared/NoteList/getEmptyFolderMessage';
import Folder from '@joplin/lib/models/Folder';
import { _ } from '@joplin/lib/locale';
import useActiveDescendantId from './utils/useActiveDescendantId';
import getNoteElementIdFromJoplinId from '../NoteListItem/utils/getNoteElementIdFromJoplinId';
import useFocusVisible from './utils/useFocusVisible';
const { connect } = require('react-redux');
const commands = {
@@ -30,7 +34,7 @@ const commands = {
};
const NoteList = (props: Props) => {
const listRef = useRef(null);
const listRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<Record<string, HTMLDivElement>>({});
const listRenderer = props.listRenderer;
@@ -65,7 +69,8 @@ const NoteList = (props: Props) => {
props.notes.length,
);
const focusNote = useFocusNote(itemRefs, props.notes, makeItemIndexVisible);
const { activeNoteId, setActiveNoteId } = useActiveDescendantId(props.selectedFolderId, props.selectedNoteIds);
const focusNote = useFocusNote(listRef, props.notes, makeItemIndexVisible, setActiveNoteId);
const moveNote = useMoveNote(
props.notesParentType,
@@ -98,6 +103,7 @@ const NoteList = (props: Props) => {
const onNoteClick = useOnNoteClick(props.dispatch, focusNote);
const onKeyDown = useOnKeyDown(
activeNoteId,
props.selectedNoteIds,
moveNote,
makeItemIndexVisible,
@@ -177,6 +183,10 @@ const NoteList = (props: Props) => {
// }
// }, [makeItemIndexVisible, previousSelectedNoteIds, previousNoteCount, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
const { focusVisible, onFocus, onBlur, onKeyUp } = useFocusVisible(listRef, () => {
focusNote(activeNoteId);
});
const highlightedWords = useMemo(() => {
if (props.notesParentType === 'Search') {
const query = BaseModel.byId(props.searches, props.selectedSearchId);
@@ -197,12 +207,13 @@ const NoteList = (props: Props) => {
};
const renderNotes = () => {
if (!props.notes.length) return null;
if (!props.notes.length) return [];
const output: JSX.Element[] = [];
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
const note = props.notes[i];
const isSelected = props.selectedNoteIds.includes(note.id);
output.push(
<NoteListItem
@@ -222,7 +233,9 @@ const NoteList = (props: Props) => {
isProvisional={props.provisionalNoteIds.includes(note.id)}
flow={listRenderer.flow}
note={note}
isSelected={props.selectedNoteIds.includes(note.id)}
tabIndex={-1}
focusVisible={focusVisible && activeNoteId === note.id}
isSelected={isSelected}
isWatched={props.watchedNoteFiles.includes(note.id)}
listRenderer={listRenderer}
dispatch={props.dispatch}
@@ -262,18 +275,35 @@ const NoteList = (props: Props) => {
return output;
}, [listRenderer.flow]);
const onContainerContextMenu = useCallback((event: React.MouseEvent) => {
const isFromKeyboard = event.button === -1;
if (event.isDefaultPrevented() || !isFromKeyboard) return;
onItemContextMenu({ itemId: activeNoteId });
}, [onItemContextMenu, activeNoteId]);
return (
<div
role='listbox'
aria-label={_('Notes')}
aria-activedescendant={getNoteElementIdFromJoplinId(activeNoteId)}
aria-multiselectable={true}
tabIndex={0}
onFocus={onFocus}
onBlur={onBlur}
className="note-list"
style={noteListStyle}
ref={listRef}
onScroll={onScroll}
onKeyDown={onKeyDown}
onKeyUp={onKeyUp}
onDrop={onDrop}
onContextMenu={onContainerContextMenu}
>
{renderEmptyList()}
{renderFiller('top', topFillerStyle)}
<div className="notes" style={notesStyle}>
<div className='notes' role='presentation' style={notesStyle}>
{renderNotes()}
</div>
{renderFiller('bottom', bottomFillerStyle)}

View File

@@ -18,6 +18,12 @@
background-color: var(--joplin-background-color);
font-family: var(--joplin-font-family);
}
// focus-visible is communicated by displaying the active item in a different style.
// As such, an outline is unnecessary.
&:focus-visible {
outline: unset;
}
}
.note-list-item {

View File

@@ -0,0 +1,38 @@
import { useEffect, useRef, useState } from 'react';
import usePrevious from '@joplin/lib/hooks/usePrevious';
const useActiveDescendantId = (selectedFolderId: string, selectedNoteIds: string[]) => {
const selectedNoteIdsRef = useRef(selectedNoteIds);
selectedNoteIdsRef.current = selectedNoteIds;
const [activeNoteId, setActiveNoteId] = useState('');
useEffect(() => {
setActiveNoteId(selectedNoteIdsRef.current?.[0] ?? '');
}, [selectedFolderId]);
const previousNoteIdsRef = useRef<string[]>();
previousNoteIdsRef.current = usePrevious(selectedNoteIds);
useEffect(() => {
const previousNoteIds = previousNoteIdsRef.current ?? [];
setActiveNoteId(current => {
if (selectedNoteIds.includes(current)) {
return current;
} else {
// Prefer added items
for (const id of selectedNoteIds) {
if (!previousNoteIds.includes(id)) {
return id;
}
}
return selectedNoteIds[0] ?? '';
}
});
}, [selectedNoteIds]);
return { activeNoteId, setActiveNoteId };
};
export default useActiveDescendantId;

View File

@@ -1,44 +1,33 @@
import shim from '@joplin/lib/shim';
import { useRef, useCallback, MutableRefObject } from 'react';
import { useRef, useCallback, RefObject } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
import { NoteEntity } from '@joplin/lib/services/database/types';
export type FocusNote = (noteId: string)=> void;
type ItemRefs = MutableRefObject<Record<string, HTMLDivElement>>;
type ContainerRef = RefObject<HTMLElement>;
type OnMakeIndexVisible = (i: number)=> void;
type OnSetActiveId = (id: string)=> void;
const useFocusNote = (itemRefs: ItemRefs, notes: NoteEntity[], makeItemIndexVisible: OnMakeIndexVisible) => {
const focusItemIID = useRef(null);
const useFocusNote = (
containerRef: ContainerRef, notes: NoteEntity[], makeItemIndexVisible: OnMakeIndexVisible, setActiveNoteId: OnSetActiveId,
) => {
const notesRef = useRef(notes);
notesRef.current = notes;
const focusNote: FocusNote = useCallback((noteId: string) => {
// - We need to focus the item manually otherwise focus might be lost when the
// list is scrolled and items within it are being rebuilt.
// - We need to use an interval because when leaving the arrow pressed or scrolling
// offscreen items into view, the rendering of items might lag behind and so the
// ref is not yet available at this point.
if (!itemRefs.current[noteId]) {
const targetIndex = notesRef.current.findIndex(note => note.id === noteId);
if (targetIndex > -1) {
makeItemIndexVisible(targetIndex);
}
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
focusItemIID.current = shim.setInterval(() => {
if (itemRefs.current[noteId]) {
focus('useFocusNote1', itemRefs.current[noteId]);
shim.clearInterval(focusItemIID.current);
focusItemIID.current = null;
}
}, 10);
} else {
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
focus('useFocusNote2', itemRefs.current[noteId]);
if (noteId) {
setActiveNoteId(noteId);
}
}, [itemRefs, makeItemIndexVisible]);
// The note list container should have focus even when a note list item is visibly selected.
// The visibly focused item is determined by activeNoteId and is communicated to accessibility
// tools using aria- attributes
focus('useFocusNote', containerRef.current);
const targetIndex = notesRef.current.findIndex(note => note.id === noteId);
if (targetIndex > -1) {
makeItemIndexVisible(targetIndex);
}
}, [containerRef, makeItemIndexVisible, setActiveNoteId]);
return focusNote;
};

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