1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

182 Commits

Author SHA1 Message Date
Laurent Cozic
aa23d5cdff Android 3.1.1 2024-08-09 12:14:25 +01:00
Laurent Cozic
a97c04c21c Desktop release v3.1.1 2024-08-09 11:48:06 +01:00
Laurent Cozic
9f66d7cfcd Chore: Setup new release 3.1 2024-08-09 11:47:43 +01:00
Laurent Cozic
a52b206dfb Chore: Temporarily disable new auto-update code 2024-08-09 11:45:08 +01:00
Henry Heino
806377e6ee Desktop: Resolves #10835: Allow specifying custom language data URLs (#10846) 2024-08-09 11:29:39 +01:00
Henry Heino
6ce55a5737 Chore: Update ignore files (#10845) 2024-08-08 20:03:02 +01:00
Henry Heino
cd40861ec8 Chore: Generator: Update types (#10844) 2024-08-08 20:02:53 +01:00
Henry Heino
a1aa4f78c9 Desktop: Resolves #10746: Fix "View OCR text" not present in context menu when right-clicking an image (#10842) 2024-08-08 20:02:08 +01:00
Henry Heino
0caecedb8f Desktop: Fixes #10828: Fix error when canceling bulk PDF export (#10839) 2024-08-08 20:00:19 +01:00
Henry Heino
af7e172438 Desktop: Fix crash on opening certain plugin dialogs (#10838) 2024-08-08 20:00:13 +01:00
Henry Heino
1f45252fc6 Docs: Update the Markdown editor plugin tutorial to reflect recent Joplin changes (#10830) 2024-08-08 19:57:56 +01:00
Henry Heino
d2ae02d066 Mobile: Include commit information in version information screen (#10829) 2024-08-08 19:57:32 +01:00
丛林意志
6d98f8102d Chore: i18n: Fixed localization issue and added spaces around words in Chinese-English text (#10821) 2024-08-08 19:56:34 +01:00
Henry Heino
08eab7a73a Desktop: Use Electron safeStorage for keychain support (#10535) 2024-08-08 19:53:43 +01:00
Joplin Bot
8d8c91ef50 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-08 12:23:28 +00:00
Alice
88b3c7f526 Desktop: Seamless-Updates - creation of update notification (#10791) 2024-08-08 10:49:21 +01:00
github-actions[bot]
24731edf92 @rxliuli has signed the CLA in laurent22/joplin#10843 2024-08-08 08:12:21 +00:00
Joplin Bot
85557b6882 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-07 18:19:36 +00:00
Joplin Bot
8dfc873ceb Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-07 12:23:29 +00:00
Laurent Cozic
8961aebc3a Doc: Fixed release cycle 2024-08-06 19:25:48 +01:00
renovate[bot]
500c31565d Update dependency @types/node to v18.19.33 (#10837) 2024-08-06 16:16:30 +00:00
Henry Heino
223a685529 Docs: Fix typo: Change "Table of Content" to "Table of Contents" (#10832) 2024-08-06 17:00:23 +01:00
Andy Piper
135d46f31b Doc: Correct typo and add detail to config_screen.md (#10823) 2024-08-05 21:34:09 +01:00
Henry Heino
9f997c2fb6 Desktop: Accessibility: Fix screen reader doesn't read Goto Anything search results or help button label (#10816) 2024-08-05 19:37:23 +01:00
renovate[bot]
1c2c071952 Update dependency @types/node to v18.19.32 (#10827) 2024-08-05 15:03:38 +00:00
github-actions[bot]
08348c88eb @shivam1234100 has signed the CLA in laurent22/joplin#10825 2024-08-05 14:38:34 +00:00
github-actions[bot]
70bfb9f18d @conglinyizhi has signed the CLA in laurent22/joplin#10821 2024-08-04 05:17:18 +00:00
Henry Heino
292d2fbc15 Desktop: Accessibility: Restore keyboard focus when closing a dialog (#10817) 2024-08-03 16:43:16 +01:00
Henry Heino
88cf1d6232 Desktop: Fix prompt tag dialog input can be wider than its container (#10818) 2024-08-03 16:43:00 +01:00
Henry Heino
9cf298ef44 Desktop: Accessibility: Improve keyboard navigation in the Markdown and note toolbar (#10819) 2024-08-03 16:42:46 +01:00
Henry Heino
19af6a8722 Desktop,Mobile: Fixes #10785: Fix math is invisible in certain mermaid diagrams (#10820) 2024-08-03 16:42:16 +01:00
pedr
5c8be448ab Desktop: Fixes #10668: Tags and Delete note not being available on Search and on All Notes list (#10729) 2024-08-02 17:44:30 +01:00
Henry Heino
f69dffcf23 Mobile: Support building for web (#10650) 2024-08-02 14:51:49 +01:00
Henry Heino
88271bf1a7 Desktop: Fixes #10815: Fix Enter key submits dialogs even if a button has focus (#10814) 2024-08-02 14:51:25 +01:00
Henry Heino
14cc053094 Desktop: Accessibility: Improve settings screen keyboard navigation and screen reader accessibility (#10812) 2024-08-02 14:49:15 +01:00
Henry Heino
65ef700fdc Mobile, Desktop: Improve RTL support in the Markdown editor (#10810) 2024-08-02 14:47:56 +01:00
Henry Heino
9dbd481f28 Desktop: Fix images fail to render in the preview pane for HTML notes (#10806) 2024-08-02 14:47:43 +01:00
Henry Heino
e5c8b4bb66 Desktop: Resolves #9450: Make the beta markdown editor the default (#10796) 2024-08-02 14:47:26 +01:00
renovate[bot]
ff6d700499 Update dependency @types/nodemailer to v6.4.15 (#10808) 2024-08-01 05:52:33 +00:00
Joplin Bot
fc1699ac91 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-08-01 00:47:00 +00:00
Henry Heino
596bcd8d8b Desktop: Accessibility: Improve focus handling for plugin and prompt dialogs (#10801) 2024-07-31 14:10:58 +01:00
Pavel Koneski
ecc4f3e22a Deskop: Linux: Add option to select installation directory (#10800) 2024-07-31 13:57:04 +01:00
renovate[bot]
c0dc30d0c4 Update dependency @types/markdown-it to v13.0.8 (#10804) 2024-07-31 09:04:19 +00:00
github-actions[bot]
f02af3af3b @BCSharp has signed the CLA in laurent22/joplin#10800 2024-07-30 19:09:01 +00:00
Joplin Bot
8179d3e723 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-30 18:18:57 +00:00
Joplin Bot
06264847cc Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-29 18:19:24 +00:00
Laurent Cozic
3137d5be33 Merge branch 'release-3.0' into dev 2024-07-29 13:47:43 +01:00
Joplin Bot
d4c35b8c0b Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-28 18:18:14 +00:00
Laurent Cozic
39ad1e23a8 iOS 13.0.7 2024-07-28 15:08:29 +01:00
Laurent Cozic
d6dd23e921 Android 3.0.9 2024-07-28 15:06:25 +01:00
Henry Heino
b108bf799d Desktop: Accessibility: Add missing labels and role information to several controls (#10788) 2024-07-28 14:53:32 +01:00
Henry Heino
6d92e982dc Chore: iOS: Fix build by downgrading @react-native-clipboard/clipboard (#10787) 2024-07-28 14:53:17 +01:00
Henry Heino
40bd2dfe21 Desktop: Rich Text Editor: Preserve cursor location when updating editor content (#10781) 2024-07-28 14:49:51 +01:00
Joplin Bot
819de1cfa4 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-28 12:21:12 +00:00
Alice
a6d6e70b3d Chore: Desktop: Seamless-Updates - creation of auto updater service (#10772) 2024-07-28 12:09:30 +01:00
Laurent Cozic
05cf51ec65 Desktop release v3.0.14 2024-07-26 12:44:35 +01:00
Henry Heino
0935b6f697 Desktop: Fixes #10672: External editing: Fix notes often not updated when saved from Vim (#10780) 2024-07-26 12:39:21 +01:00
Henry Heino
dd5240d018 Desktop: Improve focus handling for notebook edit, share, and sync dialogs (#10779) 2024-07-26 12:39:01 +01:00
Henry Heino
3fbb3b6b82 Desktop: Fixes #10768: Make :w trigger sync in the beta editor's Vim mode (#10778) 2024-07-26 12:38:07 +01:00
Henry Heino
77b74daa0e Android: Fix pasting PNG and JPEG images from the clipboard (#10777) 2024-07-26 12:37:55 +01:00
Henry Heino
8c0769fdb3 Chore: Mobile: Add note screen tests (#10766) 2024-07-26 12:35:50 +01:00
Henry Heino
d2028588e8 Desktop: Resolves #10424: Move the note viewer to a separate process (#10678) 2024-07-26 12:22:49 +01:00
Laurent Cozic
4b99c2062c Update dictionary 2024-07-26 08:23:57 +01:00
Joplin Bot
ce0218700e Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-26 00:43:08 +00:00
Laurent Cozic
d63f498f4c Doc: Add sponsor 2024-07-25 22:41:26 +01:00
Laurent Cozic
56d2aced8a Server v3.0.1 2024-07-25 18:18:48 +01:00
Henry Heino
db2a194b69 Merge remote-tracking branch 'origin/release-3.0' into dev 2024-07-25 08:10:48 -07:00
github-actions[bot]
f7a970f466 @Aarya01Patil has signed the CLA in laurent22/joplin#10783 2024-07-25 11:57:59 +00:00
Henry Heino
f7fa7a195f Server: Allow web client sync (#10775) 2024-07-24 23:45:11 +01:00
Laurent Cozic
e6ec27a501 Server: Prevent item size calculation task from failing when a user has been deleted 2024-07-24 19:16:40 +01:00
Henry Heino
331f7ebe5c Mobile: Plugins: Fix incorrect Node exports emulation (#10776) 2024-07-23 20:10:59 +01:00
joe
afcd2d2a39 update zh_TW.po file (#10773) 2024-07-23 12:56:08 +01:00
github-actions[bot]
8129f4a89f @fishpcblog has signed the CLA in laurent22/joplin#10773 2024-07-23 11:08:23 +00:00
github-actions[bot]
72c1bb3724 @BHAV0207 has signed the CLA in laurent22/joplin#10769 2024-07-20 10:59:43 +00:00
cedecode
8fdccd287e Update de_DE.po (#10767) 2024-07-19 21:44:06 +01:00
Joplin Bot
b69a7403bc Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-19 18:19:04 +00:00
Laurent Cozic
bdc9fa9dc3 Doc: Added sponsor 2024-07-19 15:54:41 +01:00
Laurent Cozic
9c07e57e28 lock file 2024-07-18 15:06:47 +01:00
Henry Heino
821daeca94 Chore: Mobile: Add NoteBodyViewer tests (#10747) 2024-07-18 09:44:13 +01:00
ERYpTION
480bf238f6 Update da_DK.po (#10758) 2024-07-18 09:44:00 +01:00
Henry Heino
8ff13e5fc4 Android: Fixes #10681: Fix Dropbox sync for certain device languages (#10759) 2024-07-18 09:43:49 +01:00
Henry Heino
8e1970d08e Desktop: Update bundled Backup plugin to v1.4.2 (#10760)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-07-18 09:43:20 +01:00
Henry Heino
86d92dd302 Chore: Fix indentation in generate-plugin-doc/package.json (#10762)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2024-07-18 09:43:07 +01:00
Henry Heino
71b466507f Mobile: Upgrade react-native-webview to 13.8.6 to fix CI (#10761) 2024-07-17 22:49:10 +01:00
renovate[bot]
11ce5f6c52 Update dependency nodemailer to v6.9.13 (#10699) 2024-07-17 20:38:53 +00:00
renovate[bot]
630b4061f0 Update dependency pg to v8.11.5 (#10700) 2024-07-17 20:38:51 +00:00
renovate[bot]
912c943114 Update dependency react-native-webview to v13.8.4 (#10702) 2024-07-17 20:38:49 +00:00
renovate[bot]
8e377e0306 Update dependency sharp to v0.33.3 (#10704) 2024-07-17 20:38:13 +00:00
renovate[bot]
1535e020a3 Update dependency style-to-js to v1.1.12 (#10705) 2024-07-17 20:38:11 +00:00
renovate[bot]
23d5d3426d Update dependency tar to v6.2.1 (#10706) 2024-07-17 20:38:04 +00:00
Laurent Cozic
6ab7a0836e Tools: Automerge pull requests with the "automerge" label 2024-07-17 20:51:54 +01:00
Laurent Cozic
278691211d Tools: Automerge pull requests with the "automerge" label 2024-07-17 20:06:26 +01:00
Laurent Cozic
356d4688a0 Tools: Automerge pull requests with the "automerge" label 2024-07-17 19:52:52 +01:00
renovate[bot]
6b1d31387b Update dependency @react-native-community/geolocation to v3.2.1 (#10757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 18:56:46 +00:00
pedr
70bfb26c9a Mobile: Fixes: #10677: Following a link to a previously open note wouldn't work (#10750) 2024-07-16 19:38:54 +01:00
Henry Heino
71f70f4d2c Mobile: Resolves #9017: Support pasting images (#10751) 2024-07-16 19:28:05 +01:00
Henry Heino
64e4ebb1f3 Mobile: Fix manual resource download mode (#10748) 2024-07-16 19:27:08 +01:00
Liffindra Angga Zaaldian
2d984ce9a8 Update Indonesian translation (#10741) 2024-07-16 19:25:54 +01:00
Henry Heino
eaf160e0b1 Docs: Update user-facing plugin documentation to reflect that plugins are now supported on mobile (#10738) 2024-07-16 19:25:38 +01:00
Henry Heino
624bfd9175 Desktop: Fixes #10733: Fix not-yet-created images lost while editing with the Rich Text Editor (#10734) 2024-07-16 19:25:23 +01:00
Henry Heino
9ad1249f11 Chore: Mobile: Migrate shim-init-react to TypeScript (#10731) 2024-07-16 19:23:03 +01:00
sysescool
668849603d Desktop: Fixes #10716: fix joplin install fails because ldconfig not found libfuse2 but it is indeed installed. (#10717) 2024-07-16 19:20:51 +01:00
renovate[bot]
24f4c8e6ab Update dependency terminal-kit to v3.1.1 (#10713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 19:03:33 +01:00
renovate[bot]
46f5784edc Update dependency react-native-get-random-values to v1.11.0 (#10712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 19:01:49 +01:00
renovate[bot]
fae2443481 Update dependency react-native-device-info to v10.13.1 (#10711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 19:01:40 +01:00
renovate[bot]
37d65e000a Update dependency jsdom to v23.2.0 (#10709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 19:01:17 +01:00
renovate[bot]
6dd90eb03f Update dependency @react-native-community/geolocation to v3.2.0 (#10708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 19:01:07 +01:00
CUI Hao
3d8f713eb7 Chore: Add a sleep in the note duplication test (#10719)
Fixes a test failure on very fast computers.
2024-07-15 07:33:59 -07:00
renovate[bot]
c35efe15d2 Update dependency @react-native-community/slider to v4.5.2 (#10744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 02:46:52 +00:00
Henry Heino
1596b46b86 Chore: Fixes #10721: Fix test failure in CI (#10735) 2024-07-11 12:59:56 -07:00
Henry Heino
4de0236194 Chore: Docs: Fix missing closing tag (#10730)
The video element is not a void element. As such, if it's missing a closing tag, it's considered to be unclosed.
See https://developer.mozilla.org/en-US/docs/Glossary/Void_element#self-closing_tags.
2024-07-11 10:16:33 -07:00
Jeremy Kao
2ab9702e32 Docs: Update the UI path for sync status on the mobile app (#10718)
The instructions for how to view synchronisation status on mobile were out of date.
2024-07-11 07:44:09 -07:00
github-actions[bot]
24954bd0f0 @shubhiscoding has signed the CLA in laurent22/joplin#10728 2024-07-09 11:06:20 +00:00
renovate[bot]
2d4322be56 Update dependency @types/node to v18.19.31 (#10727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 10:37:55 +00:00
renovate[bot]
abb069bf50 Update dependency @react-native-community/slider to v4.5.1 (#10726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 10:35:50 +00:00
renovate[bot]
a81d9fe17a Update dependency koa to v2.15.2 (#10698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 08:11:32 +00:00
renovate[bot]
6d44158050 Update dependency glob to v10.3.12 (#10697)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 08:10:19 +00:00
Dmitriy Q
a63cf3a90d All: Translation: Update ru_RU.po (#10720) 2024-07-09 04:08:49 -04:00
github-actions[bot]
ddb4f8c45b @cuihaoleo has signed the CLA in laurent22/joplin#10719 2024-07-08 04:28:43 +00:00
github-actions[bot]
d7adab59ef @imsardine has signed the CLA in laurent22/joplin#10718 2024-07-08 03:49:24 +00:00
github-actions[bot]
e41374496e @sysescool has signed the CLA in laurent22/joplin#10717 2024-07-08 03:19:52 +00:00
Arda Kılıçdağı
62d514463c Turkish Translations updated (#10692) 2024-07-06 18:26:25 +02:00
jduar
332078b4ea Update Portuguese pt_PT.po translation. (#10691) 2024-07-06 18:26:13 +02:00
github-actions[bot]
c60e11646d @jduar has signed the CLA in laurent22/joplin#10691 2024-07-06 14:06:06 +00:00
Joplin Bot
c607fe9c75 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-06 12:20:41 +00:00
Laurent Cozic
1a4ba2c74a Merge branch 'release-3.0' into dev 2024-07-06 13:41:53 +02:00
Laurent Cozic
e49bca8315 iOS 13.0.6 2024-07-06 13:23:17 +02:00
Laurent Cozic
636fbdf7d0 Android 3.0.8 2024-07-06 13:21:55 +02:00
Laurent Cozic
ee97434bb0 Tools: Skip more minor changes for changelog generation 2024-07-06 12:19:17 +02:00
Laurent Cozic
599cf5b86f Desktop release v3.0.13 2024-07-06 12:12:46 +02:00
Henry Heino
2fd6a3a2fa Desktop: Fixes #10685: Fix shift-delete asks to permanently delete the current note, rather than cut text, when the editor is selected. (#10687) 2024-07-06 12:05:35 +02:00
Henry Heino
a3e04103de Desktop: Fixes #10679: Fix incorrect text rendering on MacOS by changing the default font to Avenir Next (#10686) 2024-07-05 19:58:09 +02:00
renovate[bot]
731260926d Update dependency @types/node to v18.19.30 (#10684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-05 13:49:54 +02:00
ben-igel
a43635610a Update de_DE.po to make the german translation of the sort lines command better understandable (#10682) 2024-07-05 13:18:58 +02:00
github-actions[bot]
e307459652 @ben-igel has signed the CLA in laurent22/joplin#10682 2024-07-04 17:51:49 +00:00
Henry Heino
c197a83de8 Desktop: Fix error in plugin content scripts generated with Webpack (#10680) 2024-07-04 14:58:26 +02:00
Henry Heino
320d0df60d Desktop, Mobile: Fixes #10674: Fix sidebar performance regression with many nested notebooks (#10676) 2024-07-04 14:56:57 +02:00
Joplin Bot
7e4533d811 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-03 12:22:19 +00:00
cedecode
f32fe63205 All: Update de_DE.po (small improvements) (#10673) 2024-07-03 13:53:32 +02:00
Laurent Cozic
be117bca86 Tools: Run packageJsonLint hook only when JSON files are being committed 2024-07-03 10:54:13 +02:00
Laurent Cozic
2b7bd902f3 Merge branch 'release-3.0' into dev 2024-07-03 10:47:27 +02:00
Laurent Cozic
3e0fb48e44 Api: Do not return deleted notes in folders/:id/notes call 2024-07-03 10:41:31 +02:00
Laurent Cozic
6d7fd19167 Chore: Add word to dic 2024-07-03 00:52:08 +02:00
Laurent Cozic
c3520d9eb1 CLI v3.0.1 2024-07-02 21:00:43 +02:00
Laurent Cozic
5fd3cecc96 Lock file 2024-07-02 21:00:43 +02:00
Laurent Cozic
0d8666c946 Releasing sub-packages 2024-07-02 21:00:42 +02:00
Laurent Cozic
4a475f1b53 CLI v3.0.1 2024-07-02 20:58:38 +02:00
Laurent Cozic
8679cc5704 Lock file 2024-07-02 20:41:46 +02:00
Laurent Cozic
a48c4ba93f Releasing sub-packages 2024-07-02 20:40:42 +02:00
Joplin Bot
12db667128 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-02 18:18:51 +00:00
Laurent Cozic
6215de6080 Doc: Added new sponsor 2024-07-02 19:16:15 +02:00
renovate[bot]
7d2f384475 Update dependency @types/node to v18.19.29 (#10671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 19:13:33 +02:00
Laurent Cozic
6ea1ac09a4 Doc: Fixed news filename 2024-07-02 15:17:20 +02:00
Laurent Cozic
f2841a9a94 Tools: Add script to validate Markdown filenames on commit 2024-07-02 15:16:16 +02:00
Joplin Bot
46ade2e0f8 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-01 18:19:29 +00:00
Henry Heino
d89be23069 Chore: Migrate SQL queries in preparation for web support (#10670) 2024-07-01 19:56:40 +02:00
Laurent Cozic
337d50437b Doc: Add release notes 3.0 2024-07-01 18:16:08 +02:00
Laurent Cozic
2479a8471e Merge branch 'release-3.0' into dev 2024-07-01 18:02:27 +02:00
Laurent Cozic
16e82b5462 iOS 13.0.5 2024-07-01 17:48:06 +02:00
rnbastos
6c091910cd Update pt_BR.po (#10667) 2024-07-01 17:32:43 +02:00
pedr
a074532497 Desktop, Mobile: Fixes #10645: Show notification in case Joplin Cloud credential is not valid anymore (#10649) 2024-07-01 17:21:17 +02:00
Joplin Bot
5d2df358ac Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-07-01 00:47:08 +00:00
Laurent Cozic
dfdc2fda27 Fixed tests 2024-06-29 18:38:18 +02:00
Laurent Cozic
a1f9c9c3d8 All: Set min version for synchronising to 3.0.0 2024-06-29 18:38:18 +02:00
Joplin Bot
838da6f161 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-06-29 12:22:44 +00:00
Laurent Cozic
a86ee1d34e Merge branch 'release-3.0' into dev 2024-06-29 12:25:11 +02:00
Laurent Cozic
5f34a1bc92 Doc: Add Multi-factor authentication guide 2024-06-28 19:43:59 +02:00
renovate[bot]
f781face3a Update dependency @types/node to v18.19.28 (#10663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-28 13:03:05 +02:00
Laurent Cozic
78ecd28d73 Merge branch 'release-3.0' into dev 2024-06-27 11:54:19 +02:00
github-actions[bot]
85e57a3953 @cedecode has signed the CLA in laurent22/joplin#10658 2024-06-25 22:11:34 +00:00
qx100
95968f6690 All: Update zh_CN.po (#10651) 2024-06-25 15:05:06 +02:00
Henry Heino
f0b73ee916 Docs: Document creating and managing to-do notes (#10563) 2024-06-25 15:01:54 +02:00
Henry Heino
a44412ae78 Chore: Increase strength of Settings types (#10605) 2024-06-25 15:01:39 +02:00
Henry Heino
c7116b135f Chore: Refactor mobile plugin logic into locations more consistent with other parts of the app (#10636) 2024-06-25 14:59:59 +02:00
Siddhant Paritosh Rao
801d36c41f Mobile: Fixes #10596: remove search bar from plugins screen (#10648) 2024-06-25 14:59:41 +02:00
Henry Heino
1d46adf801 Mobile: Fix dayjs-related startup error (#10652) 2024-06-24 17:15:57 +02:00
Laurent Cozic
94edaea210 Doc: Removed outdated hot reload information 2024-06-23 09:36:56 +01:00
Henry Heino
5db88995c0 Chore: Fix CI (#10646) 2024-06-21 09:37:25 +01:00
Laurent Cozic
8eda8d3c84 Fixed test 2024-06-20 18:25:51 +01:00
Laurent Cozic
1437dd5f27 Desktop: Use relative time in note list for today and yesterday 2024-06-20 14:01:13 +01:00
Laurent Cozic
9eb4944614 Server: Remove USERS_WITH_REPLICATION env variable 2024-06-19 23:34:00 +01:00
Joplin Bot
b4ef5abb88 Doc: Auto-update documentation
Auto-updated using release-website.sh
2024-06-19 18:18:05 +00:00
482 changed files with 22516 additions and 12477 deletions

View File

@@ -51,8 +51,10 @@ packages/app-desktop/node_modules
packages/app-desktop/packageInfo.js
packages/app-desktop/services/electron-context-menu.js
packages/app-desktop/vendor/lib/
packages/app-mobile/packageInfo.js
packages/app-mobile/android
packages/app-mobile/**/*.bundle.js
packages/app-mobile/web/public/pluginAssets/**/*
packages/app-mobile/ios
packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
@@ -167,9 +169,13 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/FontSearch.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
@@ -296,6 +302,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.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
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/index.js
@@ -432,6 +440,7 @@ 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
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useElementHeight.js
@@ -451,24 +460,30 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gulpfile.js
packages/app-desktop/integration-tests/goToAnything.spec.js
packages/app-desktop/integration-tests/main.spec.js
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/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.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/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
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.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
packages/app-desktop/services/commands/types.js
@@ -500,6 +515,10 @@ packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/customProtocols/constants.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/markupLanguageUtils.js
@@ -513,19 +532,24 @@ packages/app-mobile/commands/openItem.js
packages/app-mobile/commands/openNote.js
packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/components/ActionButton.js
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/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js
packages/app-mobile/components/ExtendedWebView/types.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
@@ -582,20 +606,44 @@ packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
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/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/TextButton.js
packages/app-mobile/components/buttons/index.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/global-style.js
packages/app-mobile/components/plugins/PluginRunner.js
packages/app-mobile/components/plugins/PluginRunnerWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/components/plugins/types.js
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
@@ -646,6 +694,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
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
@@ -660,44 +709,22 @@ packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/plugins/PlatformImplementation.js
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/plugins/PluginRunner/types.js
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
packages/app-mobile/plugins/hooks/usePlugin.js
packages/app-mobile/plugins/loadPlugins.test.js
packages/app-mobile/plugins/loadPlugins.js
packages/app-mobile/plugins/testing/MockPluginRunner.js
packages/app-mobile/index.web.js
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/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/tools/copyAssets.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
@@ -706,9 +733,13 @@ packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/fs-driver/tarCreate.js
packages/app-mobile/utils/fs-driver/tarExtract.test.js
@@ -717,17 +748,29 @@ 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/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
packages/app-mobile/utils/initializeCommandService.js
packages/app-mobile/utils/injectedJs.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
packages/app-mobile/utils/lockToSingleInstance.js
packages/app-mobile/utils/makeShowMessageBox.js
packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
packages/app-mobile/utils/polyfills/index.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareFile.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/showMessageBox.js
packages/app-mobile/utils/shim-init-react/index.js
packages/app-mobile/utils/shim-init-react/index.web.js
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/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
packages/default-plugins/buildDefaultPlugins.js
packages/default-plugins/commands/buildAll.js
@@ -784,6 +827,7 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
packages/editor/CodeMirror/testUtil/typeText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
@@ -798,6 +842,7 @@ 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/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
@@ -898,6 +943,7 @@ packages/lib/geolocation-node.js
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
@@ -944,8 +990,10 @@ packages/lib/models/Tag.test.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/settings/builtInMetadata.js
packages/lib/models/settings/settingValidations.test.js
packages/lib/models/settings/settingValidations.js
packages/lib/models/settings/types.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
packages/lib/models/utils/isItemId.js
@@ -1004,6 +1052,7 @@ packages/lib/services/commands/commandsToMarkdownTable.js
packages/lib/services/commands/focusEditorIfEditorCommand.js
packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.test.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
@@ -1055,8 +1104,10 @@ packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js
packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
packages/lib/services/keychain/KeychainServiceDriver.electron.js
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
@@ -1110,7 +1161,11 @@ packages/lib/services/plugins/api/noteListType.js
packages/lib/services/plugins/api/types.js
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
packages/lib/services/plugins/loadPlugins.test.js
packages/lib/services/plugins/loadPlugins.js
packages/lib/services/plugins/reducer.js
packages/lib/services/plugins/testing/MockPlatformImplementation.js
packages/lib/services/plugins/testing/MockPluginRunner.js
packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
@@ -1252,13 +1307,17 @@ packages/lib/urlUtils.js
packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/dom/makeSandboxedIframe.js
packages/lib/utils/focusHandler.js
packages/lib/utils/frontMatter.js
packages/lib/utils/ipc/RemoteMessenger.test.js
packages/lib/utils/ipc/RemoteMessenger.js
packages/lib/utils/ipc/TestMessenger.js
packages/lib/utils/ipc/WindowMessenger.js
packages/lib/utils/ipc/WorkerMessenger.js
packages/lib/utils/ipc/WorkerToWindowMessenger.js
packages/lib/utils/ipc/types.js
packages/lib/utils/ipc/utils/isTransferableObject.js
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
@@ -1269,6 +1328,8 @@ packages/lib/utils/joplinCloud/types.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
packages/lib/utils/resolvePathWithinDir.test.js
packages/lib/utils/resolvePathWithinDir.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js
@@ -1386,6 +1447,7 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js
packages/tools/website/buildTranslations.js
packages/tools/website/processDocs.test.js

View File

@@ -15,6 +15,19 @@ module.exports = {
'globals': {
'Atomics': 'readonly',
'SharedArrayBuffer': 'readonly',
'BufferEncoding': 'readonly',
'AsyncIterable': 'readonly',
'FileSystemFileHandle': 'readonly',
'FileSystemDirectoryHandle': 'readonly',
'ReadableStreamDefaultReader': 'readonly',
'FileSystemCreateWritableOptions': 'readonly',
'FileSystemHandle': 'readonly',
'IDBTransactionMode': 'readonly',
// ServiceWorker
'ExtendableEvent': 'readonly',
'WindowClient': 'readonly',
'FetchEvent': 'readonly',
// Jest variables
'test': 'readonly',

17
.github/workflows/automerge.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: automerge
on:
schedule:
- cron: '*/10 * * * *'
jobs:
automerge:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- id: automerge
name: automerge
uses: "pascalgn/automerge-action@v0.16.3"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: "squash"
LOG: "DEBUG"

122
.gitignore vendored
View File

@@ -146,9 +146,13 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/FontSearch.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/FontSearch.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/SettingComponent.js
packages/app-desktop/gui/ConfigScreen/controls/SettingDescription.js
packages/app-desktop/gui/ConfigScreen/controls/SettingHeader.js
packages/app-desktop/gui/ConfigScreen/controls/SettingLabel.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
@@ -275,6 +279,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.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
packages/app-desktop/gui/NoteEditor/WarningBanner/BannerContent.js
packages/app-desktop/gui/NoteEditor/WarningBanner/WarningBanner.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteBody.js
packages/app-desktop/gui/NoteEditor/commands/focusElementNoteTitle.js
packages/app-desktop/gui/NoteEditor/commands/index.js
@@ -411,6 +417,7 @@ 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
packages/app-desktop/gui/dialogs.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useElementHeight.js
@@ -430,24 +437,30 @@ packages/app-desktop/gui/utils/convertToScreenCoordinates.js
packages/app-desktop/gui/utils/dragAndDrop.js
packages/app-desktop/gui/utils/loadScript.js
packages/app-desktop/gulpfile.js
packages/app-desktop/integration-tests/goToAnything.spec.js
packages/app-desktop/integration-tests/main.spec.js
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/SettingsScreen.js
packages/app-desktop/integration-tests/models/Sidebar.js
packages/app-desktop/integration-tests/noteList.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/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/getImageSourceSize.js
packages/app-desktop/integration-tests/util/setFilePickerResponse.js
packages/app-desktop/integration-tests/util/setMessageBoxResponse.js
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.js
packages/app-desktop/services/bridge.js
packages/app-desktop/services/commands/stateToWhenClauseContext.js
packages/app-desktop/services/commands/types.js
@@ -479,6 +492,10 @@ packages/app-desktop/utils/7zip/pathToBundled7Zip.js
packages/app-desktop/utils/checkForUpdatesUtils.test.js
packages/app-desktop/utils/checkForUpdatesUtils.js
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
packages/app-desktop/utils/customProtocols/constants.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.test.js
packages/app-desktop/utils/customProtocols/handleCustomProtocols.js
packages/app-desktop/utils/customProtocols/registerCustomProtocols.js
packages/app-desktop/utils/isSafeToOpen.test.js
packages/app-desktop/utils/isSafeToOpen.js
packages/app-desktop/utils/markupLanguageUtils.js
@@ -492,19 +509,24 @@ packages/app-mobile/commands/openItem.js
packages/app-mobile/commands/openNote.js
packages/app-mobile/commands/scrollToHash.js
packages/app-mobile/commands/util/goToNote.js
packages/app-mobile/components/ActionButton.js
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/DialogManager.js
packages/app-mobile/components/DismissibleDialog.js
packages/app-mobile/components/Dropdown.test.js
packages/app-mobile/components/Dropdown.js
packages/app-mobile/components/ExtendedWebView.js
packages/app-mobile/components/ExtendedWebView/index.jest.js
packages/app-mobile/components/ExtendedWebView/index.js
packages/app-mobile/components/ExtendedWebView/index.web.js
packages/app-mobile/components/ExtendedWebView/types.js
packages/app-mobile/components/FolderPicker.js
packages/app-mobile/components/Icon.js
packages/app-mobile/components/IconButton.js
packages/app-mobile/components/Modal.js
packages/app-mobile/components/ModalDialog.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.test.js
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.test.js
packages/app-mobile/components/NoteBodyViewer/bundledJs/Renderer.js
@@ -561,20 +583,44 @@ packages/app-mobile/components/ProfileSwitcher/useProfileConfig.js
packages/app-mobile/components/ScreenHeader/WarningBanner.test.js
packages/app-mobile/components/ScreenHeader/WarningBanner.js
packages/app-mobile/components/ScreenHeader/WarningBox.js
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/TextInput.js
packages/app-mobile/components/accessibility/AccessibleModalMenu.js
packages/app-mobile/components/accessibility/AccessibleView.js
packages/app-mobile/components/app-nav.js
packages/app-mobile/components/base-screen.js
packages/app-mobile/components/biometrics/BiometricPopup.js
packages/app-mobile/components/biometrics/biometricAuthenticate.js
packages/app-mobile/components/biometrics/sensorInfo.js
packages/app-mobile/components/buttons/FloatingActionButton.js
packages/app-mobile/components/buttons/TextButton.js
packages/app-mobile/components/buttons/index.js
packages/app-mobile/components/getResponsiveValue.test.js
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/global-style.js
packages/app-mobile/components/plugins/PluginRunner.js
packages/app-mobile/components/plugins/PluginRunnerWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/components/plugins/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/components/plugins/backgroundPage/startStopPlugin.js
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.test.js
packages/app-mobile/components/plugins/backgroundPage/utils/getFormData.js
packages/app-mobile/components/plugins/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/components/plugins/backgroundPage/utils/wrapConsoleLog.js
packages/app-mobile/components/plugins/dialogs/PluginDialogManager.js
packages/app-mobile/components/plugins/dialogs/PluginDialogWebView.js
packages/app-mobile/components/plugins/dialogs/PluginPanelViewer.js
packages/app-mobile/components/plugins/dialogs/PluginUserWebView.js
packages/app-mobile/components/plugins/dialogs/hooks/useDialogMessenger.js
packages/app-mobile/components/plugins/dialogs/hooks/useDialogSize.js
packages/app-mobile/components/plugins/dialogs/hooks/useViewInfos.js
packages/app-mobile/components/plugins/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/components/plugins/types.js
packages/app-mobile/components/plugins/utils/createOnLogHandler.js
packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.js
packages/app-mobile/components/screens/ConfigScreen/FileSystemPathSelector.js
packages/app-mobile/components/screens/ConfigScreen/JoplinCloudConfig.js
@@ -625,6 +671,7 @@ packages/app-mobile/components/screens/ConfigScreen/plugins/utils/useUpdateState
packages/app-mobile/components/screens/ConfigScreen/types.js
packages/app-mobile/components/screens/JoplinCloudLoginScreen.js
packages/app-mobile/components/screens/LogScreen.js
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
@@ -639,44 +686,22 @@ packages/app-mobile/components/screens/status.js
packages/app-mobile/components/side-menu-content.js
packages/app-mobile/components/voiceTyping/VoiceTypingDialog.js
packages/app-mobile/gulpfile.js
packages/app-mobile/plugins/PlatformImplementation.js
packages/app-mobile/plugins/PluginRunner/PluginRunner.js
packages/app-mobile/plugins/PluginRunner/PluginRunnerWebView.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializeDialogWebView.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/initializePluginBackgroundIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/pluginRunnerBackgroundPage.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/startStopPlugin.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.test.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/getFormData.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/makeSandboxedIframe.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/reportUnhandledErrors.js
packages/app-mobile/plugins/PluginRunner/backgroundPage/utils/wrapConsoleLog.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogManager.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginDialogWebView.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginPanelViewer.js
packages/app-mobile/plugins/PluginRunner/dialogs/PluginUserWebView.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogMessenger.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useDialogSize.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useViewInfos.js
packages/app-mobile/plugins/PluginRunner/dialogs/hooks/useWebViewSetup.js
packages/app-mobile/plugins/PluginRunner/types.js
packages/app-mobile/plugins/PluginRunner/utils/createOnLogHandler.js
packages/app-mobile/plugins/hooks/usePlugin.js
packages/app-mobile/plugins/loadPlugins.test.js
packages/app-mobile/plugins/loadPlugins.js
packages/app-mobile/plugins/testing/MockPluginRunner.js
packages/app-mobile/index.web.js
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/e2ee/RSA.react-native.js
packages/app-mobile/services/plugins/PlatformImplementation.js
packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/services/voiceTyping/vosk.js
packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/tools/copyAssets.js
packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js
@@ -685,9 +710,13 @@ packages/app-mobile/utils/appDefaultState.js
packages/app-mobile/utils/autodetectTheme.js
packages/app-mobile/utils/checkPermissions.js
packages/app-mobile/utils/createRootStyle.js
packages/app-mobile/utils/database-driver-react-native.js
packages/app-mobile/utils/database-driver-react-native.web.js
packages/app-mobile/utils/debounce.js
packages/app-mobile/utils/fs-driver/constants.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.js
packages/app-mobile/utils/fs-driver/fs-driver-rn.web.worker.js
packages/app-mobile/utils/fs-driver/runOnDeviceTests.js
packages/app-mobile/utils/fs-driver/tarCreate.js
packages/app-mobile/utils/fs-driver/tarExtract.test.js
@@ -696,17 +725,29 @@ 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/image/fileToImage.web.js
packages/app-mobile/utils/image/getImageDimensions.js
packages/app-mobile/utils/image/resizeImage.js
packages/app-mobile/utils/initializeCommandService.js
packages/app-mobile/utils/injectedJs.js
packages/app-mobile/utils/ipc/RNToWebViewMessenger.js
packages/app-mobile/utils/ipc/WebViewToRNMessenger.js
packages/app-mobile/utils/lockToSingleInstance.js
packages/app-mobile/utils/makeShowMessageBox.js
packages/app-mobile/utils/pickDocument.js
packages/app-mobile/utils/polyfills/bufferPolyfill.js
packages/app-mobile/utils/polyfills/index.js
packages/app-mobile/utils/setupNotifications.js
packages/app-mobile/utils/shareFile.js
packages/app-mobile/utils/shareHandler.js
packages/app-mobile/utils/showMessageBox.js
packages/app-mobile/utils/shim-init-react/index.js
packages/app-mobile/utils/shim-init-react/index.web.js
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/types.js
packages/app-mobile/web/serviceWorker.js
packages/default-plugins/build.js
packages/default-plugins/buildDefaultPlugins.js
packages/default-plugins/commands/buildAll.js
@@ -763,6 +804,7 @@ packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js
packages/editor/CodeMirror/testUtil/typeText.js
packages/editor/CodeMirror/theme.js
packages/editor/CodeMirror/utils/biDirectionalTextExtension.js
packages/editor/CodeMirror/utils/formatting/RegionSpec.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js
packages/editor/CodeMirror/utils/formatting/findInlineMatch.js
@@ -777,6 +819,7 @@ 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/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js
@@ -877,6 +920,7 @@ packages/lib/geolocation-node.js
packages/lib/hooks/useAsyncEffect.js
packages/lib/hooks/useElementSize.js
packages/lib/hooks/useEventListener.js
packages/lib/hooks/usePlugin.js
packages/lib/hooks/usePrevious.js
packages/lib/htmlUtils.test.js
packages/lib/htmlUtils.js
@@ -923,8 +967,10 @@ packages/lib/models/Tag.test.js
packages/lib/models/Tag.js
packages/lib/models/dateTimeFormats.test.js
packages/lib/models/settings/FileHandler.js
packages/lib/models/settings/builtInMetadata.js
packages/lib/models/settings/settingValidations.test.js
packages/lib/models/settings/settingValidations.js
packages/lib/models/settings/types.js
packages/lib/models/utils/getCollator.js
packages/lib/models/utils/getConflictFolderId.js
packages/lib/models/utils/isItemId.js
@@ -983,6 +1029,7 @@ packages/lib/services/commands/commandsToMarkdownTable.js
packages/lib/services/commands/focusEditorIfEditorCommand.js
packages/lib/services/commands/isEditorCommand.js
packages/lib/services/commands/propsHaveChanged.js
packages/lib/services/commands/stateToWhenClauseContext.test.js
packages/lib/services/commands/stateToWhenClauseContext.js
packages/lib/services/contextkey/contextkey.js
packages/lib/services/database/addMigrationFile.js
@@ -1034,8 +1081,10 @@ packages/lib/services/interop/Module.js
packages/lib/services/interop/types.js
packages/lib/services/joplinCloudUtils.js
packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js
packages/lib/services/keychain/KeychainService.test.js
packages/lib/services/keychain/KeychainService.js
packages/lib/services/keychain/KeychainServiceDriver.dummy.js
packages/lib/services/keychain/KeychainServiceDriver.electron.js
packages/lib/services/keychain/KeychainServiceDriver.mobile.js
packages/lib/services/keychain/KeychainServiceDriver.node.js
packages/lib/services/keychain/KeychainServiceDriverBase.js
@@ -1089,7 +1138,11 @@ packages/lib/services/plugins/api/noteListType.js
packages/lib/services/plugins/api/types.js
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
packages/lib/services/plugins/loadPlugins.test.js
packages/lib/services/plugins/loadPlugins.js
packages/lib/services/plugins/reducer.js
packages/lib/services/plugins/testing/MockPlatformImplementation.js
packages/lib/services/plugins/testing/MockPluginRunner.js
packages/lib/services/plugins/utils/createViewHandle.js
packages/lib/services/plugins/utils/executeSandboxCall.js
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
@@ -1231,13 +1284,17 @@ packages/lib/urlUtils.js
packages/lib/utils/ActionLogger.test.js
packages/lib/utils/ActionLogger.js
packages/lib/utils/credentialFiles.js
packages/lib/utils/dom/makeSandboxedIframe.js
packages/lib/utils/focusHandler.js
packages/lib/utils/frontMatter.js
packages/lib/utils/ipc/RemoteMessenger.test.js
packages/lib/utils/ipc/RemoteMessenger.js
packages/lib/utils/ipc/TestMessenger.js
packages/lib/utils/ipc/WindowMessenger.js
packages/lib/utils/ipc/WorkerMessenger.js
packages/lib/utils/ipc/WorkerToWindowMessenger.js
packages/lib/utils/ipc/types.js
packages/lib/utils/ipc/utils/isTransferableObject.js
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.test.js
packages/lib/utils/ipc/utils/mergeCallbacksAndSerializable.js
packages/lib/utils/ipc/utils/separateCallbacksFromSerializable.test.js
@@ -1248,6 +1305,8 @@ packages/lib/utils/joplinCloud/types.js
packages/lib/utils/processStartFlags.js
packages/lib/utils/replaceUnsupportedCharacters.test.js
packages/lib/utils/replaceUnsupportedCharacters.js
packages/lib/utils/resolvePathWithinDir.test.js
packages/lib/utils/resolvePathWithinDir.js
packages/lib/utils/userFetcher.js
packages/lib/utils/webDAVUtils.test.js
packages/lib/utils/webDAVUtils.js
@@ -1365,6 +1424,7 @@ packages/tools/updateMarkdownDoc.js
packages/tools/utils/discourse.js
packages/tools/utils/loadSponsors.js
packages/tools/utils/translation.js
packages/tools/validateFilenames.js
packages/tools/website/build.js
packages/tools/website/buildTranslations.js
packages/tools/website/processDocs.test.js

View File

@@ -0,0 +1,118 @@
# Fixes sync issues caused by locale-sensitive lowercasing
# of HTTP headers.
# See https://github.com/laurent22/joplin/issues/10681
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
index 8ac9e7a855162cefbf99024eb013c8a3b11de1ec..1c639cf9d84821b6ffc132960e2d1c044bedbd48 100644
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobConfig.java
@@ -2,6 +2,7 @@ package com.RNFetchBlob;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
+import java.util.Locale;
class RNFetchBlobConfig {
@@ -33,7 +34,7 @@ class RNFetchBlobConfig {
}
if(options.hasKey("binaryContentTypes"))
this.binaryContentTypes = options.getArray("binaryContentTypes");
- if(this.path != null && path.toLowerCase().contains("?append=true")) {
+ if(this.path != null && path.toLowerCase(Locale.ROOT).contains("?append=true")) {
this.overwrite = false;
}
if(options.hasKey("overwrite"))
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
index a4d70153f41e6c14eec65412b5b59822f1c6750b..d98c439f7b0aeb79afc82ab9f653e9c021086426 100644
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobFS.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
+import java.util.Locale;
class RNFetchBlobFS {
@@ -210,7 +211,7 @@ class RNFetchBlobFS {
return;
}
- switch (encoding.toLowerCase()) {
+ switch (encoding.toLowerCase(Locale.ROOT)) {
case "base64" :
promise.resolve(Base64.encodeToString(bytes, Base64.NO_WRAP));
break;
@@ -1050,7 +1051,7 @@ class RNFetchBlobFS {
if(encoding.equalsIgnoreCase("ascii")) {
return data.getBytes(Charset.forName("US-ASCII"));
}
- else if(encoding.toLowerCase().contains("base64")) {
+ else if(encoding.toLowerCase(Locale.ROOT).contains("base64")) {
return Base64.decode(data, Base64.NO_WRAP);
}
diff --git a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
index a8abd71833879201e3438b2fa51d712a311c4551..b70cc13c004229f69157de5f82ae5ec3abf4358e 100644
--- a/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
+++ b/android/src/main/java/com/RNFetchBlob/RNFetchBlobReq.java
@@ -49,6 +49,7 @@ import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@@ -300,14 +301,14 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
responseFormat = ResponseFormat.UTF8;
}
else {
- builder.header(key.toLowerCase(), value);
- mheaders.put(key.toLowerCase(), value);
+ builder.header(key.toLowerCase(Locale.ROOT), value);
+ mheaders.put(key.toLowerCase(Locale.ROOT), value);
}
}
}
if(method.equalsIgnoreCase("post") || method.equalsIgnoreCase("put") || method.equalsIgnoreCase("patch")) {
- String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase();
+ String cType = getHeaderIgnoreCases(mheaders, "Content-Type").toLowerCase(Locale.ROOT);
if(rawRequestBodyArray != null) {
requestType = RequestType.Form;
@@ -323,7 +324,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
|| rawRequestBody.startsWith(RNFetchBlobConst.CONTENT_PREFIX)) {
requestType = RequestType.SingleFile;
}
- else if (cType.toLowerCase().contains(";base64") || cType.toLowerCase().startsWith("application/octet")) {
+ else if (cType.toLowerCase(Locale.ROOT).contains(";base64") || cType.toLowerCase(Locale.ROOT).startsWith("application/octet")) {
cType = cType.replace(";base64","").replace(";BASE64","");
if(mheaders.containsKey("content-type"))
mheaders.put("content-type", cType);
@@ -686,7 +687,7 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
boolean isCustomBinary = false;
if(options.binaryContentTypes != null) {
for(int i = 0; i< options.binaryContentTypes.size();i++) {
- if(ctype.toLowerCase().contains(options.binaryContentTypes.getString(i).toLowerCase())) {
+ if(ctype.toLowerCase(Locale.ROOT).contains(options.binaryContentTypes.getString(i).toLowerCase(Locale.ROOT))) {
isCustomBinary = true;
break;
}
@@ -698,13 +699,13 @@ public class RNFetchBlobReq extends BroadcastReceiver implements Runnable {
private String getHeaderIgnoreCases(Headers headers, String field) {
String val = headers.get(field);
if(val != null) return val;
- return headers.get(field.toLowerCase()) == null ? "" : headers.get(field.toLowerCase());
+ return headers.get(field.toLowerCase(Locale.ROOT)) == null ? "" : headers.get(field.toLowerCase(Locale.ROOT));
}
private String getHeaderIgnoreCases(HashMap<String,String> headers, String field) {
String val = headers.get(field);
if(val != null) return val;
- String lowerCasedValue = headers.get(field.toLowerCase());
+ String lowerCasedValue = headers.get(field.toLowerCase(Locale.ROOT));
return lowerCasedValue == null ? "" : lowerCasedValue;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,4 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Fri, 01 Mar 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 01 Jul 2024 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.0]]></title><description><![CDATA[<h2>Desktop application<a name="desktop-application" href="#desktop-application" class="heading-anchor">🔗</a></h2>
<h3>Trash folder<a name="trash-folder" href="#trash-folder" class="heading-anchor">🔗</a></h3>
<p>Joplin now support a trash folder - any deleted notes or notebooks will be moved to that folder. You can also choose to have these notes permanently deleted after a number of days.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-trash.png" alt=""></p>
<p>Support for the trash folder has a somewhat long history in Joplin since it's an obvious and important feature to add, yet it can be particularly tricky once you start realising how many parts of the app it's going to impact.</p>
<p>Many attempts have been made over time: my first attempt was based on the note history feature. Indeed since this feature already saves versions of notes, it seems to make sense to use it for the trash feature, and indeed the note history feature <a href="https://joplinapp.org/news/20190523-221026">was designed for this originally</a>. However that approach turned to be needlessly complicated and after modifying hundreds of files just for this, the idea was dropped.</p>
<p>The next one was based on using a <a href="https://github.com/laurent22/joplin/issues/483">special &quot;trash&quot; tag</a> - deleted notes would have this tag attached to them and would appear in a special &quot;trash&quot; folder. This approach also had <a href="https://github.com/laurent22/joplin/issues/483">many issues</a> probably the main one being that notebooks can't be tagged, which means we would have to add support for tagged notebooks and that in itself would also be a massive change.</p>
<p><a href="https://discourse.joplinapp.org/t/trashcan/3998/16">Various</a>, <a href="https://discourse.joplinapp.org/t/poll-trash-bin-plugin/19951">ideas,</a> were also attempted using plugins, by creating a special &quot;trash folder&quot;, but in the end no such plugin was ever created, probably due to limitations of the plugin API.</p>
<p>In the end, turned out that this <a href="https://github.com/laurent22/joplin/issues/483#issuecomment-585655742">old idea</a> of adding a &quot;deleted&quot; property to each note and notebook was the easiest approach. With this it was simpler to get to a working solution relatively quickly, and then it was a matter of ensuring that deleted notes don't appear where they shouldn't, such as search results, etc.</p>
<h3>Joplin Cloud multi-factor authentication<a name="joplin-cloud-multi-factor-authentication" href="#joplin-cloud-multi-factor-authentication" class="heading-anchor">🔗</a></h3>
<p>Multi-factor authentication (MFA), also known as two-factor authentication (2FA) is a security process that requires you to provide two or more verification factors to gain access to a system or account. It typically includes something you know (password), something you have (security token), and something you are (biometric verification).</p>
<p>To better secure your account, Joplin Cloud and all Joplin applications now support MFA. To enable it, go to your Joplin Cloud profile, click on &quot;Enable multi-factor authentication&quot; and follow the instructions. Please note that all your applications will then be disconnected, so you will need to login again (your data of course will remain on the app so you won't have to download it again).</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mfa.png" alt=""></p>
<h3>Note list with multiple columns<a name="note-list-with-multiple-columns" href="#note-list-with-multiple-columns" class="heading-anchor">🔗</a></h3>
<p>In this release we add support for multiple columns in the note list. You can display various properties of the notes, as well as sort the notes by these properties. As usual this feature can be controlled and customised by plugins so for example it should be possible to display custom columns, and display custom information including thumbnails.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-note-list-multi.png" alt=""></p>
<h3>Plugin API enhancement<a name="plugin-api-enhancement" href="#plugin-api-enhancement" class="heading-anchor">🔗</a></h3>
<p>The plugin API has received several updates to facilitate easy customisation of the app As mentioned above, it is now possible to customise the new note list. Besides this, we've added support for loading PDFs and creating images from them, which can for example be used to create thumbnails.</p>
<p>Many other small enhancements have been made to the plugin API to help you tailor the app to your needs!</p>
<h3>View OCR data<a name="view-ocr-data" href="#view-ocr-data" class="heading-anchor">🔗</a></h3>
<p>Now when you right-click on an image or PDF you have an option to view the OCR (Optical character recognition) data associated with it. That will allow you for example to easily copy and paste the text.</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-ocr-data.png" alt=""></p>
<h2>Plugin support on mobile<a name="plugin-support-on-mobile" href="#plugin-support-on-mobile" class="heading-anchor">🔗</a></h2>
<p>As always, most of the above changes also apply to mobile (iOS and Android), for example the trash folder and MFA support.</p>
<p>Additionally the mobile application now adds support for plugins. To enable the feature, go to the settings then to the &quot;Plugins&quot; section. The feature is currently in Beta, in particular it means that some plugins do not work or only partially work. Normally the app should not offer you to install a non-working plugin but that may still happen. In general if you notice any issue with this beta feature please let me us know as we're keen to improve it.</p>
<p>Support for cross-platform plugins in Joplin is great news as it means a lot of new features become available on mobile. As of now, we have checked the following plugins and can confirm that they work on mobile:</p>
<ul>
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.quick-links/">Quick Links</a></li>
<li><a href="https://joplinapp.org/plugins/plugin/com.whatever.inline-tags/">Inline Tags</a></li>
<li><a href="https://joplinapp.org/plugins/plugin/io.github.personalizedrefrigerator.codemirror6-settings/">CodeMirror 6 settings</a></li>
<li><a href="https://joplinapp.org/plugins/plugin/com.hieuthi.joplin.function-plot/">Function plot</a></li>
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.space-indenter/">Space indenter</a></li>
<li><a href="https://joplinapp.org/plugins/plugin/joplin.plugin.alondmnt.tag-navigator/">Inline Tag Navigator</a></li>
</ul>
<p>Those are just some examples - many more are working!</p>
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20240701-mobile-plugins.png" alt=""></p>
<h1>Full changelogs<a name="full-changelogs" href="#full-changelogs" class="heading-anchor">🔗</a></h1>
<p>This is just an overview of the main features. The full changelogs are available there:</p>
<ul>
<li>Desktop: <a href="https://joplinapp.org/help/about/changelog/desktop">https://joplinapp.org/help/about/changelog/desktop</a></li>
<li>Android: <a href="https://joplinapp.org/help/about/changelog/android/">https://joplinapp.org/help/about/changelog/android/</a></li>
<li>iOS: <a href="https://joplinapp.org/help/about/changelog/ios/">https://joplinapp.org/help/about/changelog/ios/</a></li>
</ul>
]]></description><link>https://joplinapp.org/news/20240701-release-3-0</link><guid isPermaLink="false">20240701-release-3-0</guid><pubDate>Mon, 01 Jul 2024 00:00:00 GMT</pubDate><twitter-text>What&apos;s new in Joplin 3.0</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.14]]></title><description><![CDATA[<h2>OCR<a name="ocr" href="#ocr" class="heading-anchor">🔗</a></h2>
<p>Optical Character Recognition (OCR) in Joplin enables the transformation of text-containing images into machine-readable text formats. From this version you can enable OCR in the Configuration screen under the &quot;General&quot; section. Once activated, Joplin scans images and PDFs, extracting text data for searchability.</p>
<p>While OCR search is available on both desktop and mobile apps, document scanning is limited to the desktop due to resource demands. For more information head to the <a href="https://joplinapp.org/help/apps/ocr">OCR official documentation</a>!</p>
<h2>Bundled plugins<a name="bundled-plugins" href="#bundled-plugins" class="heading-anchor">🔗</a></h2>
@@ -394,7 +437,4 @@ sys 0m38.013s</p>
</tr>
</tbody>
</table>
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item><item><title><![CDATA[GSoC "Contributor Proposals" phase is starting now!]]></title><description><![CDATA[<p>The &quot;Contributor Proposals&quot; phase of GSoC 2022 is starting today! If you would like to be a contributor, now is the time to choose your project idea, write your proposal, and upload it to <a href="https://summerofcode.withgoogle.com/">https://summerofcode.withgoogle.com/</a></p>
<p>When it's done, please also let us know by posting an update on your forum introduction post.</p>
<p>If you haven't created a pull request yet, it's still time to create one. Doing so will greatly increase your chances of being selected!</p>
]]></description><link>https://joplinapp.org/news/20220405-gsoc-contributor-proposals</link><guid isPermaLink="false">20220405-gsoc-contributor-proposals</guid><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
]]></description><link>https://joplinapp.org/news/20220522-gsoc-contributors</link><guid isPermaLink="false">20220522-gsoc-contributors</guid><pubDate>Sun, 22 May 2022 00:00:00 GMT</pubDate><twitter-text>Joplin received 6 Contributor Projects for GSoC 2022! Welcome to our new contributors who will be working on these projects over summer!</twitter-text></item></channel></rss>

View File

@@ -28,6 +28,7 @@ SILENT=false
ALLOW_ROOT=false
SHOW_CHANGELOG=false
INCLUDE_PRE_RELEASE=false
INSTALL_DIR="${HOME}/.joplin" # default installation directory
print() {
if [[ "${SILENT}" == false ]]; then
@@ -57,6 +58,7 @@ showHelp() {
print "\t" "--force" "\t" "Always download the latest version"
print "\t" "--silent" "\t" "Don't print any output"
print "\t" "--prerelease" "\t" "Check for new Versions including Pre-Releases"
print "\t" "--install-dir" "\t" "Set installation directory; default: \"${INSTALL_DIR}\""
if [[ ! -z $1 ]]; then
print "\n" "${COLOR_RED}ERROR: " "$*" "${COLOR_RESET}" "\n"
@@ -84,6 +86,7 @@ while getopts "${optspec}" OPT; do
force ) FORCE=true ;;
changelog ) SHOW_CHANGELOG=true ;;
prerelease ) INCLUDE_PRE_RELEASE=true ;;
install-dir ) INSTALL_DIR="$OPTARG" ;;
[^\?]* ) showHelp "Illegal option --${OPT}"; exit 2 ;;
\? ) showHelp "Illegal option -${OPTARG}"; exit 2 ;;
esac
@@ -120,9 +123,10 @@ fi
print "Checking dependencies..."
## Check if libfuse2 is present.
if [[ $(command -v ldconfig) ]]; then
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
else
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
LIBFUSE=$(ldconfig -p | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
LIBFUSE=$(find /lib /usr/lib /lib64 /usr/lib64 /usr/local/lib -name "libfuse.so.2" 2>/dev/null | grep "libfuse.so.2" || echo '')
fi
if [[ $LIBFUSE == "" ]]; then
print "${COLOR_RED}Error: Can't get libfuse2 on system, please install libfuse2${COLOR_RESET}"
@@ -142,17 +146,17 @@ else
fi
# Check if it's in the latest version
if [[ -e ~/.joplin/VERSION ]] && [[ $(< ~/.joplin/VERSION) == "${RELEASE_VERSION}" ]]; then
if [[ -e "${INSTALL_DIR}/VERSION" ]] && [[ $(< "${INSTALL_DIR}/VERSION") == "${RELEASE_VERSION}" ]]; then
print "${COLOR_GREEN}You already have the latest version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
([[ "$FORCE" == true ]] && print "Forcing installation...") || exit 0
else
[[ -e ~/.joplin/VERSION ]] && CURRENT_VERSION=$(< ~/.joplin/VERSION)
[[ -e "${INSTALL_DIR}/VERSION" ]] && CURRENT_VERSION=$(< "${INSTALL_DIR}/VERSION")
print "The latest version is ${RELEASE_VERSION}, but you have ${CURRENT_VERSION:-no version} installed."
fi
# Check if it's an update or a new install
DOWNLOAD_TYPE="New"
if [[ -f ~/.joplin/Joplin.AppImage ]]; then
if [[ -f "${INSTALL_DIR}/Joplin.AppImage" ]]; then
DOWNLOAD_TYPE="Update"
fi
@@ -165,16 +169,16 @@ wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------
print 'Installing Joplin...'
# Delete previous version (in future versions joplin.desktop shouldn't exist)
rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/VERSION
rm -f "${INSTALL_DIR}"/*.AppImage ~/.local/share/applications/joplin.desktop "${INSTALL_DIR}/VERSION"
# Creates the folder where the binary will be stored
mkdir -p ~/.joplin/
mkdir -p "${INSTALL_DIR}/"
# Download the latest version
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
mv "${TEMP_DIR}/Joplin.AppImage" "${INSTALL_DIR}/Joplin.AppImage"
# Gives execution privileges
chmod +x ~/.joplin/Joplin.AppImage
chmod +x "${INSTALL_DIR}/Joplin.AppImage"
print "${COLOR_GREEN}OK${COLOR_RESET}"
@@ -253,7 +257,7 @@ if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cin
Encoding=UTF-8
Name=Joplin
Comment=Joplin for Desktop
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE ${HOME}/.joplin/Joplin.AppImage ${SANDBOXPARAM} %u
Exec=env APPIMAGELAUNCHER_DISABLE=TRUE "${INSTALL_DIR}/Joplin.AppImage" ${SANDBOXPARAM} %u
Icon=joplin
StartupWMClass=Joplin
Type=Application
@@ -278,7 +282,7 @@ fi
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
# Record version
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
echo "$RELEASE_VERSION" > "${INSTALL_DIR}/VERSION"
#-----------------------------------------------------
if [[ "$SHOW_CHANGELOG" == true ]]; then

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://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>
<!-- SPONSORS-ORG -->
* * *

View File

@@ -15,7 +15,6 @@
# SLAVE_POSTGRES_USER=joplin
# SLAVE_POSTGRES_PORT=5433
# SLAVE_POSTGRES_HOST=localhost
# USERS_WITH_REPLICATION=ID1,ID2,...
version: '2'

View File

@@ -21,7 +21,8 @@ module.exports = {
// See https://github.com/lint-staged/lint-staged/issues/934#issuecomment-743299357
'*.{js,jsx,ts,tsx,task1}': 'yarn checkIgnoredFiles',
'*.{js,jsx,ts,tsx,task2}': 'yarn spellcheck',
'*.{js,jsx,ts,tsx,task3}': 'yarn packageJsonLint',
'*.{js,jsx,ts,tsx,task4}': 'yarn linter-precommit',
'*.{md,mdx}': 'yarn spellcheck',
'*.{js,jsx,ts,tsx,task3}': 'yarn linter-precommit',
'*.{json,task4}': 'yarn packageJsonLint',
'*.{md,mdx,task5}': 'yarn spellcheck',
'*.{md,mdx,task6}': 'yarn validateFilenames',
};

View File

@@ -59,6 +59,7 @@
"updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc",
"updateNews": "node ./packages/tools/website/updateNews",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"validateFilenames": "node ./packages/tools/validateFilenames.js",
"watch": "yarn workspaces foreach --parallel --verbose --interlaced --jobs 999 run watch",
"watchWebsite": "nodemon --delay 1 --watch Assets/WebsiteAssets --watch packages/tools/website --watch packages/tools/website/utils --watch packages/doc-builder/build --ext md,ts,js,mustache,css,tsx,gif,png,svg --exec \"node packages/tools/website/build.js && http-server --port 8077 ../joplin-website/docs -a localhost\""
},
@@ -82,7 +83,7 @@
"eslint-plugin-react": "7.33.2",
"execa": "5.1.1",
"fs-extra": "11.2.0",
"glob": "10.3.10",
"glob": "10.3.12",
"gulp": "4.0.2",
"husky": "3.1.0",
"lerna": "3.22.1",
@@ -109,6 +110,7 @@
"@react-native-community/slider": "patch:@react-native-community/slider@npm%3A4.4.4#./.yarn/patches/@react-native-community-slider-npm-4.4.4-d78e472f48.patch",
"husky": "patch:husky@npm%3A3.1.0#./.yarn/patches/husky-npm-3.1.0-5cc13e4e34.patch",
"chokidar@^2.0.0": "3.5.3",
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch"
"react-native@0.74.1": "patch:react-native@npm%3A0.74.1#./.yarn/patches/react-native-npm-0.74.1-754c02ae9e.patch",
"rn-fetch-blob@0.12.0": "patch:rn-fetch-blob@npm%3A0.12.0#./.yarn/patches/rn-fetch-blob-npm-0.12.0-cf02e3c544.patch"
}
}

View File

@@ -1,4 +1,4 @@
import Setting, { SettingStorage } from '@joplin/lib/models/Setting';
import Setting, { AppType, SettingStorage } from '@joplin/lib/models/Setting';
import { SettingItemType } from '@joplin/lib/services/plugins/api/types';
import shim from '@joplin/lib/shim';
@@ -61,7 +61,7 @@ class Command extends BaseCommand {
const description: string[] = [];
if (md.label && md.label()) description.push(md.label());
if (md.description && md.description('desktop')) description.push(md.description('desktop'));
if (md.description && md.description(AppType.Desktop)) description.push(md.description(AppType.Desktop));
if (description.length) props.description = description.join('. ');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -35,15 +35,15 @@
],
"owner": "Laurent Cozic"
},
"version": "3.0.0",
"version": "3.1.0",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"
},
"dependencies": {
"@joplin/lib": "~3.0",
"@joplin/renderer": "~3.0",
"@joplin/utils": "~3.0",
"@joplin/lib": "~3.1",
"@joplin/renderer": "~3.1",
"@joplin/utils": "~3.1",
"aws-sdk": "2.1340.0",
"chalk": "4.1.2",
"compare-version": "0.1.2",
@@ -57,23 +57,23 @@
"proper-lockfile": "4.1.2",
"read-chunk": "2.1.0",
"server-destroy": "1.0.1",
"sharp": "0.33.2",
"sharp": "0.33.3",
"sprintf-js": "1.1.3",
"sqlite3": "5.1.6",
"string-padding": "1.0.2",
"strip-ansi": "6.0.1",
"tcp-port-used": "1.0.2",
"terminal-kit": "3.0.2",
"terminal-kit": "3.1.1",
"tkwidgets": "0.5.27",
"url-parse": "1.5.10",
"word-wrap": "1.2.5",
"yargs-parser": "21.1.1"
},
"devDependencies": {
"@joplin/tools": "~3.0",
"@joplin/tools": "~3.1",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.8",
"@types/node": "18.19.26",
"@types/node": "18.19.33",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",

View File

@@ -2,13 +2,16 @@ import MdToHtml 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';
import { RenderOptions } from '@joplin/renderer/types';
import { isResourceUrl, resourceUrlToId } from '@joplin/lib/models/utils/resourceUtils';
const { themeStyle } = require('@joplin/lib/theme');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
function newTestMdToHtml(options: any = null) {
options = {
ResourceModel: {
isResourceUrl: () => false,
isResourceUrl: isResourceUrl,
urlToId: resourceUrlToId,
},
fsDriver: shim.fsDriver(),
...options,
@@ -39,7 +42,7 @@ describe('MdToHtml', () => {
// if (mdFilename !== 'sanitize_9.md') continue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const mdToHtmlOptions: any = {
const mdToHtmlOptions: RenderOptions = {
bodyOnly: true,
};
@@ -51,6 +54,8 @@ describe('MdToHtml', () => {
};
} else if (mdFilename.startsWith('sourcemap_')) {
mdToHtmlOptions.mapsToLine = true;
} else if (mdFilename.startsWith('resource_')) {
mdToHtmlOptions.resources = {};
}
const markdown = await shim.fsDriver().readFile(mdFilePath);

View File

@@ -0,0 +1,48 @@
<p>Markdown images:</p>
<ul>
<li>
With ALT and title:
<div
class="not-loaded-resource not-loaded-image-resource resource-status-test"
data-original-alt="test"
data-original-title="testing"
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
>
<img src="data:image/svg+xml;utf8,some-icon-here"/>
</div>
</li>
<li>
With neither ALT nor title:
<div
class="not-loaded-resource not-loaded-image-resource resource-status-error"
data-original-alt=""
data-original-title=""
data-resource-id="0a25d61cc33e57afa6dde45948c3177f"
>
<img src="data:image/svg+xml;utf8,some-icon-here"/>
</div>
</li>
</ul>
<p>HTML images:</p>
<ul>
<li>
<div
class="not-loaded-resource not-loaded-image-resource resource-status-error"
data-original-before=" width=&quot;230&quot;"
data-original-after=" style=&quot;border: 32px inset red;&quot;/"
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
>
<img src="data:image/svg+xml;utf8,some-icon-here"/>
</div>
</li>
<li>
<div
class="not-loaded-resource not-loaded-image-resource resource-status-error"
data-original-after="/"
data-resource-id="0415d61cc33e47afa6dde45948c3177f"
>
<img src="data:image/svg+xml;utf8,some-icon-here"/>
</div>
</li>
</ul>

View File

@@ -0,0 +1,9 @@
Markdown images:
- With ALT and title:![test](:/0415d61cc33e47afa6dde45948c3177f "testing")
- With neither ALT nor title:![](:/0a25d61cc33e57afa6dde45948c3177f)
HTML images:
- <img width="230" src=":/0415d61cc33e47afa6dde45948c3177f" style="border: 32px inset red;"/>
- <img src=":/0415d61cc33e47afa6dde45948c3177f" />

View File

@@ -0,0 +1,15 @@
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22345" data-original-alt data-original-title="test" contenteditable="false"><img src="data:image/svg+xml;utf8,
&Tab;&Tab;&lt;svg width=&quot;1700&quot; height=&quot;1536&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&Tab;&Tab; &lt;path d=&quot;M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z&quot;/&gt;
&Tab;&Tab;&lt;/svg&gt;
&Tab;"/></div>
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22346" data-original-alt="test" data-original-title contenteditable="false"><img src="data:image/svg+xml;utf8,
&Tab;&Tab;&lt;svg width=&quot;1700&quot; height=&quot;1536&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&Tab;&Tab; &lt;path d=&quot;M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z&quot;/&gt;
&Tab;&Tab;&lt;/svg&gt;
&Tab;"/></div>
<div class="not-loaded-resource not-loaded-image-resource resource-status-notDownloaded" data-resource-id="a1test2a1test2a1test2a1test22347" data-original-before=" " data-original-after=" class=&quot;jop-noMdConv&quot;/" contenteditable="false"><img src="data:image/svg+xml;utf8,
&Tab;&Tab;&lt;svg width=&quot;1700&quot; height=&quot;1536&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&Tab;&Tab; &lt;path d=&quot;M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z&quot;/&gt;
&Tab;&Tab;&lt;/svg&gt;
&Tab;"/></div>

View File

@@ -0,0 +1,3 @@
![](:/a1test2a1test2a1test2a1test22345 "test")
![test](:/a1test2a1test2a1test2a1test22346)
<img src=":/a1test2a1test2a1test2a1test22347"/>

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Joplin Web Clipper [DEV]",
"version": "3.0.0",
"version": "3.1.0",
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
"homepage_url": "https://joplinapp.org",
"content_security_policy": {
@@ -54,8 +54,9 @@
}
},
"background": {
"scripts": ["service_worker.mjs"],
"scripts": [
"service_worker.mjs"
],
"service_worker": "service_worker.mjs",
"type": "module"
},

View File

@@ -1,5 +1,6 @@
import Logger from '@joplin/utils/Logger';
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 { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
@@ -13,6 +14,7 @@ const fs = require('fs-extra');
import { dialog, ipcMain } from 'electron';
import { _ } from '@joplin/lib/locale';
import restartInSafeModeFromMain from './utils/restartInSafeModeFromMain';
import handleCustomProtocols, { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
import { clearTimeout, setTimeout } from 'timers';
interface RendererProcessQuitReply {
@@ -40,6 +42,8 @@ export default class ElectronAppWrapper {
private rendererProcessQuitReply_: RendererProcessQuitReply = null;
private pluginWindows_: PluginWindows = {};
private initialCallbackUrl_: string = null;
// private updaterService_: AutoUpdaterService = null;
private customProtocolHandler_: CustomProtocolHandler = 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) {
@@ -454,6 +458,14 @@ export default class ElectronAppWrapper {
return false;
}
public initializeCustomProtocolHandler(logger: LoggerWrapper) {
this.customProtocolHandler_ ??= handleCustomProtocols(logger);
}
public getCustomProtocolHandler() {
return this.customProtocolHandler_;
}
public async start() {
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is ready.
@@ -464,6 +476,13 @@ 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

@@ -71,6 +71,7 @@ import OcrService from '@joplin/lib/services/ocr/OcrService';
import OcrDriverTesseract from '@joplin/lib/services/ocr/drivers/OcrDriverTesseract';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
import { PackageInfo } from '@joplin/lib/versionInfo';
import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols';
import { refreshFolders } from '@joplin/lib/folders-screen-utils';
const pluginClasses = [
@@ -88,6 +89,7 @@ class Application extends BaseApplication {
private checkAllPluginStartedIID_: any = null;
private initPluginServiceDone_ = false;
private ocrService_: OcrService;
private protocolHandler_: CustomProtocolHandler;
public constructor() {
super();
@@ -129,7 +131,7 @@ class Application extends BaseApplication {
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'ocr.enabled' || action.type === 'SETTING_UPDATE_ALL') {
this.setupOcrService();
void this.setupOcrService();
}
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'style.editor.fontFamily' || action.type === 'SETTING_UPDATE_ALL') {
@@ -167,6 +169,12 @@ class Application extends BaseApplication {
this.handleThemeAutoDetect();
}
if (action.type === 'PLUGIN_ADD') {
const plugin = PluginService.instance().pluginById(action.plugin.id);
this.protocolHandler_.allowReadAccessToDirectory(plugin.baseDir);
this.protocolHandler_.allowReadAccessToDirectory(plugin.dataDir);
}
return result;
}
@@ -204,7 +212,7 @@ class Application extends BaseApplication {
public updateEditorFont() {
const fontFamilies = [];
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
fontFamilies.push('Avenir, Arial, sans-serif');
fontFamilies.push('\'Avenir Next\', Avenir, Arial, sans-serif');
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155
@@ -353,16 +361,29 @@ class Application extends BaseApplication {
Setting.setValue('wasClosedSuccessfully', false);
}
private setupOcrService() {
private async setupOcrService() {
if (Setting.value('ocr.clearLanguageDataCache')) {
Setting.setValue('ocr.clearLanguageDataCache', false);
try {
await OcrDriverTesseract.clearLanguageDataCache();
} catch (error) {
this.logger().warn('OCR: Failed to clear language data cache.', error);
}
}
if (Setting.value('ocr.enabled')) {
if (!this.ocrService_) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const Tesseract = (window as any).Tesseract;
const driver = new OcrDriverTesseract(
{ createWorker: Tesseract.createWorker },
`${bridge().buildDir()}/tesseract.js/worker.min.js`,
`${bridge().buildDir()}/tesseract.js-core`,
{
workerPath: `${bridge().buildDir()}/tesseract.js/worker.min.js`,
corePath: `${bridge().buildDir()}/tesseract.js-core`,
languageDataPath: Setting.value('ocr.languageDataPath') || null,
},
);
this.ocrService_ = new OcrService(driver);
@@ -427,6 +448,20 @@ class Application extends BaseApplication {
bridge().openDevTools();
}
bridge().electronApp().initializeCustomProtocolHandler(
Logger.create('handleCustomProtocols'),
);
this.protocolHandler_ = bridge().electronApp().getCustomProtocolHandler();
this.protocolHandler_.allowReadAccessToDirectory(__dirname); // App bundle directory
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('cacheDir'));
this.protocolHandler_.allowReadAccessToDirectory(Setting.value('resourceDir'));
// this.protocolHandler_.allowReadAccessTo(Setting.value('tempDir'));
// For now, this doesn't seem necessary:
// this.protocolHandler_.allowReadAccessTo(Setting.value('profileDir'));
// If it is needed, note that they decrease the security of the protcol
// handler, and, as such, it may make sense to also limit permissions of
// allowed pages with a Content Security Policy.
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
PluginManager.instance().setLogger(reg.logger());
PluginManager.instance().register(pluginClasses);

View File

@@ -1,7 +1,7 @@
import ElectronAppWrapper from './ElectronAppWrapper';
import shim from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions } from 'electron';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron';
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
import { fileUriToPath } from '@joplin/utils/url';
import { urlDecode } from '@joplin/lib/string-utils';
@@ -485,6 +485,21 @@ export class Bridge {
return nativeImage.createFromPath(path);
}
public safeStorage = {
isEncryptionAvailable() {
return safeStorage.isEncryptionAvailable();
},
encryptString(data: string) {
return safeStorage.encryptString(data).toString('base64');
},
decryptString(base64Data: string) {
return safeStorage.decryptString(Buffer.from(base64Data, 'base64'));
},
getSelectedStorageBackend() {
return safeStorage.getSelectedStorageBackend();
},
};
}
let bridge_: Bridge = null;

View File

@@ -36,6 +36,9 @@ interface Props {
isSquare?: boolean;
iconOnly?: boolean;
fontSize?: number;
'aria-controls'?: string;
'aria-expanded'?: string;
}
const StyledTitle = styled.span`
@@ -220,7 +223,14 @@ const Button = React.forwardRef((props: Props, ref: any) => {
function renderIcon() {
if (!props.iconName) return null;
return <StyledIcon aria-label={props.iconLabel} animation={props.iconAnimation} mr={iconOnly ? '0' : '6px'} color={props.color} className={props.iconName}/>;
return <StyledIcon
aria-label={props.iconLabel ?? ''}
animation={props.iconAnimation}
mr={iconOnly ? '0' : '6px'}
color={props.color}
className={props.iconName}
role='img'
/>;
}
function renderTitle() {
@@ -234,7 +244,22 @@ const Button = React.forwardRef((props: Props, ref: any) => {
}
return (
<StyledButton ref={ref} fontSize={props.fontSize} isSquare={props.isSquare} size={props.size} style={props.style} disabled={props.disabled} title={props.tooltip} className={props.className} iconOnly={iconOnly} onClick={onClick}>
<StyledButton
ref={ref}
fontSize={props.fontSize}
isSquare={props.isSquare}
size={props.size}
style={props.style}
disabled={props.disabled}
title={props.tooltip}
className={props.className}
iconOnly={iconOnly}
onClick={onClick}
aria-disabled={props.disabled}
aria-expanded={props['aria-expanded']}
aria-controls={props['aria-controls']}
>
{renderIcon()}
{renderTitle()}
</StyledButton>

View File

@@ -1,16 +1,14 @@
import * as React from 'react';
import Sidebar from './Sidebar';
import ButtonBar from './ButtonBar';
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
import Button, { ButtonLevel } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SettingItemSubType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/plugins/PluginsStates';
import Setting, { AppType, SettingValueType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
import { reg } from '@joplin/lib/registry';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
import * as pathUtils from '@joplin/lib/path-utils';
import { themeStyle } from '@joplin/lib/theme';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
import ClipperConfigScreen from '../ClipperConfigScreen';
@@ -20,12 +18,8 @@ import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButto
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import FontSearch from './FontSearch';
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};
interface Font {
family: string;
@@ -34,6 +28,7 @@ interface Font {
declare global {
interface Window {
queryLocalFonts(): Promise<Font[]>;
openChangelogLink: ()=> void;
}
}
@@ -67,9 +62,6 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.onCancelClick = this.onCancelClick.bind(this);
this.onSaveClick = this.onSaveClick.bind(this);
this.onApplyClick = this.onApplyClick.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.renderDescription = this.renderDescription.bind(this);
this.renderHeader = this.renderHeader.bind(this);
this.handleSettingButton = this.handleSettingButton.bind(this);
}
@@ -113,6 +105,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
Setting.setValue('sync.startupOperation', SyncStartupOperation.ClearLocalData);
await Setting.saveAll();
await restart();
} else if (key === 'ocr.clearLanguageDataCacheButton') {
if (!confirm(this.restartMessage())) return;
Setting.setValue('ocr.clearLanguageDataCache', true);
await restart();
} else if (key === 'sync.openSyncWizard') {
this.props.dispatch({
type: 'DIALOG_OPEN',
@@ -237,7 +233,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
if (syncTargetMd.supportsConfigCheck) {
const messages = shared.checkSyncConfigMessages(this);
const statusComp = !messages.length ? null : (
<div style={statusStyle}>
<div style={statusStyle} aria-live='polite'>
{messages[0]}
{messages.length >= 1 ? <p>{messages[1]}</p> : null}
</div>
@@ -277,12 +273,14 @@ class ConfigScreenComponent extends React.Component<any, any> {
let advancedSettingsButton = null;
const advancedSettingsSectionStyle = { display: 'none' };
const advancedSettingsGroupId = `advanced_settings_${key}`;
if (advancedSettingComps.length) {
advancedSettingsButton = (
<ToggleAdvancedSettingsButton
onClick={() => shared.advancedSettingsButton_click(this)}
advancedSettingsVisible={this.state.showAdvancedSettings}
aria-controls={advancedSettingsGroupId}
/>
);
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
@@ -293,425 +291,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
{this.renderSectionDescription(section)}
<div>{settingComps}</div>
{advancedSettingsButton}
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
<div
style={advancedSettingsSectionStyle}
id={advancedSettingsGroupId}
role='group'
>{advancedSettingComps}</div>
</div>
);
}
private labelStyle(themeId: number) {
const theme = themeStyle(themeId);
return { ...theme.textStyle, display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.083333,
fontWeight: 500,
marginBottom: theme.mainPadding / 2 };
}
private descriptionStyle(themeId: number) {
const theme = themeStyle(themeId);
return { ...theme.textStyle, color: theme.colorFaded,
fontStyle: 'italic',
maxWidth: '70em',
marginTop: 5 };
}
private renderLabel(themeId: number, label: string) {
const labelStyle = this.labelStyle(themeId);
return (
<div style={labelStyle}>
<label>{label}</label>
</div>
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private renderHeader(themeId: number, label: string, style: any = null) {
const theme = themeStyle(themeId);
const labelStyle = { ...theme.textStyle, display: 'block',
color: theme.color,
fontSize: theme.fontSize * 1.25,
fontWeight: 500,
marginBottom: theme.mainPadding,
...style };
return (
<div style={labelStyle}>
<label>{label}</label>
</div>
);
}
private renderDescription(themeId: number, description: string) {
return description ? <div style={this.descriptionStyle(themeId)}>{description}</div> : null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public settingToComponent(key: string, value: any) {
const theme = themeStyle(this.props.themeId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const output: any = null;
const rowStyle = {
marginBottom: theme.mainPadding * 1.5,
};
const labelStyle = this.labelStyle(this.props.themeId);
const subLabel = { ...labelStyle, display: 'block',
opacity: 0.7,
marginBottom: labelStyle.marginBottom };
const checkboxLabelStyle = { ...labelStyle, marginLeft: 8,
display: 'inline',
backgroundColor: 'transparent' };
const controlStyle = {
display: 'inline-block',
color: theme.color,
fontFamily: theme.fontFamily,
backgroundColor: theme.backgroundColor,
};
const textInputBaseStyle = { ...controlStyle, fontFamily: theme.fontFamily,
border: '1px solid',
padding: '4px 6px',
boxSizing: 'border-box',
borderColor: theme.borderColor4,
borderRadius: 3,
paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const updateSettingValue = (key: string, value: any) => {
const md = Setting.settingMetadata(key);
if (md.needRestart) {
this.setState({ needRestart: true });
}
shared.updateSettingValue(this, key, value);
};
private onUpdateSettingValue = ({ key, value }: UpdateSettingValueEvent) => {
const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
const descriptionComp = this.renderDescription(this.props.themeId, descriptionText);
if (settingKeyToControl[key]) {
const SettingComponent = settingKeyToControl[key];
const label = md.label ? this.renderLabel(this.props.themeId, md.label()) : null;
return (
<div key={key} style={rowStyle}>
{label}
{this.renderDescription(this.props.themeId, md.description ? md.description() : null)}
<SettingComponent
metadata={md}
value={value}
themeId={this.props.themeId}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
updateSettingValue(key, event.value);
}}
renderLabel={this.renderLabel}
renderDescription={this.renderDescription}
renderHeader={this.renderHeader}
/>
</div>
);
} else if (md.isEnum) {
const items = [];
const settingOptions = md.options();
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
valueKey: 'key',
labelKey: 'label',
});
for (let i = 0; i < array.length; i++) {
const e = array[i];
items.push(
<option value={e.key.toString()} key={e.key}>
{settingOptions[e.key]}
</option>,
);
}
const selectStyle = { ...controlStyle, paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
borderColor: theme.borderColor4,
borderRadius: 3 };
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<select
value={value}
style={selectStyle}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
updateSettingValue(key, event.target.value);
}}
>
{items}
</select>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_BOOL) {
const onCheckboxClick = () => {
updateSettingValue(key, !value);
};
const checkboxSize = theme.fontSize * 1.1666666666666;
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
// There's probably a better way to do this but can't figure it out.
return (
<div key={key + (`${value}`).toString()} style={rowStyle}>
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
<input
id={`setting_checkbox_${key}`}
type="checkbox"
checked={!!value}
onChange={() => {
onCheckboxClick();
}}
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
/>
<label
onClick={() => {
onCheckboxClick();
}}
style={{ ...checkboxLabelStyle, marginLeft: 5, marginBottom: 0 }}
htmlFor={`setting_checkbox_${key}`}
>
{md.label()}
</label>
</div>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_STRING) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
minWidth: '20em' };
const inputType = md.secure === true ? 'password' : 'text';
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
inputStyle.marginBottom = subLabel.marginBottom;
const splitCmd = (cmdString: string) => {
// Normally not necessary but certain plugins found a way to
// set the set the value to "undefined", leading to a crash.
// This is now fixed at the model level but to be sure we
// check here too, to handle any already existing data.
// https://github.com/laurent22/joplin/issues/7621
if (!cmdString) cmdString = '';
const path = pathUtils.extractExecutablePath(cmdString);
const args = cmdString.substr(path.length + 1);
return [pathUtils.unquotePath(path), args];
};
const joinCmd = (cmdArray: string[]) => {
if (!cmdArray[0] && !cmdArray[1]) return '';
let cmdString = pathUtils.quotePath(cmdArray[0]);
if (!cmdString) cmdString = '""';
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
return cmdString;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onPathChange = (event: any) => {
if (md.subType === 'file_path_and_args') {
const cmd = splitCmd(this.state.settings[key]);
cmd[0] = event.target.value;
updateSettingValue(key, joinCmd(cmd));
} else {
updateSettingValue(key, event.target.value);
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onArgsChange = (event: any) => {
const cmd = splitCmd(this.state.settings[key]);
cmd[1] = event.target.value;
updateSettingValue(key, joinCmd(cmd));
};
const browseButtonClick = async () => {
if (md.subType === 'directory_path') {
const paths = await bridge().showOpenDialog({
properties: ['openDirectory'],
});
if (!paths || !paths.length) return;
updateSettingValue(key, paths[0]);
} else {
const paths = await bridge().showOpenDialog();
if (!paths || !paths.length) return;
if (md.subType === 'file_path') {
updateSettingValue(key, paths[0]);
} else {
const cmd = splitCmd(this.state.settings[key]);
cmd[0] = paths[0];
updateSettingValue(key, joinCmd(cmd));
}
}
};
const cmd = splitCmd(this.state.settings[key]);
const path = md.subType === 'file_path_and_args' ? cmd[0] : this.state.settings[key];
const argComp = md.subType !== 'file_path_and_args' ? null : (
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>{_('Arguments:')}</div>
<input
type={inputType}
style={inputStyle}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onArgsChange(event);
}}
value={cmd[1]}
spellCheck={false}
/>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
);
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div style={subLabel}>{_('Path:')}</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
<input
type={inputType}
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onPathChange(event);
}}
value={path}
spellCheck={false}
/>
<Button
level={ButtonLevel.Secondary}
title={_('Browse...')}
onClick={browseButtonClick}
size={ButtonSize.Small}
/>
</div>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
</div>
</div>
{argComp}
</div>
);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onTextChange = (event: any) => {
updateSettingValue(key, event.target.value);
};
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
{
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
<FontSearch
type={inputType}
style={inputStyle}
value={this.state.settings[key]}
availableFonts={this.state.fonts}
onChange={fontFamily => updateSettingValue(key, fontFamily)}
subtype={md.subType}
/> :
<input
type={inputType}
style={inputStyle}
value={this.state.settings[key]}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onTextChange(event);
}}
spellCheck={false}
/>
}
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
);
}
} else if (md.type === Setting.TYPE_INT) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onNumChange = (event: any) => {
updateSettingValue(key, event.target.value);
};
const label = [md.label()];
if (md.unitLabel) label.push(`(${md.unitLabel()})`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const inputStyle: any = { ...textInputBaseStyle };
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{label.join(' ')}</label>
</div>
<input
type="number"
style={inputStyle}
value={this.state.settings[key]}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onNumChange(event);
}}
min={md.minimum}
max={md.maximum}
step={md.step}
spellCheck={false}
/>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_BUTTON) {
const labelComp = md.hideLabel ? null : (
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
);
return (
<div key={key} style={rowStyle}>
{labelComp}
<Button level={ButtonLevel.Secondary} title={md.label()} onClick={md.onClick ? md.onClick : () => this.handleSettingButton(key)}/>
{descriptionComp}
</div>
);
} else {
console.warn(`Type not implemented: ${key}`);
if (md.needRestart) {
this.setState({ needRestart: true });
}
shared.updateSettingValue(this, key, value);
};
return output;
public settingToComponent<T extends string>(key: T, value: SettingValueType<T>) {
return (
<SettingComponent
themeId={this.props.themeId}
key={key}
settingKey={key}
value={value}
fonts={this.state.fonts}
onUpdateSettingValue={this.onUpdateSettingValue}
onSettingButtonClick={this.handleSettingButton}
/>
);
}
private restartMessage() {
@@ -768,7 +376,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
const settings = this.state.settings;
const containerStyle = {
const containerStyle: React.CSSProperties = {
overflow: 'auto',
padding: theme.configScreenPadding,
paddingTop: 0,
@@ -800,6 +408,35 @@ class ConfigScreenComponent extends React.Component<any, any> {
const rightStyle = { ...style, flex: 1 };
delete style.width;
const tabComponents: React.ReactNode[] = [];
for (const section of sections) {
const sectionId = `setting-section-${section.name}`;
let content = null;
const visible = section.name === this.state.selectedSectionName;
if (visible) {
content = (
<>
{screenComp}
<div style={containerStyle}>{settingComps}</div>
</>
);
}
tabComponents.push(
<div
key={sectionId}
id={sectionId}
className={`setting-tab-panel ${!visible ? '-hidden' : ''}`}
hidden={!visible}
aria-labelledby={`setting-tab-${section.name}`}
tabIndex={0}
role='tabpanel'
>
{content}
</div>,
);
}
return (
<div className="config-screen" style={{ display: 'flex', flexDirection: 'row', height: this.props.style.height }}>
<Sidebar
@@ -808,9 +445,8 @@ class ConfigScreenComponent extends React.Component<any, any> {
sections={sections}
/>
<div style={rightStyle}>
{screenComp}
{needRestartComp}
<div style={containerStyle}>{settingComps}</div>
{tabComponents}
<ButtonBar
hasChanges={hasChanges}
backButtonTitle={hasChanges && !screenComp ? _('Cancel') : _('Back')}

View File

@@ -1,18 +1,22 @@
import { AppType, SettingSectionSource } from '@joplin/lib/models/Setting';
import { AppType, MetadataBySection, SettingMetadataSection, SettingSectionSource } from '@joplin/lib/models/Setting';
import * as React from 'react';
import Setting from '@joplin/lib/models/Setting';
import { _ } from '@joplin/lib/locale';
import { useCallback, useRef } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
const styled = require('styled-components').default;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
type StyleProps = any;
interface SectionChangeEvent {
section: SettingMetadataSection;
}
interface Props {
selection: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onSelectionChange: Function;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
sections: any[];
onSelectionChange: (event: SectionChangeEvent)=> void;
sections: MetadataBySection;
}
export const StyledRoot = styled.div`
@@ -73,24 +77,63 @@ export const StyledListItemIcon = styled.i`
`;
export default function Sidebar(props: Props) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
const buttons: any[] = [];
const buttonRefs = useRef<HTMLElement[]>([]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied;
function renderButton(section: any) {
// Making a tabbed region accessible involves supporting keyboard interaction.
// See https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ for details
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback((event) => {
const selectedIndex = props.sections.findIndex(section => section.name === props.selection);
let newIndex = selectedIndex;
if (event.code === 'ArrowUp') {
newIndex --;
} else if (event.code === 'ArrowDown') {
newIndex ++;
} else if (event.code === 'Home') {
newIndex = 0;
} else if (event.code === 'End') {
newIndex = props.sections.length - 1;
}
if (newIndex < 0) newIndex += props.sections.length;
newIndex %= props.sections.length;
if (newIndex !== selectedIndex) {
event.preventDefault();
props.onSelectionChange({ section: props.sections[newIndex] });
const targetButton = buttonRefs.current[newIndex];
if (targetButton) {
focus('Sidebar', targetButton);
}
}
}, [props.sections, props.selection, props.onSelectionChange]);
const buttons: React.ReactNode[] = [];
function renderButton(section: SettingMetadataSection, index: number) {
const selected = props.selection === section.name;
return (
<StyledListItem
key={section.name}
href='#'
role='tab'
ref={(item: HTMLElement) => { buttonRefs.current[index] = item; }}
id={`setting-tab-${section.name}`}
aria-controls={`setting-section-${section.name}`}
aria-selected={selected}
tabIndex={selected ? 0 : -1}
isSubSection={Setting.isSubSection(section.name)}
selected={selected}
onClick={() => { props.onSelectionChange({ section: section }); }}
onKeyDown={onKeyDown}
>
<StyledListItemIcon
aria-label=''
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
role='img'
/>
<StyledListItemLabel>
{Setting.sectionNameToLabel(section.name)}
@@ -109,13 +152,15 @@ export default function Sidebar(props: Props) {
let pluginDividerAdded = false;
let index = 0;
for (const section of props.sections) {
if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) {
buttons.push(renderDivider('divider-plugins'));
pluginDividerAdded = true;
}
buttons.push(renderButton(section));
buttons.push(renderButton(section, index));
index ++;
}
return (

View File

@@ -9,6 +9,7 @@ interface Props {
style: CSSProperties;
value: string;
availableFonts: string[];
inputId: string;
onChange: (font: string)=> void;
subtype: string;
}
@@ -108,6 +109,7 @@ const FontSearch = (props: Props) => {
onFocus={handleFocus}
onBlur={handleBlur}
spellCheck={false}
id={props.inputId}
ref={fontInputRef}
/>
<div

View File

@@ -0,0 +1,381 @@
import Setting, { AppType, SettingItemSubType } from '@joplin/lib/models/Setting';
import { themeStyle } from '@joplin/lib/theme';
import * as React from 'react';
import { useCallback, useId } from 'react';
import control_PluginsStates from './plugins/PluginsStates';
import bridge from '../../../services/bridge';
import { _ } from '@joplin/lib/locale';
import Button, { ButtonLevel, ButtonSize } from '../../Button/Button';
import FontSearch from './FontSearch';
import * as pathUtils from '@joplin/lib/path-utils';
import SettingLabel from './SettingLabel';
import SettingDescription from './SettingDescription';
const settingKeyToControl: Record<string, typeof control_PluginsStates> = {
'plugins.states': control_PluginsStates,
};
export interface UpdateSettingValueEvent {
key: string;
value: unknown;
}
interface Props {
themeId: number;
settingKey: string;
value: unknown;
fonts: string[];
onUpdateSettingValue: (event: UpdateSettingValueEvent)=> void;
onSettingButtonClick: (key: string)=> void;
}
const SettingComponent: React.FC<Props> = props => {
const theme = themeStyle(props.themeId);
const output: React.ReactNode = null;
const updateSettingValue = useCallback((key: string, value: unknown) => {
props.onUpdateSettingValue({ key, value });
}, [props.onUpdateSettingValue]);
const rowStyle = {
marginBottom: theme.mainPadding * 1.5,
};
const controlStyle = {
display: 'inline-block',
color: theme.color,
fontFamily: theme.fontFamily,
backgroundColor: theme.backgroundColor,
};
const textInputBaseStyle: React.CSSProperties = {
...controlStyle,
fontFamily: theme.fontFamily,
border: '1px solid',
padding: '4px 6px',
boxSizing: 'border-box',
borderColor: theme.borderColor4,
borderRadius: 3,
paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
};
const key = props.settingKey;
const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, AppType.Desktop);
const inputId = useId();
const descriptionId = useId();
const descriptionComp = <SettingDescription id={descriptionId} text={descriptionText}/>;
if (key in settingKeyToControl) {
const CustomSettingComponent = settingKeyToControl[key];
const label = md.label ? <SettingLabel text={md.label()} htmlFor={null} /> : null;
return (
<div style={rowStyle}>
{label}
<SettingDescription id={descriptionId} text={md.description ? md.description(AppType.Desktop) : null}/>
<CustomSettingComponent
value={props.value}
themeId={props.themeId}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
updateSettingValue(key, event.value);
}}
/>
</div>
);
} else if (md.isEnum) {
const value = props.value as string;
const items = [];
const settingOptions = md.options();
const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], {
valueKey: 'key',
labelKey: 'label',
});
for (let i = 0; i < array.length; i++) {
const e = array[i];
items.push(
<option value={e.key.toString()} key={e.key}>
{settingOptions[e.key]}
</option>,
);
}
const selectStyle = { ...controlStyle, paddingLeft: 6,
paddingRight: 6,
paddingTop: 4,
paddingBottom: 4,
borderColor: theme.borderColor4,
borderRadius: 3 };
return (
<div style={rowStyle}>
<SettingLabel htmlFor={inputId} text={md.label()}/>
<select
value={value}
style={selectStyle}
onChange={(event) => {
updateSettingValue(key, event.target.value);
}}
id={inputId}
aria-describedby={descriptionId}
>
{items}
</select>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_BOOL) {
const value = props.value as boolean;
const checkboxSize = theme.fontSize * 1.1666666666666;
return (
<div style={rowStyle}>
<div style={{ ...controlStyle, backgroundColor: 'transparent', display: 'flex', alignItems: 'center' }}>
<input
id={inputId}
type="checkbox"
checked={!!value}
onChange={event => updateSettingValue(key, event.target.checked)}
style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }}
// Prefer aria-details to aria-describedby for checkbox inputs --
// on MacOS, VoiceOver reads "checked"/"unchecked" only after reading the
// potentially-lengthy description. For other input types, the input value
// is read first.
aria-details={descriptionId}
/>
<label
className='setting-label -for-checkbox'
htmlFor={inputId}
>
{md.label()}
</label>
</div>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_STRING) {
const value = props.value as string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const inputStyle: any = { ...textInputBaseStyle, width: '50%',
minWidth: '20em' };
const inputType = md.secure === true ? 'password' : 'text';
if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') {
inputStyle.marginBottom = theme.mainPadding / 2;
const splitCmd = (cmdString: string) => {
// Normally not necessary but certain plugins found a way to
// set the set the value to "undefined", leading to a crash.
// This is now fixed at the model level but to be sure we
// check here too, to handle any already existing data.
// https://github.com/laurent22/joplin/issues/7621
if (!cmdString) cmdString = '';
const path = pathUtils.extractExecutablePath(cmdString);
const args = cmdString.substr(path.length + 1);
return [pathUtils.unquotePath(path), args];
};
const joinCmd = (cmdArray: string[]) => {
if (!cmdArray[0] && !cmdArray[1]) return '';
let cmdString = pathUtils.quotePath(cmdArray[0]);
if (!cmdString) cmdString = '""';
if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`;
return cmdString;
};
const onPathChange: React.ChangeEventHandler<HTMLInputElement> = event => {
if (md.subType === 'file_path_and_args') {
const cmd = splitCmd(value);
cmd[0] = event.target.value;
updateSettingValue(key, joinCmd(cmd));
} else {
updateSettingValue(key, event.target.value);
}
};
const onArgsChange: React.ChangeEventHandler<HTMLInputElement> = event => {
const cmd = splitCmd(value);
cmd[1] = event.target.value;
updateSettingValue(key, joinCmd(cmd));
};
const browseButtonClick = async () => {
if (md.subType === 'directory_path') {
const paths = await bridge().showOpenDialog({
properties: ['openDirectory'],
});
if (!paths || !paths.length) return;
updateSettingValue(key, paths[0]);
} else {
const paths = await bridge().showOpenDialog();
if (!paths || !paths.length) return;
if (md.subType === 'file_path') {
updateSettingValue(key, paths[0]);
} else {
const cmd = splitCmd(value);
cmd[0] = paths[0];
updateSettingValue(key, joinCmd(cmd));
}
}
};
const cmd = splitCmd(value);
const path = md.subType === 'file_path_and_args' ? cmd[0] : value;
const argInputId = `setting_path_arg_${key}`;
const argComp = md.subType !== 'file_path_and_args' ? null : (
<div style={{ ...rowStyle, marginBottom: 5 }}>
<label
className='setting-label -sub-label'
htmlFor={argInputId}
>{_('Arguments:')}</label>
<input
type={inputType}
style={inputStyle}
onChange={onArgsChange}
value={cmd[1]}
spellCheck={false}
id={argInputId}
aria-describedby={descriptionId}
/>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
);
const pathDescriptionId = `setting_path_label_${key}`;
return (
<div style={rowStyle}>
<SettingLabel text={md.label()} htmlFor={inputId}/>
<div style={{ display: 'flex' }}>
<div style={{ flex: 1 }}>
<div style={{ ...rowStyle, marginBottom: 5 }}>
<div
className='setting-label -sub-label'
id={pathDescriptionId}
>{_('Path:')}</div>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: inputStyle.marginBottom }}>
<input
type={inputType}
style={{ ...inputStyle, marginBottom: 0, marginRight: 5 }}
onChange={onPathChange}
value={path}
spellCheck={false}
id={inputId}
aria-describedby={pathDescriptionId}
aria-details={descriptionId}
/>
<Button
level={ButtonLevel.Secondary}
title={_('Browse...')}
onClick={browseButtonClick}
size={ButtonSize.Small}
/>
</div>
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
</div>
</div>
{argComp}
</div>
);
} else {
const onTextChange: React.ChangeEventHandler<HTMLInputElement> = event => {
updateSettingValue(key, event.target.value);
};
return (
<div style={rowStyle}>
<SettingLabel text={md.label()} htmlFor={inputId}/>
{
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
<FontSearch
type={inputType}
style={inputStyle}
value={props.value as string}
availableFonts={props.fonts}
onChange={fontFamily => updateSettingValue(key, fontFamily)}
subtype={md.subType}
inputId={inputId}
/> :
<input
type={inputType}
style={inputStyle}
value={props.value as string|number}
onChange={onTextChange}
spellCheck={false}
id={inputId}
aria-describedby={descriptionId}
/>
}
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
</div>
);
}
} else if (md.type === Setting.TYPE_INT) {
const value = props.value as number;
const onNumChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
updateSettingValue(key, event.target.value);
};
const label = [md.label()];
if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`);
return (
<div style={rowStyle}>
<SettingLabel htmlFor={inputId} text={label.join(' ')}/>
<input
type="number"
style={textInputBaseStyle}
value={value}
onChange={onNumChange}
min={md.minimum}
max={md.maximum}
step={md.step}
spellCheck={false}
id={inputId}
aria-describedby={descriptionId}
/>
{descriptionComp}
</div>
);
} else if (md.type === Setting.TYPE_BUTTON) {
const labelComp = md.hideLabel ? null : (
<SettingLabel text={md.label()} htmlFor={null} />
);
return (
<div style={rowStyle}>
{labelComp}
<Button
level={ButtonLevel.Secondary}
title={md.label()}
onClick={md.onClick ? md.onClick : () => props.onSettingButtonClick(key)}
/>
{descriptionComp}
</div>
);
} else {
console.warn(`Type not implemented: ${key}`);
}
return output;
};
export default SettingComponent;

View File

@@ -0,0 +1,12 @@
import * as React from 'react';
interface Props {
text: string;
id?: string;
}
const SettingDescription: React.FC<Props> = props => {
return props.text ? <div className='setting-description' id={props.id}>{props.text}</div> : null;
};
export default SettingDescription;

View File

@@ -0,0 +1,15 @@
import * as React from 'react';
interface Props {
text: string;
}
const SettingHeader: React.FC<Props> = props => {
return (
<div className='setting-header'>
<label>{props.text}</label>
</div>
);
};
export default SettingHeader;

View File

@@ -0,0 +1,16 @@
import * as React from 'react';
interface Props {
htmlFor: string|null;
text: string;
}
const SettingLabel: React.FC<Props> = props => {
return (
<div className='setting-label'>
<label htmlFor={props.htmlFor}>{props.text}</label>
</div>
);
};
export default SettingLabel;

View File

@@ -6,6 +6,7 @@ import { _ } from '@joplin/lib/locale';
interface Props {
onClick: ()=> void;
advancedSettingsVisible: boolean;
'aria-controls': string;
}
const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
@@ -16,6 +17,10 @@ const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
level={ButtonLevel.Secondary}
onClick={props.onClick}
iconName={iconName}
aria-controls={props['aria-controls']}
aria-expanded={props.advancedSettingsVisible}
title={_('Show Advanced Settings')}
/>
</div>

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { useCallback, useMemo } from 'react';
import { useCallback, useId, useMemo } from 'react';
import { _ } from '@joplin/lib/locale';
import styled from 'styled-components';
import ToggleButton from '../../../lib/ToggleButton/ToggleButton';
@@ -173,6 +173,7 @@ export default function(props: Props) {
themeId={props.themeId}
value={item.enabled}
onToggle={() => props.onToggle({ item })}
aria-label={_('Enabled')}
/>;
}
@@ -256,10 +257,17 @@ export default function(props: Props) {
return <RecommendedBadge href="#" title={_('The Joplin team has vetted this plugin and it meets our standards for security and performance.')} onClick={onRecommendedClick}><i className="fas fa-crown"></i></RecommendedBadge>;
}
const nameLabelId = useId();
return (
<CellRoot isCompatible={props.isCompatible}>
<CellRoot isCompatible={props.isCompatible} role='group' aria-labelledby={nameLabelId}>
<CellTop>
<StyledNameAndVersion mb={'5px'}><StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }}>{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}</StyledName><StyledVersion>v{item.manifest.version}</StyledVersion></StyledNameAndVersion>
<StyledNameAndVersion mb={'5px'}>
<StyledName onClick={onNameClick} href="#" style={{ marginRight: 5 }} id={nameLabelId}>
{item.manifest.name} {item.deleted ? _('(%s)', 'Deleted') : ''}
</StyledName>
<StyledVersion>v{item.manifest.version}</StyledVersion>
</StyledNameAndVersion>
{renderToggleButton()}
{renderRecommendedBadge()}
</CellTop>

View File

@@ -17,6 +17,8 @@ import useOnDeleteHandler from '@joplin/lib/components/shared/config/plugins/use
import Logger from '@joplin/utils/Logger';
import StyledMessage from '../../../style/StyledMessage';
import StyledLink from '../../../style/StyledLink';
import SettingHeader from '../SettingHeader';
import SettingDescription from '../SettingDescription';
const { space } = require('styled-system');
const logger = Logger.create('PluginState');
@@ -51,12 +53,6 @@ interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onChange: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
renderLabel: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
renderDescription: Function;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
renderHeader: Function;
}
let repoApi_: RepositoryApi = null;
@@ -281,7 +277,7 @@ export default function(props: Props) {
if (!pluginItems.length || allDeleted) {
return (
<UserPluginsRoot mb={'10px'}>
{props.renderDescription(props.themeId, _('You do not have any installed plugin.'))}
<SettingDescription text={_('You do not have any installed plugin.')}/>
</UserPluginsRoot>
);
} else {
@@ -311,7 +307,6 @@ export default function(props: Props) {
pluginSettings={pluginSettings}
onSearchQueryChange={onSearchQueryChange}
onPluginSettingsChange={onSearchPluginSettingsChange}
renderDescription={props.renderDescription}
repoApi={repoApi}
/>
</div>
@@ -333,7 +328,7 @@ export default function(props: Props) {
<div style={{ display: 'flex', flexDirection: 'row', maxWidth }}>
<ToolsButton size={ButtonSize.Small} tooltip={_('Plugin tools')} iconName="fas fa-cog" level={ButtonLevel.Secondary} onClick={onToolsClick}/>
<div style={{ display: 'flex', flex: 1 }}>
{props.renderHeader(props.themeId, _('Manage your plugins'))}
<SettingHeader text={_('Manage your plugins')}/>
</div>
</div>
{renderUserPlugins(pluginItems)}

View File

@@ -10,6 +10,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
import { _ } from '@joplin/lib/locale';
import useOnInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
import { themeStyle } from '@joplin/lib/theme';
import SettingDescription from '../SettingDescription';
const Root = styled.div`
`;
@@ -26,8 +27,6 @@ interface Props {
pluginSettings: PluginSettings;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onPluginSettingsChange(event: any): void;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
renderDescription: Function;
maxWidth: number;
repoApi(): RepositoryApi;
disabled: boolean;
@@ -81,7 +80,7 @@ export default function(props: Props) {
function renderResults(query: string, manifests: PluginManifest[]) {
if (query && !manifests.length) {
if (searchResultCount === null) return ''; // Search in progress
return props.renderDescription(props.themeId, _('No results'));
return <SettingDescription text={_('No results')}/>;
} else {
const output = [];

View File

@@ -1,3 +1,5 @@
@use "./styles/index.scss";
.config-screen-content-wrapper {
padding: 24px;
overflow: auto;

View File

@@ -0,0 +1,5 @@
@use "./setting-description.scss";
@use "./setting-label.scss";
@use "./setting-header.scss";
@use "./setting-tab-panel.scss";

View File

@@ -0,0 +1,9 @@
.setting-description {
color: var(--joplin-color-faded);
font-size: var(--joplin-font-size);
line-height: var(--joplin-line-height);
font-style: italic;
max-width: 70em;
margin-top: 5px;
}

View File

@@ -0,0 +1,9 @@
.setting-header {
display: block;
color: var(--joplin-color);
font-size: calc(var(--joplin-font-size) * 1.25);
font-weight: 500;
margin-bottom: var(--joplin-main-padding);
line-height: var(--joplin-line-height);
}

View File

@@ -0,0 +1,20 @@
.setting-label {
display: block;
color: var(--joplin-color);
font-size: calc(var(--joplin-font-size) * 1.083333);
font-weight: 500;
margin-bottom: calc(var(--joplin-main-padding) / 2);
line-height: var(--joplin-line-height);
&.-sub-label {
opacity: 0.7;
}
&.-for-checkbox {
margin-left: 5px;
margin-bottom: 0;
display: inline;
background-color: transparent;
}
}

View File

@@ -0,0 +1,18 @@
.setting-tab-panel {
display: flex;
flex-grow: 1;
flex-shrink: 1;
min-height: 0;
&.-hidden {
display: none;
}
&:focus-visible {
// Use a border rather than an outline -- an outline would be shown outside of the screen
// and thus invisible.
border: 1px solid var(--joplin-focus-outline-color);
outline: none;
}
}

View File

@@ -1,49 +1,128 @@
import styled from 'styled-components';
import * as React from 'react';
import { ReactNode, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { blur, focus } from '@joplin/lib/utils/focusHandler';
const DialogModalLayer = styled.div`
z-index: 9999;
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
align-items: flex-start;
justify-content: center;
overflow: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
`;
const DialogRoot = styled.div`
background-color: ${props => props.theme.backgroundColor};
padding: 16px;
box-shadow: 6px 6px 20px rgba(0,0,0,0.5);
margin: 20px;
min-height: fit-content;
display: flex;
flex-direction: column;
border-radius: 10px;
`;
type OnCancelListener = ()=> void;
interface Props {
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
renderContent: Function;
className?: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onClose?: Function;
onCancel?: OnCancelListener;
contentStyle?: React.CSSProperties;
children: ReactNode;
}
export default function Dialog(props: Props) {
return (
<DialogModalLayer className={props.className}>
<DialogRoot>
{props.renderContent()}
</DialogRoot>
</DialogModalLayer>
const Dialog: React.FC<Props> = props => {
// For correct focus handling, the dialog element needs to be managed separately from React. In particular,
// just after creating the dialog, we need to call .showModal() and just **before** closing the dialog, we
// need to call .close(). This second requirement is particularly difficult, as this needs to happen even
// if the dialog is closed by removing its parent from the React DOM.
//
// Because useEffect cleanup can happen after an element is removed from the HTML DOM, the dialog is managed
// using native HTML APIs. This allows us to call .close() while the dialog is still attached to the DOM, which
// allows the browser to restore the focus from before the dialog was opened.
const dialogElement = useDialogElement(props.onCancel);
useDialogClassNames(dialogElement, props.className);
const [contentRendered, setContentRendered] = useState(false);
useEffect(() => {
if (!dialogElement || !contentRendered) return;
if (!dialogElement.open) {
dialogElement.showModal();
}
}, [dialogElement, contentRendered]);
if (dialogElement && !contentRendered) {
setContentRendered(true);
}
const content = (
<div className='content' style={props.contentStyle}>
{props.children}
</div>
);
}
return <>
{dialogElement && createPortal(content, dialogElement)}
</>;
};
const useDialogElement = (onCancel: undefined|OnCancelListener) => {
const [dialogElement, setDialogElement] = useState<HTMLDialogElement|null>(null);
const onCancelRef = useRef(onCancel);
onCancelRef.current = onCancel;
useEffect(() => {
const dialog = document.createElement('dialog');
dialog.addEventListener('click', event => {
const onCancel = onCancelRef.current;
const isBackgroundClick = event.target === dialog;
if (isBackgroundClick && onCancel) {
onCancel();
}
});
dialog.classList.add('dialog-modal-layer');
dialog.addEventListener('cancel', event => {
const canCancel = !!onCancelRef.current;
if (!canCancel) {
// Prevents [Escape] from closing the dialog. In many places, this is handled
// by external logic.
// See https://stackoverflow.com/a/61021326
event.preventDefault();
}
});
const removedReturnValue = 'removed-from-dom';
dialog.addEventListener('close', () => {
const closedByCancel = dialog.returnValue !== removedReturnValue;
if (closedByCancel) {
onCancelRef.current?.();
}
// Work around what seems to be an Electron bug -- if an input or contenteditable region is refocused after
// dismissing a dialog, it won't be editable.
// Note: While this addresses the issue in the note title input, it does not address the issue in the Rich Text Editor.
if (document.activeElement?.tagName === 'INPUT') {
const element = document.activeElement as HTMLElement;
blur('Dialog', element);
focus('Dialog', element);
}
});
document.body.appendChild(dialog);
setDialogElement(dialog);
return () => {
if (dialog.open) {
// .close: Instructs the browser to restore keyboard focus to whatever was focused
// before the dialog.
dialog.close(removedReturnValue);
}
dialog.remove();
};
}, []);
return dialogElement;
};
const useDialogClassNames = (dialogElement: HTMLElement|null, classNames: undefined|string) => {
useEffect(() => {
if (!dialogElement || !classNames) {
return () => {};
}
// The React className prop can include multiple space-separated classes
const newClassNames = classNames
.split(/\s+/)
.filter(name => name.length && !dialogElement.classList.contains(name));
dialogElement.classList.add(...newClassNames);
return () => {
dialogElement.classList.remove(...newClassNames);
};
}, [dialogElement, classNames]);
};
export default Dialog;

View File

@@ -1,3 +1,4 @@
import * as React from 'react';
import { useEffect, useState, useRef, useCallback } from 'react';
import { isInsideContainer } from '@joplin/lib/dom';
@@ -40,8 +41,7 @@ export default (props: Props) => {
return false;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onKeyDown = useCallback((event: any) => {
const onKeyDown = useCallback((event: KeyboardEvent|React.KeyboardEvent) => {
// Early exit if it's neither ENTER nor ESCAPE, because isInSubModal
// function can be costly.
if (event.keyCode !== 13 && event.keyCode !== 27) return;
@@ -49,8 +49,12 @@ export default (props: Props) => {
if (!isTopDialog() || isInSubModal(event.target)) return;
if (event.keyCode === 13) {
if (event.target.nodeName !== 'TEXTAREA') {
props.onOkButtonClick();
if ('nodeName' in event.target && event.target.nodeName === 'INPUT') {
const target = event.target as HTMLInputElement;
if (target.type !== 'button' && target.type !== 'checkbox') {
props.onOkButtonClick();
}
}
} else if (event.keyCode === 27) {
props.onCancelButtonClick();

View File

@@ -179,6 +179,6 @@ export default function(props: Props) {
}
return (
<Dialog onClose={onClose} className="master-password-dialog" renderContent={renderDialogWrapper}/>
<Dialog onCancel={onClose} className="master-password-dialog">{renderDialogWrapper()}</Dialog>
);
}

View File

@@ -21,7 +21,7 @@ interface Props {
export const IconSelector = (props: Props) => {
const [emojiButtonClassReady, setEmojiButtonClassReady] = useState<boolean>(false);
const [picker, setPicker] = useState<EmojiButton>();
const buttonRef = useRef(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useAsyncEffect(async (event: AsyncEffectEvent) => {
const loadScripts = async () => {
@@ -61,6 +61,7 @@ export const IconSelector = (props: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const p: EmojiButton = new (window as any).EmojiButton({
zIndex: 10000,
rootElement: buttonRef.current?.parentElement,
});
const onEmoji = (selection: FolderIcon) => {
@@ -73,6 +74,7 @@ export const IconSelector = (props: Props) => {
return () => {
p.off('emoji', onEmoji);
p.destroyPicker();
};
}, [emojiButtonClassReady, props.onChange]);

View File

@@ -10,7 +10,7 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
import Button, { ButtonLevel } from '../Button/Button';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useId, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
@@ -350,7 +350,7 @@ const EncryptionConfigScreen = (props: Props) => {
t = `<p>${t}</p>`;
return (
<div>
<>
<h2>{_('Re-encryption')}</h2>
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
<span style={{ marginRight: 10 }}>
@@ -358,7 +358,7 @@ const EncryptionConfigScreen = (props: Props) => {
</span>
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
</div>
</>
);
};
@@ -368,6 +368,7 @@ const EncryptionConfigScreen = (props: Props) => {
setShowAdvanced(!showAdvanced);
}, [showAdvanced]);
const advancedSettingsId = useId();
const renderAdvancedSection = () => {
const reEncryptSection = renderReencryptData();
@@ -378,8 +379,12 @@ const EncryptionConfigScreen = (props: Props) => {
<div>
<ToggleAdvancedSettingsButton
onClick={toggleAdvanced}
advancedSettingsVisible={showAdvanced}/>
{ showAdvanced ? reEncryptSection : null }
advancedSettingsVisible={showAdvanced}
aria-controls={advancedSettingsId}
/>
<div id={advancedSettingsId}>
{ showAdvanced ? reEncryptSection : null }
</div>
</div>
);
};

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
const { connect } = require('react-redux');
import { themeStyle } from '@joplin/lib/theme';
import { AppState } from '../app.reducer';
import { _ } from '@joplin/lib/locale';
interface Props {
tip: string;
@@ -10,6 +11,9 @@ interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
style: any;
'aria-controls'?: string;
'aria-expanded'?: string;
}
class HelpButtonComponent extends React.Component<Props> {
@@ -29,11 +33,21 @@ class HelpButtonComponent extends React.Component<Props> {
const helpIconStyle = { flex: 0, width: 16, height: 16, marginLeft: 10 };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const extraProps: any = {};
if (this.props.tip) extraProps['data-tip'] = this.props.tip;
if (this.props.tip) {
extraProps['data-tip'] = this.props.tip;
extraProps['aria-description'] = this.props.tip;
}
return (
<a href="#" style={style} onClick={this.onClick} {...extraProps}>
<i style={helpIconStyle} className={'fa fa-question-circle'}></i>
</a>
<button
style={style}
onClick={this.onClick}
className='flat-button'
aria-controls={this.props['aria-controls']}
aria-expanded={this.props['aria-expanded']}
{...extraProps}
>
<i style={helpIconStyle} className={'fa fa-question-circle'} role='img' aria-label={_('Help')}></i>
</button>
);
}
}

View File

@@ -10,6 +10,10 @@ interface Props<ItemType> {
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
className?: string;
onItemDrop?: DragEventHandler<HTMLElement>;
id?: string;
role?: string;
'aria-label'?: string;
}
interface State {
@@ -164,7 +168,20 @@ class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
if (this.props.className) classes.push(this.props.className);
return (
<div ref={this.listRef} className={classes.join(' ')} style={style} onScroll={this.onScroll} onKeyDown={this.onKeyDown} onDrop={this.onDrop}>
<div
ref={this.listRef}
className={classes.join(' ')}
style={style}
id={this.props.id}
role={this.props.role}
aria-label={this.props['aria-label']}
aria-setsize={items.length}
onScroll={this.onScroll}
onKeyDown={this.onKeyDown}
onDrop={this.onDrop}
>
{itemComps}
</div>
);

View File

@@ -48,6 +48,7 @@ import NotePropertiesDialog from '../NotePropertiesDialog';
import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType';
import validateColumns from '../NoteListHeader/utils/validateColumns';
import TrashNotification from '../TrashNotification/TrashNotification';
import UpdateNotification from '../UpdateNotification/UpdateNotification';
const PluginManager = require('@joplin/lib/services/PluginManager');
const ipcRenderer = require('electron').ipcRenderer;
@@ -85,7 +86,7 @@ interface Props {
startupPluginsLoaded: boolean;
shareInvitations: ShareInvitation[];
isSafeMode: boolean;
enableBetaMarkdownEditor: boolean;
enableLegacyMarkdownEditor: boolean;
needApiAuth: boolean;
processingShareInvitationResponse: boolean;
isResettingLayout: boolean;
@@ -783,12 +784,12 @@ class MainScreenComponent extends React.Component<Props, State> {
},
editor: () => {
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
let bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror6' : 'TinyMCE';
if (this.props.isSafeMode) {
bodyEditor = 'PlainText';
} else if (this.props.settingEditorCodeView && this.props.enableBetaMarkdownEditor) {
bodyEditor = 'CodeMirror6';
} else if (this.props.settingEditorCodeView && this.props.enableLegacyMarkdownEditor) {
bodyEditor = 'CodeMirror5';
}
return <NoteEditor key={key} bodyEditor={bodyEditor} />;
},
@@ -935,6 +936,7 @@ class MainScreenComponent extends React.Component<Props, State> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
dispatch={this.props.dispatch as any}
/>
<UpdateNotification themeId={this.props.themeId} />
{messageComp}
{layoutComp}
{pluginDialog}
@@ -969,7 +971,7 @@ const mapStateToProps = (state: AppState) => {
shareInvitations: state.shareService.shareInvitations,
processingShareInvitationResponse: state.shareService.processingShareInvitationResponse,
isSafeMode: state.settings.isSafeMode,
enableBetaMarkdownEditor: state.settings['editor.beta'],
enableLegacyMarkdownEditor: state.settings['editor.legacyMarkdown'],
needApiAuth: state.needApiAuth,
isResettingLayout: state.isResettingLayout,
listRendererId: state.settings['notes.listRendererId'],

View File

@@ -3,7 +3,7 @@ import shim from '@joplin/lib/shim';
import InteropServiceHelper from '../../../InteropServiceHelper';
import { _ } from '@joplin/lib/locale';
import Note from '@joplin/lib/models/Note';
const bridge = require('@electron/remote').require('./bridge').default;
import bridge from '../../../services/bridge';
export const declaration: CommandDeclaration = {
name: 'exportPdf',
@@ -31,6 +31,14 @@ export const runtime = (comp: any): CommandRuntime => {
});
}
if (Array.isArray(path)) {
if (path.length > 1) {
throw new Error('Only one output directory can be selected');
}
path = path[0];
}
if (!path) return;
for (let i = 0; i < noteIds.length; i++) {

View File

@@ -240,6 +240,6 @@ export default function(props: Props) {
}
return (
<Dialog onClose={onClose} className="master-password-dialog" renderContent={renderDialogWrapper}/>
<Dialog onCancel={onClose} className="master-password-dialog">{renderDialogWrapper()}</Dialog>
);
}

View File

@@ -713,7 +713,7 @@ function useMenu(props: Props) {
label: layoutButtonSequenceOptions[value],
type: 'checkbox',
click: () => {
Setting.setValue('layoutButtonSequence', value);
Setting.setValue('layoutButtonSequence', Number(value));
},
});
}

View File

@@ -5,13 +5,13 @@ import DialogButtonRow from './DialogButtonRow';
const { themeStyle } = require('@joplin/lib/theme');
const Countable = require('@joplin/lib/countable/Countable');
import markupLanguageUtils from '../utils/markupLanguageUtils';
import Dialog from './Dialog';
interface NoteContentPropertiesDialogProps {
themeId: number;
text: string;
markupLanguage: number;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onClose: Function;
onClose: ()=> void;
}
interface TextPropertiesMap {
@@ -159,22 +159,20 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
const readTimeLabel = _('Read time: %s min', formatReadTime(strippedReadTime));
return (
<div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}>
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
<table>
<thead>
{tableHeader}
</thead>
<tbody>
{tableBodyComps}
</tbody>
</table>
<div style={{ ...labelCompStyle, marginTop: 10 }}>
{readTimeLabel}
</div>
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
<Dialog onCancel={props.onClose}>
<div style={dialogBoxHeadingStyle}>{_('Statistics')}</div>
<table>
<thead>
{tableHeader}
</thead>
<tbody>
{tableBodyComps}
</tbody>
</table>
<div style={{ ...labelCompStyle, marginTop: 10 }}>
{readTimeLabel}
</div>
</div>
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</Dialog>
);
}

View File

@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
import { AppState } from '../../../../app.reducer';
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import stateToWhenClauseContext from '../../../../services/commands/stateToWhenClauseContext';
import { _ } from '@joplin/lib/locale';
const { buildStyle } = require('@joplin/lib/theme');
interface ToolbarProps {
@@ -29,7 +30,14 @@ const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());
function Toolbar(props: ToolbarProps) {
const styles = styles_(props);
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={!!props.disabled} />;
return (
<ToolbarBase
style={styles.root}
items={props.toolbarButtonInfos}
disabled={!!props.disabled}
aria-label={_('Editor actions')}
/>
);
}
const mapStateToProps = (state: AppState) => {

View File

@@ -672,7 +672,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
const percent = getLineScrollPercent();
setEditorPercentScroll(percent);
options.percent = percent;
webviewRef.current.send('setHtml', renderedBody.html, options);
webviewRef.current.setHtml(renderedBody.html, options);
} else {
console.error('Trying to set HTML on an undefined webview ref');
}

View File

@@ -160,7 +160,7 @@ export default function useKeymap(CodeMirror: any) {
keymapService.on(EventName.KeymapChange, registerKeymap);
setupEmacs();
setupVim(CodeMirror);
setupVim(CodeMirror, null);
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, []);
}

View File

@@ -289,7 +289,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
const percent = getLineScrollPercent();
setEditorPercentScroll(percent);
options.percent = percent;
webviewRef.current.send('setHtml', renderedBody.html, options);
webviewRef.current.setHtml(renderedBody.html, options);
} else {
console.error('Trying to set HTML on an undefined webview ref');
}
@@ -385,6 +385,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
ref={editorRef}
settings={editorSettings}
pluginStates={props.plugins}
onPasteFile={null}
onEvent={onEditorEvent}
onLogMessage={logDebug}
onEditorPaste={onEditorPaste}

View File

@@ -12,6 +12,7 @@ import setupVim from '@joplin/editor/CodeMirror/utils/setupVim';
import { dirname } from 'path';
import useKeymap from './utils/useKeymap';
import useEditorSearch from '../utils/useEditorSearchExtension';
import CommandService from '@joplin/lib/services/CommandService';
interface Props extends EditorProps {
style: React.CSSProperties;
@@ -145,7 +146,11 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
return;
}
setupVim(editor);
setupVim(editor, {
sync: () => {
void CommandService.instance().execute('synchronize');
},
});
}, [editor]);
useKeymap(editor);

View File

@@ -942,6 +942,13 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
);
if (cancelled) return;
// Use an offset bookmark -- the default bookmark type is not preserved after unloading
// and reloading the editor.
// See https://github.com/tinymce/tinymce/issues/9736 for a brief description of the
// different bookmark types. An offset bookmark seems to have the smallest change
// when the note content is updated externally.
const offsetBookmarkId = 2;
const bookmark = editor.selection.getBookmark(offsetBookmarkId);
editor.setContent(awfulInitHack(result.html));
if (lastOnChangeEventInfo.current.contentKey !== props.contentKey) {
@@ -960,6 +967,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// times would result in an empty note.
// https://github.com/laurent22/joplin/issues/3534
editor.undoManager.reset();
} else {
// Restore the cursor location
editor.selection.bookmarkManager.moveToBookmark(bookmark);
}
lastOnChangeEventInfo.current = {

View File

@@ -35,7 +35,6 @@ import NoteSearchBar from '../NoteSearchBar';
import { reg } from '@joplin/lib/registry';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import bridge from '../../services/bridge';
import NoteRevisionViewer from '../NoteRevisionViewer';
import { parseShareCache } from '@joplin/lib/services/share/reducer';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
@@ -51,6 +50,7 @@ import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginS
import { MarkupLanguage } from '@joplin/renderer';
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
import WarningBanner from './WarningBanner/WarningBanner';
const debounce = require('debounce');
const commands = [
@@ -138,7 +138,7 @@ function NoteEditor(props: NoteEditorProps) {
const theme = themeStyle(options.themeId ? options.themeId : props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: props.customCss,
});
@@ -434,7 +434,7 @@ function NoteEditor(props: NoteEditorProps) {
editor = <TinyMCE {...editorProps}/>;
} else if (props.bodyEditor === 'PlainText') {
editor = <PlainEditor {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror') {
} else if (props.bodyEditor === 'CodeMirror5') {
editor = <CodeMirror5 {...editorProps}/>;
} else if (props.bodyEditor === 'CodeMirror6') {
editor = <CodeMirror6 {...editorProps}/>;
@@ -442,22 +442,6 @@ function NoteEditor(props: NoteEditorProps) {
throw new Error(`Invalid editor: ${props.bodyEditor}`);
}
const onRichTextReadMoreLinkClick = useCallback(() => {
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
}, []);
const onRichTextDismissLinkClick = useCallback(() => {
Setting.setValue('richTextBannerDismissed', true);
}, []);
const wysiwygBanner = props.bodyEditor !== 'TinyMCE' || props.richTextBannerDismissed ? null : (
<div style={styles.warningBanner}>
{_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')}
&nbsp;&nbsp;<a onClick={onRichTextReadMoreLinkClick} style={styles.warningBannerLink} href="#">[ {_('Read more about it')} ]</a>
&nbsp;&nbsp;<a onClick={onRichTextDismissLinkClick} style={styles.warningBannerLink} href="#">[ {_('Dismiss')} ]</a>
</div>
);
const noteRevisionViewer_onBack = useCallback(() => {
setShowRevisions(false);
}, []);
@@ -612,7 +596,7 @@ function NoteEditor(props: NoteEditorProps) {
{renderTagButton()}
{renderTagBar()}
</div>
{wysiwygBanner}
<WarningBanner bodyEditor={props.bodyEditor}/>
</div>
</div>
);
@@ -636,7 +620,6 @@ const mapStateToProps = (state: AppState) => {
syncStarted: state.syncStarted,
decryptionStarted: state.decryptionWorker?.state !== 'idle',
themeId: state.settings.theme,
richTextBannerDismissed: state.settings.richTextBannerDismissed,
watchedNoteFiles: state.watchedNoteFiles,
notesParentType: state.notesParentType,
selectedNoteTags: state.selectedNoteTags,

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { _ } from '@joplin/lib/locale';
interface Props {
children: React.ReactNode;
acceptMessage: string;
onAccept: ()=> void;
onDismiss: ()=> void;
visible: boolean;
}
const BannerContent: React.FC<Props> = props => {
if (!props.visible) {
return null;
}
return <div className='warning-banner'>
{props.children}
&nbsp;&nbsp;<a onClick={props.onAccept} className='warning-banner-link' href="#">[ {props.acceptMessage} ]</a>
&nbsp;&nbsp;<a onClick={props.onDismiss} className='warning-banner-link' href="#">[ {_('Dismiss')} ]</a>
</div>;
};
export default BannerContent;

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { AppState } from '../../../app.reducer';
import Setting from '@joplin/lib/models/Setting';
import BannerContent from './BannerContent';
import { _ } from '@joplin/lib/locale';
import bridge from '../../../services/bridge';
import { useMemo } from 'react';
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import PluginService from '@joplin/lib/services/plugins/PluginService';
interface Props {
bodyEditor: string;
richTextBannerDismissed: boolean;
pluginCompatibilityBannerDismissedFor: string[];
plugins: PluginStates;
}
const onRichTextDismissLinkClick = () => {
Setting.setValue('richTextBannerDismissed', true);
};
const onRichTextReadMoreLinkClick = () => {
void bridge().openExternal('https://joplinapp.org/help/apps/rich_text_editor');
};
const onSwitchToLegacyEditor = () => {
Setting.setValue('editor.legacyMarkdown', true);
};
const onDismissLegacyEditorPrompt = () => {
Setting.setValue('editor.pluginCompatibilityBannerDismissedFor', [...PluginService.instance().pluginIds]);
};
const incompatiblePluginIds = [
// cSpell:disable
'com.septemberhx.Joplin.Enhancement',
'ylc395.noteLinkSystem',
'outline',
'joplin.plugin.cmoptions',
'plugin.calebjohn.MathMode',
'com.ckant.joplin-plugin-better-code-blocks',
// cSpell:enable
];
const WarningBanner: React.FC<Props> = props => {
const wysiwygBanner = (
<BannerContent
acceptMessage={_('Read more about it')}
onAccept={onRichTextReadMoreLinkClick}
onDismiss={onRichTextDismissLinkClick}
visible={props.bodyEditor === 'TinyMCE' && !props.richTextBannerDismissed}
>
{_('This Rich Text editor has a number of limitations and it is recommended to be aware of them before using it.')}
</BannerContent>
);
const incompatiblePluginNames = useMemo(() => {
if (props.bodyEditor !== 'CodeMirror6') {
return [];
}
const runningPluginIds = Object.keys(props.plugins);
return runningPluginIds.map((id): string|string[] => {
if (props.pluginCompatibilityBannerDismissedFor?.includes(id)) {
return [];
}
if (incompatiblePluginIds.includes(id)) {
return PluginService.instance().pluginById(id).manifest.name;
} else {
return [];
}
}).flat();
}, [props.bodyEditor, props.plugins, props.pluginCompatibilityBannerDismissedFor]);
const markdownPluginBanner = (
<BannerContent
acceptMessage={_('Switch to the legacy editor')}
onAccept={onSwitchToLegacyEditor}
onDismiss={onDismissLegacyEditorPrompt}
visible={incompatiblePluginNames.length > 0}
>
{_('The following plugins may not support the current markdown editor:')}
<ul>
{incompatiblePluginNames.map((name, index) => <li key={index}>{name}</li>)}
</ul>
</BannerContent>
);
return <>
{wysiwygBanner}
{markdownPluginBanner}
</>;
};
export default connect((state: AppState) => {
return {
richTextBannerDismissed: state.settings.richTextBannerDismissed,
pluginCompatibilityBannerDismissedFor: state.settings['editor.pluginCompatibilityBannerDismissedFor'],
plugins: state.pluginService.plugins,
};
})(WarningBanner);

View File

@@ -0,0 +1,3 @@
@use "./styles/warning-banner.scss";
@use "./styles/warning-banner-link.scss";

View File

@@ -0,0 +1,6 @@
.warning-banner-link {
color: var(--joplin-color);
font-family: var(--joplin-font-family);
font-size: var(--joplin-font-siize);
font-weight: bold;
}

View File

@@ -0,0 +1,13 @@
.warning-banner {
background: var(--joplin-warning-background-color);
font-family: var(--joplin-font-family);
padding: 10px;
font-size: var(--joplin-font-size);
line-height: 1.6em;
margin-top: 5px;
margin-bottom: 5px;
max-height: 25vh;
overflow-y: auto;
}

View File

@@ -150,7 +150,9 @@ export function menuItems(dispatch: Function, htmlToMd: HtmlToMarkdownHandler, m
bridge().showInfoMessageBox(_('This attachment does not have OCR data (Status: %s)', resourceOcrStatusToString(resource.ocr_status)));
}
},
isActive: (itemType: ContextMenuItemType, _options: ContextMenuOptions) => itemType === ContextMenuItemType.Resource,
isActive: (itemType: ContextMenuItemType, options: ContextMenuOptions) => {
return itemType === ContextMenuItemType.Resource || (itemType === ContextMenuItemType.Image && options.resourceId);
},
},
copyPathToClipboard: {
label: _('Copy path to clipboard'),

View File

@@ -50,7 +50,6 @@ export interface NoteEditorProps {
plugins: PluginStates;
toolbarButtonInfos: ToolbarButtonInfo[];
setTagsToolbarButtonInfo: ToolbarButtonInfo;
richTextBannerDismissed: boolean;
contentMaxWidth: number;
isSafeMode: boolean;
useCustomPdfViewer: boolean;

View File

@@ -22,7 +22,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
const markupToHtml = useMemo(() => {
return markupLanguageUtils.newMarkupToHtml(plugins, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: customCss || '',
});
}, [plugins, customCss]);

View File

@@ -104,10 +104,17 @@ const useOnKeyDown = (
event.preventDefault();
}
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
event.preventDefault();
if (CommandService.instance().isEnabled('deleteNote')) {
void CommandService.instance().execute('deleteNote', noteIds);
if (noteIds.length) {
if (key === 'Delete' && event.shiftKey) {
event.preventDefault();
if (CommandService.instance().isEnabled('permanentlyDeleteNote')) {
void CommandService.instance().execute('permanentlyDeleteNote', noteIds);
}
} else if (key === 'Delete' || (key === 'Backspace' && event.metaKey)) {
event.preventDefault();
if (CommandService.instance().isEnabled('deleteNote')) {
void CommandService.instance().execute('deleteNote', noteIds);
}
}
}

View File

@@ -8,14 +8,14 @@ import bridge from '../services/bridge';
import shim from '@joplin/lib/shim';
import { NoteEntity } from '@joplin/lib/services/database/types';
import { focus } from '@joplin/lib/utils/focusHandler';
import Dialog from './Dialog';
const Datetime = require('react-datetime').default;
const { clipboard } = require('electron');
const formatcoords = require('formatcoords');
interface Props {
noteId: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onClose: Function;
onClose: ()=> void;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onRevisionLinkClick: Function;
themeId: number;
@@ -174,7 +174,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
textDecoration: 'none',
backgroundColor: theme.backgroundColor,
padding: '.14em',
display: 'flex',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
marginLeft: '0.5em',
@@ -281,11 +281,13 @@ class NotePropertiesDialog extends React.Component<Props, State> {
public createNoteField(key: keyof FormNote, value: any) {
const styles = this.styles(this.props.themeId);
const theme = themeStyle(this.props.themeId);
const labelComp = <label style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{this.formatLabel(key)}</label>;
const labelText = this.formatLabel(key);
const labelComp = <label role='rowheader' style={{ ...theme.textStyle, ...theme.controlBoxLabel }}>{labelText}</label>;
let controlComp = null;
let editComp = null;
let editCompHandler = null;
let editCompIcon = null;
let editComDescription = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onKeyDown = (event: any) => {
@@ -320,6 +322,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
void this.saveProperty();
};
editCompIcon = 'fa-save';
editComDescription = _('Save changes');
} else {
controlComp = (
<input
@@ -374,28 +377,35 @@ class NotePropertiesDialog extends React.Component<Props, State> {
this.editPropertyButtonClick(key, value);
};
editCompIcon = 'fa-edit';
editComDescription = _('Edit');
}
// Add the copy icon and the 'copy on click' event
if (key === 'id') {
editCompIcon = 'fa-copy';
editCompHandler = () => clipboard.writeText(value);
editComDescription = _('Copy');
}
}
if (editCompHandler && !this.isReadOnly()) {
editComp = (
<a href="#" onClick={editCompHandler} style={styles.editPropertyButton}>
<a
href="#"
onClick={editCompHandler}
style={styles.editPropertyButton}
aria-label={editComDescription}
title={editComDescription}
>
<i className={`fas ${editCompIcon}`} aria-hidden="true"></i>
</a>
);
}
return (
<div key={key} style={theme.controlBox} className="note-property-box">
<div role='row' key={key} style={theme.controlBox} className="note-property-box">
{labelComp}
{controlComp}
{editComp}
<span role='cell'>{controlComp} {editComp}</span>
</div>
);
}
@@ -437,13 +447,13 @@ class NotePropertiesDialog extends React.Component<Props, State> {
}
return (
<div style={theme.dialogModalLayer}>
<div style={theme.dialogBox}>
<div style={theme.dialogTitle}>{_('Note properties')}</div>
<div>{noteComps}</div>
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
<Dialog onCancel={this.props.onClose}>
<div style={theme.dialogTitle} id='note-properties-dialog-title'>{_('Note properties')}</div>
<div role='table' aria-labelledby='note-properties-dialog-title'>
{noteComps}
</div>
</div>
<DialogButtonRow themeId={this.props.themeId} okButtonShow={!this.isReadOnly()} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
</Dialog>
);
}
}

View File

@@ -140,7 +140,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
const theme = themeStyle(this.props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: this.props.customCss ? this.props.customCss : '',
});
@@ -150,7 +150,7 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
postMessageSyntax: 'ipcProxySendToHost',
});
this.viewerRef_.current.send('setHtml', result.html, {
this.viewerRef_.current.setHtml(result.html, {
// cssFiles: result.cssFiles,
pluginAssets: result.pluginAssets,
});

View File

@@ -1,6 +1,7 @@
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import * as React from 'react';
import { reg } from '@joplin/lib/registry';
import bridge from '../services/bridge';
import { focus } from '@joplin/lib/utils/focusHandler';
interface Props {
@@ -14,6 +15,12 @@ interface Props {
themeId: number;
}
type RemovePluginAssetsCallback = ()=> void;
interface SetHtmlOptions {
pluginAssets: { path: string }[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export default class NoteTextViewerComponent extends React.Component<Props, any> {
@@ -23,6 +30,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
private webviewRef_: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private webviewListeners_: any = null;
private removePluginAssetsCallback_: RemovePluginAssetsCallback|null = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public constructor(props: any) {
@@ -64,8 +72,8 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
this.webview_domReady({});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private webview_message(event: any) {
private webview_message(event: MessageEvent) {
if (event.source !== this.webviewRef_.current?.contentWindow) return;
if (!event.data || event.data.target !== 'main') return;
const callName = event.data.name;
@@ -100,7 +108,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
wv.addEventListener(n, fn);
}
this.webviewRef_.current.contentWindow.addEventListener('message', this.webview_message);
window.addEventListener('message', this.webview_message);
}
public destroyWebview() {
@@ -113,17 +121,12 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
wv.removeEventListener(n, fn);
}
try {
// It seems this can throw a cross-origin error in a way that is hard to replicate so just wrap
// it in try/catch since it's not critical.
// https://github.com/laurent22/joplin/issues/3835
this.webviewRef_.current.contentWindow.removeEventListener('message', this.webview_message);
} catch (error) {
reg.logger().warn('Error destroying note viewer', error);
}
window.removeEventListener('message', this.webview_message);
this.initialized_ = false;
this.domReady_ = false;
this.removePluginAssetsCallback_?.();
}
public focus() {
@@ -163,6 +166,7 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
win.postMessage({ target: 'webview', name: 'focus', data: {} }, '*');
}
// External code should use .setHtml (rather than send('setHtml', ...))
if (channel === 'setHtml') {
win.postMessage({ target: 'webview', name: 'setHtml', data: { html: arg0, options: arg1 } }, '*');
}
@@ -180,12 +184,48 @@ export default class NoteTextViewerComponent extends React.Component<Props, any>
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
public setHtml(html: string, options: SetHtmlOptions) {
// Grant & remove asset access.
if (options.pluginAssets) {
this.removePluginAssetsCallback_?.();
const protocolHandler = bridge().electronApp().getCustomProtocolHandler();
const pluginAssetPaths: string[] = options.pluginAssets.map((asset) => asset.path);
const assetAccesses = pluginAssetPaths.map(
path => protocolHandler.allowReadAccessToFile(path),
);
this.removePluginAssetsCallback_ = () => {
for (const accessControl of assetAccesses) {
accessControl.remove();
}
this.removePluginAssetsCallback_ = null;
};
}
this.send('setHtml', html, options);
}
// ----------------------------------------------------------------
// Wrap WebView functions (END)
// ----------------------------------------------------------------
public render() {
const viewerStyle = { border: 'none', ...this.props.viewerStyle };
return <iframe className="noteTextViewer" ref={this.webviewRef_} style={viewerStyle} src="gui/note-viewer/index.html"></iframe>;
// allow=fullscreen: Required to allow the user to fullscreen videos.
return (
<iframe
className="noteTextViewer"
ref={this.webviewRef_}
style={viewerStyle}
allow='fullscreen=* autoplay=* local-fonts=* encrypted-media=*'
allowFullScreen={true}
src={`joplin-content://note-viewer/${__dirname}/note-viewer/index.html`}
></iframe>
);
}
}

View File

@@ -6,6 +6,7 @@ import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/comm
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
const { connect } = require('react-redux');
import { buildStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
interface NoteToolbarProps {
themeId: number;
@@ -29,7 +30,14 @@ function styles_(props: NoteToolbarProps) {
function NoteToolbar(props: NoteToolbarProps) {
const styles = styles_(props);
return <ToolbarBase style={styles.root} items={props.toolbarButtonInfos} disabled={props.disabled}/>;
return (
<ToolbarBase
style={styles.root}
items={props.toolbarButtonInfos}
disabled={props.disabled}
aria-label={_('Note')}
/>
);
}
const toolbarButtonUtils = new ToolbarButtonUtils(CommandService.instance());

View File

@@ -7,6 +7,8 @@ import CreatableSelect from 'react-select/creatable';
import Select from 'react-select';
import makeAnimated from 'react-select/animated';
import { focus } from '@joplin/lib/utils/focusHandler';
import Dialog from './Dialog';
interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -90,31 +92,6 @@ export default class PromptDialog extends React.Component<Props, any> {
this.styles_ = {};
const paddingTop = 20;
this.styles_.modalLayer = {
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height,
boxSizing: 'border-box',
backgroundColor: 'rgba(0,0,0,0.6)',
display: visible ? 'flex' : 'none',
alignItems: 'flex-start',
justifyContent: 'center',
paddingTop: `${paddingTop}px`,
};
this.styles_.promptDialog = {
backgroundColor: theme.backgroundColor,
padding: 16,
display: 'inline-block',
maxWidth: width * 0.5,
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
};
this.styles_.button = {
minWidth: theme.buttonMinWidth,
minHeight: theme.buttonMinHeight,
@@ -214,10 +191,14 @@ export default class PromptDialog extends React.Component<Props, any> {
this.styles_.desc = { ...theme.textStyle, marginTop: 10 };
this.styles_.dialog = { maxWidth: width };
return this.styles_;
}
public render() {
if (!this.state.visible) return null;
const style = this.props.style;
const theme = themeStyle(this.props.themeId);
const buttonTypes = this.props.buttons ? this.props.buttons : ['ok', 'cancel'];
@@ -325,16 +306,14 @@ export default class PromptDialog extends React.Component<Props, any> {
}
return (
<div className="modal-layer" style={styles.modalLayer}>
<div className="modal-dialog" style={styles.promptDialog}>
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
{inputComp}
{descComp}
</div>
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
<Dialog className='prompt-dialog' contentStyle={styles.dialog}>
<label style={styles.label}>{this.props.label ? this.props.label : ''}</label>
<div style={{ display: 'inline-block', color: 'black', backgroundColor: theme.backgroundColor }}>
{inputComp}
{descComp}
</div>
</div>
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
</Dialog>
);
}
}

View File

@@ -167,7 +167,7 @@ class RootComponent extends React.Component<Props, any> {
);
};
return <Dialog renderContent={renderContent}/>;
return <Dialog>{renderContent()}</Dialog>;
}
private modalDialogProps(): ModalDialogProps {

View File

@@ -407,7 +407,7 @@ function ShareFolderDialog(props: Props) {
}
return (
<Dialog renderContent={renderContent}/>
<Dialog>{renderContent()}</Dialog>
);
}

View File

@@ -226,7 +226,7 @@ export function ShareNoteDialog(props: Props) {
};
return (
<Dialog renderContent={renderContent}/>
<Dialog>{renderContent()}</Dialog>
);
}

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { FolderListItem, HeaderId, HeaderListItem, ListItem, ListItemType, TagListItem } from '../types';
import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
import { renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
import { _ } from '@joplin/lib/locale';
import CommandService from '@joplin/lib/services/CommandService';
import Setting from '@joplin/lib/models/Setting';
@@ -35,10 +35,13 @@ const useSidebarListData = (props: Props): ListItem[] => {
});
}, [props.tags]);
const folderTree = useMemo(() => {
return buildFolderTree(props.folders);
}, [props.folders]);
const folderItems = useMemo(() => {
const renderProps = {
folders: props.folders,
folderTree,
collapsedFolderIds: props.collapsedFolderIds,
};
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
@@ -50,7 +53,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
key: folder.id,
};
});
}, [props.folders, props.collapsedFolderIds]);
}, [folderTree, props.collapsedFolderIds]);
return useMemo(() => {
const foldersHeader: HeaderListItem = {

View File

@@ -2,12 +2,14 @@ import * as React from 'react';
import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
import ExpandLink from './ExpandLink';
import { StyledListItem, StyledListItemAnchor, StyledNoteCount, StyledShareIcon, StyledSpanFix } from '../styles';
import { StyledListItem, StyledListItemAnchor, StyledShareIcon, StyledSpanFix } from '../styles';
import { ItemClickListener, ItemContextMenuListener, ItemDragListener } from '../types';
import FolderIconBox from '../../FolderIconBox';
import { getTrashFolderIcon, getTrashFolderId } from '@joplin/lib/services/trash';
import Folder from '@joplin/lib/models/Folder';
import { ModelType } from '@joplin/lib/BaseModel';
import { _ } from '@joplin/lib/locale';
import NoteCount from './NoteCount';
const renderFolderIcon = (folderIcon: FolderIcon) => {
if (!folderIcon) {
@@ -47,8 +49,8 @@ interface FolderItemProps {
function FolderItem(props: FolderItemProps) {
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
const noteCountComp = noteCount ? <StyledNoteCount className="note-count-label">{noteCount}</StyledNoteCount> : null;
const shareIcon = shareId && !parentId ? <StyledShareIcon className="fas fa-share-alt"></StyledShareIcon> : null;
const shareTitle = _('Shared');
const shareIcon = shareId && !parentId ? <StyledShareIcon aria-label={shareTitle} title={shareTitle} className="fas fa-share-alt"/> : null;
const draggable = ![getTrashFolderId(), Folder.conflictFolderId()].includes(folderId);
const doRenderFolderIcon = () => {
@@ -69,6 +71,7 @@ function FolderItem(props: FolderItemProps) {
isConflictFolder={folderId === Folder.conflictFolderId()}
href="#"
selected={selected}
aria-selected={selected}
shareId={shareId}
data-id={folderId}
data-type={ModelType.Folder}
@@ -80,7 +83,7 @@ function FolderItem(props: FolderItemProps) {
onDoubleClick={onFolderToggleClick_}
>
{doRenderFolderIcon()}<StyledSpanFix className="title">{folderTitle}</StyledSpanFix>
{shareIcon} {noteCountComp}
{shareIcon} <NoteCount count={noteCount}/>
</StyledListItemAnchor>
</StyledListItem>
);

View File

@@ -61,7 +61,7 @@ const HeaderItem: React.FC<Props> = props => {
tabIndex={0}
ref={props.anchorRef}
>
<StyledHeaderIcon className={item.iconName}/>
<StyledHeaderIcon aria-label='' className={item.iconName}/>
<StyledHeaderLabel>{item.label}</StyledHeaderLabel>
</StyledHeader>
{ item.onPlusButtonClick && addButton }

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { StyledNoteCount } from '../styles';
import { _n } from '@joplin/lib/locale';
interface Props {
@@ -8,7 +8,8 @@ interface Props {
const NoteCount: React.FC<Props> = props => {
const count = props.count;
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
const title = _n('Contains %d note', 'Contains %d notes', count, count);
return count ? <div role='note' aria-label={title} title={title} className="note-count-label">{count}</div> : null;
};
export default NoteCount;

View File

@@ -33,10 +33,12 @@ const TagItem = (props: Props) => {
}, [props.onClick, tag]);
return (
<StyledListItem selected={selected}
<StyledListItem
selected={selected}
className={`list-item-container ${selected ? 'selected' : ''}`}
onDrop={props.onTagDrop}
data-tag-id={tag.id}
aria-selected={selected}
>
<EmptyExpandLink/>
<StyledListItemAnchor

View File

@@ -1,4 +1,5 @@
@use 'styles/folder-and-tag-list.scss';
@use 'styles/note-count-label.scss';
@use 'styles/sidebar-expand-icon.scss';
@use 'styles/sidebar-expand-link.scss';
@use 'styles/sidebar-header-container.scss';

View File

@@ -95,12 +95,6 @@ export const StyledShareIcon = styled.i`
margin-left: 8px;
`;
export const StyledNoteCount = styled.div`
color: ${(props: StyleProps) => props.theme.colorFaded2};
padding-left: 8px;
user-select: none;
`;
export const StyledSynchronizeButton = styled(Button)`
width: 100%;
`;

View File

@@ -0,0 +1,6 @@
.note-count-label {
color: var(--joplin-color-faded2);
padding-left: 8px;
user-select: none;
}

View File

@@ -140,17 +140,16 @@ type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud';
export default function(props: Props) {
const joplinCloudDescriptionRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
function closeDialog(dispatch: Function) {
dispatch({
const closeDialog = useCallback(() => {
props.dispatch({
type: 'DIALOG_CLOSE',
name: 'syncWizard',
});
}
}, [props.dispatch]);
const onButtonRowClick = useCallback(() => {
closeDialog(props.dispatch);
}, [props.dispatch]);
closeDialog();
}, [closeDialog]);
const { height: descriptionHeight } = useElementSize(joplinCloudDescriptionRef);
@@ -184,12 +183,12 @@ export default function(props: Props) {
Setting.setValue('sync.target', route.target);
await Setting.saveAll();
closeDialog(props.dispatch);
closeDialog();
props.dispatch({
type: 'NAV_GO',
routeName: route.name,
});
}, [props.dispatch]);
}, [props.dispatch, closeDialog]);
function renderSelectArea(info: SyncTargetInfo) {
return (
@@ -229,7 +228,7 @@ export default function(props: Props) {
}
const onSelfHostingClick = useCallback(() => {
closeDialog(props.dispatch);
closeDialog();
props.dispatch({
type: 'NAV_GO',
@@ -238,7 +237,7 @@ export default function(props: Props) {
defaultSection: 'sync',
},
});
}, [props.dispatch]);
}, [props.dispatch, closeDialog]);
function renderContent() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -278,6 +277,6 @@ export default function(props: Props) {
}
return (
<Dialog renderContent={renderDialogWrapper}/>
<Dialog onCancel={closeDialog}>{renderDialogWrapper()}</Dialog>
);
}

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import styles_ from './styles';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { _ } from '@joplin/lib/locale';
export enum Value {
Markdown = 'markdown',
@@ -11,6 +12,8 @@ export interface Props {
themeId: number;
value: Value;
toolbarButtonInfo: ToolbarButtonInfo;
tabIndex?: number;
buttonRef?: React.Ref<HTMLButtonElement>;
}
export default function ToggleEditorsButton(props: Props) {
@@ -18,14 +21,17 @@ export default function ToggleEditorsButton(props: Props) {
return (
<button
ref={props.buttonRef}
style={style.button}
disabled={!props.toolbarButtonInfo.enabled}
aria-label={props.toolbarButtonInfo.tooltip}
aria-description={_('Switch to the %s Editor', props.value !== Value.Markdown ? _('Markdown') : _('Rich Text'))}
title={props.toolbarButtonInfo.tooltip}
type="button"
className={`tox-tbtn ${props.value}-active`}
aria-pressed="false"
onClick={props.toolbarButtonInfo.onClick}
tabIndex={props.tabIndex}
>
<div style={style.leftInnerButton}>
<i style={style.leftIcon} className="fab fa-markdown"></i>

View File

@@ -2,98 +2,207 @@ import * as React from 'react';
import ToolbarButton from './ToolbarButton/ToolbarButton';
import ToggleEditorsButton, { Value } from './ToggleEditorsButton/ToggleEditorsButton';
import ToolbarSpace from './ToolbarSpace';
const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme');
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { AppState } from '../app.reducer';
import { connect } from 'react-redux';
import { useCallback, useMemo, useRef, useState } from 'react';
import { focus } from '@joplin/lib/utils/focusHandler';
interface ToolbarItemInfo extends ToolbarButtonInfo {
type?: string;
}
interface Props {
themeId: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
style: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
items: any[];
style: React.CSSProperties;
items: ToolbarItemInfo[];
disabled: boolean;
'aria-label': string;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
class ToolbarBaseComponent extends React.Component<Props, any> {
const getItemType = (item: ToolbarItemInfo) => {
return item.type ?? 'button';
};
public render() {
const theme = themeStyle(this.props.themeId);
const isFocusable = (item: ToolbarItemInfo) => {
if (!item.enabled) {
return false;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const style: any = { display: 'flex',
flexDirection: 'row',
boxSizing: 'border-box',
backgroundColor: theme.backgroundColor3,
padding: theme.toolbarPadding,
paddingRight: theme.mainPadding, ...this.props.style };
return getItemType(item) === 'button';
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const groupStyle: any = {
display: 'flex',
flexDirection: 'row',
boxSizing: 'border-box',
minWidth: 0,
};
const useCategorizedItems = (items: ToolbarItemInfo[]) => {
return useMemo(() => {
const itemsLeft: ToolbarItemInfo[] = [];
const itemsCenter: ToolbarItemInfo[] = [];
const itemsRight: ToolbarItemInfo[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const leftItemComps: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const centerItemComps: any[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const rightItemComps: any[] = [];
if (this.props.items) {
for (let i = 0; i < this.props.items.length; i++) {
const o = this.props.items[i];
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
key += o.name ? o.name : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${i}`;
const props = {
key: key,
themeId: this.props.themeId,
disabled: this.props.disabled,
...o,
};
if (o.name === 'toggleEditors') {
rightItemComps.push(<ToggleEditorsButton
key={o.name}
value={Value.Markdown}
themeId={this.props.themeId}
toolbarButtonInfo={o}
/>);
} else if (itemType === 'button') {
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(o.name) ? leftItemComps : centerItemComps;
target.push(<ToolbarButton {...props} />);
} else if (itemType === 'separator') {
centerItemComps.push(<ToolbarSpace {...props} />);
if (items) {
for (const item of items) {
const type = getItemType(item);
if (item.name === 'toggleEditors') {
itemsRight.push(item);
} else if (type === 'button') {
const target = ['historyForward', 'historyBackward', 'toggleExternalEditing'].includes(item.name) ? itemsLeft : itemsCenter;
target.push(item);
} else if (type === 'separator') {
itemsCenter.push(item);
}
}
}
return (
<div className="editor-toolbar" style={style}>
<div style={groupStyle}>
{leftItemComps}
</div>
<div style={groupStyle}>
{centerItemComps}
</div>
<div style={{ ...groupStyle, flex: 1, justifyContent: 'flex-end' }}>
{rightItemComps}
</div>
</div>
);
}
}
return {
itemsLeft,
itemsCenter,
itemsRight,
allItems: itemsLeft.concat(itemsCenter, itemsRight),
};
}, [items]);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const mapStateToProps = (state: any) => {
const useKeyboardHandler = (
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>,
focusableItems: ToolbarItemInfo[],
) => {
const onKeyDown: React.KeyboardEventHandler<HTMLElement> = useCallback(event => {
let direction = 0;
if (event.code === 'ArrowRight') {
direction = 1;
} else if (event.code === 'ArrowLeft') {
direction = -1;
}
let handled = true;
if (direction !== 0) {
setSelectedIndex(index => {
let newIndex = (index + direction) % focusableItems.length;
if (newIndex < 0) {
newIndex += focusableItems.length;
}
return newIndex;
});
} else if (event.code === 'End') {
setSelectedIndex(focusableItems.length - 1);
} else if (event.code === 'Home') {
setSelectedIndex(0);
} else {
handled = false;
}
if (handled) {
event.preventDefault();
}
}, [focusableItems, setSelectedIndex]);
return onKeyDown;
};
const ToolbarBaseComponent: React.FC<Props> = props => {
const { itemsLeft, itemsCenter, itemsRight, allItems } = useCategorizedItems(props.items);
const [selectedIndex, setSelectedIndex] = useState(0);
const focusableItems = useMemo(() => {
return allItems.filter(isFocusable);
}, [allItems]);
const containerRef = useRef<HTMLDivElement|null>(null);
const containerHasFocus = !!containerRef.current?.contains(document.activeElement);
let keyCounter = 0;
const renderItem = (o: ToolbarItemInfo, indexInFocusable: number) => {
let key = o.iconName ? o.iconName : '';
key += o.title ? o.title : '';
key += o.name ? o.name : '';
const itemType = !('type' in o) ? 'button' : o.type;
if (!key) key = `${o.type}_${keyCounter++}`;
const buttonProps = {
key,
themeId: props.themeId,
disabled: props.disabled || !o.enabled,
...o,
};
const tabIndex = indexInFocusable === (selectedIndex % focusableItems.length) ? 0 : -1;
const setButtonRefCallback = (button: HTMLButtonElement) => {
if (tabIndex === 0 && containerHasFocus) {
focus('ToolbarBase', button);
}
};
if (o.name === 'toggleEditors') {
return <ToggleEditorsButton
key={o.name}
buttonRef={setButtonRefCallback}
value={Value.Markdown}
themeId={props.themeId}
toolbarButtonInfo={o}
tabIndex={tabIndex}
/>;
} else if (itemType === 'button') {
return (
<ToolbarButton
tabIndex={tabIndex}
buttonRef={setButtonRefCallback}
{...buttonProps}
/>
);
} else if (itemType === 'separator') {
return <ToolbarSpace {...buttonProps} />;
}
return null;
};
let focusableIndex = 0;
const renderList = (items: ToolbarItemInfo[]) => {
const result: React.ReactNode[] = [];
for (const item of items) {
result.push(renderItem(item, focusableIndex));
if (isFocusable(item)) {
focusableIndex ++;
}
}
return result;
};
const leftItemComps = renderList(itemsLeft);
const centerItemComps = renderList(itemsCenter);
const rightItemComps = renderList(itemsRight);
const onKeyDown = useKeyboardHandler(
setSelectedIndex,
focusableItems,
);
return (
<div
ref={containerRef}
className='editor-toolbar'
style={props.style}
role='toolbar'
aria-label={props['aria-label']}
onKeyDown={onKeyDown}
>
<div className='group'>
{leftItemComps}
</div>
<div className='group'>
{centerItemComps}
</div>
<div className='group -right'>
{rightItemComps}
</div>
</div>
);
};
const mapStateToProps = (state: AppState) => {
return { themeId: state.settings.theme };
};

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { StyledRoot, StyledIconSpan, StyledIconI } from './styles';
import { StyledIconSpan, StyledIconI } from './styles';
interface Props {
readonly themeId: number;
@@ -10,6 +10,9 @@ interface Props {
readonly iconName?: string;
readonly disabled?: boolean;
readonly backgroundHover?: boolean;
readonly tabIndex?: number;
buttonRef?: React.Ref<HTMLButtonElement>;
}
function isFontAwesomeIcon(iconName: string) {
@@ -34,7 +37,7 @@ export default function ToolbarButton(props: Props) {
const iconName = getProp(props, 'iconName');
if (iconName) {
const IconClass = isFontAwesomeIcon(iconName) ? StyledIconI : StyledIconSpan;
icon = <IconClass className={iconName} title={title}/>;
icon = <IconClass className={iconName} aria-label='' hasTitle={!!title} role='img'/>;
}
// Keep this for legacy compatibility but for consistency we should use "disabled" prop
@@ -42,28 +45,37 @@ export default function ToolbarButton(props: Props) {
if (isEnabled === null) isEnabled = true;
if (props.disabled) isEnabled = false;
const classes = ['button'];
const classes = ['button', 'toolbar-button'];
if (!isEnabled) classes.push('disabled');
if (title) classes.push('-has-title');
const onClick = getProp(props, 'onClick');
const style: React.CSSProperties = {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis' };
const disabled = !isEnabled;
return (
<StyledRoot
<button
className={classes.join(' ')}
disabled={!isEnabled}
title={tooltip}
href="#"
hasTitle={!!title}
onClick={() => {
if (isEnabled && onClick) onClick();
}}
ref={props.buttonRef}
// At least on MacOS, the disabled HTML prop isn't sufficient for the screen reader
// to read the element as disable. For this, aria-disabled is necessary.
disabled={disabled}
aria-label={!title ? tooltip : undefined}
aria-description={title ? tooltip : undefined}
aria-disabled={!isEnabled}
tabIndex={props.tabIndex}
>
{icon}
<span style={style}>{title}</span>
</StyledRoot>
</button>
);
}

View File

@@ -3,46 +3,15 @@ import { ThemeStyle } from '@joplin/lib/theme';
const styled = require('styled-components').default;
const { css } = require('styled-components');
interface RootProps {
readonly theme: ThemeStyle;
readonly disabled: boolean;
readonly hasTitle: boolean;
}
export const StyledRoot = styled.a<RootProps>`
opacity: ${(props: RootProps) => props.disabled ? 0.3 : 1};
height: ${(props: RootProps) => props.theme.toolbarHeight}px;
min-height: ${(props: RootProps) => props.theme.toolbarHeight}px;
width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
max-width: ${(props: RootProps) => props.hasTitle ? 'auto' : `${props.theme.toolbarHeight}px`};
display: flex;
align-items: center;
justify-content: center;
cursor: default;
border-radius: 3px;
box-sizing: border-box;
color: ${(props: RootProps) => props.theme.color3};
font-size: ${(props: RootProps) => props.theme.toolbarIconSize * 0.8}px;
padding-left: 5px;
padding-right: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: ${(props: RootProps) => props.disabled ? 'none' : props.theme.backgroundColorHover3};
}
`;
interface IconProps {
readonly theme: ThemeStyle;
readonly title: string;
readonly hasTitle: boolean;
}
const iconStyle = css<IconProps>`
font-size: ${(props: IconProps) => props.theme.toolbarIconSize}px;
color: ${(props: IconProps) => props.theme.color3};
margin-right: ${(props: IconProps) => props.title ? 5 : 0}px;
margin-right: ${(props: IconProps) => props.hasTitle ? 5 : 0}px;
pointer-events: none; /* Need this to get button tooltip to work */
`;

View File

@@ -0,0 +1,85 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdateNotificationEvents = void 0;
const React = require("react");
const react_1 = require("react");
const theme_1 = require("@joplin/lib/theme");
const NotyfContext_1 = require("../NotyfContext");
const electron_1 = require("electron");
const AutoUpdaterService_1 = require("../../services/autoUpdater/AutoUpdaterService");
const locale_1 = require("@joplin/lib/locale");
const html_1 = require("@joplin/utils/html");
var UpdateNotificationEvents;
(function (UpdateNotificationEvents) {
UpdateNotificationEvents["ApplyUpdate"] = "apply-update";
UpdateNotificationEvents["Dismiss"] = "dismiss-update-notification";
})(UpdateNotificationEvents || (exports.UpdateNotificationEvents = UpdateNotificationEvents = {}));
const changelogLink = 'https://github.com/laurent22/joplin/releases';
window.openChangelogLink = () => {
electron_1.ipcRenderer.send('open-link', changelogLink);
};
const UpdateNotification = ({ themeId }) => {
const notyfContext = (0, react_1.useContext)(NotyfContext_1.default);
const notificationRef = (0, react_1.useRef)(null); // Use ref to hold the current notification
const theme = (0, react_1.useMemo)(() => (0, theme_1.themeStyle)(themeId), [themeId]);
const notyf = (0, react_1.useMemo)(() => {
const output = notyfContext;
output.options.types = notyfContext.options.types.map(type => {
if (type.type === 'success') {
type.background = theme.backgroundColor5;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
type.icon.color = theme.backgroundColor5;
}
return type;
});
return output;
}, [notyfContext, theme]);
const handleDismissNotification = (0, react_1.useCallback)(() => {
notyf.dismiss(notificationRef.current);
notificationRef.current = null;
}, [notyf]);
const handleApplyUpdate = (0, react_1.useCallback)(() => {
electron_1.ipcRenderer.send('apply-update-now');
handleDismissNotification();
}, [handleDismissNotification]);
const handleUpdateDownloaded = (0, react_1.useCallback)((_event, info) => {
if (notificationRef.current)
return;
const updateAvailableHtml = (0, html_1.htmlentities)((0, locale_1._)('A new update (%s) is available', info.version));
const seeChangelogHtml = (0, html_1.htmlentities)((0, locale_1._)('See changelog'));
const restartNowHtml = (0, html_1.htmlentities)((0, locale_1._)('Restart now'));
const updateLaterHtml = (0, html_1.htmlentities)((0, locale_1._)('Update later'));
const messageHtml = `
<div class="update-notification" style="color: ${theme.color2};">
${updateAvailableHtml} <a href="#" onclick="openChangelogLink()" style="color: ${theme.color2};">${seeChangelogHtml}</a>
<div style="display: flex; gap: 10px; margin-top: 8px;">
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.ApplyUpdate}'))" class="notyf__button notyf__button--confirm" style="color: ${theme.color2};">${restartNowHtml}</button>
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.Dismiss}'))" class="notyf__button notyf__button--dismiss" style="color: ${theme.color2};">${updateLaterHtml}</button>
</div>
</div>
`;
const notification = notyf.open({
type: 'success',
message: messageHtml,
position: {
x: 'right',
y: 'bottom',
},
duration: 0,
});
notificationRef.current = notification;
}, [notyf, theme]);
(0, react_1.useEffect)(() => {
electron_1.ipcRenderer.on(AutoUpdaterService_1.AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
return () => {
electron_1.ipcRenderer.removeListener(AutoUpdaterService_1.AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
};
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
return (React.createElement("div", { style: { display: 'none' } }));
};
exports.default = UpdateNotification;
//# sourceMappingURL=UpdateNotification.js.map

View File

@@ -0,0 +1,106 @@
import * as React from 'react';
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { themeStyle } from '@joplin/lib/theme';
import NotyfContext from '../NotyfContext';
import { UpdateInfo } from 'electron-updater';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
import { NotyfNotification } from 'notyf';
import { _ } from '@joplin/lib/locale';
import { htmlentities } from '@joplin/utils/html';
interface UpdateNotificationProps {
themeId: number;
}
export enum UpdateNotificationEvents {
ApplyUpdate = 'apply-update',
Dismiss = 'dismiss-update-notification',
}
const changelogLink = 'https://github.com/laurent22/joplin/releases';
window.openChangelogLink = () => {
ipcRenderer.send('open-link', changelogLink);
};
const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
const notyfContext = useContext(NotyfContext);
const notificationRef = useRef<NotyfNotification | null>(null); // Use ref to hold the current notification
const theme = useMemo(() => themeStyle(themeId), [themeId]);
const notyf = useMemo(() => {
const output = notyfContext;
output.options.types = notyfContext.options.types.map(type => {
if (type.type === 'success') {
type.background = theme.backgroundColor5;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(type.icon as any).color = theme.backgroundColor5;
}
return type;
});
return output;
}, [notyfContext, theme]);
const handleDismissNotification = useCallback(() => {
notyf.dismiss(notificationRef.current);
notificationRef.current = null;
}, [notyf]);
const handleApplyUpdate = useCallback(() => {
ipcRenderer.send('apply-update-now');
handleDismissNotification();
}, [handleDismissNotification]);
const handleUpdateDownloaded = useCallback((_event: IpcRendererEvent, info: UpdateInfo) => {
if (notificationRef.current) return;
const updateAvailableHtml = htmlentities(_('A new update (%s) is available', info.version));
const seeChangelogHtml = htmlentities(_('See changelog'));
const restartNowHtml = htmlentities(_('Restart now'));
const updateLaterHtml = htmlentities(_('Update later'));
const messageHtml = `
<div class="update-notification" style="color: ${theme.color2};">
${updateAvailableHtml} <a href="#" onclick="openChangelogLink()" style="color: ${theme.color2};">${seeChangelogHtml}</a>
<div style="display: flex; gap: 10px; margin-top: 8px;">
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.ApplyUpdate}'))" class="notyf__button notyf__button--confirm" style="color: ${theme.color2};">${restartNowHtml}</button>
<button onclick="document.dispatchEvent(new CustomEvent('${UpdateNotificationEvents.Dismiss}'))" class="notyf__button notyf__button--dismiss" style="color: ${theme.color2};">${updateLaterHtml}</button>
</div>
</div>
`;
const notification: NotyfNotification = notyf.open({
type: 'success',
message: messageHtml,
position: {
x: 'right',
y: 'bottom',
},
duration: 0,
});
notificationRef.current = notification;
}, [notyf, theme]);
useEffect(() => {
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
return () => {
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
};
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
return (
<div style={{ display: 'none' }}/>
);
};
export default UpdateNotification;

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