Compare commits
57 Commits
android-v3
...
cli-v3.2.3
Author | SHA1 | Date | |
---|---|---|---|
|
ca64451503 | ||
|
216b750a90 | ||
|
219d5bcae3 | ||
|
a4b1b9a2bf | ||
|
fc8ea6df0b | ||
|
2fba101333 | ||
|
35a0b22df2 | ||
|
e177bffb1c | ||
|
f95ca578c2 | ||
|
4bed47a1af | ||
|
5a0b0e6314 | ||
|
f119212068 | ||
|
cd12de78d6 | ||
|
6aa2c5f116 | ||
|
e287e5cbab | ||
|
d70a5b25a0 | ||
|
d2df7e6feb | ||
|
e9ee8c8419 | ||
|
8d2ae7e20e | ||
|
50d5843344 | ||
|
1fdc327977 | ||
|
c18ab5a7fb | ||
|
11216902d0 | ||
|
950ffef84d | ||
|
86e6445526 | ||
|
ab286b6da3 | ||
|
8c24928cf4 | ||
|
3952060dac | ||
|
877f39bb0e | ||
|
652812a15c | ||
|
597f3188bd | ||
|
d7d50f4373 | ||
|
83db585c0b | ||
|
d817ddd5c6 | ||
|
98fce34fe9 | ||
|
a81af0711c | ||
|
72575e3c6f | ||
|
e8f305dea5 | ||
|
e1e2ba8888 | ||
|
633d87ebfe | ||
|
a9e1be944f | ||
|
6048f9613c | ||
|
0a76494555 | ||
|
edbb6137ea | ||
|
4d216ef907 | ||
|
2f71c40ceb | ||
|
d3ea6fbe1d | ||
|
d45864888a | ||
|
0e92ab654a | ||
|
9e5c0ef3ce | ||
|
431cc15a51 | ||
|
82118810d9 | ||
|
bacaf800f2 | ||
|
4d827afccb | ||
|
e70efcbd60 | ||
|
ac154ee1e8 | ||
|
6220267abb |
@@ -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
@@ -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
|
||||
|
@@ -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."
|
||||
+
|
||||
|
@@ -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 |
BIN
Assets/WebsiteAssets/images/sponsors/WebDesignAgency.png
Normal file
After Width: | Height: | Size: 138 KiB |
@@ -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
|
||||
|
@@ -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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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&mtm_kwd=joplinapp&mtm_source=joplinapp-webseite&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 -->
|
||||
|
||||
|
@@ -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('');
|
||||
|
@@ -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();
|
||||
|
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.3",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
2
packages/app-cli/tests/md_to_html/sanitize_22.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<img src="test/" class="jop-noMdConv"/>
|
||||
<img src="http://example.com/test.png" class="jop-noMdConv"/>
|
3
packages/app-cli/tests/md_to_html/sanitize_22.md
Normal file
@@ -0,0 +1,3 @@
|
||||
<img name=getElementById src=test/>
|
||||
|
||||
<IMG NAME="getElementById" SRC="http://example.com/test.png">
|
@@ -0,0 +1 @@
|
||||

|
@@ -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: {},
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 = () => {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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 () => {};
|
||||
|
@@ -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'],
|
||||
|
@@ -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";
|
||||
|
@@ -0,0 +1,7 @@
|
||||
|
||||
.revision-viewer-root {
|
||||
background-color: var(--joplin-background-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
43
packages/app-desktop/gui/NoteEditor/utils/useMarkupToHtml.js
Normal 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
|
@@ -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;
|
@@ -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(() => {
|
||||
|
@@ -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'],
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
@@ -91,6 +91,7 @@ function FolderItem(props: FolderItemProps) {
|
||||
isConflictFolder={folderId === Folder.conflictFolderId()}
|
||||
selected={selected}
|
||||
shareId={shareId}
|
||||
data-folder-id={folderId}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
|
||||
onClick={() => {
|
||||
|
@@ -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);
|
||||
|
@@ -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',
|
||||
|
@@ -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]);
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
@@ -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();
|
||||
});
|
||||
});
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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'}
|
||||
|
@@ -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 ++;
|
||||
}
|
||||
|
@@ -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;
|
||||
};
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
@@ -11,7 +11,7 @@ const startAutosaveLoop = async (
|
||||
|
||||
const createAutosave = async () => {
|
||||
const savedSVG = await editor.toSVGAsync();
|
||||
saveDrawing(savedSVG, true);
|
||||
saveDrawing(savedSVG.outerHTML, true);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
@@ -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,
|
||||
|
@@ -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;
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -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>;
|
||||
};
|
||||
|
@@ -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')}
|
||||
/>
|
||||
);
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
}
|
||||
|
||||
|
@@ -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')}
|
||||
|
@@ -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)",
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
0
packages/app-mobile/ios/Joplin/Images.xcassets/AppIcon.appiconset/ios_marketing1024x1024.png
Normal file → Executable file
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 973 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5.1 KiB |
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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) {
|
||||
|
@@ -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 }) => {
|
||||
|
@@ -5,6 +5,7 @@ const vosk: VoiceTypingProvider = {
|
||||
modelLocalFilepath: () => null,
|
||||
getDownloadUrl: () => null,
|
||||
getUuidPath: () => null,
|
||||
deleteCachedModels: () => null,
|
||||
build: async () => {
|
||||
throw new Error('Unsupported!');
|
||||
},
|
||||
|
@@ -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');
|
||||
},
|
||||
|
@@ -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: {
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|