1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

57 Commits

Author SHA1 Message Date
Laurent Cozic
ca64451503 CLI v3.2.3 2025-01-16 15:23:18 +00:00
Laurent Cozic
216b750a90 Lock file 2025-01-16 01:10:48 +00:00
Laurent Cozic
219d5bcae3 Releasing sub-packages 2025-01-16 01:09:52 +00:00
Laurent Cozic
a4b1b9a2bf iOS 13.2.5 2025-01-13 17:19:33 +00:00
Laurent Cozic
fc8ea6df0b Android 3.2.7 2025-01-13 17:12:08 +00:00
Laurent Cozic
2fba101333 Desktop release v3.2.11 2025-01-13 16:34:20 +00:00
Henry Heino
35a0b22df2 Desktop: Accessibility: Add setting to increase scrollbar and other small control sizes (#11627) 2025-01-13 16:33:42 +00:00
Laurent Cozic
e177bffb1c Doc: Update sponsor ALT tag 2025-01-13 16:10:31 +00:00
Laurent Cozic
f95ca578c2 Doc: Trying to fix sponsor image 2025-01-13 16:05:35 +00:00
Laurent Cozic
4bed47a1af Update translations 2025-01-13 12:16:32 +00:00
Joplin Bot
5a0b0e6314 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-12 00:55:11 +00:00
Laurent Cozic
f119212068 Android 3.2.6 2025-01-11 22:03:42 +00:00
Liffindra Angga Zaaldian
cd12de78d6 update Indonesian translation (#11628) 2025-01-11 21:52:46 +00:00
Laurent Cozic
6aa2c5f116 Tools: Run yarn build when releasing Android APK 2025-01-11 21:48:48 +00:00
Joplin Bot
e287e5cbab Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-11 18:21:48 +00:00
Laurent Cozic
d70a5b25a0 Doc: Updated sponsors 2025-01-11 17:08:39 +00:00
Henry Heino
d2df7e6feb Desktop: Fixes #11624: Fix double-click to collapse notebooks (#11625) 2025-01-11 12:16:07 +00:00
Joplin Bot
e9ee8c8419 Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-10 12:27:02 +00:00
Laurent Cozic
8d2ae7e20e Tools: Better handling of different platforms in git-changelog 2025-01-10 10:25:11 +00:00
Laurent Cozic
50d5843344 Desktop release v3.2.10 2025-01-10 00:54:33 +00:00
Laurent Cozic
1fdc327977 Merge branch 'release-3.2' into dev 2025-01-10 00:54:05 +00:00
Joplin Bot
c18ab5a7fb Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-10 00:52:39 +00:00
Laurent Cozic
11216902d0 Desktop release v3.2.9 2025-01-09 20:58:24 +00:00
Henry Heino
950ffef84d Windows: Resolves #11508: Allow installer to skip uninstallation step after repeated failures (#11612) 2025-01-09 20:57:47 +00:00
pedr
86e6445526 Desktop: Fixes #11617: Links from imported notes from OneNote were being wrongly rendered (#11618) 2025-01-09 20:57:23 +00:00
pedr
ab286b6da3 Desktop: Fixes #11600: OneNote imported notes have broken links when there are chineses characters on link (#11602) 2025-01-09 19:24:10 +00:00
ERYpTION
8c24928cf4 Update da_DK.po (#11601) 2025-01-09 15:29:03 +00:00
Henry Heino
3952060dac Chore: Retry flaky tests in Note.test.tsx (#11615) 2025-01-09 15:28:51 +00:00
Henry Heino
877f39bb0e Deskotp: Legacy Markdown Editor: Fix styles in seconary windows and remove red focus-visible border (#11614) 2025-01-09 15:28:45 +00:00
Henry Heino
652812a15c Desktop: Drawing: Fix "insert drawing" button is not disabled in read-only notes (Upgrade Freehand Drawing to v2.14.0) (#11613) 2025-01-09 15:28:40 +00:00
Henry Heino
597f3188bd Desktop: Fixes #11594: Fix syncLockGoneError on sync with certain share configs (#11611) 2025-01-09 15:28:24 +00:00
Henry Heino
d7d50f4373 Chore: Plugin repo CLI: Only match packages with the joplin-plugin keyword (#11599) 2025-01-09 15:27:41 +00:00
Self Not Found
83db585c0b Desktop: Resolves #11575: Remove "URI malformed" alert (#11576) 2025-01-09 15:26:20 +00:00
Maxim Medvedev
d817ddd5c6 Server: use node:18 (bookworm) instead node:18-bullseye (#11554) 2025-01-09 15:25:56 +00:00
Henry Heino
98fce34fe9 Web: Add support for auto-reloading dev plugins on change (#11545) 2025-01-09 15:25:06 +00:00
pedr
a81af0711c Desktop: Fixes #11597: OneNote Importer should only use text on fallback title (#11598) 2025-01-09 15:22:12 +00:00
Henry Heino
72575e3c6f Mobile: Fixes #11455: Clicking on an external note link from within a note logs an error (#11619) 2025-01-09 15:21:06 +00:00
pedr
e8f305dea5 CLI: Fixes #11577: Revert deprecation warning suppression (#11620) 2025-01-09 15:20:43 +00:00
Henry Heino
e1e2ba8888 Desktop: Fix keyboard can't add text after certain error/info dialogs are shown (#11603) 2025-01-08 12:30:16 +00:00
Henry Heino
633d87ebfe Android: Fix clicking "Draw picture" results in blank screen with very old WebView versions (#11604) 2025-01-08 12:29:47 +00:00
Joplin Bot
a9e1be944f Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-08 12:27:30 +00:00
Joplin Bot
6048f9613c Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-08 00:53:08 +00:00
Laurent Cozic
0a76494555 iOS 13.2.4 2025-01-08 00:12:25 +00:00
Laurent Cozic
edbb6137ea Revert "iOS: Resolves #11119: Add iOS Dark Icon (#11460)"
This reverts commit dc445579da.

Reason: Trying to fix error "Asset validation failed"
2025-01-08 00:11:06 +00:00
Laurent Cozic
4d216ef907 iOS 13.2.3 2025-01-07 23:52:48 +00:00
Laurent Cozic
2f71c40ceb lock file 2025-01-07 23:51:44 +00:00
Laurent Cozic
d3ea6fbe1d Android 3.2.5 2025-01-07 23:41:48 +00:00
Laurent Cozic
d45864888a Desktop release v3.2.8 2025-01-07 23:18:54 +00:00
Jozef Gaal
0e92ab654a All: Translation: Update sk_SK.po (#11593) 2025-01-06 18:46:19 -05:00
Joplin Bot
9e5c0ef3ce Doc: Auto-update documentation
Auto-updated using release-website.sh
2025-01-06 18:22:50 +00:00
Nilakh(s)hya
431cc15a51 Doc: Update s3.md for provider Tebi (#11572)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2025-01-06 17:35:36 +00:00
Vladimir Fedorchuk
82118810d9 Documentation: Fixed two broken links in command-apidoc.ts (rest_api.md) (#11549) 2025-01-06 17:34:11 +00:00
Henry Heino
bacaf800f2 Android: Allow re-downloading voice typing models on URL change and error (#11557) 2025-01-06 17:33:44 +00:00
Henry Heino
4d827afccb Desktop: Fixes #11226: Fix reordering notes in custom sort order when some notes are deleted (#11592) 2025-01-06 17:33:31 +00:00
Henry Heino
e70efcbd60 Desktop: Security: Remove the name attribute when rendering to HTML (#11591) 2025-01-06 17:33:19 +00:00
Henry Heino
ac154ee1e8 Desktop: Fixes #11445: Link watched files to the current window (#11590) 2025-01-06 17:33:02 +00:00
Henry Heino
6220267abb Mobile: Upgrade js-draw to 1.26.0 (#11589) 2025-01-06 17:32:19 +00:00
245 changed files with 15690 additions and 12412 deletions

View File

@@ -289,12 +289,12 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
@@ -479,6 +479,7 @@ packages/app-desktop/gui/hooks/useDocument.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useElementHeight.js
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
packages/app-desktop/gui/hooks/useMarkupToHtml.js
packages/app-desktop/gui/hooks/usePrevious.js
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/lib/SearchInput/SearchInput.js
@@ -640,10 +641,12 @@ packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/ImageEditor/utils/useEditorMessenger.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
@@ -701,6 +704,7 @@ 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/plugins/utils/useOnDevPluginsUpdated.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

6
.gitignore vendored
View File

@@ -264,12 +264,12 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.js
packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
packages/app-desktop/gui/NoteEditor/utils/useResourceUnwatcher.js
packages/app-desktop/gui/NoteEditor/utils/useScheduleSaveCallbacks.js
packages/app-desktop/gui/NoteEditor/utils/useScrollWhenReadyOptions.js
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
@@ -454,6 +454,7 @@ packages/app-desktop/gui/hooks/useDocument.js
packages/app-desktop/gui/hooks/useEffectDebugger.js
packages/app-desktop/gui/hooks/useElementHeight.js
packages/app-desktop/gui/hooks/useImperativeHandlerDebugger.js
packages/app-desktop/gui/hooks/useMarkupToHtml.js
packages/app-desktop/gui/hooks/usePrevious.js
packages/app-desktop/gui/hooks/usePropsDebugger.js
packages/app-desktop/gui/lib/SearchInput/SearchInput.js
@@ -615,10 +616,12 @@ packages/app-mobile/components/NoteEditor/ImageEditor/isEditableResource.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/applyTemplateToEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.test.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/polyfills.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/startAutosaveLoop.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/types.js
packages/app-mobile/components/NoteEditor/ImageEditor/js-draw/watchEditorForTemplateChanges.js
packages/app-mobile/components/NoteEditor/ImageEditor/promptRestoreAutosave.js
packages/app-mobile/components/NoteEditor/ImageEditor/utils/useEditorMessenger.js
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/SearchPanel.js
@@ -676,6 +679,7 @@ 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/plugins/utils/useOnDevPluginsUpdated.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

View File

@@ -1,7 +1,15 @@
# This patch prevents the installer from considering itself as a running instance of Joplin.
# This patch's goal is to work around an issue in the NSIS uninstaller on Windows:
# - For future uninstallers, this patch backports an upstream commit that changes how
# running copies of the app are found.
# - See https://github.com/electron-userland/electron-builder/pull/8133
# - If an existing uninstaller fails, gives an option to continue with the installation
# despite the failure.
# - Updates "uninstall failed" error messages to state that uninstallation failed (rather
# than incorrectly stating that the issue was with closing the app).
#
# See https://github.com/laurent22/joplin/pull/11541
diff --git a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
index fe5d45c730f36c9fe8d8cfea12e242e501b67139..af2ce5c90ac910b079e24992519bffe33d57668a 100644
index fe5d45c730f36c9fe8d8cfea12e242e501b67139..97b27fce6798e30e3e631221435f09b3579e77c3 100644
--- a/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
+++ b/templates/nsis/include/allowOnlyOneInstallerInstance.nsh
@@ -42,7 +42,7 @@
@@ -9,7 +17,74 @@ index fe5d45c730f36c9fe8d8cfea12e242e501b67139..af2ce5c90ac910b079e24992519bffe3
!else
# find process owned by current user
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
+ nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c tasklist /FI "USERNAME eq %USERNAME%" /FI "PID ne $pid" /FI "IMAGENAME eq ${_FILE}" /FO csv | %SYSTEMROOT%\System32\find.exe "${_FILE}"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c tasklist /FI "USERNAME eq %USERNAME%" /FI "IMAGENAME eq ${_FILE}" /FO csv | "$SYSDIR\find.exe" "${_FILE}"`
Pop ${_ERR}
!endif
!macroend
@@ -73,7 +73,7 @@
!ifdef INSTALL_MODE_PER_ALL_USERS
nsExec::Exec `taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
!else
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
!endif
# to ensure that files are not "in-use"
Sleep 300
@@ -91,7 +91,7 @@
!ifdef INSTALL_MODE_PER_ALL_USERS
nsExec::Exec `taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid"`
!else
- nsExec::Exec `%SYSTEMROOT%\System32\cmd.exe /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
+ nsExec::Exec `"$SYSDIR\cmd.exe" /c taskkill /f /im "${APP_EXECUTABLE_FILENAME}" /fi "PID ne $pid" /fi "USERNAME eq %USERNAME%"`
!endif
!insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
${If} $R0 == 0
diff --git a/templates/nsis/include/installUtil.nsh b/templates/nsis/include/installUtil.nsh
index 47367741632726ba0886ac516461dbe98b7aea58..675965762375925a505ca6d8bbb67507ef696c2e 100644
--- a/templates/nsis/include/installUtil.nsh
+++ b/templates/nsis/include/installUtil.nsh
@@ -126,10 +126,11 @@ Function handleUninstallResult
Return
${if} $R0 != 0
- MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
+ # MessageBox MB_OK|MB_ICONEXCLAMATION "$(uninstallFailed): $R0"
DetailPrint `Uninstall was not successful. Uninstaller error code: $R0.`
- SetErrorLevel 2
- Quit
+ DetailPrint `Continuing anyway. See https://github.com/laurent22/joplin/pull/11612.`
+ # SetErrorLevel 2
+ # Quit
${endif}
FunctionEnd
@@ -216,11 +217,13 @@ Function uninstallOldVersion
IntOp $R5 $R5 + 1
${if} $R5 > 5
- MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY OneMoreAttempt
- Return
+ MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeUninstalled)" /SD IDCANCEL IDRETRY ContinueWithoutUninstall
+ Abort ; Exit early
+ ContinueWithoutUninstall:
+ Return
${endIf}
- OneMoreAttempt:
+# OneMoreAttempt: ; Commented out because unused
ExecWait '"$uninstallerFileNameTemp" /S /KEEP_APP_DATA $0 _?=$installationDir' $R0
ifErrors TryInPlace CheckResult
diff --git a/templates/nsis/messages.yml b/templates/nsis/messages.yml
index a1c2847fa48d79f835b30b48e999ccaf3c818657..6884c18d1e77dbd6be114401d23cf5caf3e0dd94 100644
--- a/templates/nsis/messages.yml
+++ b/templates/nsis/messages.yml
@@ -235,3 +235,8 @@ uninstallFailed:
sv: Det gick inte att avinstallera gamla programfiler. Försök att köra installationsprogrammet igen.
uk: Не вдалось видалити старі файли застосунку. Будь ласка, спробуйте запустити встановлювач знов.
zh_TW: 無法俺安裝舊的應用程式檔案。 請嘗試再次執行安裝程式。
+
+
+appCannotBeUninstalled:
+ en: "The old version of ${PRODUCT_NAME} could not be removed. \nClick Retry to skip this step."
+

View File

@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 682.66669 682.66669"
height="682.66669"
width="682.66669"
xml:space="preserve"
id="svg2"
version="1.1"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="JoplinLetterBlue.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview13"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="0.77490232"
inkscape:cx="366.49781"
inkscape:cy="360.69062"
inkscape:window-width="1366"
inkscape:window-height="708"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<defs
id="defs6">
<linearGradient
id="linearGradient26"
spreadMethod="pad"
gradientTransform="matrix(-4387.91,4387.91,4387.91,4387.91,4753.95,366.05)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop22"
offset="0"
style="stop-opacity:1;stop-color:#004caf" />
<stop
id="stop24"
offset="1"
style="stop-opacity:1;stop-color:#1f95f8" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath829"><path
id="path831"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.999997"
d="M 3961.59,4435.23 H 2570.18 c -13.15,0 -23.78,-10.64 -23.78,-23.77 v -441.84 c 0,-14.87 12.04,-26.92 26.92,-26.92 h 190.77 c 77.16,0 139.73,-59.35 146.43,-134.77 V 3505 3336.23 1728.75 1717.36 h -0.052 c 0.48,-16.84 -0.1898,-33.4 -1.83,-49.71 -0.18,-2.38 -0.5003,-4.73 -0.7902,-7.09 -1.0998,-9.53 -2.3199,-19.01 -4.17,-28.29 -1.0098,-5.29 -2.4399,-10.44 -3.7098,-15.65 -1.71,-6.93 -3.09,-13.97 -5.22,-20.75 -12.5802,-40.27 -32.4702,-77.62 -59.9802,-110.5 -1.0098,-1.17 -2.2599,-2.25 -3.2598,-3.41 -8.3901,-9.72 -17.2002,-19.19 -26.9502,-28.06 -9.84,-8.95 -20.2599,-17.27 -31.2099,-25 -77.8401,-55.14 -182.61,-79.4 -299.67,-68.2 -149.2599,14.03 -297.3399,81.72 -417.03,190.62 -119.6701,108.89 -194.08,243.62 -209.4799,379.41 -13.8501,121.48 22.5498,228.38 102.42,301.05 0.21,0.1598 0.3997,0.3098 0.5602,0.48 3.09,2.77 6.4901,5.2 9.6701,7.87 57.16,47.89 131.6701,76.91 216.7,84.91 0.96,0.09 1.8801,0.24 2.79,0.3203 8.9499,0.79 18.0699,1.15 27.27,1.49 4.8099,0.1598 9.5601,0.5003 14.4399,0.54 1.62,0.023 3.1602,0.1898 4.7802,0.1898 2.8998,0 5.91,-0.3803 8.8098,-0.42 13.4001,-0.21 26.9001,-0.7601 40.6701,-1.9401 1.74,-0.1402 3.3999,-0.08 5.19,-0.24 1.2699,-0.1297 2.5299,-0.4102 3.8001,-0.54 78,-7.82 155.2299,-31.11 228.5199,-66.3999 1.53,-0.068 3.3,-0.54 5.5099,-1.7601 22.34,-12.3399 26.6201,0.9 27.2801,9.6501 v 382.2399 282.8201 c 0,19.05 -13.2501,35.8999 -31.83,39.99 -394.7601,86.88 -782.08,-3.5501 -1055.38,-252.3401 -238.7499,-217.1799 -354.24,-530.5799 -316.8201,-859.7899 33.39,-293.23 183.9102,-574.94 423.88,-793.33 233.8901,-212.79003 531.69,-345.86006 838.8801,-374.80106 42.33,-3.918 84.8601,-5.93797 126.36,-5.93797 293.3799,0 565.6099,100.59802 766.54,283.37903 190.3401,173.3 304.35,411.27 321.0799,670.16 l 1.55,1697.91 h 0.1703 v 453.97 h 0.06 v 7.92 c 1.72,80.1199 67.05,144.58 147.61,144.58 h 190.77 c 14.8599,0 26.9199,12.05 26.9199,26.9199 v 441.84 c 0,13.13 -10.6299,23.77 -23.7799,23.77" /></clipPath></defs>
<g
id="g14"
transform="matrix(0.13333333,0,0,-0.13333333,0,682.66667)"
mask="none"
clip-path="url(#clipPath829)">
<g
clip-path="url(#clipPath20)"
id="g16">
<path
id="path28"
style="fill:url(#linearGradient26);fill-opacity:1;fill-rule:nonzero;stroke:none"
d="M 3873.89,0 H 1246.11 C 560.754,0 0,560.75 0,1246.11 V 3873.88 C 0,4559.25 560.754,5120 1246.11,5120 H 3873.89 C 4559.25,5120 5120,4559.25 5120,3873.88 V 1246.11 C 5120,560.75 4559.25,0 3873.89,0" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -2,11 +2,11 @@
# Build stage
# =============================================================================
FROM node:18-bullseye AS builder
FROM node:18 AS builder
RUN apt-get update \
&& apt-get install -y \
python tini \
python3 tini \
&& rm -rf /var/lib/apt/lists/*
# Enables Yarn
@@ -56,7 +56,7 @@ RUN BUILD_SEQUENCIAL=1 yarn install --inline-builds \
# from a smaller base image.
# =============================================================================
FROM node:18-bullseye-slim
FROM node:18-slim
ARG user=joplin
RUN useradd --create-home --shell /bin/bash $user

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://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://ca.edubirdie.com/"><img title="Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!" width="256" src="https://joplinapp.org/images/sponsors/Edubirdie.png" alt="EduBirdie"/></a>
<a href="https://seirei.ne.jp"><img title="Serei Network" width="256" src="https://joplinapp.org/images/sponsors/SeireiNetwork.png"/></a> <a href="https://www.hosting.de/nextcloud/?mtm_campaign=managed-nextcloud&amp;mtm_kwd=joplinapp&amp;mtm_source=joplinapp-webseite&amp;mtm_medium=banner"><img title="Hosting.de" width="256" src="https://joplinapp.org/images/sponsors/HostingDe.png"/></a> <a href="https://citricsheep.com"><img title="Citric Sheep" width="256" src="https://joplinapp.org/images/sponsors/CitricSheep.png"/></a> <a href="https://sorted.travel/?utm_source=joplinapp"><img title="Sorted Travel" width="256" src="https://joplinapp.org/images/sponsors/SortedTravel.png"/></a> <a href="https://celebian.com"><img title="Celebian" width="256" src="https://joplinapp.org/images/sponsors/Celebian.png"/></a> <a href="https://bestkru.com"><img title="BestKru" width="256" src="https://joplinapp.org/images/sponsors/BestKru.png"/></a> <a href="https://www.socialfollowers.uk/buy-tiktok-followers/"><img title="Social Followers" width="256" src="https://joplinapp.org/images/sponsors/SocialFollowers.png"/></a> <a href="https://stormlikes.com/"><img title="Stormlikes" width="256" src="https://joplinapp.org/images/sponsors/Stormlikes.png"/></a> <a href="https://route4me.com"><img title="Route4Me" width="256" src="https://joplinapp.org/images/sponsors/Route4Me.png"/></a> <a href="https://buyyoutubviews.com"><img title="BYTV" width="256" src="https://joplinapp.org/images/sponsors/BYTV.png"/></a> <a href="https://casinoreviews.net"><img title="Casino Reviews" width="256" src="https://joplinapp.org/images/sponsors/CasinoReviews.png"/></a> <a href="https://useviral.com.br/"><img title="Comprar seguidores Instagram" width="256" src="https://joplinapp.org/images/sponsors/Useviral.png"/></a> <a href="https://ca.edubirdie.com/"><img title="Achieve academic success with Edubirdie — your trusted partner for expert writing assistance and resources!" width="256" src="https://joplinapp.org/images/sponsors/Edubirdie.png" alt="EduBirdie"/></a> <a href="https://topagency.webflow.io"><img title="WebDesignAgency" width="256" src="https://joplinapp.org/images/sponsors/WebDesignAgency.png"/></a>
<!-- SPONSORS-ORG -->
* * *
@@ -40,8 +40,8 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
| | | | |
| :---: | :---: | :---: | :---: |
| <img width="50" src="https://avatars2.githubusercontent.com/u/97193607?s=96&v=4"/></br>[Akhil-CM](https://github.com/Akhil-CM) | <img width="50" src="https://avatars2.githubusercontent.com/u/552452?s=96&v=4"/></br>[andypiper](https://github.com/andypiper) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1177810?s=96&v=4"/></br>[felixstorm](https://github.com/felixstorm) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
| | | | |
<!-- SPONSORS-GITHUB -->

View File

@@ -189,9 +189,9 @@ async function fetchAllNotes() {
lines.push('## Searching');
lines.push('');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/help/#searching');
lines.push('Call **GET /search?query=YOUR_QUERY** to search for notes. This end-point supports the `field` parameter which is recommended to use so that you only get the data that you need. The query syntax is as described in the main documentation: https://joplinapp.org/help/apps/search');
lines.push('');
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-id). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
lines.push('To retrieve non-notes items, such as notebooks or tags, add a `type` parameter and set it to the required [item type name](#item-type-ids). In that case, full text search will not be used - instead it will be a simple case-insensitive search. You can also use `*` as a wildcard. This is convenient for example to retrieve notebooks or tags by title.');
lines.push('');
lines.push('For example, to retrieve the notebook named `recipes`: **GET /search?query=recipes&type=folder**');
lines.push('');

View File

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

View File

@@ -35,7 +35,7 @@
],
"owner": "Laurent Cozic"
},
"version": "3.2.2",
"version": "3.2.3",
"bin": "./main.js",
"engines": {
"node": ">=10.0.0"

View File

@@ -0,0 +1,2 @@
<img src="test/" class="jop-noMdConv"/>
<img src="http://example.com/test.png" class="jop-noMdConv"/>

View File

@@ -0,0 +1,3 @@
<img name=getElementById src=test/>
<IMG NAME="getElementById" SRC="http://example.com/test.png">

View File

@@ -0,0 +1 @@
![malformed link](https://malformed_uri/%E0%A4%A.jpg)

View File

@@ -40,6 +40,8 @@ export interface AppWindowState extends WindowState {
visibleDialogs: VisibleDialogs;
dialogs: AppStateDialog[];
devToolsVisible: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
watchedResources: any;
}
interface BackgroundWindowStates {
@@ -62,8 +64,6 @@ export interface AppState extends State, AppWindowState {
modalOverlayMessage: string|null;
// Extra reducer keys go here
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
watchedResources: any;
mainLayout: LayoutItem;
isResettingLayout: boolean;
}
@@ -76,6 +76,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
noteVisiblePanes: ['editor', 'viewer'],
editorCodeView: true,
devToolsVisible: false,
watchedResources: {},
};
};

View File

@@ -653,6 +653,7 @@ class Application extends BaseApplication {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
(action: any) => { this.store().dispatch(action); },
(path: string) => bridge().openItem(path),
() => this.store().getState().windowId,
);
// Forwards the local event to the global event manager, so that it can

View File

@@ -1,5 +1,5 @@
import ElectronAppWrapper from './ElectronAppWrapper';
import shim from '@joplin/lib/shim';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import { _, setLocale } from '@joplin/lib/locale';
import { BrowserWindow, nativeTheme, nativeImage, shell, dialog, MessageBoxSyncOptions, safeStorage } from 'electron';
import { dirname, toSystemSlashes } from '@joplin/lib/path-utils';
@@ -384,9 +384,14 @@ export class Bridge {
/* returns the index of the clicked button */
public showMessageBox(message: string, options: MessageDialogOptions = {}) {
const defaultButtons = [_('OK')];
if (options.type !== MessageBoxType.Error && options.type !== MessageBoxType.Info) {
defaultButtons.push(_('Cancel'));
}
const result = this.showMessageBox_(this.activeWindow(), { type: 'question',
message: message,
buttons: [_('OK'), _('Cancel')], ...options });
buttons: defaultButtons, ...options });
return result;
}

View File

@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration } from '@joplin/lib/services/CommandService';
import { _ } from '@joplin/lib/locale';
import shim, { MessageBoxType } from '@joplin/lib/shim';
const app = require('@electron/remote').app;
const { clipboard } = require('electron');
@@ -14,7 +15,7 @@ export const runtime = (): CommandRuntime => {
const appPath = app.getPath('exe');
const cmd = `${appPath} --env dev`;
clipboard.writeText(cmd);
alert(`The dev mode command has been copied to clipboard:\n\n${cmd}`);
await shim.showMessageBox(`The dev mode command has been copied to clipboard:\n\n${cmd}`, { type: MessageBoxType.Info });
},
};
};

View File

@@ -1,5 +1,6 @@
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
import RevisionService from '@joplin/lib/services/RevisionService';
import shim, { MessageBoxType } from '@joplin/lib/shim';
export const declaration: CommandDeclaration = {
name: 'restoreNoteRevision',
@@ -11,9 +12,9 @@ export const runtime = (): CommandRuntime => {
execute: async (_context: CommandContext, noteId: string, reverseRevIndex = 0) => {
try {
const note = await RevisionService.instance().restoreNoteById(noteId, reverseRevIndex);
alert(RevisionService.instance().restoreSuccessMessage(note));
await shim.showMessageBox(RevisionService.instance().restoreSuccessMessage(note), { type: MessageBoxType.Info });
} catch (error) {
alert(error.message);
await shim.showErrorDialog(error.message);
}
},
};

View File

@@ -9,6 +9,7 @@ import ClipperServer from '@joplin/lib/ClipperServer';
import Setting from '@joplin/lib/models/Setting';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { AppState } from '../app.reducer';
import shim, { MessageBoxType } from '@joplin/lib/shim';
class ClipperConfigScreenComponent extends React.Component {
public constructor() {
@@ -30,7 +31,7 @@ class ClipperConfigScreenComponent extends React.Component {
private copyToken_click() {
clipboard.writeText(this.props.apiToken);
alert(_('Token has been copied to the clipboard!'));
void shim.showMessageBox(_('Token has been copied to the clipboard!'), { type: MessageBoxType.Info });
}
private renewToken_click() {

View File

@@ -19,6 +19,7 @@ import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/conf
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import SettingComponent, { UpdateSettingValueEvent } from './controls/SettingComponent';
import shim from '@joplin/lib/shim';
interface Font {
@@ -144,7 +145,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
screenName = section.name;
if (this.hasChanges()) {
const ok = confirm(_('This will open a new screen. Save your current changes?'));
const ok = await shim.showConfirmationDialog(_('This will open a new screen. Save your current changes?'));
if (ok) {
await shared.saveSettings(this);
}

View File

@@ -3,7 +3,7 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import time from '@joplin/lib/time';
import shim from '@joplin/lib/shim';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import dialogs from '../dialogs';
import { decryptedStatText, determineKeyPassword, dontReencryptData, enableEncryptionConfirmationMessages, onSavePasswordClick, onToggleEnabledClick, reencryptData, upgradeMasterKey, useInputPasswords, useNeedMasterPassword, usePasswordChecker, useStats, useToggleShowDisabledMasterKeys } from '@joplin/lib/components/EncryptionConfigScreen/utils';
import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
@@ -47,7 +47,7 @@ const EncryptionConfigScreen = (props: Props) => {
const onUpgradeMasterKey = useCallback(async (mk: MasterKeyEntity) => {
const password = determineKeyPassword(mk.id, masterPasswordKeys, props.masterPassword, props.passwords);
const result = await upgradeMasterKey(mk, password);
alert(result);
await shim.showMessageBox(result, { type: MessageBoxType.Info });
}, [props.passwords, masterPasswordKeys, props.masterPassword]);
const renderNeedUpgradeSection = () => {

View File

@@ -11,6 +11,7 @@ import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
import KvStore from '@joplin/lib/services/KvStore';
import ShareService from '@joplin/lib/services/share/ShareService';
import LabelledPasswordInput from '../PasswordInput/LabelledPasswordInput';
import shim from '@joplin/lib/shim';
interface Props {
themeId: number;
@@ -80,7 +81,7 @@ export default function(props: Props) {
void reg.waitForSyncFinishedThenSync();
onClose();
} catch (error) {
alert(error.message);
void shim.showErrorDialog(error.message);
} finally {
setUpdatingPassword(false);
}

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, ForwardedRef, useContext } from 'react';
// eslint-disable-next-line no-unused-vars
import { EditorCommand, MarkupToHtmlOptions, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
import { EditorCommand, NoteBodyEditorProps, NoteBodyEditorRef } from '../../../utils/types';
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../../utils/types';
import { CommandValue } from '../../../utils/types';
@@ -34,6 +34,7 @@ import useWebviewIpcMessage from '../utils/useWebviewIpcMessage';
import useEditorSearchHandler from '../utils/useEditorSearchHandler';
import { focus } from '@joplin/lib/utils/focusHandler';
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
import { MarkupToHtmlOptions } from '../../../../hooks/useMarkupToHtml';
function markupRenderOptions(override: MarkupToHtmlOptions = null): MarkupToHtmlOptions {
return { ...override };
@@ -48,7 +49,10 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
const [webviewReady, setWebviewReady] = useState(false);
const editorRef = useRef(null);
const rootRef = useRef(null);
const [editorRoot, setEditorRoot] = useState<HTMLDivElement|null>(null);
const rootRef = useRef<HTMLDivElement|null>(null);
rootRef.current = editorRoot;
const webviewRef = useRef(null);
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
const props_onChangeRef = useRef<Function>(null);
@@ -410,6 +414,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
}, [styles.editor.codeMirrorTheme]);
useEffect(() => {
if (!editorRoot) return () => {};
const theme = themeStyle(props.themeId);
// Selection in dark mode is hard to see so make it brighter.
@@ -431,10 +437,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
max-width: ${props.contentMaxWidth}px !important;
` : '';
const element = document.createElement('style');
const ownerDoc = editorRoot.ownerDocument;
const element = ownerDoc.createElement('style');
element.setAttribute('id', 'codemirrorStyle');
document.head.appendChild(element);
element.appendChild(document.createTextNode(`
ownerDoc.head.appendChild(element);
element.appendChild(ownerDoc.createTextNode(`
/* These must be important to prevent the codemirror defaults from taking over*/
.CodeMirror {
font-family: monospace;
@@ -449,6 +456,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
line-height: ${theme.lineHeight} !important;
}
.CodeMirror-code:focus-visible {
/* Avoid showing additional focus-visible decoration */
outline: none;
}
.CodeMirror-lines {
/* This is used to enable the scroll-past end behaviour. The same height should */
/* be applied to the viewer. */
@@ -591,10 +603,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
`));
return () => {
document.head.removeChild(element);
ownerDoc.head.removeChild(element);
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [props.themeId, props.contentMaxWidth]);
}, [props.themeId, props.contentMaxWidth, props.fontSize, editorRoot]);
const webview_domReady = useCallback(() => {
setWebviewReady(true);
@@ -774,7 +785,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
const windowId = useContext(WindowIdContext);
return (
<ErrorBoundary message="The text editor encountered a fatal error and could not continue. The error might be due to a plugin, so please try to disable some of them and try again.">
<div style={styles.root} ref={rootRef}>
<div style={styles.root} ref={setEditorRoot}>
<div style={styles.rowToolbar}>
<Toolbar themeId={props.themeId} windowId={windowId}/>
{props.noteToolbar}

View File

@@ -1035,6 +1035,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const allAssetsOptions: NoteStyleOptions = {
contentMaxWidthTarget: '.mce-content-body',
scrollbarSize: props.scrollbarSize,
themeId: props.contentMarkupLanguage === MarkupLanguage.Html ? 1 : null,
whiteBackgroundNoteRendering: props.whiteBackgroundNoteRendering,
};
@@ -1051,7 +1052,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
cancelled = true;
};
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
}, [editor, props.themeId, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
}, [editor, props.themeId, props.scrollbarSize, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey, props.contentMarkupLanguage, props.whiteBackgroundNoteRendering]);
useEffect(() => {
if (!editor) return () => {};

View File

@@ -9,7 +9,7 @@ import useNoteSearchBar from './utils/useNoteSearchBar';
import useMessageHandler from './utils/useMessageHandler';
import useWindowCommandHandler from './utils/useWindowCommandHandler';
import useDropHandler from './utils/useDropHandler';
import useMarkupToHtml from './utils/useMarkupToHtml';
import useMarkupToHtml from '../hooks/useMarkupToHtml';
import useFormNote, { OnLoadEvent, OnSetFormNote } from './utils/useFormNote';
import useEffectiveNoteId from './utils/useEffectiveNoteId';
import useFolder from './utils/useFolder';
@@ -45,7 +45,6 @@ import PlainEditor from './NoteBody/PlainEditor/PlainEditor';
import CodeMirror6 from './NoteBody/CodeMirror/v6/CodeMirror';
import CodeMirror5 from './NoteBody/CodeMirror/v5/CodeMirror';
import { openItemById } from './utils/contextMenu';
import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue';
import { MarkupLanguage } from '@joplin/renderer';
import useScrollWhenReadyOptions from './utils/useScrollWhenReadyOptions';
import useScheduleSaveCallbacks from './utils/useScheduleSaveCallbacks';
@@ -59,6 +58,7 @@ import { EditorActivationCheckFilterObject } from '@joplin/lib/services/plugins/
import PluginService from '@joplin/lib/services/plugins/PluginService';
import WebviewController from '@joplin/lib/services/plugins/WebviewController';
import AsyncActionQueue, { IntervalType } from '@joplin/lib/AsyncActionQueue';
import useResourceUnwatcher from './utils/useResourceUnwatcher';
const debounce = require('debounce');
@@ -179,7 +179,7 @@ function NoteEditorContent(props: NoteEditorProps) {
whiteBackgroundNoteRendering,
customCss: props.customCss,
plugins: props.plugins,
settingValue: getPluginSettingValue,
scrollbarSize: props.scrollbarSize,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -199,9 +199,10 @@ function NoteEditorContent(props: NoteEditorProps) {
return markupToHtml.allAssets(markupLanguage, theme, {
contentMaxWidth: props.contentMaxWidth,
contentMaxWidthTarget: options.contentMaxWidthTarget,
scrollbarSize: props.scrollbarSize,
whiteBackgroundNoteRendering: options.whiteBackgroundNoteRendering,
});
}, [props.themeId, props.customCss, props.contentMaxWidth]);
}, [props.themeId, props.scrollbarSize, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {
if (props.isProvisional) {
@@ -358,6 +359,8 @@ function NoteEditorContent(props: NoteEditorProps) {
const windowId = useContext(WindowIdContext);
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
useResourceUnwatcher({ noteId: formNote.id, windowId });
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const externalEditWatcher_noteChange = useCallback((event: any) => {
if (event.id === formNote.id) {
@@ -491,6 +494,7 @@ function NoteEditorContent(props: NoteEditorProps) {
plugins: props.plugins,
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
scrollbarSize: props.scrollbarSize,
isSafeMode: props.isSafeMode,
useCustomPdfViewer: props.useCustomPdfViewer,
// We need it to identify the context for which media is rendered.
@@ -729,7 +733,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
selectedSearchId: windowState.selectedSearchId,
customCss: state.customViewerCss,
noteVisiblePanes: windowState.noteVisiblePanes,
watchedResources: state.watchedResources,
watchedResources: windowState.watchedResources,
highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
@@ -744,6 +748,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
'setTags',
], whenClauseContext)[0] as ToolbarButtonInfo,
contentMaxWidth: state.settings['style.editor.contentMaxWidth'],
scrollbarSize: state.settings['style.scrollbarSize'],
isSafeMode: state.settings.isSafeMode,
useCustomPdfViewer: false,
syncUserId: state.settings['sync.userId'],

View File

@@ -5,3 +5,5 @@
@use "./styles/note-title-wrapper.scss";
@use "./styles/note-editor-wrapper.scss";
@use "./styles/note-editor-viewer-row.scss";
@use "./styles/revision-viewer-root.scss";
@use "./styles/revision-viewer-title.scss";

View File

@@ -0,0 +1,7 @@
.revision-viewer-root {
background-color: var(--joplin-background-color);
display: flex;
flex-direction: column;
flex: 1;
}

View File

@@ -0,0 +1,20 @@
.revision-viewer-title {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
border-width: 1px;
border-bottom-style: solid;
border-color: var(--joplin-divider-color);
padding-bottom: 10px;
> .revisions {
margin-left: 10px;
flex: 0.5;
}
> .title {
flex: 1;
}
}

View File

@@ -9,6 +9,8 @@ import { DropHandler } from './useDropHandler';
import { SearchMarkers } from './useSearchMarkers';
import { ParseOptions } from '@joplin/lib/HtmlToMd';
import { ScrollStrategy } from '@joplin/editor/CodeMirror/CodeMirrorControl';
import { MarkupToHtmlOptions } from '../../hooks/useMarkupToHtml';
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
@@ -51,6 +53,7 @@ export interface NoteEditorProps {
toolbarButtonInfos: ToolbarItem[];
setTagsToolbarButtonInfo: ToolbarButtonInfo;
contentMaxWidth: number;
scrollbarSize: ScrollbarSize;
isSafeMode: boolean;
useCustomPdfViewer: boolean;
shareCacheSetting: string;
@@ -72,22 +75,7 @@ export interface NoteBodyEditorRef {
execCommand(command: CommandValue): Promise<void>;
}
export interface MarkupToHtmlOptions {
replaceResourceInternalToExternalLinks?: boolean;
resourceInfos?: ResourceInfos;
contentMaxWidth?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
plugins?: Record<string, any>;
bodyOnly?: boolean;
mapsToLine?: boolean;
useCustomPdfViewer?: boolean;
noteId?: string;
vendorDir?: string;
platformName?: string;
allowedFilePrefixes?: string[];
whiteBackgroundNoteRendering?: boolean;
}
export { MarkupToHtmlOptions };
export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string, parseOptions?: ParseOptions)=> Promise<string>;
@@ -105,6 +93,8 @@ export interface NoteBodyEditorProps {
// avoid cases where black text is rendered over a dark background.
whiteBackgroundNoteRendering: boolean;
scrollbarSize: ScrollbarSize;
content: string;
contentKey: string;
contentMarkupLanguage: number;

View File

@@ -0,0 +1,43 @@
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const react_1 = require('react');
const markupLanguageUtils_1 = require('@joplin/lib/utils/markupLanguageUtils');
const Setting_1 = require('@joplin/lib/models/Setting');
const shim_1 = require('@joplin/lib/shim');
const { themeStyle } = require('@joplin/lib/theme');
const Note_1 = require('@joplin/lib/models/Note');
const resourceUtils_1 = require('@joplin/lib/models/utils/resourceUtils');
function useMarkupToHtml(deps) {
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
const resourceBaseUrl = (0, react_1.useMemo)(() => {
return `joplin-content://note-viewer/${Setting_1.default.value('resourceDir')}/`;
}, []);
const markupToHtml = (0, react_1.useMemo)(() => {
return markupLanguageUtils_1.default.newMarkupToHtml(plugins, {
resourceBaseUrl,
customCss: customCss || '',
});
}, [plugins, customCss, resourceBaseUrl]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return (0, react_1.useCallback)(async (markupLanguage, md, options = null) => {
options = { replaceResourceInternalToExternalLinks: false, resourceInfos: {}, platformName: shim_1.default.platformName(), ...options };
md = md || '';
const theme = themeStyle(themeId);
let resources = {};
if (options.replaceResourceInternalToExternalLinks) {
md = await Note_1.default.replaceResourceInternalToExternalLinks(md, { useAbsolutePaths: true });
} else {
resources = options.resourceInfos;
}
delete options.replaceResourceInternalToExternalLinks;
const result = await markupToHtml.render(markupLanguage, md, theme, { codeTheme: theme.codeThemeCss, resources: resources, postMessageSyntax: 'ipcProxySendToHost', splitted: true, externalAssetsOnly: true, codeHighlightCacheKey: 'useMarkupToHtml', settingValue: deps.settingValue, whiteBackgroundNoteRendering, itemIdToUrl: (id, urlParameters = '') => {
if (!(id in resources) || !resources[id]) {
return null;
}
return (0, resourceUtils_1.resourceFullPath)(resources[id].item, resourceBaseUrl) + urlParameters;
}, ...options });
return result;
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, resourceBaseUrl, deps.settingValue]);
}
exports.default = useMarkupToHtml;
// # sourceMappingURL=useMarkupToHtml.js.map

View File

@@ -0,0 +1,21 @@
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
import { useEffect } from 'react';
interface Props {
noteId: string;
windowId: string;
}
const useResourceUnwatcher = ({ noteId, windowId }: Props) => {
useEffect(() => {
// All resources associated with the current window should no longer be watched after:
// 1. The editor unloads, or
// 2. The note shown in the editor changes.
// Unwatching in a cleanup callback handles both cases.
return () => {
void ResourceEditWatcher.instance().stopWatchingAll(windowId);
};
}, [noteId, windowId]);
};
export default useResourceUnwatcher;

View File

@@ -1,7 +1,6 @@
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher';
import type { EditorScrollPercents } from '../../../app.reducer';
interface Props {
@@ -30,8 +29,6 @@ const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollP
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
});
void ResourceEditWatcher.instance().stopWatchingAll();
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
const clearScrollWhenReady = useCallback(() => {

View File

@@ -1,163 +1,124 @@
import * as React from 'react';
import { themeStyle } from '@joplin/lib/theme';
import { _ } from '@joplin/lib/locale';
import NoteTextViewer from './NoteTextViewer';
import NoteTextViewer, { NoteViewerControl } from './NoteTextViewer';
import HelpButton from './HelpButton';
import BaseModel from '@joplin/lib/BaseModel';
import Revision from '@joplin/lib/models/Revision';
import Setting from '@joplin/lib/models/Setting';
import RevisionService from '@joplin/lib/services/RevisionService';
import { MarkupToHtml } from '@joplin/renderer';
import { MarkupLanguage } from '@joplin/renderer';
import time from '@joplin/lib/time';
import bridge from '../services/bridge';
import markupLanguageUtils from '@joplin/lib/utils/markupLanguageUtils';
import { NoteEntity, RevisionEntity } from '@joplin/lib/services/database/types';
import { AppState } from '../app.reducer';
const urlUtils = require('@joplin/lib/urlUtils');
const ReactTooltip = require('react-tooltip');
const { connect } = require('react-redux');
import shared from '@joplin/lib/components/shared/note-screen-shared';
import shim, { MessageBoxType } from '@joplin/lib/shim';
import { RefObject, useCallback, useRef, useState } from 'react';
import useQueuedAsyncEffect from '@joplin/lib/hooks/useQueuedAsyncEffect';
import useMarkupToHtml from './hooks/useMarkupToHtml';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
interface Props {
themeId: number;
noteId: string;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
onBack: Function;
onBack: ()=> void;
customCss: string;
scrollbarSize: ScrollbarSize;
}
interface State {
note: NoteEntity;
revisions: RevisionEntity[];
currentRevId: string;
restoring: boolean;
}
const useNoteContent = (
viewerRef: RefObject<NoteViewerControl>,
currentRevId: string,
revisions: RevisionEntity[],
themeId: number,
customCss: string,
scrollbarSize: ScrollbarSize,
) => {
const [note, setNote] = useState<NoteEntity>(null);
class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
const markupToHtml = useMarkupToHtml({
themeId,
customCss,
plugins: {},
whiteBackgroundNoteRendering: false,
scrollbarSize,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private viewerRef_: any;
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
private helpButton_onClick: Function;
useAsyncEffect(async (event) => {
if (!revisions.length || !currentRevId) {
setNote(null);
} else {
const revIndex = BaseModel.modelIndexById(revisions, currentRevId);
const note = await RevisionService.instance().revisionNote(revisions, revIndex);
if (!note || event.cancelled) return;
setNote(note);
}
}, [revisions, currentRevId, themeId, customCss, viewerRef]);
public constructor(props: Props) {
super(props);
useQueuedAsyncEffect(async () => {
const noteBody = note?.body ?? _('This note has no history');
const markupLanguage = note.markup_language ?? MarkupLanguage.Markdown;
const result = await markupToHtml(markupLanguage, noteBody, {
resources: await shared.attachedResources(noteBody),
whiteBackgroundNoteRendering: markupLanguage === MarkupLanguage.Html,
});
this.state = {
revisions: [],
currentRevId: '',
note: null,
restoring: false,
};
viewerRef.current.setHtml(result.html, {
pluginAssets: result.pluginAssets,
});
}, [note, viewerRef]);
this.viewerRef_ = React.createRef();
return note;
};
this.viewer_domReady = this.viewer_domReady.bind(this);
this.revisionList_onChange = this.revisionList_onChange.bind(this);
this.importButton_onClick = this.importButton_onClick.bind(this);
this.backButton_click = this.backButton_click.bind(this);
this.webview_ipcMessage = this.webview_ipcMessage.bind(this);
}
const NoteRevisionViewerComponent: React.FC<Props> = ({ themeId, noteId, onBack, customCss, scrollbarSize }) => {
const helpButton_onClick = useCallback(() => {}, []);
const viewerRef = useRef<NoteViewerControl|null>(null);
public style() {
const theme = themeStyle(this.props.themeId);
const [revisions, setRevisions] = useState<RevisionEntity[]>([]);
const [currentRevId, setCurrentRevId] = useState('');
const [restoring, setRestoring] = useState(false);
const style = {
root: {
backgroundColor: theme.backgroundColor,
display: 'flex',
flex: 1,
flexDirection: 'column',
},
titleInput: { ...theme.inputStyle, flex: 1 },
revisionList: { ...theme.dropdownList, marginLeft: 10, flex: 0.5 },
};
const note = useNoteContent(viewerRef, currentRevId, revisions, themeId, customCss, scrollbarSize);
return style;
}
private async viewer_domReady() {
const viewer_domReady = useCallback(async () => {
// this.viewerRef_.current.openDevTools();
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, this.props.noteId);
const revisions = await Revision.allByType(BaseModel.TYPE_NOTE, noteId);
this.setState(
{
revisions: revisions,
currentRevId: revisions.length ? revisions[revisions.length - 1].id : '',
},
() => {
void this.reloadNote();
},
);
}
setRevisions(revisions);
setCurrentRevId(revisions.length ? revisions[revisions.length - 1].id : '');
}, [noteId]);
private async importButton_onClick() {
if (!this.state.note) return;
this.setState({ restoring: true });
await RevisionService.instance().importRevisionNote(this.state.note);
this.setState({ restoring: false });
alert(RevisionService.instance().restoreSuccessMessage(this.state.note));
}
const importButton_onClick = useCallback(async () => {
if (!note) return;
setRestoring(true);
await RevisionService.instance().importRevisionNote(note);
setRestoring(false);
await shim.showMessageBox(RevisionService.instance().restoreSuccessMessage(note), { type: MessageBoxType.Info });
}, [note]);
private backButton_click() {
if (this.props.onBack) this.props.onBack();
}
const backButton_click = useCallback(() => {
if (onBack) onBack();
}, [onBack]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private revisionList_onChange(event: any) {
const revisionList_onChange: React.ChangeEventHandler<HTMLSelectElement> = useCallback((event) => {
const value = event.target.value;
if (!value) {
if (this.props.onBack) this.props.onBack();
if (onBack) onBack();
} else {
this.setState(
{
currentRevId: value,
},
() => {
void this.reloadNote();
},
);
setCurrentRevId(value);
}
}
public async reloadNote() {
let noteBody = '';
let markupLanguage = MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;
if (!this.state.revisions.length || !this.state.currentRevId) {
noteBody = _('This note has no history');
this.setState({ note: null });
} else {
const revIndex = BaseModel.modelIndexById(this.state.revisions, this.state.currentRevId);
const note = await RevisionService.instance().revisionNote(this.state.revisions, revIndex);
if (!note) return;
noteBody = note.body;
markupLanguage = note.markup_language;
this.setState({ note: note });
}
const theme = themeStyle(this.props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
resourceBaseUrl: `joplin-content://note-viewer/${Setting.value('resourceDir')}/`,
customCss: this.props.customCss ? this.props.customCss : '',
});
const result = await markupToHtml.render(markupLanguage, noteBody, theme, {
codeTheme: theme.codeThemeCss,
resources: await shared.attachedResources(noteBody),
postMessageSyntax: 'ipcProxySendToHost',
});
this.viewerRef_.current.setHtml(result.html, {
// cssFiles: result.cssFiles,
pluginAssets: result.pluginAssets,
});
}
}, [onBack]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
private async webview_ipcMessage(event: any) {
const webview_ipcMessage = useCallback(async (event: any) => {
// For the revision view, we only support a minimal subset of the IPC messages.
// For example, we don't need interactive checkboxes or sync between viewer and editor view.
// We try to get most links work though, except for internal (joplin://) links.
@@ -181,60 +142,57 @@ class NoteRevisionViewerComponent extends React.PureComponent<Props, State> {
console.warn(error);
bridge().showErrorMessageBox(error.message);
}
}
}, []);
public render() {
const theme = themeStyle(this.props.themeId);
const style = this.style();
const theme = themeStyle(themeId);
const revisionListItems = [];
const revs = this.state.revisions.slice().reverse();
for (let i = 0; i < revs.length; i++) {
const rev = revs[i];
const stats = Revision.revisionPatchStatsText(rev);
const revisionListItems = [];
const revs = revisions.slice().reverse();
for (let i = 0; i < revs.length; i++) {
const rev = revs[i];
const stats = Revision.revisionPatchStatsText(rev);
revisionListItems.push(
<option key={rev.id} value={rev.id}>
{`${time.formatMsToLocal(rev.item_updated_time)} (${stats})`}
</option>,
);
}
const restoreButtonTitle = _('Restore');
const helpMessage = _('Click "%s" to restore the note. It will be copied in the notebook named "%s". The current version of the note will not be replaced or modified.', restoreButtonTitle, RevisionService.instance().restoreFolderTitle());
const titleInput = (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginBottom: 10, borderWidth: 1, borderBottomStyle: 'solid', borderColor: theme.dividerColor, paddingBottom: 10 }}>
<button onClick={this.backButton_click} style={{ ...theme.buttonStyle, marginRight: 10, height: theme.inputStyle.height }}>
<i style={theme.buttonIconStyle} className={'fa fa-chevron-left'}></i>{_('Back')}
</button>
<input readOnly type="text" style={style.titleInput} value={this.state.note ? this.state.note.title : ''} />
<select disabled={!this.state.revisions.length} value={this.state.currentRevId} style={style.revisionList} onChange={this.revisionList_onChange}>
{revisionListItems}
</select>
<button disabled={!this.state.revisions.length || this.state.restoring} onClick={this.importButton_onClick} style={{ ...theme.buttonStyle, marginLeft: 10, height: theme.inputStyle.height }}>
{restoreButtonTitle}
</button>
<HelpButton tip={helpMessage} id="noteRevisionHelpButton" onClick={this.helpButton_onClick} />
</div>
);
const viewer = <NoteTextViewer themeId={this.props.themeId} viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={this.viewerRef_} onDomReady={this.viewer_domReady} onIpcMessage={this.webview_ipcMessage} />;
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
<div style={style.root as any}>
{titleInput}
{viewer}
<ReactTooltip place="bottom" delayShow={300} className="help-tooltip" />
</div>
revisionListItems.push(
<option key={rev.id} value={rev.id}>
{`${time.formatMsToLocal(rev.item_updated_time)} (${stats})`}
</option>,
);
}
}
const restoreButtonTitle = _('Restore');
const helpMessage = _('Click "%s" to restore the note. It will be copied in the notebook named "%s". The current version of the note will not be replaced or modified.', restoreButtonTitle, RevisionService.instance().restoreFolderTitle());
const titleInput = (
<div className='revision-viewer-title'>
<button onClick={backButton_click} style={{ ...theme.buttonStyle, marginRight: 10, height: theme.inputStyle.height }}>
<i style={theme.buttonIconStyle} className={'fa fa-chevron-left'}></i>{_('Back')}
</button>
<input readOnly type="text" className='title' style={theme.inputStyle} value={note?.title ?? ''} />
<select disabled={!revisions.length} value={currentRevId} className='revisions' style={theme.dropdownList} onChange={revisionList_onChange}>
{revisionListItems}
</select>
<button disabled={!revisions.length || restoring} onClick={importButton_onClick} className='restore'style={{ ...theme.buttonStyle, marginLeft: 10, height: theme.inputStyle.height }}>
{restoreButtonTitle}
</button>
<HelpButton tip={helpMessage} id="noteRevisionHelpButton" onClick={helpButton_onClick} />
</div>
);
const viewer = <NoteTextViewer themeId={themeId} viewerStyle={{ display: 'flex', flex: 1, borderLeft: 'none' }} ref={viewerRef} onDomReady={viewer_domReady} onIpcMessage={webview_ipcMessage} />;
return (
<div className='revision-viewer-root'>
{titleInput}
{viewer}
<ReactTooltip place="bottom" delayShow={300} className="help-tooltip" />
</div>
);
};
const mapStateToProps = (state: AppState) => {
return {
themeId: state.settings.theme,
scrollbarSize: state.settings['style.scrollbarSize'],
};
};

View File

@@ -18,6 +18,7 @@ import { connect } from 'react-redux';
import { reg } from '@joplin/lib/registry';
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
import { ChangeEvent, Dropdown, DropdownOptions, DropdownVariant } from '../Dropdown/Dropdown';
import shim from '@joplin/lib/shim';
const logger = Logger.create('ShareFolderDialog');
@@ -242,13 +243,13 @@ function ShareFolderDialog(props: Props) {
}
async function recipient_delete(event: RecipientDeleteEvent) {
if (!confirm(_('Delete this invitation? The recipient will no longer have access to this shared notebook.'))) return;
if (!await shim.showConfirmationDialog(_('Delete this invitation? The recipient will no longer have access to this shared notebook.'))) return;
try {
await ShareService.instance().deleteShareRecipient(event.shareUserId);
} catch (error) {
logger.error(error);
alert(_('The recipient could not be removed from the list. Please try again.\n\nThe error was: "%s"', error.message));
await shim.showErrorDialog(_('The recipient could not be removed from the list. Please try again.\n\nThe error was: "%s"', error.message));
}
await ShareService.instance().refreshShareUsers(share.id);
@@ -290,7 +291,7 @@ function ShareFolderDialog(props: Props) {
});
await ShareService.instance().setPermissions(share.id, shareUserId, permissionsFromString(value));
} catch (error) {
alert(`Could not set permissions: ${error.message}`);
void shim.showErrorDialog(`Could not set permissions: ${error.message}`);
logger.error(error);
} finally {
setRecipientsBeingUpdated(prev => {
@@ -383,7 +384,9 @@ function ShareFolderDialog(props: Props) {
async function buttonRow_click(event: ClickEvent) {
if (event.buttonName === 'unshare') {
if (!confirm(_('Unshare this notebook? The recipients will no longer have access to its content.'))) return;
if (!await shim.showConfirmationDialog(_('Unshare this notebook? The recipients will no longer have access to its content.'))) {
return;
}
await ShareService.instance().unshareFolder(props.folderId);
void synchronize();
}

View File

@@ -16,6 +16,7 @@ import { connect } from 'react-redux';
import { AppState } from '../app.reducer';
import { getEncryptionEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import shim from '@joplin/lib/shim';
const { clipboard } = require('electron');
interface Props {
@@ -146,7 +147,7 @@ export function ShareNoteDialog(props: Props) {
reg.logger().error('ShareNoteDialog: Cannot publish note:', error);
setSharesState('idle');
alert(JoplinServerApi.connectionErrorMessage(error));
void shim.showErrorDialog(JoplinServerApi.connectionErrorMessage(error));
}
break;

View File

@@ -31,6 +31,7 @@ import HeaderItem from '../listItemComponents/HeaderItem';
import AllNotesItem from '../listItemComponents/AllNotesItem';
import ListItemWrapper from '../listItemComponents/ListItemWrapper';
import { focus } from '@joplin/lib/utils/focusHandler';
import shim from '@joplin/lib/shim';
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@@ -309,7 +310,7 @@ const useOnRenderItem = (props: Props) => {
}
} catch (error) {
logger.error(error);
alert(error.message);
await shim.showErrorDialog(error.message);
}
}, []);

View File

@@ -91,6 +91,7 @@ function FolderItem(props: FolderItemProps) {
isConflictFolder={folderId === Folder.conflictFolderId()}
selected={selected}
shareId={shareId}
data-folder-id={folderId}
onDoubleClick={onFolderToggleClick_}
onClick={() => {

View File

@@ -17,9 +17,11 @@ import { themeStyle } from '@joplin/lib/theme';
import useDocument from '../hooks/useDocument';
import { connect } from 'react-redux';
import { AppState } from '../../app.reducer';
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
interface Props {
themeId: number;
scrollbarSize: ScrollbarSize;
editorFontSetting: string;
customChromeCssPaths: string[];
}
@@ -106,6 +108,11 @@ const StyleSheetContainer: React.FC<Props> = props => {
/* Theme CSS */
${themeCss}
/* Base scrollbar size */
:root {
--scrollbar-size: ${Number(props.scrollbarSize)}px;
}
/* Editor font CSS */
${editorCss}
`);
@@ -118,6 +125,7 @@ export default connect((state: AppState) => {
return {
themeId: state.settings.theme,
editorFontSetting: state.settings['style.editor.fontFamily'] as string,
scrollbarSize: state.settings['style.scrollbarSize'],
customChromeCssPaths: state.customChromeCssPaths,
};
})(StyleSheetContainer);

View File

@@ -2,6 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
import { _ } from '@joplin/lib/locale';
import ShareService from '@joplin/lib/services/share/ShareService';
import Logger from '@joplin/utils/Logger';
import shim from '@joplin/lib/shim';
const logger = Logger.create('leaveSharedFolder');
@@ -13,7 +14,7 @@ export const declaration: CommandDeclaration = {
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, folderId: string = null) => {
const answer = confirm(_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'));
const answer = await shim.showConfirmationDialog(_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'));
if (!answer) return;
try {
@@ -28,7 +29,7 @@ export const runtime = (): CommandRuntime => {
await ShareService.instance().leaveSharedFolder(folderId, share.user.id);
} catch (error) {
logger.error(error);
alert(_('Error: %s', error.message));
await shim.showErrorDialog(_('Error: %s', error.message));
}
},
enabledCondition: 'joplinServerConnected && folderIsShareRootAndNotOwnedByUser',

View File

@@ -6,20 +6,27 @@ import shim from '@joplin/lib/shim';
const { themeStyle } = require('@joplin/lib/theme');
import Note from '@joplin/lib/models/Note';
import { MarkupToHtmlOptions, ResourceInfos } from './types';
import { ResourceInfos } from '../NoteEditor/utils/types';
import { resourceFullPath } from '@joplin/lib/models/utils/resourceUtils';
import { RenderOptions } from '@joplin/renderer/types';
import getPluginSettingValue from '@joplin/lib/services/plugins/utils/getPluginSettingValue';
import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
export interface MarkupToHtmlOptions extends RenderOptions {
resourceInfos?: ResourceInfos;
replaceResourceInternalToExternalLinks?: boolean;
}
interface HookDependencies {
themeId: number;
customCss: string;
plugins: PluginStates;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
settingValue: (pluginId: string, key: string)=> any;
whiteBackgroundNoteRendering: boolean;
scrollbarSize: ScrollbarSize;
}
export default function useMarkupToHtml(deps: HookDependencies) {
const { themeId, customCss, plugins, whiteBackgroundNoteRendering } = deps;
const { themeId, customCss, plugins, whiteBackgroundNoteRendering, scrollbarSize } = deps;
const resourceBaseUrl = useMemo(() => {
return `joplin-content://note-viewer/${Setting.value('resourceDir')}/`;
@@ -32,8 +39,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
});
}, [plugins, customCss, resourceBaseUrl]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions = null): Promise<any> => {
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions|null = null) => {
options = {
replaceResourceInternalToExternalLinks: false,
resourceInfos: {},
@@ -61,8 +67,9 @@ export default function useMarkupToHtml(deps: HookDependencies) {
splitted: true,
externalAssetsOnly: true,
codeHighlightCacheKey: 'useMarkupToHtml',
settingValue: deps.settingValue,
settingValue: getPluginSettingValue,
whiteBackgroundNoteRendering,
scrollbarSize: scrollbarSize,
itemIdToUrl: (id: string, urlParameters = '') => {
if (!(id in resources) || !resources[id]) {
return null;
@@ -74,5 +81,5 @@ export default function useMarkupToHtml(deps: HookDependencies) {
});
return result;
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, resourceBaseUrl, deps.settingValue]);
}, [themeId, markupToHtml, whiteBackgroundNoteRendering, scrollbarSize, resourceBaseUrl]);
}

View File

@@ -415,6 +415,12 @@
addPluginAssets(event.options.pluginAssets);
if (event.options.increaseControlSize) {
document.documentElement.classList.add('-larger-controls');
} else {
document.documentElement.classList.remove('-larger-controls');
}
if (event.options.downloadResources === 'manual') {
webviewLib.setupResourceManualDownload();
}

View File

@@ -101,4 +101,25 @@ test.describe('sidebar', () => {
await expect(mainWindow.getByText('Another note in Folder A')).toBeAttached();
await expect(mainWindow.getByText('A note in Folder B')).toBeAttached();
});
test('double-clicking should collapse/expand folders in the sidebar', async ({ mainWindow }) => {
const mainScreen = await new MainScreen(mainWindow).setup();
const sidebar = mainScreen.sidebar;
const testFolderA = await sidebar.createNewFolder('Folder A');
const testFolderB = await sidebar.createNewFolder('Folder B');
// Convert folder B to a subfolder
await testFolderB.dragTo(testFolderA);
await expect(testFolderB).toBeVisible();
// Collapse
await testFolderA.dblclick();
await expect(testFolderB).not.toBeVisible();
// Expand
await testFolderA.dblclick();
await expect(testFolderB).toBeVisible();
});
});

View File

@@ -31,8 +31,8 @@ a {
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
width: var(--scrollbar-size, 7px);
height: var(--scrollbar-size, 7px);
}
::-webkit-scrollbar-corner {
@@ -45,7 +45,7 @@ a {
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 5px;
border-radius: calc(var(--scrollbar-size, 7px) * 0.7);
}
::-webkit-scrollbar-track:hover {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "3.2.7",
"version": "3.2.11",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -79,8 +79,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097760
versionName "3.2.4"
versionCode 2097763
versionName "3.2.7"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -8,6 +8,7 @@ import BaseItem from '@joplin/lib/models/BaseItem';
import { BaseItemEntity } from '@joplin/lib/services/database/types';
import { ModelType } from '@joplin/lib/BaseModel';
import showResource from './util/showResource';
import { isCallbackUrl, parseCallbackUrl } from '@joplin/lib/callbackUrlUtils';
const logger = Logger.create('openItemCommand');
@@ -15,32 +16,48 @@ export const declaration: CommandDeclaration = {
name: 'openItem',
};
const openItemById = async (itemId: string, hash?: string) => {
logger.info(`Navigating to item ${itemId}`);
const item: BaseItemEntity = await BaseItem.loadItemById(itemId);
if (item.type_ === ModelType.Note) {
await goToNote(itemId, hash);
} else if (item.type_ === ModelType.Resource) {
await showResource(item);
} else {
throw new Error(`Unsupported item type for links: ${item.type_}`);
}
};
export const runtime = (): CommandRuntime => {
return {
execute: async (_context: CommandContext, link: string) => {
if (!link) throw new Error('Link cannot be empty');
if (link.startsWith('joplin://') || link.startsWith(':/')) {
const parsedUrl = parseResourceUrl(link);
if (parsedUrl) {
const { itemId, hash } = parsedUrl;
try {
if (link.startsWith('joplin://') || link.startsWith(':/')) {
const parsedResourceUrl = parseResourceUrl(link);
const parsedCallbackUrl = isCallbackUrl(link) ? parseCallbackUrl(link) : null;
logger.info(`Navigating to item ${itemId}`);
const item: BaseItemEntity = await BaseItem.loadItemById(itemId);
if (item.type_ === ModelType.Note) {
await goToNote(itemId, hash);
} else if (item.type_ === ModelType.Resource) {
await showResource(item);
if (parsedResourceUrl) {
const { itemId, hash } = parsedResourceUrl;
await openItemById(itemId, hash);
} else if (parsedCallbackUrl) {
const id = parsedCallbackUrl.params.id;
if (!id) {
throw new Error('Missing item ID');
}
await openItemById(id);
} else {
logger.error('Unsupported item type for links:', item.type_);
throw new Error('Unsupported link format.');
}
} else if (urlProtocol(link)) {
shim.openUrl(link);
} else {
logger.error(`Invalid Joplin link: ${link}`);
throw new Error('Unsupported protocol');
}
} else if (urlProtocol(link)) {
shim.openUrl(link);
} else {
const errorMessage = _('Unsupported link or message: %s', link);
} catch (error) {
const errorMessage = _('Unsupported link or message: %s.\nError: %s', link, error);
logger.error(errorMessage);
await shim.showErrorDialog(errorMessage);
}

View File

@@ -114,7 +114,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
// allow-popups-to-escape-sandbox: Allows PDF previews to work on target="_blank" links.
// allow-popups: Allows links to open in a new tab.
permissions: 'allow-scripts allow-modals allow-popups allow-popups-to-escape-sandbox',
allow: 'clipboard-write=(self) fullscreen=(self) autoplay=(self) local-fonts=* encrypted-media=*',
allow: 'clipboard-write; clipboard-read; fullscreen \'self\'; autoplay \'self\'; local-fonts \'self\'; encrypted-media \'self\'',
});
if (containerRef.current) {

View File

@@ -8,12 +8,11 @@ import { Theme } from '@joplin/lib/themes/type';
import { MutableRefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { BackHandler, Platform } from 'react-native';
import ExtendedWebView from '../../ExtendedWebView';
import { WebViewControl } from '../../ExtendedWebView/types';
import { clearAutosave, writeAutosave } from './autosave';
import { OnMessageEvent, WebViewControl } from '../../ExtendedWebView/types';
import { clearAutosave } from './autosave';
import { LocalizedStrings } from './js-draw/types';
import VersionInfo from 'react-native-version-info';
import { DialogContext } from '../../DialogManager';
import { OnMessageEvent } from '../../ExtendedWebView/types';
import useEditorMessenger from './utils/useEditorMessenger';
const logger = Logger.create('ImageEditor');
@@ -172,7 +171,7 @@ const ImageEditor = (props: Props) => {
const appInfo = useMemo(() => {
return {
name: 'Joplin',
description: `v${VersionInfo.appVersion}`,
description: `v${shim.appVersion()}`,
};
}, []);
@@ -189,79 +188,23 @@ const ImageEditor = (props: Props) => {
);
};
const setImageHasChanges = (hasChanges) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: 'set-image-has-changes',
data: hasChanges,
}),
);
};
window.updateEditorTemplate = (templateData) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: 'set-image-template-data',
data: templateData,
}),
);
};
const notifyReadyToLoadSVG = () => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: 'ready-to-load-data',
})
);
};
const saveDrawing = async (drawing, isAutosave) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: isAutosave ? 'autosave' : 'save',
data: drawing.outerHTML,
}),
);
};
const closeEditor = (promptIfUnsaved) => {
window.ReactNativeWebView.postMessage(JSON.stringify({
action: 'close',
promptIfUnsaved,
}));
};
const saveThenClose = (drawing) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({
action: 'save-and-close',
data: drawing.outerHTML,
}),
);
};
try {
if (window.editorControl === undefined) {
${shim.injectedJs('svgEditorBundle')}
window.editorControl = svgEditorBundle.createJsDrawEditor(
{
saveDrawing,
closeEditor,
saveThenClose,
updateEditorTemplate,
setImageHasChanges,
},
svgEditorBundle.createMessenger().remoteApi,
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
${JSON.stringify(Setting.value('locale'))},
${JSON.stringify(localizedStrings)},
${JSON.stringify({ appInfo })},
${JSON.stringify({
appInfo,
...(shim.mobilePlatform() === 'web' ? {
// Use the browser-default clipboard API on web.
clipboardApi: null,
} : {}),
})},
);
// Start loading the SVG file (if present) after loading the editor.
// This shows the user that progress is being made (loading large SVGs
// from disk into memory can take several seconds).
notifyReadyToLoadSVG();
}
} catch(e) {
window.ReactNativeWebView.postMessage(
@@ -273,7 +216,7 @@ const ImageEditor = (props: Props) => {
useEffect(() => {
webviewRef.current?.injectJS(`
document.querySelector('#main-style').innerText = ${JSON.stringify(css)};
document.querySelector('#main-style').textContent = ${JSON.stringify(css)};
if (window.editorControl) {
window.editorControl.onThemeUpdate();
@@ -308,48 +251,36 @@ const ImageEditor = (props: Props) => {
})();`);
}, [webviewRef, props.resourceFilename]);
const onMessage = useCallback(async (event: OnMessageEvent) => {
const data = event.nativeEvent.data;
if (data.startsWith('error:')) {
logger.error('ImageEditor:', data);
return;
}
const json = JSON.parse(data);
if (json.action === 'save') {
await clearAutosave();
await props.onSave(json.data);
} else if (json.action === 'autosave') {
await writeAutosave(json.data);
} else if (json.action === 'save-toolbar') {
Setting.setValue('imageeditor.jsdrawToolbar', json.data);
} else if (json.action === 'close') {
onRequestCloseEditor(json.promptIfUnsaved);
} else if (json.action === 'save-and-close') {
await props.onSave(json.data);
onRequestCloseEditor(json.promptIfUnsaved);
} else if (json.action === 'ready-to-load-data') {
void onReadyToLoadData();
} else if (json.action === 'set-image-has-changes') {
setImageChanged(json.data);
} else if (json.action === 'set-image-template-data') {
Setting.setValue('imageeditor.imageTemplate', json.data);
} else {
logger.error('Unknown action,', json.action);
}
}, [props.onSave, onRequestCloseEditor, onReadyToLoadData]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const onError = useCallback((event: any) => {
logger.error('ImageEditor: WebView error: ', event);
}, []);
const messenger = useEditorMessenger({
webviewRef,
setImageChanged,
onReadyToLoadData,
onSave: props.onSave,
onRequestCloseEditor,
});
const onMessage = useCallback((event: OnMessageEvent) => {
const data = event.nativeEvent.data;
if (typeof data === 'string' && data.startsWith('error:')) {
logger.error(data);
return;
}
messenger.onWebViewMessage(event);
}, [messenger]);
return (
<ExtendedWebView
html={html}
injectedJavaScript={injectedJavaScript}
allowFileAccessFromJs={true}
onMessage={onMessage}
onLoadEnd={messenger.onWebViewLoaded}
onError={onError}
ref={webviewRef}
webviewInstanceId={'image-editor-js-draw'}

View File

@@ -21,11 +21,15 @@ const createEditorWithCallbacks = (callbacks: Partial<ImageEditorCallbacks>) =>
const locale = 'en';
const allCallbacks: ImageEditorCallbacks = {
saveDrawing: () => {},
save: () => {},
saveThenClose: ()=> {},
closeEditor: ()=> {},
setImageHasChanges: ()=> {},
updateEditorTemplate: ()=> {},
updateToolbarState: ()=> {},
onLoadedEditor: ()=> {},
writeClipboardText: async ()=>{},
readClipboardText: async ()=> '',
...callbacks,
};
@@ -51,7 +55,7 @@ describe('createJsDrawEditor', () => {
jest.useFakeTimers();
const editorControl = createEditorWithCallbacks({
saveDrawing: (_drawing: SVGElement, isAutosave: boolean) => {
save: (_drawing: string, isAutosave: boolean) => {
if (isAutosave) {
calledAutosaveCount ++;
}

View File

@@ -4,13 +4,11 @@ import { MaterialIconProvider } from '@js-draw/material-icons';
import 'js-draw/bundledStyles';
import applyTemplateToEditor from './applyTemplateToEditor';
import watchEditorForTemplateChanges from './watchEditorForTemplateChanges';
import { ImageEditorCallbacks, LocalizedStrings } from './types';
import { ImageEditorCallbacks, ImageEditorControl, LocalizedStrings } from './types';
import startAutosaveLoop from './startAutosaveLoop';
import WebViewToRNMessenger from '../../../../utils/ipc/WebViewToRNMessenger';
import './polyfills';
declare namespace ReactNativeWebView {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const postMessage: (data: any)=> void;
}
const restoreToolbarState = (toolbar: AbstractToolbar, state: string) => {
if (state) {
@@ -23,19 +21,13 @@ const restoreToolbarState = (toolbar: AbstractToolbar, state: string) => {
}
};
const listenToolbarState = (editor: Editor, toolbar: AbstractToolbar) => {
editor.notifier.on(EditorEventType.ToolUpdated, () => {
const state = toolbar.serializeState();
ReactNativeWebView.postMessage(
JSON.stringify({
action: 'save-toolbar',
data: state,
}),
);
});
export const createMessenger = () => {
const messenger = new WebViewToRNMessenger<ImageEditorControl, ImageEditorCallbacks>(
'image-editor', {},
);
return messenger;
};
export const createJsDrawEditor = (
callbacks: ImageEditorCallbacks,
initialToolbarState: string,
@@ -54,6 +46,38 @@ export const createJsDrawEditor = (
...defaultLocalizations,
},
iconProvider: new MaterialIconProvider(),
clipboardApi: {
read: async () => {
const result = new Map<string, string>();
const clipboardText = await callbacks.readClipboardText();
if (clipboardText) {
result.set('text/plain', clipboardText);
}
return result;
},
write: async (data) => {
const getTextForMime = async (mime: string) => {
const text = data.get(mime);
if (typeof text === 'string') {
return text;
}
if (text) {
return await (await text).text();
}
return null;
};
const svgData = await getTextForMime('image/svg+xml');
if (svgData) {
return callbacks.writeClipboardText(svgData);
}
const textData = await getTextForMime('text/plain');
return callbacks.writeClipboardText(textData);
},
},
...editorSettings,
});
@@ -95,11 +119,11 @@ export const createJsDrawEditor = (
return editor.toSVG({
// Grow small images to this minimum size
minDimension: 50,
});
}).outerHTML;
};
const saveNow = () => {
callbacks.saveDrawing(getEditorSVG(), false);
callbacks.save(getEditorSVG(), false);
// The image is now up-to-date with the resource
setImageHasChanges(false);
@@ -109,7 +133,9 @@ export const createJsDrawEditor = (
// Load and save toolbar-related state (e.g. pen sizes/colors).
restoreToolbarState(toolbar, initialToolbarState);
listenToolbarState(editor, toolbar);
editor.notifier.on(EditorEventType.ToolUpdated, () => {
callbacks.updateToolbarState(toolbar.serializeState());
});
setImageHasChanges(false);
@@ -171,7 +197,7 @@ export const createJsDrawEditor = (
// We can now edit and save safely (without data loss).
editor.setReadOnly(false);
void startAutosaveLoop(editor, callbacks.saveDrawing);
void startAutosaveLoop(editor, callbacks.save);
watchEditorForTemplateChanges(editor, templateData, callbacks.updateEditorTemplate);
},
onThemeUpdate: () => {
@@ -187,6 +213,8 @@ export const createJsDrawEditor = (
editorControl.onThemeUpdate();
callbacks.onLoadedEditor();
return editorControl;
};

View File

@@ -0,0 +1,11 @@
// .replaceChildren is not supported in Chromium 83, which is the default for Android 11
// (unless auto-updated from the Google Play store).
HTMLElement.prototype.replaceChildren ??= function(this: HTMLElement, ...nodes: Node[]) {
while (this.children.length) {
this.children[0].remove();
}
for (const node of nodes) {
this.appendChild(node);
}
};

View File

@@ -11,7 +11,7 @@ const startAutosaveLoop = async (
const createAutosave = async () => {
const savedSVG = await editor.toSVGAsync();
saveDrawing(savedSVG, true);
saveDrawing(savedSVG.outerHTML, true);
};
while (true) {

View File

@@ -1,16 +1,25 @@
export type SaveDrawingCallback = (svgElement: SVGElement, isAutosave: boolean)=> void;
export type SaveDrawingCallback = (svgData: string, isAutosave: boolean)=> void;
export type UpdateEditorTemplateCallback = (newTemplate: string)=> void;
export type UpdateToolbarCallback = (toolbarData: string)=> void;
export interface ImageEditorCallbacks {
saveDrawing: SaveDrawingCallback;
updateEditorTemplate: UpdateEditorTemplateCallback;
onLoadedEditor: ()=> void;
saveThenClose: (svgData: SVGElement)=> void;
save: SaveDrawingCallback;
updateEditorTemplate: UpdateEditorTemplateCallback;
updateToolbarState: UpdateToolbarCallback;
saveThenClose: (svgData: string)=> void;
closeEditor: (promptIfUnsaved: boolean)=> void;
setImageHasChanges: (hasChanges: boolean)=> void;
writeClipboardText: (text: string)=> Promise<void>;
readClipboardText: ()=> Promise<string>;
}
export interface ImageEditorControl {}
// Overrides translations in js-draw -- as of the time of this writing,
// Joplin has many common strings localized better than js-draw.
export interface LocalizedStrings {

View File

@@ -0,0 +1,63 @@
import { RefObject, useMemo } from 'react';
import { WebViewControl } from '../../../ExtendedWebView/types';
import { ImageEditorCallbacks, ImageEditorControl } from '../js-draw/types';
import Setting from '@joplin/lib/models/Setting';
import RNToWebViewMessenger from '../../../../utils/ipc/RNToWebViewMessenger';
import { writeAutosave } from '../autosave';
import Clipboard from '@react-native-clipboard/clipboard';
interface Props {
webviewRef: RefObject<WebViewControl>;
setImageChanged(changed: boolean): void;
onReadyToLoadData(): void;
onSave(data: string): void;
onRequestCloseEditor(promptIfUnsaved: boolean): void;
}
const useEditorMessenger = ({
webviewRef, setImageChanged, onReadyToLoadData, onRequestCloseEditor, onSave,
}: Props) => {
return useMemo(() => {
const localApi: ImageEditorCallbacks = {
updateEditorTemplate: newTemplate => {
Setting.setValue('imageeditor.imageTemplate', newTemplate);
},
updateToolbarState: newData => {
Setting.setValue('imageeditor.jsdrawToolbar', newData);
},
setImageHasChanges: hasChanges => {
setImageChanged(hasChanges);
},
onLoadedEditor: () => {
onReadyToLoadData();
},
saveThenClose: svgData => {
onSave(svgData);
onRequestCloseEditor(false);
},
save: (svgData, isAutosave) => {
if (isAutosave) {
return writeAutosave(svgData);
} else {
return onSave(svgData);
}
},
closeEditor: promptIfUnsaved => {
onRequestCloseEditor(promptIfUnsaved);
},
writeClipboardText: async text => {
Clipboard.setString(text);
},
readClipboardText: async () => {
return Clipboard.getString();
},
};
const messenger = new RNToWebViewMessenger<ImageEditorCallbacks, ImageEditorControl>(
'image-editor', webviewRef, localApi,
);
return messenger;
}, [webviewRef, setImageChanged, onReadyToLoadData, onRequestCloseEditor, onSave]);
};
export default useEditorMessenger;

View File

@@ -15,6 +15,7 @@ import { AppState } from '../../utils/types';
import usePrevious from '@joplin/lib/hooks/usePrevious';
import PlatformImplementation from '../../services/plugins/PlatformImplementation';
import AccessibleView from '../accessibility/AccessibleView';
import useOnDevPluginsUpdated from './utils/useOnDevPluginsUpdated';
const logger = Logger.create('PluginRunnerWebView');
@@ -29,20 +30,33 @@ const usePlugins = (
pluginRunner: PluginRunner,
webviewLoaded: boolean,
pluginSettings: PluginSettings,
pluginSupportEnabled: boolean,
devPluginPath: string,
) => {
const store = useStore<AppState>();
const lastPluginRunner = usePrevious(pluginRunner);
const [reloadCounter, setReloadCounter] = useState(0);
// Only set reloadAll to true here -- this ensures that all plugins are reloaded,
// even if loadPlugins is cancelled and re-run.
const reloadAllRef = useRef(false);
reloadAllRef.current ||= pluginRunner !== lastPluginRunner;
useOnDevPluginsUpdated(async (pluginId: string) => {
logger.info(`Dev plugin ${pluginId} updated. Reloading...`);
await PluginService.instance().unloadPlugin(pluginId);
setReloadCounter(counter => counter + 1);
}, devPluginPath, pluginSupportEnabled);
useAsyncEffect(async (event) => {
if (!webviewLoaded) {
return;
}
if (reloadCounter > 0) {
logger.debug('Reloading with counter set to', reloadCounter);
}
await loadPlugins({
pluginRunner,
pluginSettings,
@@ -56,7 +70,7 @@ const usePlugins = (
if (!event.cancelled) {
reloadAllRef.current = false;
}
}, [pluginRunner, store, webviewLoaded, pluginSettings]);
}, [pluginRunner, store, webviewLoaded, pluginSettings, reloadCounter]);
};
const useUnloadPluginsOnGlobalDisable = (
@@ -79,6 +93,7 @@ interface Props {
serializedPluginSettings: SerializedPluginSettings;
pluginSupportEnabled: boolean;
pluginStates: PluginStates;
devPluginPath: string;
pluginHtmlContents: PluginHtmlContents;
themeId: number;
}
@@ -98,7 +113,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
}, [webviewReloadCounter]);
const pluginSettings = usePluginSettings(props.serializedPluginSettings);
usePlugins(pluginRunner, webviewLoaded, pluginSettings);
usePlugins(pluginRunner, webviewLoaded, pluginSettings, props.pluginSupportEnabled, props.devPluginPath);
useUnloadPluginsOnGlobalDisable(props.pluginStates, props.pluginSupportEnabled);
const onLoadStart = useCallback(() => {
@@ -183,6 +198,7 @@ export default connect((state: AppState) => {
const result: Props = {
serializedPluginSettings: state.settings['plugins.states'],
pluginSupportEnabled: state.settings['plugins.pluginSupportEnabled'],
devPluginPath: state.settings['plugins.devPluginPaths'],
pluginStates: state.pluginService.plugins,
pluginHtmlContents: state.pluginService.pluginHtmlContents,
themeId: state.settings.theme,

View File

@@ -0,0 +1,60 @@
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
import shim from '@joplin/lib/shim';
import time from '@joplin/lib/time';
import { basename, join } from 'path';
import { useRef } from 'react';
type OnDevPluginChange = (id: string)=> void;
const useOnDevPluginsUpdated = (onDevPluginChange: OnDevPluginChange, devPluginPath: string, pluginSupportEnabled: boolean) => {
const onDevPluginChangeRef = useRef(onDevPluginChange);
onDevPluginChangeRef.current = onDevPluginChange;
const isFirstUpdateRef = useRef(true);
useAsyncEffect(async (event) => {
if (!devPluginPath || !pluginSupportEnabled) return;
const itemToLastModTime = new Map<string, number>();
// publishPath should point to the publish/ subfolder of a plugin's development
// directory.
const checkPluginChange = async (pluginPublishPath: string) => {
const dirStats = await shim.fsDriver().readDirStats(pluginPublishPath);
let hasChange = false;
let changedPluginId = '';
for (const item of dirStats) {
if (item.path.endsWith('.jpl')) {
const lastModTime = itemToLastModTime.get(item.path);
const modTime = item.mtime.getTime();
if (lastModTime === undefined || lastModTime < modTime) {
itemToLastModTime.set(item.path, modTime);
hasChange = true;
changedPluginId = basename(item.path, '.jpl');
break;
}
}
}
if (hasChange) {
if (isFirstUpdateRef.current) {
// Avoid sending an event the first time the hook is called. The first iteration
// collects initial timestamp information. In that case, hasChange
// will always be true, even with no plugin reload.
isFirstUpdateRef.current = false;
} else {
onDevPluginChangeRef.current(changedPluginId);
}
}
};
while (!event.cancelled) {
const publishFolder = join(devPluginPath, 'publish');
await checkPluginChange(publishFolder);
const pollingIntervalSeconds = 5;
await time.sleep(pollingIntervalSeconds);
}
}, [devPluginPath, pluginSupportEnabled]);
};
export default useOnDevPluginsUpdated;

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import { Platform, Linking, View, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService';
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
@@ -12,7 +12,6 @@ import { connect } from 'react-redux';
import ScreenHeader from '../../ScreenHeader';
import { _ } from '@joplin/lib/locale';
import BaseScreenComponent from '../../base-screen';
import { themeStyle } from '../../global-style';
import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
@@ -36,6 +35,8 @@ import EnablePluginSupportPage from './plugins/EnablePluginSupportPage';
import getVersionInfoText from '../../../utils/getVersionInfoText';
import JoplinCloudConfig, { emailToNoteDescription, emailToNoteLabel } from './JoplinCloudConfig';
import shim from '@joplin/lib/shim';
import SettingsToggle from './SettingsToggle';
import { UpdateSettingValueCallback } from './types';
interface ConfigScreenState {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
@@ -673,22 +674,16 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
);
}
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
private renderToggle(key: string, label: string, value: any, updateSettingValue: Function, descriptionComp: any = null) {
const theme = themeStyle(this.props.themeId);
return (
<View key={key}>
<View style={this.styles().getContainerStyle(false)}>
<Text key="label" style={this.styles().styleSheet.switchSettingText}>
{label}
</Text>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied */}
<Switch key="control" style={this.styles().styleSheet.switchSettingControl} trackColor={{ false: theme.dividerColor }} value={value} onValueChange={(value: any) => void updateSettingValue(key, value)} />
</View>
{descriptionComp}
</View>
);
private renderToggle(key: string, label: string, value: unknown, updateSettingValue: UpdateSettingValueCallback) {
return <SettingsToggle
key={key}
settingId={key}
value={value}
label={label}
updateSettingValue={updateSettingValue}
styles={this.styles()}
themeId={this.props.themeId}
/>;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied

View File

@@ -3,18 +3,23 @@ import * as React from 'react';
import shim from '@joplin/lib/shim';
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles';
import { View, Text } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import Setting, { SettingItem } from '@joplin/lib/models/Setting';
import { openDocumentTree } from '@joplin/react-native-saf-x';
import { UpdateSettingValueCallback } from './types';
import { reg } from '@joplin/lib/registry';
import type FsDriverWeb from '../../../utils/fs-driver/fs-driver-rn.web';
import { TouchableRipple } from 'react-native-paper';
import { IconButton, TouchableRipple } from 'react-native-paper';
import { _ } from '@joplin/lib/locale';
type Mode = 'read'|'readwrite';
interface Props {
themeId: number;
styles: ConfigScreenStyles;
settingMetadata: SettingItem;
mode: 'read'|'readwrite';
mode: Mode;
description: React.ReactNode|null;
updateSettingValue: UpdateSettingValueCallback;
}
@@ -23,30 +28,28 @@ type ExtendedSelf = (typeof window.self) & {
};
declare const self: ExtendedSelf;
const FileSystemPathSelector: FunctionComponent<Props> = props => {
const useFileSystemPath = (settingId: string, updateSettingValue: UpdateSettingValueCallback, accessMode: Mode) => {
const [fileSystemPath, setFileSystemPath] = useState<string>('');
const settingId = props.settingMetadata.key;
useEffect(() => {
setFileSystemPath(Setting.value(settingId));
}, [settingId]);
const selectDirectoryButtonPress = useCallback(async () => {
const showDirectoryPicker = useCallback(async () => {
if (shim.mobilePlatform() === 'web') {
// Directory picker IDs can't include certain characters.
const pickerId = `setting-${settingId}`.replace(/[^a-zA-Z]/g, '_');
const handle = await self.showDirectoryPicker({ id: pickerId, mode: props.mode });
const handle = await self.showDirectoryPicker({ id: pickerId, mode: accessMode });
const fsDriver = shim.fsDriver() as FsDriverWeb;
const uri = await fsDriver.mountExternalDirectory(handle, pickerId, props.mode);
await props.updateSettingValue(settingId, uri);
const uri = await fsDriver.mountExternalDirectory(handle, pickerId, accessMode);
await updateSettingValue(settingId, uri);
setFileSystemPath(uri);
} else {
try {
const doc = await openDocumentTree(true);
if (doc?.uri) {
setFileSystemPath(doc.uri);
await props.updateSettingValue(settingId, doc.uri);
await updateSettingValue(settingId, doc.uri);
} else {
throw new Error('User cancelled operation');
}
@@ -54,32 +57,78 @@ const FileSystemPathSelector: FunctionComponent<Props> = props => {
reg.logger().info('Didn\'t pick sync dir: ', e);
}
}
}, [props.updateSettingValue, settingId, props.mode]);
}, [updateSettingValue, settingId, accessMode]);
const clearPath = useCallback(() => {
setFileSystemPath('');
void updateSettingValue(settingId, '');
}, [updateSettingValue, settingId]);
// Supported on Android and some versions of Chrome
const supported = shim.fsDriver().isUsingAndroidSAF() || (shim.mobilePlatform() === 'web' && 'showDirectoryPicker' in self);
if (!supported) {
return null;
}
return { clearPath, showDirectoryPicker, fileSystemPath, supported };
};
const pathSelectorStyles = StyleSheet.create({
innerContainer: {
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
},
mainButton: {
flexGrow: 1,
flexShrink: 1,
paddingHorizontal: 16,
paddingVertical: 22,
margin: 0,
},
buttonContent: {
flexDirection: 'row',
},
});
const FileSystemPathSelector: FunctionComponent<Props> = props => {
const settingId = props.settingMetadata.key;
const { clearPath, showDirectoryPicker, fileSystemPath, supported } = useFileSystemPath(settingId, props.updateSettingValue, props.mode);
const styleSheet = props.styles.styleSheet;
return (
const clearButton = (
<IconButton
icon='delete'
accessibilityLabel={_('Clear')}
onPress={clearPath}
/>
);
const containerStyles = props.styles.getContainerStyle(!!props.description);
const control = <View style={[containerStyles.innerContainer, pathSelectorStyles.innerContainer]}>
<TouchableRipple
onPress={selectDirectoryButtonPress}
style={styleSheet.settingContainer}
onPress={showDirectoryPicker}
style={pathSelectorStyles.mainButton}
role='button'
>
<View style={styleSheet.settingContainer}>
<View style={pathSelectorStyles.buttonContent}>
<Text key="label" style={styleSheet.settingText}>
{props.settingMetadata.label()}
</Text>
<Text style={styleSheet.settingControl}>
<Text style={styleSheet.settingControl} numberOfLines={1}>
{fileSystemPath}
</Text>
</View>
</TouchableRipple>
);
{fileSystemPath ? clearButton : null}
</View>;
if (!supported) return null;
return <View style={containerStyles.outerContainer}>
{control}
{props.description}
</View>;
};
export default FileSystemPathSelector;

View File

@@ -38,7 +38,7 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
const styleSheet = props.styles.styleSheet;
const descriptionComp = !settingDescription ? null : <Text style={styleSheet.settingDescriptionText}>{settingDescription}</Text>;
const containerStyle = props.styles.getContainerStyle(!!settingDescription);
const containerStyles = props.styles.getContainerStyle(!!settingDescription);
const labelId = useId();
@@ -49,8 +49,8 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
const label = md.label();
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View style={containerStyle}>
<View key={props.settingId} style={containerStyles.outerContainer}>
<View style={containerStyles.innerContainer}>
<Text key="label" style={styleSheet.settingText}>
{label}
</Text>
@@ -125,17 +125,19 @@ const SettingComponent: React.FunctionComponent<Props> = props => {
if (['sync.2.path', 'plugins.devPluginPaths'].includes(md.key) && (shim.fsDriver().isUsingAndroidSAF() || shim.mobilePlatform() === 'web')) {
return (
<FileSystemPathSelector
themeId={props.themeId}
mode={md.key === 'sync.2.path' ? 'readwrite' : 'read'}
styles={props.styles}
settingMetadata={md}
updateSettingValue={props.updateSettingValue}
description={descriptionComp}
/>
);
}
return (
<View key={props.settingId} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
<View key={props.settingId} style={containerStyle}>
<View key={props.settingId} style={containerStyles.outerContainer}>
<View key={props.settingId} style={containerStyles.innerContainer}>
<Text key="label" style={styleSheet.settingText} nativeID={labelId}>
{md.label()}
</Text>

View File

@@ -24,9 +24,11 @@ const SettingsToggle: FunctionComponent<Props> = props => {
const theme = themeStyle(props.themeId);
const styleSheet = props.styles.styleSheet;
const containerStyles = props.styles.getContainerStyle(!!props.description);
return (
<View>
<View style={props.styles.getContainerStyle(false)}>
<View style={containerStyles.outerContainer}>
<View style={containerStyles.innerContainer}>
<Text key="label" style={styleSheet.switchSettingText}>
{props.label}
</Text>

View File

@@ -6,8 +6,11 @@ type SidebarButtonStyle = ViewStyle & { height: number };
export interface ConfigScreenStyleSheet {
body: ViewStyle;
settingOuterContainer: ViewStyle;
settingOuterContainerNoBorder: ViewStyle;
settingContainer: ViewStyle;
settingContainerNoBottomBorder: ViewStyle;
headerWrapperStyle: ViewStyle;
headerTextStyle: TextStyle;
@@ -39,12 +42,17 @@ export interface ConfigScreenStyleSheet {
settingControl: TextStyle;
}
interface ContainerStyles {
outerContainer: ViewStyle;
innerContainer: ViewStyle;
}
export interface ConfigScreenStyles {
styleSheet: ConfigScreenStyleSheet;
selectedSectionButtonColor: string;
keyboardAppearance: 'default'|'light'|'dark';
getContainerStyle(hasDescription: boolean): ViewStyle;
getContainerStyle(hasDescription: boolean): ContainerStyles;
}
const configScreenStyles = (themeId: number): ConfigScreenStyles => {
@@ -107,6 +115,14 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
justifyContent: 'flex-start',
flexDirection: 'column',
},
settingOuterContainer: {
flexDirection: 'column',
borderBottomWidth: 1,
borderBottomColor: theme.dividerColor,
},
settingOuterContainerNoBorder: {
flexDirection: 'column',
},
settingContainer: settingContainerStyle,
settingContainerNoBottomBorder: {
...settingContainerStyle,
@@ -229,7 +245,9 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
selectedSectionButtonColor: theme.selectedColor,
keyboardAppearance: theme.keyboardAppearance,
getContainerStyle: (hasDescription) => {
return !hasDescription ? styleSheet.settingContainer : styleSheet.settingContainerNoBottomBorder;
const outerContainer = hasDescription ? styleSheet.settingOuterContainer : styleSheet.settingOuterContainerNoBorder;
const innerContainer = hasDescription ? styleSheet.settingContainerNoBottomBorder : styleSheet.settingContainer;
return { outerContainer, innerContainer };
},
};
};

View File

@@ -92,12 +92,20 @@ const PluginChips: React.FC<Props> = props => {
return <PluginChip faded={true}>{_('Installed')}</PluginChip>;
};
const renderDevChip = () => {
if (!item.devMode) {
return null;
}
return <PluginChip faded={true}>{_('Dev')}</PluginChip>;
};
return <View style={containerStyle}>
{renderIncompatibleChip()}
{renderInstalledChip()}
{renderErrorsChip()}
{renderBuiltInChip()}
{renderUpdatableChip()}
{renderDevChip()}
{renderDisabledChip()}
</View>;
};

View File

@@ -203,7 +203,7 @@ const PluginInfoModalContent: React.FC<Props> = props => {
item={item}
type={ButtonType.Delete}
onPress={props.pluginCallbacks.onDelete}
disabled={item.builtIn || (item?.deleted ?? true)}
disabled={item.builtIn || item.devMode || (item?.deleted ?? true)}
title={item?.deleted ? _('Deleted') : _('Delete')}
/>
);

View File

@@ -91,7 +91,7 @@ const PluginUploadButton: React.FC<Props> = props => {
}, [props.pluginSettings, props.updatePluginStates]);
return (
<View style={props.styles.getContainerStyle(false)}>
<View style={props.styles.getContainerStyle(false).innerContainer}>
<TextButton
type={ButtonType.Primary}
onPress={onInstallFromFile}

View File

@@ -8,7 +8,7 @@ export interface CustomSettingSection {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
export type UpdateSettingValueCallback = (key: string, value: any)=> Promise<void>;
export type UpdateSettingValueCallback = (key: string, value: any)=> void|Promise<void>;
export interface PluginStatusRecord {
[pluginId: string]: boolean;

View File

@@ -29,6 +29,8 @@ import TestProviderStack from '../../testing/TestProviderStack';
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
import CommandService from '@joplin/lib/services/CommandService';
jest.retryTimes(2);
interface WrapperProps {
}

View File

@@ -11,6 +11,9 @@ import { AppState } from '../../utils/types';
import { connect } from 'react-redux';
import { View, StyleSheet } from 'react-native';
import AccessibleView from '../accessibility/AccessibleView';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('VoiceTypingDialog');
interface Props {
locale: string;
@@ -34,10 +37,11 @@ interface UseVoiceTypingProps {
onText: OnTextCallback;
}
const useWhisper = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingProps): [Error | null, boolean, VoiceTypingSession|null] => {
const useVoiceTyping = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingProps) => {
const [voiceTyping, setVoiceTyping] = useState<VoiceTypingSession>(null);
const [error, setError] = useState<Error>(null);
const [mustDownloadModel, setMustDownloadModel] = useState<boolean | null>(null);
const [modelIsOutdated, setModelIsOutdated] = useState(false);
const onTextRef = useRef(onText);
onTextRef.current = onText;
@@ -51,9 +55,20 @@ const useWhisper = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingPr
return new VoiceTyping(locale, provider?.startsWith('whisper') ? [whisper] : [vosk]);
}, [locale, provider]);
const [redownloadCounter, setRedownloadCounter] = useState(0);
useEffect(() => {
if (modelIsOutdated) {
logger.info('The downloaded version of the model is from an outdated URL.');
}
}, [modelIsOutdated]);
useAsyncEffect(async (event: AsyncEffectEvent) => {
try {
await voiceTypingRef.current?.stop();
onSetPreviewRef.current?.('');
setModelIsOutdated(await builder.isDownloadedFromOutdatedUrl());
if (!await builder.isDownloaded()) {
if (event.cancelled) return;
@@ -72,7 +87,7 @@ const useWhisper = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingPr
} finally {
setMustDownloadModel(false);
}
}, [builder]);
}, [builder, redownloadCounter]);
useAsyncEffect(async (_event: AsyncEffectEvent) => {
setMustDownloadModel(!(await builder.isDownloaded()));
@@ -82,7 +97,16 @@ const useWhisper = ({ locale, provider, onSetPreview, onText }: UseVoiceTypingPr
void voiceTypingRef.current?.stop();
}, []);
return [error, mustDownloadModel, voiceTyping];
const onRequestRedownload = useCallback(async () => {
await voiceTypingRef.current?.stop();
await builder.clearDownloads();
setMustDownloadModel(true);
setRedownloadCounter(value => value + 1);
}, [builder]);
return {
error, mustDownloadModel, voiceTyping, onRequestRedownload, modelIsOutdated,
};
};
const styles = StyleSheet.create({
@@ -112,7 +136,13 @@ const styles = StyleSheet.create({
const VoiceTypingDialog: React.FC<Props> = props => {
const [recorderState, setRecorderState] = useState<RecorderState>(RecorderState.Loading);
const [preview, setPreview] = useState<string>('');
const [modelError, mustDownloadModel, voiceTyping] = useWhisper({
const {
error: modelError,
mustDownloadModel,
voiceTyping,
onRequestRedownload,
modelIsOutdated,
} = useVoiceTyping({
locale: props.locale,
onSetPreview: setPreview,
onText: props.onText,
@@ -172,6 +202,11 @@ const VoiceTypingDialog: React.FC<Props> = props => {
return <Text variant='labelSmall'>{preview}</Text>;
};
const reDownloadButton = <Button onPress={onRequestRedownload}>
{modelIsOutdated ? _('Download updated model') : _('Re-download model')}
</Button>;
const allowReDownload = recorderState === RecorderState.Error || modelIsOutdated;
return (
<Surface>
<View style={styles.container}>
@@ -203,6 +238,7 @@ const VoiceTypingDialog: React.FC<Props> = props => {
</View>
</View>
<View style={styles.actionContainer}>
{allowReDownload ? reDownloadButton : null}
<Button
onPress={onDismiss}
accessibilityHint={_('Ends voice typing')}

View File

@@ -535,13 +535,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 130;
CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.2;
MARKETING_VERSION = 13.2.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -567,12 +567,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 130;
CURRENT_PROJECT_VERSION = 133;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 13.2.2;
MARKETING_VERSION = 13.2.5;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -758,14 +758,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 130;
CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.2;
MARKETING_VERSION = 13.2.5;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@@ -797,14 +797,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 130;
CURRENT_PROJECT_VERSION = 133;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 13.2.2;
MARKETING_VERSION = 13.2.5;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"$(inherited)",

View File

@@ -1,37 +1,116 @@
{
"images" : [
"images": [
{
"filename" : "ios_marketing1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"filename": "ios_marketing1024x1024.png",
"idiom": "ios-marketing",
"size": "1024x1024",
"scale": "1x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "ios_marketing_dark1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"filename": "iphone_notification20x20@2x.png",
"idiom": "iphone",
"size": "20x20",
"scale": "2x"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
"filename": "iphone_notification20x20@3x.png",
"idiom": "iphone",
"size": "20x20",
"scale": "3x"
},
{
"filename": "iphone_settings29x29@2x.png",
"idiom": "iphone",
"size": "29x29",
"scale": "2x"
},
{
"filename": "iphone_settings29x29@3x.png",
"idiom": "iphone",
"size": "29x29",
"scale": "3x"
},
{
"filename": "iphone_spotlight40x40@2x.png",
"idiom": "iphone",
"size": "40x40",
"scale": "2x"
},
{
"filename": "iphone_spotlight40x40@3x.png",
"idiom": "iphone",
"size": "40x40",
"scale": "3x"
},
{
"filename": "iphone_app60x60@2x.png",
"idiom": "iphone",
"size": "60x60",
"scale": "2x"
},
{
"filename": "iphone_app60x60@3x.png",
"idiom": "iphone",
"size": "60x60",
"scale": "3x"
},
{
"filename": "ipad_notification20x20.png",
"idiom": "ipad",
"size": "20x20",
"scale": "1x"
},
{
"filename": "ipad_notification20x20@2x.png",
"idiom": "ipad",
"size": "20x20",
"scale": "2x"
},
{
"filename": "ipad_settings29x29.png",
"idiom": "ipad",
"size": "29x29",
"scale": "1x"
},
{
"filename": "ipad_settings29x29@2x.png",
"idiom": "ipad",
"size": "29x29",
"scale": "2x"
},
{
"filename": "ipad_spotlight40x40.png",
"idiom": "ipad",
"size": "40x40",
"scale": "1x"
},
{
"filename": "ipad_spotlight40x40@2x.png",
"idiom": "ipad",
"size": "40x40",
"scale": "2x"
},
{
"filename": "ipad_app76x76.png",
"idiom": "ipad",
"size": "76x76",
"scale": "1x"
},
{
"filename": "ipad_app76x76@2x.png",
"idiom": "ipad",
"size": "76x76",
"scale": "2x"
},
{
"filename": "ipad_pro_app83.5x83.5@2x.png",
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
"info": {
"version": 1,
"author": "xcode"
}
}
}

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1337,7 +1337,7 @@ PODS:
- React-utils (= 0.74.1)
- rn-fetch-blob (0.12.0):
- React-Core
- RNCClipboard (1.14.1):
- RNCClipboard (1.14.2):
- React-Core
- RNCPushNotificationIOS (1.11.0):
- React-Core
@@ -1758,7 +1758,7 @@ SPEC CHECKSUMS:
React-utils: 3285151c9d1e3a28a9586571fc81d521678c196d
ReactCommon: f42444e384d82ab89184aed5d6f3142748b54768
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCClipboard: 0a720adef5ec193aa0e3de24c3977222c7e52a37
RNCClipboard: 5e503962f0719ace8f7fdfe9c60282b526305c85
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14
RNDeviceInfo: 59344c19152c4b2b32283005f9737c5c64b42fba

View File

@@ -91,7 +91,7 @@
"@babel/preset-env": "7.24.7",
"@babel/runtime": "7.24.7",
"@joplin/tools": "~3.2",
"@js-draw/material-icons": "1.20.3",
"@js-draw/material-icons": "1.26.0",
"@react-native/babel-preset": "0.74.86",
"@react-native/metro-config": "0.74.87",
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
@@ -116,7 +116,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0",
"js-draw": "1.20.3",
"js-draw": "1.26.0",
"jsdom": "24.1.1",
"nodemon": "3.1.7",
"punycode": "2.3.1",

View File

@@ -2,6 +2,7 @@ import shim from '@joplin/lib/shim';
import Logger from '@joplin/utils/Logger';
import { PermissionsAndroid, Platform } from 'react-native';
import unzip from './utils/unzip';
import { _ } from '@joplin/lib/locale';
const md5 = require('md5');
const logger = Logger.create('voiceTyping');
@@ -30,6 +31,7 @@ export interface VoiceTypingProvider {
modelName: string;
supported(): boolean;
modelLocalFilepath(locale: string): string;
deleteCachedModels(locale: string): Promise<void>;
getDownloadUrl(locale: string): string;
getUuidPath(locale: string): string;
build(options: BuildProviderOptions): Promise<VoiceTypingSession>;
@@ -39,9 +41,9 @@ export default class VoiceTyping {
private provider: VoiceTypingProvider|null = null;
public constructor(
private locale: string,
providers: VoiceTypingProvider[],
allProviders: VoiceTypingProvider[],
) {
this.provider = providers.find(p => p.supported()) ?? null;
this.provider = allProviders.find(p => p.supported()) ?? null;
}
public supported() {
@@ -67,10 +69,31 @@ export default class VoiceTyping {
);
}
public async isDownloadedFromOutdatedUrl() {
const uuidPath = this.getUuidPath();
if (!await shim.fsDriver().exists(uuidPath)) {
// Not downloaded at all
return false;
}
const modelUrl = this.provider.getDownloadUrl(this.locale);
const urlHash = await shim.fsDriver().readFile(uuidPath);
return urlHash.trim() !== md5(modelUrl);
}
public async isDownloaded() {
return await shim.fsDriver().exists(this.getUuidPath());
}
public async clearDownloads() {
const confirmed = await shim.showConfirmationDialog(
_('Delete model and re-download?\nThis cannot be undone.'),
);
if (confirmed) {
await this.provider.deleteCachedModels(this.locale);
}
}
public async download() {
const modelPath = this.getModelPath();
const modelUrl = this.provider.getDownloadUrl(this.locale);
@@ -104,16 +127,18 @@ export default class VoiceTyping {
logger.info(`Moving ${fullUnzipPath} => ${modelPath}`);
await shim.fsDriver().move(fullUnzipPath, modelPath);
await shim.fsDriver().writeFile(this.getUuidPath(), md5(modelUrl), 'utf8');
if (!await this.isDownloaded()) {
logger.warn('Model should be downloaded!');
}
} finally {
await shim.fsDriver().remove(unzipDir);
await shim.fsDriver().remove(downloadPath);
}
}
await shim.fsDriver().writeFile(this.getUuidPath(), md5(modelUrl), 'utf8');
if (!await this.isDownloaded()) {
logger.warn('Model should be downloaded!');
} else {
logger.info('Model stats', await shim.fsDriver().stat(modelPath));
}
}
public async build(callbacks: SpeechToTextCallbacks) {

View File

@@ -175,6 +175,10 @@ export const startRecording = (vosk: Vosk, options: StartOptions): VoiceTypingSe
const vosk: VoiceTypingProvider = {
supported: () => true,
modelLocalFilepath: (locale: string) => getModelDir(locale),
deleteCachedModels: async (locale: string) => {
const path = getModelDir(locale);
await shim.fsDriver().remove(path, { recursive: true });
},
getDownloadUrl: (locale) => languageModelUrl(locale),
getUuidPath: (locale: string) => join(getModelDir(locale), 'uuid'),
build: async ({ callbacks, locale, modelPath }) => {

View File

@@ -5,6 +5,7 @@ const vosk: VoiceTypingProvider = {
modelLocalFilepath: () => null,
getDownloadUrl: () => null,
getUuidPath: () => null,
deleteCachedModels: () => null,
build: async () => {
throw new Error('Unsupported!');
},

View File

@@ -106,6 +106,10 @@ const whisper: VoiceTypingProvider = {
return urlTemplate.replace(/\{task\}/g, 'whisper_tiny.onnx');
},
deleteCachedModels: async (locale) => {
await shim.fsDriver().remove(modelLocalFilepath());
await shim.fsDriver().remove(whisper.getUuidPath(locale));
},
getUuidPath: () => {
return join(dirname(modelLocalFilepath()), 'uuid');
},

View File

@@ -58,18 +58,18 @@ export default class BundledFile {
// Some libraries don't work with older browsers/WebViews.
// Because Babel transpilation can be slow, we only transpile
// these libraries.
// For now, it's just Replit's CodeMirror-vim library. This library
// uses `a?.b` syntax, which seems to be unsupported in iOS 12 Safari.
const moduleNeedsTranspilation = !!(/.*node_modules.*replit.*\.[mc]?js$/.exec(value));
const moduleNeedsTranspilation = !!(
// Replit's CodeMirror-vim library uses a?.b syntax which seems to be unsupported in iOS 12 Safari.
/.*node_modules.*replit.*\.[mc]?js$/.exec(value) ||
// js-draw uses a ??= b syntax, which is unsupported in old Android WebView versions
/.*node_modules.*js-draw.*\.[mc]?js$/.exec(value)
);
if (isModuleFile && !moduleNeedsTranspilation) {
return false;
}
const isJsFile = !!(/\.[cm]?js$/.exec(value));
if (isJsFile) {
console.log('Compiling with Babel:', value);
}
return isJsFile;
},
use: {

View File

@@ -7,6 +7,6 @@
"io.github.personalizedrefrigerator.js-draw": {
"cloneUrl": "https://github.com/personalizedrefrigerator/joplin-plugin-freehand-drawing.git",
"branch": "main",
"commit": "3e7eac96d10218728120ce81bee2eeffd5f8fdbb"
"commit": "9724793b4a6fb83346ff4f7c639af1e352bd7937"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.56",
"version": "4.1.57",
"author": "Felix Boehm <me@feedic.com>",
"publishConfig": {
"access": "public"

View File

@@ -2,7 +2,7 @@
"name": "@joplin/fork-sax",
"description": "An evented streaming XML parser in JavaScript",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"version": "1.2.60",
"version": "1.2.61",
"main": "lib/sax.js",
"publishConfig": {
"access": "public"

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