You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-29 23:48:19 +02:00
Compare commits
1 Commits
server_pop
...
random_ci_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15287f7ffe |
@@ -38,10 +38,6 @@ packages/app-clipper/popup/config/webpack.config.js
|
||||
packages/app-clipper/popup/node_modules
|
||||
packages/app-clipper/popup/scripts/build.js
|
||||
packages/app-desktop/build/
|
||||
packages/app-desktop/test-results/
|
||||
packages/app-desktop/playwright-report/
|
||||
packages/app-desktop/playwright/.cache/
|
||||
packages/app-desktop/integration-tests/test-profile/
|
||||
packages/app-desktop/dist
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/supportedLocales.js
|
||||
@@ -51,7 +47,7 @@ packages/app-desktop/packageInfo.js
|
||||
packages/app-desktop/services/electron-context-menu.js
|
||||
packages/app-desktop/vendor/lib/
|
||||
packages/app-mobile/android
|
||||
packages/app-mobile/components/NoteEditor/**/*.bundle.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
|
||||
packages/app-mobile/ios
|
||||
packages/app-mobile/lib/rnInjectedJs/
|
||||
packages/app-mobile/locales
|
||||
@@ -375,13 +371,6 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -424,24 +413,12 @@ packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
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/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/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
@@ -473,7 +450,6 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
@@ -492,7 +468,6 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
@@ -516,7 +491,6 @@ packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
@@ -586,7 +560,6 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -815,7 +788,6 @@ packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
packages/lib/services/rest/routes/revisions.js
|
||||
packages/lib/services/rest/routes/search.js
|
||||
packages/lib/services/rest/routes/tags.js
|
||||
packages/lib/services/rest/utils/collectionToPaginatedResults.js
|
||||
@@ -967,7 +939,6 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
||||
2
.github/scripts/run_ci.sh
vendored
2
.github/scripts/run_ci.sh
vendored
@@ -86,7 +86,7 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
|
||||
# Allocation failed - JavaScript heap out of memory
|
||||
#
|
||||
# https://stackoverflow.com/questions/38558989
|
||||
export NODE_OPTIONS="--max-old-space-size=32768"
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
yarn run test-ci
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
|
||||
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: contributor-assistant/github-action@v2.3.1
|
||||
uses: contributor-assistant/github-action@v2.3.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
|
||||
9
.github/workflows/github-actions-main.yml
vendored
9
.github/workflows/github-actions-main.yml
vendored
@@ -3,8 +3,6 @@ on: [push, pull_request]
|
||||
jobs:
|
||||
pre_job:
|
||||
if: github.repository == 'laurent22/joplin'
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
should_skip: ${{ steps.skip_check.outputs.should_skip }}
|
||||
@@ -21,8 +19,6 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [macos-latest, ubuntu-20.04, windows-2019]
|
||||
steps:
|
||||
|
||||
@@ -55,9 +51,6 @@ jobs:
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
sudo apt-get install -y translate-toolkit
|
||||
sudo apt-get install -y rsync
|
||||
# Provides a virtual display on Linux. Used for Playwright integration
|
||||
# testing.
|
||||
sudo apt-get install -y xvfb
|
||||
|
||||
- name: Install Docker Engine
|
||||
# if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags/server-v')
|
||||
@@ -146,8 +139,6 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Do not use unbuntu-latest because it causes `The operation was canceled` failures:
|
||||
# https://github.com/actions/runner-images/issues/6709
|
||||
os: [ubuntu-20.04]
|
||||
steps:
|
||||
|
||||
|
||||
25
.gitignore
vendored
25
.gitignore
vendored
@@ -357,13 +357,6 @@ packages/app-desktop/gui/style/StyledTextInput.js
|
||||
packages/app-desktop/gui/utils/NoteListUtils.js
|
||||
packages/app-desktop/gui/utils/convertToScreenCoordinates.js
|
||||
packages/app-desktop/gui/utils/loadScript.js
|
||||
packages/app-desktop/integration-tests/main.spec.js
|
||||
packages/app-desktop/integration-tests/models/MainScreen.js
|
||||
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
|
||||
packages/app-desktop/integration-tests/models/SettingsScreen.js
|
||||
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
|
||||
packages/app-desktop/integration-tests/util/test.js
|
||||
packages/app-desktop/playwright.config.js
|
||||
packages/app-desktop/plugins/GotoAnything.js
|
||||
packages/app-desktop/services/bridge.js
|
||||
packages/app-desktop/services/commands/stateToWhenClauseContext.js
|
||||
@@ -406,24 +399,12 @@ packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/FolderPicker.js
|
||||
packages/app-mobile/components/Modal.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.test.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useEditPopup.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnMessage.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js
|
||||
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/ImageEditor.js
|
||||
packages/app-mobile/components/NoteEditor/ImageEditor/autosave.js
|
||||
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/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/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
@@ -455,7 +436,6 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExport
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/NoteExportButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/exportAllFolders.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/LogScreen.js
|
||||
packages/app-mobile/components/screens/Note.js
|
||||
packages/app-mobile/components/screens/Notes.js
|
||||
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
|
||||
@@ -474,7 +454,6 @@ packages/app-mobile/services/voiceTyping/vosk.ios.js
|
||||
packages/app-mobile/setupQuickActions.js
|
||||
packages/app-mobile/tools/buildInjectedJs.js
|
||||
packages/app-mobile/utils/ShareExtension.js
|
||||
packages/app-mobile/utils/ShareUtils.test.js
|
||||
packages/app-mobile/utils/ShareUtils.js
|
||||
packages/app-mobile/utils/TlsUtils.js
|
||||
packages/app-mobile/utils/autodetectTheme.js
|
||||
@@ -498,7 +477,6 @@ packages/editor/CodeMirror/editorCommands/editorCommands.js
|
||||
packages/editor/CodeMirror/editorCommands/supportsCommand.js
|
||||
packages/editor/CodeMirror/editorCommands/swapLine.js
|
||||
packages/editor/CodeMirror/getScrollFraction.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.test.js
|
||||
packages/editor/CodeMirror/markdown/computeSelectionFormatting.js
|
||||
packages/editor/CodeMirror/markdown/decoratorExtension.js
|
||||
packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
|
||||
@@ -568,7 +546,6 @@ packages/lib/WelcomeUtils.js
|
||||
packages/lib/array.js
|
||||
packages/lib/callbackUrlUtils.test.js
|
||||
packages/lib/callbackUrlUtils.js
|
||||
packages/lib/clipperUtils.js
|
||||
packages/lib/commands/historyBackward.js
|
||||
packages/lib/commands/historyForward.js
|
||||
packages/lib/commands/index.js
|
||||
@@ -797,7 +774,6 @@ packages/lib/services/rest/routes/master_keys.js
|
||||
packages/lib/services/rest/routes/notes.js
|
||||
packages/lib/services/rest/routes/ping.js
|
||||
packages/lib/services/rest/routes/resources.js
|
||||
packages/lib/services/rest/routes/revisions.js
|
||||
packages/lib/services/rest/routes/search.js
|
||||
packages/lib/services/rest/routes/tags.js
|
||||
packages/lib/services/rest/utils/collectionToPaginatedResults.js
|
||||
@@ -949,7 +925,6 @@ packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.js
|
||||
packages/tools/build-translation.js
|
||||
packages/tools/build-welcome.js
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.js
|
||||
|
||||
72
README.md
72
README.md
@@ -537,47 +537,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 21%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 92%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 42%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 77%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 22%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 56%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 44%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 43%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 93%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 24%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 25%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 27%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 74%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 83%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 52%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Fernando Nagase](mailto:nagase.fernando@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 68%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 48%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 76%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 28%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 53%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 70%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 49%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 34%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 73%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 35%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 94%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Титан](mailto:fignin@ya.ru) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 61%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [qx100](mailto:ztymaxwell@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 84%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [qx100](mailto:ztymaxwell@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 86%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
10
package.json
10
package.json
@@ -68,8 +68,8 @@
|
||||
"devDependencies": {
|
||||
"@joplin/utils": "~2.12",
|
||||
"@seiyab/eslint-plugin-react-hooks": "4.5.1-beta.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||
"@typescript-eslint/parser": "6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"cspell": "5.21.2",
|
||||
"eslint": "8.47.0",
|
||||
"eslint-interactive": "10.8.0",
|
||||
@@ -79,7 +79,7 @@
|
||||
"eslint-plugin-react": "7.33.2",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.4",
|
||||
"gulp": "4.0.2",
|
||||
"husky": "3.1.0",
|
||||
"lerna": "3.22.1",
|
||||
@@ -89,11 +89,11 @@
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"eslint-plugin-github": "4.9.2",
|
||||
"http-server": "14.1.1",
|
||||
"node-gyp": "9.4.0",
|
||||
"nodemon": "3.0.1"
|
||||
"nodemon": "2.0.22"
|
||||
},
|
||||
"packageManager": "yarn@3.6.3",
|
||||
"resolutions": {
|
||||
|
||||
@@ -50,9 +50,6 @@ class Command extends BaseCommand {
|
||||
{
|
||||
type: BaseModel.TYPE_TAG,
|
||||
},
|
||||
{
|
||||
type: BaseModel.TYPE_REVISION,
|
||||
},
|
||||
];
|
||||
|
||||
const lines = [];
|
||||
@@ -414,7 +411,7 @@ async function fetchAllNotes() {
|
||||
lines.push('');
|
||||
lines.push('If no `cursor` property is provided, the API will respond with the latest change ID. That can be used to retrieve future events later on.');
|
||||
lines.push('');
|
||||
lines.push('The results are paginated so you may need multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
|
||||
lines.push('The results are paginated so will need to may multiple calls to retrieve all the events. Use the `has_more` property to know if more can be retrieved.');
|
||||
lines.push('');
|
||||
lines.push('## GET /events/:id');
|
||||
lines.push('');
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.32.6",
|
||||
"sprintf-js": "1.1.3",
|
||||
"sharp": "0.32.5",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
"strip-ansi": "6.0.1",
|
||||
@@ -71,9 +71,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~2.13",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/node": "18.17.14",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
|
||||
<en-export export-date="20230724T173816Z" application="Evernote" version="10.58.8">
|
||||
<note>
|
||||
<title>Boomwhackers - Rio</title>
|
||||
<created>20230412T112328Z</created>
|
||||
<updated>20230524T133616Z</updated>
|
||||
<note-attributes>
|
||||
</note-attributes>
|
||||
<content>
|
||||
<![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note><div><br/></div><en-media hash="b687cb1ab2d8f3f10b95c3a500841c66" type="application/zip" /><div><br/></div></en-note> ]]>
|
||||
</content>
|
||||
<resource>
|
||||
<data encoding="base64">
|
||||
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
|
||||
</data>
|
||||
<mime>application/zip</mime>
|
||||
<resource-attributes>
|
||||
<file-name>Boomwhackers - Rio.mscz</file-name>
|
||||
<source-url>en-cache://tokenKey%3D%22AuthToken%3AUser%3A75937676%22+32040eaf-3c97-1f46-f34c-1d4e8608c682+b687cb1ab2d8f3f10b95c3a500841c66+https://www.evernote.com/shard/s438/res/9521d2af-a8de-26f3-4f86-8f7f4deebb6b</source-url>
|
||||
</resource-attributes>
|
||||
</resource>
|
||||
</note>
|
||||
</en-export>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Example
|
||||
---
|
||||
|
||||
|
||||
|
||||
note body
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: Example
|
||||
---
|
||||
note body
|
||||
@@ -1,5 +1,6 @@
|
||||
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
|
||||
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
|
||||
|
||||
/* eslint-env es6:false */
|
||||
/*
|
||||
* Copyright (c) 2010 Arc90 Inc
|
||||
*
|
||||
@@ -54,7 +55,7 @@ var REGEXPS = {
|
||||
var defaultOptions = { minScore: 20, minContentLength: 140, visibilityChecker: isNodeVisible };
|
||||
options = Object.assign(defaultOptions, options);
|
||||
|
||||
var nodes = doc.querySelectorAll("p, pre, article");
|
||||
var nodes = doc.querySelectorAll("p, pre");
|
||||
|
||||
// Get <div> nodes which have <br> node(s) and append them into the `nodes` variable.
|
||||
// Some articles' DOM structures might look like
|
||||
@@ -105,6 +106,5 @@ var REGEXPS = {
|
||||
}
|
||||
|
||||
if (typeof module === "object") {
|
||||
/* global module */
|
||||
module.exports = isProbablyReaderable;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// v0.4.4 - https://github.com/mozilla/readability/commit/49d345a455da1f4aa93f8b41e0f50422f9959c7c
|
||||
// v0.4.1 - https://github.com/mozilla/readability/commit/28843b6de84447dd6cef04058fda336938e628dc
|
||||
|
||||
/*eslint-env es6:false*/
|
||||
/*
|
||||
* Copyright (c) 2010 Arc90 Inc
|
||||
*
|
||||
@@ -26,7 +27,7 @@
|
||||
* @param {HTMLDocument} doc The document to parse.
|
||||
* @param {Object} options The options object.
|
||||
*/
|
||||
function Readability(doc, options) {
|
||||
function Readability(doc, options) {
|
||||
// In some older versions, people passed a URI as the first argument. Cope:
|
||||
if (options && options.documentElement) {
|
||||
doc = options;
|
||||
@@ -55,7 +56,6 @@ function Readability(doc, options) {
|
||||
return el.innerHTML;
|
||||
};
|
||||
this._disableJSONLD = !!options.disableJSONLD;
|
||||
this._allowedVideoRegex = options.allowedVideoRegex || this.REGEXPS.videos;
|
||||
|
||||
// Start with all flags set
|
||||
this._flags = this.FLAG_STRIP_UNLIKELYS |
|
||||
@@ -75,7 +75,12 @@ function Readability(doc, options) {
|
||||
return `<${node.localName} ${attrPairs}>`;
|
||||
};
|
||||
this.log = function () {
|
||||
if (typeof console !== "undefined") {
|
||||
if (typeof dump !== "undefined") {
|
||||
var msg = Array.prototype.map.call(arguments, function(x) {
|
||||
return (x && x.nodeName) ? logNode(x) : x;
|
||||
}).join(" ");
|
||||
dump("Reader: (Readability) " + msg + "\n");
|
||||
} else if (typeof console !== "undefined") {
|
||||
let args = Array.from(arguments, arg => {
|
||||
if (arg && arg.nodeType == this.ELEMENT_NODE) {
|
||||
return logNode(arg);
|
||||
@@ -84,12 +89,6 @@ function Readability(doc, options) {
|
||||
});
|
||||
args.unshift("Reader: (Readability)");
|
||||
console.log.apply(console, args);
|
||||
} else if (typeof dump !== "undefined") {
|
||||
/* global dump */
|
||||
var msg = Array.prototype.map.call(arguments, function(x) {
|
||||
return (x && x.nodeName) ? logNode(x) : x;
|
||||
}).join(" ");
|
||||
dump("Reader: (Readability) " + msg + "\n");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
@@ -143,9 +142,6 @@ function Readability(doc, options) {
|
||||
hashUrl: /^#.+/,
|
||||
srcsetUrl: /(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,
|
||||
b64DataUrl: /^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,
|
||||
// Commas as used in Latin, Sindhi, Chinese and various other scripts.
|
||||
// see: https://en.wikipedia.org/wiki/Comma#Comma_variants
|
||||
commas: /\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,
|
||||
// See: https://schema.org/Article
|
||||
jsonLdArticleTypes: /^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/
|
||||
},
|
||||
@@ -902,11 +898,6 @@ function Readability(doc, options) {
|
||||
let shouldRemoveTitleHeader = true;
|
||||
|
||||
while (node) {
|
||||
|
||||
if (node.tagName === "HTML") {
|
||||
this._articleLang = node.getAttribute("lang");
|
||||
}
|
||||
|
||||
var matchString = node.className + " " + node.id;
|
||||
|
||||
if (!this._isProbablyVisible(node)) {
|
||||
@@ -915,12 +906,6 @@ function Readability(doc, options) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// User is not able to see elements applied with both "aria-modal = true" and "role = dialog"
|
||||
if (node.getAttribute("aria-modal") == "true" && node.getAttribute("role") == "dialog") {
|
||||
node = this._removeAndGetNext(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check to see if this node is a byline, and remove it if it is.
|
||||
if (this._checkByline(node, matchString)) {
|
||||
node = this._removeAndGetNext(node);
|
||||
@@ -1035,7 +1020,7 @@ function Readability(doc, options) {
|
||||
contentScore += 1;
|
||||
|
||||
// Add points for any commas within this paragraph.
|
||||
contentScore += innerText.split(this.REGEXPS.commas).length;
|
||||
contentScore += innerText.split(",").length;
|
||||
|
||||
// For every 100 characters in this paragraph, add another point. Up to 3 points.
|
||||
contentScore += Math.min(Math.floor(innerText.length / 100), 3);
|
||||
@@ -1374,88 +1359,72 @@ function Readability(doc, options) {
|
||||
_getJSONLD: function (doc) {
|
||||
var scripts = this._getAllNodesWithTag(doc, ["script"]);
|
||||
|
||||
var metadata;
|
||||
|
||||
this._forEachNode(scripts, function(jsonLdElement) {
|
||||
if (!metadata && jsonLdElement.getAttribute("type") === "application/ld+json") {
|
||||
try {
|
||||
// Strip CDATA markers if present
|
||||
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
|
||||
var parsed = JSON.parse(content);
|
||||
if (
|
||||
!parsed["@context"] ||
|
||||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
|
||||
parsed = parsed["@graph"].find(function(it) {
|
||||
return (it["@type"] || "").match(
|
||||
this.REGEXPS.jsonLdArticleTypes
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
!parsed["@type"] ||
|
||||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadata = {};
|
||||
|
||||
if (typeof parsed.name === "string" && typeof parsed.headline === "string" && parsed.name !== parsed.headline) {
|
||||
// we have both name and headline element in the JSON-LD. They should both be the same but some websites like aktualne.cz
|
||||
// put their own name into "name" and the article title to "headline" which confuses Readability. So we try to check if either
|
||||
// "name" or "headline" closely matches the html title, and if so, use that one. If not, then we use "name" by default.
|
||||
|
||||
var title = this._getArticleTitle();
|
||||
var nameMatches = this._textSimilarity(parsed.name, title) > 0.75;
|
||||
var headlineMatches = this._textSimilarity(parsed.headline, title) > 0.75;
|
||||
|
||||
if (headlineMatches && !nameMatches) {
|
||||
metadata.title = parsed.headline;
|
||||
} else {
|
||||
metadata.title = parsed.name;
|
||||
}
|
||||
} else if (typeof parsed.name === "string") {
|
||||
metadata.title = parsed.name.trim();
|
||||
} else if (typeof parsed.headline === "string") {
|
||||
metadata.title = parsed.headline.trim();
|
||||
}
|
||||
if (parsed.author) {
|
||||
if (typeof parsed.author.name === "string") {
|
||||
metadata.byline = parsed.author.name.trim();
|
||||
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
|
||||
metadata.byline = parsed.author
|
||||
.filter(function(author) {
|
||||
return author && typeof author.name === "string";
|
||||
})
|
||||
.map(function(author) {
|
||||
return author.name.trim();
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
if (typeof parsed.description === "string") {
|
||||
metadata.excerpt = parsed.description.trim();
|
||||
}
|
||||
if (
|
||||
parsed.publisher &&
|
||||
typeof parsed.publisher.name === "string"
|
||||
) {
|
||||
metadata.siteName = parsed.publisher.name.trim();
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
this.log(err.message);
|
||||
}
|
||||
}
|
||||
var jsonLdElement = this._findNode(scripts, function(el) {
|
||||
return el.getAttribute("type") === "application/ld+json";
|
||||
});
|
||||
return metadata ? metadata : {};
|
||||
|
||||
if (jsonLdElement) {
|
||||
try {
|
||||
// Strip CDATA markers if present
|
||||
var content = jsonLdElement.textContent.replace(/^\s*<!\[CDATA\[|\]\]>\s*$/g, "");
|
||||
var parsed = JSON.parse(content);
|
||||
var metadata = {};
|
||||
if (
|
||||
!parsed["@context"] ||
|
||||
!parsed["@context"].match(/^https?\:\/\/schema\.org$/)
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (!parsed["@type"] && Array.isArray(parsed["@graph"])) {
|
||||
parsed = parsed["@graph"].find(function(it) {
|
||||
return (it["@type"] || "").match(
|
||||
this.REGEXPS.jsonLdArticleTypes
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!parsed ||
|
||||
!parsed["@type"] ||
|
||||
!parsed["@type"].match(this.REGEXPS.jsonLdArticleTypes)
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
if (typeof parsed.name === "string") {
|
||||
metadata.title = parsed.name.trim();
|
||||
} else if (typeof parsed.headline === "string") {
|
||||
metadata.title = parsed.headline.trim();
|
||||
}
|
||||
if (parsed.author) {
|
||||
if (typeof parsed.author.name === "string") {
|
||||
metadata.byline = parsed.author.name.trim();
|
||||
} else if (Array.isArray(parsed.author) && parsed.author[0] && typeof parsed.author[0].name === "string") {
|
||||
metadata.byline = parsed.author
|
||||
.filter(function(author) {
|
||||
return author && typeof author.name === "string";
|
||||
})
|
||||
.map(function(author) {
|
||||
return author.name.trim();
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
if (typeof parsed.description === "string") {
|
||||
metadata.excerpt = parsed.description.trim();
|
||||
}
|
||||
if (
|
||||
parsed.publisher &&
|
||||
typeof parsed.publisher.name === "string"
|
||||
) {
|
||||
metadata.siteName = parsed.publisher.name.trim();
|
||||
}
|
||||
return metadata;
|
||||
} catch (err) {
|
||||
this.log(err.message);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1654,7 +1623,12 @@ function Readability(doc, options) {
|
||||
* @param Element
|
||||
**/
|
||||
_removeScripts: function(doc) {
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["script", "noscript"]));
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["script"]), function(scriptNode) {
|
||||
scriptNode.nodeValue = "";
|
||||
scriptNode.removeAttribute("src");
|
||||
return true;
|
||||
});
|
||||
this._removeNodes(this._getAllNodesWithTag(doc, ["noscript"]));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1844,13 +1818,13 @@ function Readability(doc, options) {
|
||||
if (isEmbed) {
|
||||
// First, check the elements attributes to see if any of them contain youtube or vimeo
|
||||
for (var i = 0; i < element.attributes.length; i++) {
|
||||
if (this._allowedVideoRegex.test(element.attributes[i].value)) {
|
||||
if (this.REGEXPS.videos.test(element.attributes[i].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For embed with <object> tag, check inner HTML as well.
|
||||
if (element.tagName === "object" && this._allowedVideoRegex.test(element.innerHTML)) {
|
||||
if (element.tagName === "object" && this.REGEXPS.videos.test(element.innerHTML)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2119,13 +2093,13 @@ function Readability(doc, options) {
|
||||
for (var i = 0; i < embeds.length; i++) {
|
||||
// If this embed has attribute that matches video regex, don't delete it.
|
||||
for (var j = 0; j < embeds[i].attributes.length; j++) {
|
||||
if (this._allowedVideoRegex.test(embeds[i].attributes[j].value)) {
|
||||
if (this.REGEXPS.videos.test(embeds[i].attributes[j].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For embed with <object> tag, check inner HTML as well.
|
||||
if (embeds[i].tagName === "object" && this._allowedVideoRegex.test(embeds[i].innerHTML)) {
|
||||
if (embeds[i].tagName === "object" && this.REGEXPS.videos.test(embeds[i].innerHTML)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2143,21 +2117,6 @@ function Readability(doc, options) {
|
||||
(!isList && weight < 25 && linkDensity > 0.2) ||
|
||||
(weight >= 25 && linkDensity > 0.5) ||
|
||||
((embedCount === 1 && contentLength < 75) || embedCount > 1);
|
||||
// Allow simple lists of images to remain in pages
|
||||
if (isList && haveToRemove) {
|
||||
for (var x = 0; x < node.children.length; x++) {
|
||||
let child = node.children[x];
|
||||
// Don't filter in lists with li's that contain more than one child
|
||||
if (child.children.length > 1) {
|
||||
return haveToRemove;
|
||||
}
|
||||
}
|
||||
let li_count = node.getElementsByTagName("li").length;
|
||||
// Only allow the list to remain if every li contains an image
|
||||
if (img == li_count) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return haveToRemove;
|
||||
}
|
||||
return false;
|
||||
@@ -2290,7 +2249,6 @@ function Readability(doc, options) {
|
||||
title: this._articleTitle,
|
||||
byline: metadata.byline || this._articleByline,
|
||||
dir: this._articleDir,
|
||||
lang: this._articleLang,
|
||||
content: this._serializer(articleContent),
|
||||
textContent: textContent,
|
||||
length: textContent.length,
|
||||
@@ -2301,6 +2259,5 @@ function Readability(doc, options) {
|
||||
};
|
||||
|
||||
if (typeof module === "object") {
|
||||
/* global module */
|
||||
module.exports = Readability;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.getStyleSheets = exports.getImageSizes = void 0;
|
||||
function absoluteUrl(url) {
|
||||
if (!url) { return url; }
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) { return url; }
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
const output2 = output.split('/');
|
||||
output2.pop();
|
||||
output = output2.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) { return className; }
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
if (!output[src]) { output[src] = []; }
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) { continue; }
|
||||
const className = getJoplinClipperSvgClassName(svg); // 'joplin-clipper-svg-' + i;
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
const rect = svg.getBoundingClientRect();
|
||||
if (!output[className]) { output[className] = []; }
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getImageSizes = getImageSizes;
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) { return image.currentSrc; }
|
||||
return image.src;
|
||||
}
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
// eslint-disable-next-line
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
// eslint-disable-next-line
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
// eslint-disable-next-line
|
||||
console.info('It will downloaded by the main application.');
|
||||
// eslint-disable-next-line
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
exports.getStyleSheets = getStyleSheets;
|
||||
// # sourceMappingURL=clipperUtils.js.map
|
||||
@@ -20,6 +20,20 @@
|
||||
browserSupportsPromises_ = false;
|
||||
}
|
||||
|
||||
function absoluteUrl(url) {
|
||||
if (!url) return url;
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
|
||||
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
@@ -35,6 +49,85 @@
|
||||
return document.title.trim();
|
||||
}
|
||||
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
output = output.split('/');
|
||||
output.pop();
|
||||
output = output.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function getJoplinClipperSvgClassName(svg) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getImageSizes(element, forceAbsoluteUrls = false) {
|
||||
const output = {};
|
||||
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
|
||||
if (!output[src]) output[src] = [];
|
||||
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
|
||||
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
|
||||
if (!output[className]) output[className] = [];
|
||||
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function getAnchorNames(element) {
|
||||
const output = [];
|
||||
// Anchor names are normally in A tags but can be in SPAN too
|
||||
@@ -53,6 +146,14 @@
|
||||
return output;
|
||||
}
|
||||
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image) {
|
||||
if (image.currentSrc) return image.currentSrc;
|
||||
return image.src;
|
||||
}
|
||||
|
||||
// Cleans up element by removing all its invisible children (which we don't want to render as Markdown)
|
||||
// And hard-code the image dimensions so that the information can be used by the clipper server to
|
||||
// display them at the right sizes in the notes.
|
||||
@@ -80,7 +181,6 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'img') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(imageSrc(node));
|
||||
node.setAttribute('src', src);
|
||||
if (!(src in imageIndexes)) imageIndexes[src] = 0;
|
||||
@@ -99,7 +199,6 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'svg') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const className = getJoplinClipperSvgClassName(node);
|
||||
if (!(className in imageIndexes)) imageIndexes[className] = 0;
|
||||
|
||||
@@ -117,13 +216,11 @@
|
||||
}
|
||||
|
||||
if (nodeName === 'embed') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const src = absoluteUrl(node.src);
|
||||
node.setAttribute('src', src);
|
||||
}
|
||||
|
||||
if (nodeName === 'object') {
|
||||
// eslint-disable-next-line no-undef
|
||||
const data = absoluteUrl(node.data);
|
||||
node.setAttribute('data', data);
|
||||
}
|
||||
@@ -203,7 +300,6 @@
|
||||
let svgId = 0;
|
||||
|
||||
for (const svg of svgs) {
|
||||
// eslint-disable-next-line no-undef
|
||||
if (!getJoplinClipperSvgClassName(svg)) {
|
||||
svg.classList.add(`joplin-clipper-svg-${svgId}`);
|
||||
svgId++;
|
||||
@@ -211,6 +307,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
function getStyleSheets(doc) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
console.info('It will downloaded by the main application.');
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function documentForReadability() {
|
||||
// Readability directly change the passed document so clone it so as
|
||||
// to preserve the original web page.
|
||||
@@ -252,9 +372,7 @@
|
||||
name: shouldSendToJoplin ? 'sendContentToJoplin' : 'clippedContent',
|
||||
title: title,
|
||||
html: html,
|
||||
// eslint-disable-next-line no-undef
|
||||
base_url: baseUrl(),
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags || '',
|
||||
@@ -279,7 +397,6 @@
|
||||
response.warning = 'Could not retrieve simplified version of page - full page has been saved instead.';
|
||||
return response;
|
||||
}
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(article.title, article.body, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'isProbablyReaderable') {
|
||||
@@ -291,7 +408,6 @@
|
||||
} else if (command.name === 'completePageHtml') {
|
||||
|
||||
if (isPagePdf()) {
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), embedPageUrl(), getImageSizes(document), getAnchorNames(document));
|
||||
}
|
||||
|
||||
@@ -301,34 +417,11 @@
|
||||
// Because cleanUpElement is going to modify the DOM and remove elements we don't want to work
|
||||
// directly on the document, so we make a copy of it first.
|
||||
const cleanDocument = document.body.cloneNode(true);
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, cleanDocument, imageSizes, imageIndexes);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const stylesheets = convertToMarkup === 'html' ? getStyleSheets(document) : null;
|
||||
|
||||
// The <BODY> tag may have a style in the CSS stylesheets. This
|
||||
// style can be overriden by setting the `style` attribute on the
|
||||
// BODY tag. Since we don't keep the body tag, it means we may be
|
||||
// missing some styling, which may break the page.
|
||||
//
|
||||
// For example, on this page:
|
||||
// https://devblogs.microsoft.com/oldnewthing/20180529-00/?p=98855
|
||||
// The BODY tag has visibility set to hidden in the stylesheet, and
|
||||
// made visible by setting the style attribute. Because of that,
|
||||
// previously that imported note would show blank content, while now
|
||||
// it will be visible.
|
||||
//
|
||||
// Fixes https://github.com/laurent22/joplin/issues/7925
|
||||
if (document.body.getAttribute('style')) {
|
||||
stylesheets.push({
|
||||
type: 'text',
|
||||
value: `body { ${document.body.getAttribute('style')} }`,
|
||||
});
|
||||
}
|
||||
|
||||
return clippedContentResponse(pageTitle(), cleanDocument.innerHTML, imageSizes, getAnchorNames(document), stylesheets);
|
||||
|
||||
} else if (command.name === 'selectedHtml') {
|
||||
@@ -348,11 +441,9 @@
|
||||
container.appendChild(range.cloneContents());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const imageSizes = getImageSizes(document, true);
|
||||
const imageIndexes = {};
|
||||
cleanUpElement(convertToMarkup, container, imageSizes, imageIndexes);
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), container.innerHTML, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else if (command.name === 'screenshot') {
|
||||
@@ -455,7 +546,6 @@
|
||||
const content = {
|
||||
title: pageTitle(),
|
||||
crop_rect: selectionArea,
|
||||
// eslint-disable-next-line no-undef
|
||||
url: pageLocationOrigin() + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags,
|
||||
@@ -480,9 +570,7 @@
|
||||
|
||||
} else if (command.name === 'pageUrl') {
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const url = pageLocationOrigin() + location.pathname + location.search;
|
||||
// eslint-disable-next-line no-undef
|
||||
return clippedContentResponse(pageTitle(), url, getImageSizes(document), getAnchorNames(document));
|
||||
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const sourcePath = `${__dirname}/../../lib/randomClipperPort.js`;
|
||||
const clipperUtilsPath = `${__dirname}/../../lib/clipperUtils.js`;
|
||||
|
||||
|
||||
// Mozilla insists on building the clipper from a tarball, not from the repository
|
||||
// so we add this check and only copy the file if it's present. Normally it rarely
|
||||
@@ -12,10 +10,6 @@ if (fs.pathExistsSync(sourcePath)) {
|
||||
fs.copySync(sourcePath, `${__dirname}/src/randomClipperPort.js`);
|
||||
}
|
||||
|
||||
if (fs.pathExistsSync(clipperUtilsPath)) {
|
||||
fs.copySync(clipperUtilsPath, `${__dirname}/../content_scripts/clipperUtils.js`);
|
||||
}
|
||||
|
||||
// These files give warnings when loading the extension in Chrome, in dev mode
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_key.pem`);
|
||||
fs.removeSync(`${__dirname}/node_modules/public-encrypt/test/test_rsa_pubkey.pem`);
|
||||
|
||||
@@ -182,7 +182,6 @@ class AppComponent extends Component {
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/clipperUtils.js' });
|
||||
await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
|
||||
}
|
||||
|
||||
|
||||
4
packages/app-desktop/.gitignore
vendored
4
packages/app-desktop/.gitignore
vendored
@@ -14,7 +14,3 @@ style.min.css
|
||||
build/lib/
|
||||
vendor/*
|
||||
!vendor/loadEmojiLib.js
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
integration-tests/test-profile/
|
||||
|
||||
@@ -67,7 +67,6 @@ import eventManager from '@joplin/lib/eventManager';
|
||||
import path = require('path');
|
||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||
import { parseNotesParent } from '@joplin/lib/reducer';
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
@@ -448,35 +447,10 @@ class Application extends BaseApplication {
|
||||
// items: masterKeys,
|
||||
// });
|
||||
|
||||
const getNotesParent = async () => {
|
||||
let notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
|
||||
if (notesParent.type === 'Tag' && !(await Tag.load(notesParent.selectedItemId))) {
|
||||
notesParent = {
|
||||
type: 'Folder',
|
||||
selectedItemId: Setting.value('activeFolderId'),
|
||||
};
|
||||
}
|
||||
return notesParent;
|
||||
};
|
||||
|
||||
const notesParent = await getNotesParent();
|
||||
|
||||
if (notesParent.type === 'SmartFilter') {
|
||||
this.store().dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else if (notesParent.type === 'Tag') {
|
||||
this.store().dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
} else {
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: notesParent.selectedItemId,
|
||||
});
|
||||
}
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: Setting.value('activeFolderId'),
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
|
||||
@@ -6,7 +6,7 @@ import KvStore from '@joplin/lib/services/KvStore';
|
||||
import * as ArrayUtils from '@joplin/lib/ArrayUtils';
|
||||
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
import { compareVersions } from 'compare-versions';
|
||||
const compareVersions = require('compare-versions');
|
||||
|
||||
const logger = Logger.create('checkForUpdates');
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
<br/>
|
||||
<MacOSMissingPasswordHelpLink
|
||||
theme={theme}
|
||||
text={_('%s: Missing password.', _('Help'))}
|
||||
text={_('%s: Missing password', _('Help'))}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
import { ContextMenuEvent, ContextMenuParams } from 'electron';
|
||||
import { useEffect, RefObject } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import convertToScreenCoordinates from '../../../../utils/convertToScreenCoordinates';
|
||||
import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerService';
|
||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||
import type CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import bridge from '../../../../../services/bridge';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
@@ -34,7 +35,7 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// It might be buggy, refer to the below issue
|
||||
// https://github.com/laurent22/joplin/pull/3974#issuecomment-718936703
|
||||
useEffect(() => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: Element) => {
|
||||
const isAncestorOfCodeMirrorEditor = (elem: HTMLElement) => {
|
||||
for (; elem.parentElement; elem = elem.parentElement) {
|
||||
if (elem.classList.contains(props.editorClassName)) {
|
||||
return true;
|
||||
@@ -44,9 +45,14 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const convertFromScreenCoordinates = (zoomPercent: number, screenXY: number) => {
|
||||
const zoomFraction = zoomPercent / 100;
|
||||
return screenXY / zoomFraction;
|
||||
let lastInCodeMirrorContextMenuTimestamp = 0;
|
||||
|
||||
// The browser's contextmenu event provides additional information about the
|
||||
// target of the event, not provided by the Electron context-menu event.
|
||||
const onBrowserContextMenu = (event: Event) => {
|
||||
if (isAncestorOfCodeMirrorEditor(event.target as HTMLElement)) {
|
||||
lastInCodeMirrorContextMenuTimestamp = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
function pointerInsideEditor(params: ContextMenuParams) {
|
||||
@@ -58,15 +64,13 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// params.inputFieldType is "plainText". Thus, such a check would be inconsistent.
|
||||
if (!elements.length || !isEditable) return false;
|
||||
|
||||
// Checks whether the element the pointer clicked on is inside the editor.
|
||||
// This logic will need to be changed if the editor is eventually wrapped
|
||||
// in an iframe, as elementFromPoint will return the iframe container (and not
|
||||
// a child of the editor).
|
||||
const zoom = Setting.value('windowContentZoomFactor');
|
||||
const xScreen = convertFromScreenCoordinates(zoom, x);
|
||||
const yScreen = convertFromScreenCoordinates(zoom, y);
|
||||
const intersectingElement = document.elementFromPoint(xScreen, yScreen);
|
||||
return intersectingElement && isAncestorOfCodeMirrorEditor(intersectingElement);
|
||||
const maximumMsSinceBrowserEvent = 100;
|
||||
if (Date.now() - lastInCodeMirrorContextMenuTimestamp > maximumMsSinceBrowserEvent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rect = convertToScreenCoordinates(Setting.value('windowContentZoomFactor'), elements[0].getBoundingClientRect());
|
||||
return rect.x < x && rect.y < y && rect.right > x && rect.bottom > y;
|
||||
}
|
||||
|
||||
async function onContextMenu(event: ContextMenuEvent, params: ContextMenuParams) {
|
||||
@@ -156,8 +160,11 @@ const useContextMenu = (props: ContextMenuProps) => {
|
||||
// the listener that shows the default menu.
|
||||
bridge().window().webContents.prependListener('context-menu', onContextMenu);
|
||||
|
||||
window.addEventListener('contextmenu', onBrowserContextMenu);
|
||||
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
window.removeEventListener('contextmenu', onBrowserContextMenu);
|
||||
};
|
||||
}, [
|
||||
props.plugins, props.editorClassName, editorRef,
|
||||
|
||||
@@ -724,29 +724,19 @@ function CodeMirror(props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
// Disable this effect to fix this:
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
|
||||
// to automatically focus the editor when the layout changes. The workaround
|
||||
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
|
||||
//
|
||||
// On the other hand, if we automatically focus the editor, and the user
|
||||
// does not want this, there's no workaround, so it's better to have this
|
||||
// disabled.
|
||||
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!editorRef.current) return;
|
||||
|
||||
// // Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// // we should focus the editor
|
||||
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// // an editor interaction so users should expect the editor to be focused
|
||||
// if (editorPaneVisible) {
|
||||
// editorRef.current.focus();
|
||||
// }
|
||||
// }, [editorPaneVisible]);
|
||||
// Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// we should focus the editor
|
||||
// The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// an editor interaction so users should expect the editor to be focused
|
||||
if (editorPaneVisible) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, [editorPaneVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
@@ -310,29 +310,19 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
// Disable this effect to fix this:
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/6514 It doesn't seem essential
|
||||
// to automatically focus the editor when the layout changes. The workaround
|
||||
// is to toggle the layout Cmd+L, then manually focus the editor Cmd+Shift+B.
|
||||
//
|
||||
// On the other hand, if we automatically focus the editor, and the user
|
||||
// does not want this, there's no workaround, so it's better to have this
|
||||
// disabled.
|
||||
const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
// const editorPaneVisible = props.visiblePanes.indexOf('editor') >= 0;
|
||||
useEffect(() => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!editorRef.current) return;
|
||||
|
||||
// // Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// // we should focus the editor
|
||||
// // The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// // an editor interaction so users should expect the editor to be focused
|
||||
// if (editorPaneVisible) {
|
||||
// editorRef.current.focus();
|
||||
// }
|
||||
// }, [editorPaneVisible]);
|
||||
// Anytime the user toggles the visible panes AND the editor is visible as a result
|
||||
// we should focus the editor
|
||||
// The intuition is that a panel toggle (with editor in view) is the equivalent of
|
||||
// an editor interaction so users should expect the editor to be focused
|
||||
if (editorPaneVisible) {
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, [editorPaneVisible]);
|
||||
|
||||
useContextMenu({
|
||||
plugins: props.plugins,
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
onEditorPaste: ()=> void;
|
||||
}
|
||||
|
||||
const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
@@ -37,7 +37,8 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
}
|
||||
|
||||
const pasteEventHandler = (_editor: any, event: Event) => {
|
||||
props.onEditorPaste(event);
|
||||
event.preventDefault();
|
||||
props.onEditorPaste();
|
||||
};
|
||||
|
||||
editor.on('paste', pasteEventHandler);
|
||||
|
||||
@@ -76,11 +76,18 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
}, []);
|
||||
|
||||
const effectiveNoteId = useEffectiveNoteId(props);
|
||||
const effectiveNote = props.notes.find(n => n.id === effectiveNoteId);
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
syncStarted: props.syncStarted,
|
||||
decryptionStarted: props.decryptionStarted,
|
||||
noteId: effectiveNoteId,
|
||||
|
||||
// The effective updated_time property of the note. It may be different
|
||||
// from the last time the note was saved, if it was modified outside the
|
||||
// editor (eg. via API).
|
||||
dbNote: effectiveNote ? { id: effectiveNote.id, updated_time: effectiveNote.updated_time } : { id: '', updated_time: 0 },
|
||||
|
||||
isProvisional: props.isProvisional,
|
||||
titleInputRef: titleInputRef,
|
||||
editorRef: editorRef,
|
||||
@@ -120,12 +127,32 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
return async function() {
|
||||
const note = await formNoteToNote(formNote);
|
||||
reg.logger().debug('Saving note...', note);
|
||||
const savedNote: any = await Note.save(note);
|
||||
const noteUpdatedTime = Date.now();
|
||||
|
||||
// First we set the formNote object, then we save the note. We
|
||||
// do it in that order, otherwise `useFormNote` will be rendered
|
||||
// with the newly saved note and the timestamp of that note will
|
||||
// be more recent that the one in the editor, which will trigger
|
||||
// an update. We do not want this since we already have the
|
||||
// latest changes.
|
||||
//
|
||||
// It also means that we manually set the timestamp, so that we
|
||||
// have it before the note is saved.
|
||||
|
||||
setFormNote((prev: FormNote) => {
|
||||
return { ...prev, user_updated_time: savedNote.user_updated_time, hasChanged: false };
|
||||
return {
|
||||
...prev,
|
||||
user_updated_time: noteUpdatedTime,
|
||||
updated_time: noteUpdatedTime,
|
||||
hasChanged: false,
|
||||
};
|
||||
});
|
||||
|
||||
const savedNote = await Note.save({
|
||||
...note,
|
||||
updated_time: noteUpdatedTime,
|
||||
}, { autoTimestamp: false });
|
||||
|
||||
void ExternalEditWatcher.instance().updateNoteFile(savedNote);
|
||||
|
||||
props.dispatch({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
import { Dispatch } from 'redux';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -20,7 +21,7 @@ export interface NoteEditorProps {
|
||||
dispatch: Dispatch;
|
||||
selectedNoteIds: string[];
|
||||
selectedFolderId: string;
|
||||
notes: any[];
|
||||
notes: NoteEntity[];
|
||||
watchedNoteFiles: string[];
|
||||
isProvisional: boolean;
|
||||
editorNoteStatuses: any;
|
||||
@@ -104,6 +105,7 @@ export interface FormNote {
|
||||
markup_language: number;
|
||||
user_updated_time: number;
|
||||
encryption_applied: number;
|
||||
updated_time: number;
|
||||
|
||||
hasChanged: boolean;
|
||||
|
||||
@@ -154,6 +156,7 @@ export function defaultFormNote(): FormNote {
|
||||
hasChanged: false,
|
||||
user_updated_time: 0,
|
||||
encryption_applied: 0,
|
||||
updated_time: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
import useFormNote, { DbNote, HookDependencies } from './useFormNote';
|
||||
|
||||
const defaultFormNoteProps: HookDependencies = {
|
||||
syncStarted: false,
|
||||
@@ -12,6 +12,7 @@ const defaultFormNoteProps: HookDependencies = {
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
dbNote: { id: '', updated_time: 0 },
|
||||
};
|
||||
|
||||
describe('useFormNote', () => {
|
||||
@@ -77,36 +78,34 @@ describe('useFormNote', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// It seems this test is crashing the worker on CI (out of memory), so disabling it for now.
|
||||
it('should reload the note when it is changed outside of the editor', async () => {
|
||||
const note = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
// it('should reload the note when it is changed outside of the editor', async () => {
|
||||
// const note = await Note.save({ title: 'Test Note!' });
|
||||
const makeFormNoteProps = (dbNote: DbNote): HookDependencies => {
|
||||
return {
|
||||
...defaultFormNoteProps,
|
||||
noteId: note.id,
|
||||
dbNote,
|
||||
};
|
||||
};
|
||||
|
||||
// const makeFormNoteProps = (dbNote: DbNote): HookDependencies => {
|
||||
// return {
|
||||
// ...defaultFormNoteProps,
|
||||
// noteId: note.id,
|
||||
// dbNote,
|
||||
// };
|
||||
// };
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }),
|
||||
});
|
||||
|
||||
// const formNote = renderHook(props => useFormNote(props), {
|
||||
// initialProps: makeFormNoteProps({ id: note.id, updated_time: note.updated_time }),
|
||||
// });
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
});
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Test Note!');
|
||||
// });
|
||||
// Simulate the note being modified outside the editor
|
||||
const modifiedNote = await Note.save({ id: note.id, title: 'Modified' });
|
||||
|
||||
// // Simulate the note being modified outside the editor
|
||||
// const modifiedNote = await Note.save({ id: note.id, title: 'Modified' });
|
||||
// NoteEditor then would update `dbNote`
|
||||
formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time }));
|
||||
|
||||
// // NoteEditor then would update `dbNote`
|
||||
// formNote.rerender(makeFormNoteProps({ id: note.id, updated_time: modifiedNote.updated_time }));
|
||||
|
||||
// await formNote.waitFor(() => {
|
||||
// expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
// });
|
||||
// });
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote.title).toBe('Modified');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -7,21 +7,30 @@ import { splitHtml } from '@joplin/renderer/HtmlToHtml';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
|
||||
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
|
||||
const logger = Logger.create('useFormNote');
|
||||
|
||||
export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
updated_time: number;
|
||||
}
|
||||
|
||||
export interface DbNote {
|
||||
id: string;
|
||||
updated_time: number;
|
||||
}
|
||||
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
dbNote: DbNote;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: any;
|
||||
editorRef: any;
|
||||
@@ -63,7 +72,7 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad, dbNote,
|
||||
} = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
@@ -77,7 +86,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// a new refresh.
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: any) {
|
||||
async function initNoteState(n: NoteEntity) {
|
||||
let originalCss = '';
|
||||
|
||||
if (n.markup_language === MarkupToHtml.MARKUP_LANGUAGE_HTML) {
|
||||
@@ -85,7 +94,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
originalCss = splitted.css;
|
||||
}
|
||||
|
||||
const newFormNote = {
|
||||
const newFormNote: FormNote = {
|
||||
id: n.id,
|
||||
title: n.title,
|
||||
body: n.body,
|
||||
@@ -99,6 +108,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
hasChanged: false,
|
||||
user_updated_time: n.user_updated_time,
|
||||
encryption_applied: n.encryption_applied,
|
||||
updated_time: n.updated_time,
|
||||
};
|
||||
|
||||
// Note that for performance reason,the call to setResourceInfos should
|
||||
@@ -116,7 +126,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
useEffect(() => {
|
||||
if (formNoteRefeshScheduled <= 0) return () => {};
|
||||
|
||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||
logger.info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
@@ -128,7 +138,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
// it would not have been loaded in the editor (due to note selection changing
|
||||
// on delete)
|
||||
if (!n) {
|
||||
reg.logger().warn('Trying to reload note that has been deleted:', noteId);
|
||||
logger.warn('Trying to reload note that has been deleted:', noteId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,19 +160,19 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}, [formNoteRefeshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
// Check that synchronisation has just finished - and if the note has
|
||||
// never been changed, we reload it. If the note has already been
|
||||
// changed, it's a conflict that's already been handled by the
|
||||
// synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNote.hasChanged) return;
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
// from true to false, it doesn't cancel the note from loading.
|
||||
// Refresh the form note. This is kept separate from the above logic so
|
||||
// that when prevSyncStarted is changed from true to false, it doesn't
|
||||
// cancel the note from loading.
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
@@ -170,6 +180,18 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
formNote.hasChanged, refreshFormNote,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
// Something's not fully initialised - we skip the check
|
||||
if (!dbNote.id || !dbNote.updated_time || !formNote.updated_time) return;
|
||||
|
||||
// If the note in the database is more recent that the note in editor,
|
||||
// it was modified outside the editor, so we refresh it.
|
||||
if (dbNote.id === formNote.id && dbNote.updated_time > formNote.updated_time) {
|
||||
logger.info('Note has been changed outside the editor - reloading it');
|
||||
refreshFormNote();
|
||||
}
|
||||
}, [dbNote.id, dbNote.updated_time, formNote.updated_time, formNote.id, refreshFormNote]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
if (formNote.id) setFormNote(defaultFormNote());
|
||||
@@ -180,7 +202,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
reg.logger().debug('Loading existing note', noteId);
|
||||
logger.debug('Loading existing note', noteId);
|
||||
|
||||
function handleAutoFocus(noteIsTodo: boolean) {
|
||||
if (!isProvisional) return;
|
||||
@@ -200,15 +222,15 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
const n = await Note.load(noteId);
|
||||
if (cancelled) return;
|
||||
if (!n) throw new Error(`Cannot find note with ID: ${noteId}`);
|
||||
reg.logger().debug('Loaded note:', n);
|
||||
logger.debug('Loaded note:', n);
|
||||
|
||||
await onBeforeLoad({ formNote });
|
||||
await onBeforeLoad({ formNote, updated_time: 0 });
|
||||
|
||||
const newFormNote = await initNoteState(n);
|
||||
|
||||
setIsNewNote(isProvisional);
|
||||
|
||||
await onAfterLoad({ formNote: newFormNote });
|
||||
await onAfterLoad({ formNote: newFormNote, updated_time: n.updated_time });
|
||||
|
||||
handleAutoFocus(!!n.is_todo);
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ class NotePropertiesDialog extends React.Component<Props, State> {
|
||||
controlComp = (
|
||||
<Datetime
|
||||
ref="editField"
|
||||
initialValue={value}
|
||||
defaultValue={value}
|
||||
dateFormat={time.dateFormat()}
|
||||
timeFormat={time.timeFormat()}
|
||||
inputProps={{
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Integration tests
|
||||
|
||||
The integration tests in this directory can be run with `yarn playwright test`.
|
||||
|
||||
- Tests use a `test-profile` directory that should be re-created before every test.
|
||||
- Only one Electron application should be instantiated per test file.
|
||||
- Files in the `models/` directory follow [the page object model](https://playwright.dev/docs/pom).
|
||||
|
||||
# References
|
||||
|
||||
The following sources are helpful for designing and implementing Electron integration tests
|
||||
with Playwright:
|
||||
- [A setup guide from an organisation that uses Playwright](https://dev.to/kubeshop/testing-electron-apps-with-playwright-3f89)
|
||||
and [that organisation's test suite](https://github.com/kubeshop/monokle/blob/main/tests/base.test.ts).
|
||||
- [The Playwright ElectronApp docs](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Electron Playwright example repository](https://github.com/spaceagetv/electron-playwright-example)
|
||||
- [Playwright best practices](https://playwright.dev/docs/best-practices)
|
||||
@@ -1,79 +0,0 @@
|
||||
import { test, expect } from './util/test';
|
||||
import MainScreen from './models/MainScreen';
|
||||
import activateMainMenuItem from './util/activateMainMenuItem';
|
||||
import SettingsScreen from './models/SettingsScreen';
|
||||
|
||||
|
||||
test.describe('main', () => {
|
||||
test('app should launch', async ({ mainWindow }) => {
|
||||
// A window should open with the correct title
|
||||
expect(await mainWindow.title()).toMatch(/^Joplin/);
|
||||
|
||||
const mainPage = new MainScreen(mainWindow);
|
||||
await mainPage.waitFor();
|
||||
});
|
||||
|
||||
test('should be able to create and edit a new note', async ({ mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.newNoteButton.click();
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.waitFor();
|
||||
|
||||
// Wait for the title input to have the correct placeholder
|
||||
await mainWindow.locator('input[placeholder^="Creating new note"]').waitFor();
|
||||
|
||||
// Fill the title
|
||||
await editor.noteTitleInput.click();
|
||||
await editor.noteTitleInput.fill('Test note');
|
||||
|
||||
// Note list should contain the new note
|
||||
await expect(mainScreen.noteListContainer.getByText('Test note')).toBeVisible();
|
||||
|
||||
// Focus the editor
|
||||
await editor.codeMirrorEditor.click();
|
||||
|
||||
// Type some text
|
||||
await mainWindow.keyboard.type('# Test note!');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('New note content!');
|
||||
|
||||
// Should render
|
||||
const viewerFrame = editor.getNoteViewerIframe();
|
||||
await expect(viewerFrame.locator('h1')).toHaveText('Test note!');
|
||||
});
|
||||
|
||||
test('should be possible to remove sort order buttons in settings', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = new MainScreen(mainWindow);
|
||||
await mainScreen.waitFor();
|
||||
|
||||
// Sort order buttons should be visible by default
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).toBeVisible();
|
||||
|
||||
// Open settings (check both labels so that this works on MacOS)
|
||||
expect(
|
||||
await activateMainMenuItem(electronApp, 'Preferences...') || await activateMainMenuItem(electronApp, 'Options'),
|
||||
).toBe(true);
|
||||
|
||||
// Should be on the settings screen
|
||||
const settingsScreen = new SettingsScreen(mainWindow);
|
||||
await settingsScreen.waitFor();
|
||||
|
||||
// Open the appearance tab
|
||||
await settingsScreen.appearanceTabButton.click();
|
||||
|
||||
// Find the sort order visible checkbox
|
||||
const sortOrderVisibleCheckbox = mainWindow.getByLabel(/^Show sort order/);
|
||||
|
||||
await expect(sortOrderVisibleCheckbox).toBeChecked();
|
||||
await sortOrderVisibleCheckbox.click();
|
||||
await expect(sortOrderVisibleCheckbox).not.toBeChecked();
|
||||
|
||||
// Save settings & close
|
||||
await settingsScreen.okayButton.click();
|
||||
await mainScreen.waitFor();
|
||||
|
||||
await expect(mainScreen.noteListContainer.locator('[title^="Toggle sort order"]')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import NoteEditorScreen from './NoteEditorScreen';
|
||||
|
||||
export default class MainScreen {
|
||||
public readonly newNoteButton: Locator;
|
||||
public readonly noteListContainer: Locator;
|
||||
public readonly noteEditor: NoteEditorScreen;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.newNoteButton = page.locator('.new-note-button');
|
||||
this.noteListContainer = page.locator('.rli-noteList');
|
||||
this.noteEditor = new NoteEditorScreen(page);
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.newNoteButton.waitFor();
|
||||
await this.noteEditor.waitFor();
|
||||
await this.noteListContainer.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
import { Locator, Page } from '@playwright/test';
|
||||
|
||||
export default class NoteEditorPage {
|
||||
public readonly codeMirrorEditor: Locator;
|
||||
public readonly noteTitleInput: Locator;
|
||||
private readonly containerLocator: Locator;
|
||||
|
||||
public constructor(private readonly page: Page) {
|
||||
this.containerLocator = page.locator('.rli-editor');
|
||||
this.codeMirrorEditor = this.containerLocator.locator('.codeMirrorEditor');
|
||||
this.noteTitleInput = this.containerLocator.locator('.title-input');
|
||||
}
|
||||
|
||||
public getNoteViewerIframe() {
|
||||
// The note viewer can change content when the note re-renders. As such,
|
||||
// a new locator needs to be created after re-renders (and this can't be a
|
||||
// static property).
|
||||
return this.page.frame({ url: /.*note-viewer[/\\]index.html.*/ });
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.codeMirrorEditor.waitFor();
|
||||
await this.noteTitleInput.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
|
||||
export default class SettingsScreen {
|
||||
public readonly okayButton: Locator;
|
||||
public readonly appearanceTabButton: Locator;
|
||||
|
||||
public constructor(page: Page) {
|
||||
this.okayButton = page.locator('button', { hasText: 'OK' });
|
||||
this.appearanceTabButton = page.getByText('Appearance');
|
||||
}
|
||||
|
||||
public async waitFor() {
|
||||
await this.okayButton.waitFor();
|
||||
await this.appearanceTabButton.waitFor();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Running desktop integration tests..."
|
||||
|
||||
export CI=true
|
||||
|
||||
if test "$RUNNER_OS" = "Linux" ; then
|
||||
# The Ubuntu Github CI doesn't have a display server.
|
||||
# Start a virtual one with xvfb-run.
|
||||
xvfb-run -- yarn run playwright test
|
||||
else
|
||||
yarn run playwright test
|
||||
fi
|
||||
@@ -1,36 +0,0 @@
|
||||
|
||||
import type { ElectronApplication } from '@playwright/test';
|
||||
import type { MenuItem } from 'electron';
|
||||
|
||||
|
||||
// Roughly based on
|
||||
// https://github.com/spaceagetv/electron-playwright-helpers/blob/main/src/menu_helpers.ts
|
||||
|
||||
// `menuItemPath` should be a list of menu labels (e.g. [["&JoplinMainMenu", "&File"], "Synchronise"]).
|
||||
const activateMainMenuItem = (electronApp: ElectronApplication, menuItemLabel: string) => {
|
||||
return electronApp.evaluate(async ({ Menu }, menuItemLabel) => {
|
||||
const activateItemInSubmenu = (submenu: MenuItem[]) => {
|
||||
for (const item of submenu) {
|
||||
if (item.label === menuItemLabel && item.visible) {
|
||||
// Found!
|
||||
item.click();
|
||||
return true;
|
||||
} else if (item.submenu) {
|
||||
const foundItem = activateItemInSubmenu(item.submenu.items);
|
||||
|
||||
if (foundItem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No item found
|
||||
return false;
|
||||
};
|
||||
|
||||
const appMenu = Menu.getApplicationMenu();
|
||||
return activateItemInSubmenu(appMenu.items);
|
||||
}, menuItemLabel);
|
||||
};
|
||||
|
||||
export default activateMainMenuItem;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { resolve, join, dirname } from 'path';
|
||||
import { remove, mkdirp } from 'fs-extra';
|
||||
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
|
||||
|
||||
|
||||
type JoplinFixtures = {
|
||||
electronApp: ElectronApplication;
|
||||
mainWindow: Page;
|
||||
};
|
||||
|
||||
// A custom fixture that loads an electron app. See
|
||||
// https://playwright.dev/docs/test-fixtures
|
||||
|
||||
export const test = base.extend<JoplinFixtures>({
|
||||
// Playwright fails if we don't use the object destructuring
|
||||
// pattern in the first argument.
|
||||
//
|
||||
// See https://github.com/microsoft/playwright/issues/8798
|
||||
//
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
electronApp: async ({ }, use) => {
|
||||
const profilePath = resolve(join(dirname(__dirname), 'test-profile'));
|
||||
const profileSubdir = join(profilePath, uuid.createNano());
|
||||
await mkdirp(profileSubdir);
|
||||
|
||||
const startupArgs = ['main.js', '--env', 'dev', '--profile', profileSubdir];
|
||||
const electronApp = await electron.launch({ args: startupArgs });
|
||||
|
||||
await use(electronApp);
|
||||
|
||||
await electronApp.firstWindow();
|
||||
await electronApp.close();
|
||||
await remove(profileSubdir);
|
||||
},
|
||||
|
||||
mainWindow: async ({ electronApp }, use) => {
|
||||
const window = await electronApp.firstWindow();
|
||||
await use(window);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -40,17 +40,18 @@ a {
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(100, 100, 100, 0.3);
|
||||
background: rgba(150, 149, 149, 0.4);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(223, 223, 223, 0.6);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 100, 100, 0.7);
|
||||
}
|
||||
|
||||
.fade_out {
|
||||
-webkit-transition: 0.15s;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.13.2",
|
||||
"version": "2.13.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -14,8 +14,7 @@
|
||||
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
|
||||
"start": "gulp build && electron . --env dev --log-level debug --open-dev-tools",
|
||||
"test": "jest",
|
||||
"test-ui": "playwright test",
|
||||
"test-ci": "yarn test && sh ./integration-tests/run-ci.sh",
|
||||
"test-ci": "yarn test",
|
||||
"renameReleaseAssets": "node tools/renameReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -117,16 +116,15 @@
|
||||
"devDependencies": {
|
||||
"@electron/rebuild": "3.3.0",
|
||||
"@joplin/tools": "~2.13",
|
||||
"@playwright/test": "1.38.1",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/node": "18.17.14",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "25.8.1",
|
||||
"electron-builder": "24.4.0",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.4",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
@@ -153,7 +151,7 @@
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
"compare-versions": "6.1.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
@@ -175,7 +173,7 @@
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.1.2",
|
||||
"react-select": "5.7.5",
|
||||
"react-select": "5.7.4",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
||||
// See https://playwright.dev/docs/test-configuration.
|
||||
export default defineConfig({
|
||||
testDir: './integration-tests',
|
||||
|
||||
// Only match .ts files (no compiled .js files)
|
||||
testMatch: '*.spec.ts',
|
||||
|
||||
// Allow running tests in parallel (note: each Joplin instance
|
||||
// is given its own profile directory).
|
||||
fullyParallel: true,
|
||||
|
||||
// Fail the build on CI if you accidentally left test.only in the source code.
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
// Retry on CI only
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
|
||||
// Opt out of parallel tests on CI
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
|
||||
// Reporter to use. See https://playwright.dev/docs/test-reporters
|
||||
reporter: process.env.CI ? 'line' : 'html',
|
||||
|
||||
// Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions.
|
||||
use: {
|
||||
// Base URL to use in actions like `await page.goto('/')`.
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
});
|
||||
5
packages/app-mobile/.gitignore
vendored
5
packages/app-mobile/.gitignore
vendored
@@ -66,10 +66,9 @@ yarn-error.log
|
||||
lib/csstojs/
|
||||
lib/rnInjectedJs/
|
||||
dist/
|
||||
components/NoteEditor/**/*.bundle.js
|
||||
components/NoteEditor/CodeMirror/CodeMirror.bundle.js
|
||||
components/NoteEditor/CodeMirror/CodeMirror.bundle.min.js
|
||||
components/NoteEditor/**/*.bundle.js.md5
|
||||
components/NoteEditor/**/*.bundle.min.js
|
||||
components/NoteEditor/**/*.bundle.js.LICENSE.txt
|
||||
|
||||
utils/fs-driver-android.js
|
||||
android/app/build-*
|
||||
|
||||
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097722
|
||||
versionName "2.13.2"
|
||||
versionCode 2097720
|
||||
versionName "2.13.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const ActionButton = (props: ActionButtonProps) => {
|
||||
};
|
||||
}), [props.buttons]);
|
||||
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'add');
|
||||
const closedIcon = useIcon(props.mainButton?.icon ?? 'md-add');
|
||||
const openIcon = useIcon('close');
|
||||
|
||||
return (
|
||||
|
||||
@@ -158,11 +158,11 @@ class CameraView extends Component {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const photoIcon = this.state.snapping ? 'checkmark' : 'camera';
|
||||
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
|
||||
|
||||
const displayRatios = this.supportsRatios() && this.state.ratios.length > 1;
|
||||
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-camera-reverse', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
|
||||
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
|
||||
|
||||
let cameraRatio = '4:3';
|
||||
@@ -202,7 +202,7 @@ class CameraView extends Component {
|
||||
<TouchableOpacity onPress={this.back_onPress}>
|
||||
<View style={{ marginLeft: 5, marginTop: 5, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={'arrow-back'}
|
||||
name={'md-arrow-back'}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
|
||||
import useOnMessage from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
@@ -19,11 +19,14 @@ interface Props {
|
||||
noteResources: any;
|
||||
paddingBottom: number;
|
||||
noteHash: string;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onCheckboxChange?: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onLoadEnd?: ()=> void;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onJoplinLinkClick: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onCheckboxChange?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onMarkForDownload?: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
onLoadEnd?: Function;
|
||||
}
|
||||
|
||||
const webViewStyle = {
|
||||
@@ -44,22 +47,16 @@ export default function NoteBodyViewer(props: Props) {
|
||||
);
|
||||
|
||||
const onResourceLongPress = useOnResourceLongPress(
|
||||
{
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
},
|
||||
props.onJoplinLinkClick,
|
||||
dialogBoxRef,
|
||||
);
|
||||
|
||||
const onMessage = useOnMessage(
|
||||
props.onCheckboxChange,
|
||||
props.noteBody,
|
||||
{
|
||||
onCheckboxChange: props.onCheckboxChange,
|
||||
onMarkForDownload: props.onMarkForDownload,
|
||||
onJoplinLinkClick: props.onJoplinLinkClick,
|
||||
onRequestEditResource: props.onRequestEditResource,
|
||||
onResourceLongPress,
|
||||
},
|
||||
props.onMarkForDownload,
|
||||
props.onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
);
|
||||
|
||||
const onLoadEnd = useCallback(() => {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
// Mock react-native-vector-icons -- it uses ESM imports, which, by default, are not
|
||||
// supported by jest.
|
||||
jest.doMock('react-native-vector-icons/Ionicons', () => {
|
||||
return {
|
||||
default: {
|
||||
getImageSourceSync: () => {
|
||||
// Create an empty file that can be read/used as an image resource.
|
||||
const iconPath = join(Setting.value('cacheDir'), 'test-icon.png');
|
||||
writeFileSync(iconPath, '', 'utf-8');
|
||||
|
||||
return { uri: iconPath };
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
import { editPopupClass, getEditPopupSource } from './useEditPopup';
|
||||
import { describe, it, expect, beforeAll, jest } from '@jest/globals';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
const createEditPopup = (target: HTMLElement) => {
|
||||
const { createEditPopupSyntax } = getEditPopupSource(lightTheme);
|
||||
eval(`(${createEditPopupSyntax})`)(target, 'someresourceid', '() => {}');
|
||||
};
|
||||
|
||||
const destroyEditPopup = () => {
|
||||
const { destroyEditPopupSyntax } = getEditPopupSource(lightTheme);
|
||||
eval(`(${destroyEditPopupSyntax})`)();
|
||||
};
|
||||
|
||||
describe('useEditPopup', () => {
|
||||
beforeAll(async () => {
|
||||
// useEditPopup relies on the resourceDir setting, which is set by
|
||||
// switchClient.
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
});
|
||||
|
||||
it('should attach an edit popup to an image', () => {
|
||||
const container = document.createElement('div');
|
||||
const targetImage = document.createElement('img');
|
||||
container.appendChild(targetImage);
|
||||
|
||||
createEditPopup(targetImage);
|
||||
|
||||
// Popup should be present in the document
|
||||
expect(container.querySelector(`.${editPopupClass}`)).not.toBeNull();
|
||||
|
||||
// Destroy the edit popup
|
||||
jest.useFakeTimers();
|
||||
destroyEditPopup();
|
||||
|
||||
// Give time for the popup's fade out animation to run.
|
||||
jest.advanceTimersByTime(1000 * 10);
|
||||
|
||||
// Popup should be destroyed.
|
||||
expect(container.querySelector(`.${editPopupClass}`)).toBeNull();
|
||||
|
||||
targetImage.remove();
|
||||
});
|
||||
|
||||
it('should auto-remove the edit popup after a delay', () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const container = document.createElement('div');
|
||||
const targetImage = document.createElement('img');
|
||||
container.appendChild(targetImage);
|
||||
|
||||
jest.useFakeTimers();
|
||||
createEditPopup(targetImage);
|
||||
|
||||
|
||||
expect(container.querySelector(`.${editPopupClass}`)).not.toBeNull();
|
||||
jest.advanceTimersByTime(1000 * 20); // ms
|
||||
expect(container.querySelector(`.${editPopupClass}`)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useMemo } from 'react';
|
||||
import { extname } from 'path';
|
||||
import shim from '@joplin/lib/shim';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
export const editPopupClass = 'joplin-editPopup';
|
||||
|
||||
const getEditIconSrc = (theme: Theme) => {
|
||||
const iconUri = Icon.getImageSourceSync('pencil', 20, theme.color2).uri;
|
||||
|
||||
// Copy to a location that can be read within a WebView
|
||||
// (necessary on iOS)
|
||||
const destPath = `${Setting.value('resourceDir')}/edit-icon${extname(iconUri)}`;
|
||||
|
||||
// Copy in the background -- the edit icon popover script doesn't need the
|
||||
// icon immediately.
|
||||
void (async () => {
|
||||
await shim.fsDriver().copy(iconUri, destPath);
|
||||
})();
|
||||
|
||||
return destPath;
|
||||
};
|
||||
|
||||
// Creates JavaScript/CSS that can be used to create an "Edit" button.
|
||||
// Exported to facilitate testing.
|
||||
export const getEditPopupSource = (theme: Theme) => {
|
||||
const fadeOutDelay = 400;
|
||||
const editPopupDestroyDelay = 5000;
|
||||
|
||||
const editPopupCss = `
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.${editPopupClass} {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
/* Don't take up any space in the line, overlay the button */
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
|
||||
--edit-popup-width: 40px;
|
||||
--edit-popup-padding: 10px;
|
||||
|
||||
/* Shift the popup such that it overlaps with the previous element. */
|
||||
left: calc(0px - var(--edit-popup-width));
|
||||
|
||||
/* Match the top of the image */
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.${editPopupClass} > button {
|
||||
padding: var(--edit-popup-padding);
|
||||
width: var(--edit-popup-width);
|
||||
|
||||
animation: fade-in 0.4s ease;
|
||||
|
||||
background-color: ${theme.backgroundColor2};
|
||||
color: ${theme.color2};
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
.${editPopupClass} img {
|
||||
/* Make the image take up as much space as possible (minus padding) */
|
||||
width: calc(var(--edit-popup-width) - var(--edit-popup-padding));
|
||||
}
|
||||
|
||||
.${editPopupClass}.fadeOut {
|
||||
animation: fade-out ${fadeOutDelay}ms ease;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const destroyEditPopupSyntax = `() => {
|
||||
if (!window.editPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popup = editPopup;
|
||||
popup.classList.add('fadeOut');
|
||||
window.editPopup = null;
|
||||
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
}, ${fadeOutDelay});
|
||||
}`;
|
||||
|
||||
const createEditPopupSyntax = `(parent, resourceId, onclick) => {
|
||||
if (window.editPopupTimeout) {
|
||||
clearTimeout(window.editPopupTimeout);
|
||||
window.editPopupTimeout = undefined;
|
||||
}
|
||||
|
||||
window.editPopupTimeout = setTimeout(${destroyEditPopupSyntax}, ${editPopupDestroyDelay});
|
||||
|
||||
if (window.lastEditPopupTarget !== parent) {
|
||||
(${destroyEditPopupSyntax})();
|
||||
} else if (window.editPopup) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.editPopup = document.createElement('div');
|
||||
const popupButton = document.createElement('button');
|
||||
|
||||
const popupIcon = new Image();
|
||||
popupIcon.alt = ${JSON.stringify(_('Edit'))};
|
||||
popupIcon.title = popupIcon.alt;
|
||||
popupIcon.src = ${JSON.stringify(getEditIconSrc(theme))};
|
||||
popupButton.appendChild(popupIcon);
|
||||
|
||||
popupButton.onclick = onclick;
|
||||
editPopup.appendChild(popupButton);
|
||||
|
||||
editPopup.classList.add(${JSON.stringify(editPopupClass)});
|
||||
parent.insertAdjacentElement('afterEnd', editPopup);
|
||||
|
||||
// Ensure that the edit popup is focused immediately by screen
|
||||
// readers.
|
||||
editPopup.focus();
|
||||
window.lastEditPopupTarget = parent;
|
||||
}`;
|
||||
|
||||
return { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss };
|
||||
};
|
||||
|
||||
const useEditPopup = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
return getEditPopupSource(themeStyle(themeId));
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
export default useEditPopup;
|
||||
@@ -1,31 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
|
||||
export type HandleMessageCallback = (message: string)=> void;
|
||||
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
|
||||
|
||||
interface MessageCallbacks {
|
||||
onMarkForDownload?: OnMarkForDownloadCallback;
|
||||
onJoplinLinkClick: HandleMessageCallback;
|
||||
onResourceLongPress: HandleMessageCallback;
|
||||
onRequestEditResource?: HandleMessageCallback;
|
||||
onCheckboxChange: HandleMessageCallback;
|
||||
}
|
||||
|
||||
export default function useOnMessage(
|
||||
noteBody: string,
|
||||
callbacks: MessageCallbacks,
|
||||
) {
|
||||
// Dectructure callbacks. Because we have that ({ a: 1 }) !== ({ a: 1 }),
|
||||
// we can expect the `callbacks` variable from the last time useOnMessage was called to
|
||||
// not equal the current` callbacks` variable, even if the callbacks themselves are the
|
||||
// same.
|
||||
//
|
||||
// Thus, useCallback should depend on each callback individually.
|
||||
const {
|
||||
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
|
||||
} = callbacks;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function useOnMessage(onCheckboxChange: Function, noteBody: string, onMarkForDownload: Function, onJoplinLinkClick: Function, onResourceLongPress: Function) {
|
||||
return useCallback((event: any) => {
|
||||
// 2021-05-19: Historically this was unescaped twice as it was
|
||||
// apparently needed after an upgrade to RN 58 (or 59). However this is
|
||||
@@ -40,15 +17,13 @@ export default function useOnMessage(
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, noteBody);
|
||||
onCheckboxChange?.(newBody);
|
||||
if (onCheckboxChange) onCheckboxChange(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
const splittedMsg = msg.split(':');
|
||||
const resourceId = splittedMsg[1];
|
||||
onMarkForDownload?.({ resourceId: resourceId });
|
||||
if (onMarkForDownload) onMarkForDownload({ resourceId: resourceId });
|
||||
} else if (msg.startsWith('longclick:')) {
|
||||
onResourceLongPress(msg);
|
||||
} else if (msg.startsWith('edit:')) {
|
||||
onRequestEditResource?.(msg);
|
||||
} else if (msg.startsWith('joplin:')) {
|
||||
onJoplinLinkClick(msg);
|
||||
} else if (msg.startsWith('error:')) {
|
||||
@@ -56,12 +31,5 @@ export default function useOnMessage(
|
||||
} else {
|
||||
onJoplinLinkClick(msg);
|
||||
}
|
||||
}, [
|
||||
noteBody,
|
||||
onCheckboxChange,
|
||||
onMarkForDownload,
|
||||
onJoplinLinkClick,
|
||||
onResourceLongPress,
|
||||
onRequestEditResource,
|
||||
]);
|
||||
}, [onCheckboxChange, noteBody, onMarkForDownload, onJoplinLinkClick, onResourceLongPress]);
|
||||
}
|
||||
|
||||
@@ -6,40 +6,20 @@ import { reg } from '@joplin/lib/registry';
|
||||
const { dialogs } = require('../../../utils/dialogs.js');
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import { copyToCache } from '../../../utils/ShareUtils';
|
||||
import isEditableResource from '../../NoteEditor/ImageEditor/isEditableResource';
|
||||
const Share = require('react-native-share').default;
|
||||
|
||||
interface Callbacks {
|
||||
onJoplinLinkClick: (link: string)=> void;
|
||||
onRequestEditResource: (message: string)=> void;
|
||||
}
|
||||
|
||||
export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRef: any) {
|
||||
const { onJoplinLinkClick, onRequestEditResource } = callbacks;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
export default function useOnResourceLongPress(onJoplinLinkClick: Function, dialogBoxRef: any) {
|
||||
return useCallback(async (msg: string) => {
|
||||
try {
|
||||
const resourceId = msg.split(':')[1];
|
||||
const resource = await Resource.load(resourceId);
|
||||
|
||||
// Handle the case where it's a long press on a link with no resource
|
||||
if (!resource) {
|
||||
reg.logger().warn(`Long-press: Resource with ID ${resourceId} does not exist (may be a note).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = resource.title ? resource.title : resource.file_name;
|
||||
const mime: string|undefined = resource.mime;
|
||||
|
||||
const actions = [];
|
||||
|
||||
actions.push({ text: _('Open'), id: 'open' });
|
||||
if (mime && isEditableResource(mime)) {
|
||||
actions.push({ text: _('Edit'), id: 'edit' });
|
||||
}
|
||||
actions.push({ text: _('Share'), id: 'share' });
|
||||
|
||||
const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, actions);
|
||||
const action = await dialogs.pop({ dialogbox: dialogBoxRef.current }, name, [
|
||||
{ text: _('Open'), id: 'open' },
|
||||
{ text: _('Share'), id: 'share' },
|
||||
]);
|
||||
|
||||
if (action === 'open') {
|
||||
onJoplinLinkClick(`joplin://${resourceId}`);
|
||||
@@ -52,12 +32,11 @@ export default function useOnResourceLongPress(callbacks: Callbacks, dialogBoxRe
|
||||
url: `file://${fileToShare}`,
|
||||
failOnCancel: false,
|
||||
});
|
||||
} else if (action === 'edit') {
|
||||
onRequestEditResource(`edit:${resourceId}`);
|
||||
}
|
||||
} catch (e) {
|
||||
reg.logger().error('Could not handle link long press', e);
|
||||
ToastAndroid.show('An error occurred, check log for details', ToastAndroid.SHORT);
|
||||
}
|
||||
}, [onJoplinLinkClick, onRequestEditResource, dialogBoxRef]);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [onJoplinLinkClick]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import shim from '@joplin/lib/shim';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
import useEditPopup from './useEditPopup';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { assetsToHeaders } = require('@joplin/renderer');
|
||||
|
||||
@@ -91,8 +90,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
const onlyNoteBodyHasChanged = Object.keys(changedDeps).length === 1 && changedDeps[0];
|
||||
const onlyCheckboxesHaveChanged = previousDeps[0] && changedDeps[0] && onlyCheckboxHasChangedHack(previousDeps[0], noteBody);
|
||||
|
||||
const { createEditPopupSyntax, destroyEditPopupSyntax, editPopupCss } = useEditPopup(themeId);
|
||||
|
||||
useEffect(() => {
|
||||
if (onlyNoteBodyHasChanged && onlyCheckboxesHaveChanged) {
|
||||
logger.info('Only a checkbox has changed - not updating HTML');
|
||||
@@ -115,11 +112,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
codeTheme: theme.codeThemeCss,
|
||||
postMessageSyntax: 'window.joplinPostMessage_',
|
||||
enableLongPress: true,
|
||||
|
||||
// Show an 'edit' popup over SVG images
|
||||
editPopupFiletypes: ['image/svg+xml'],
|
||||
createEditPopupSyntax,
|
||||
destroyEditPopupSyntax,
|
||||
};
|
||||
|
||||
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
|
||||
@@ -217,9 +209,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
${defaultCss}
|
||||
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
|
||||
${editPopupCss}
|
||||
${shim.mobilePlatform() === 'ios' ? `${iOSSpecificCss}\n${defaultCss}` : defaultCss}
|
||||
</style>
|
||||
${assetsToHeaders(result.pluginAssets, { asHtml: true })}
|
||||
</head>
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
const React = require('react');
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, BackHandler } from 'react-native';
|
||||
import { WebViewMessageEvent } from 'react-native-webview';
|
||||
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
|
||||
import { clearAutosave, writeAutosave } from './autosave';
|
||||
import { LocalizedStrings } from './js-draw/types';
|
||||
|
||||
const logger = Logger.create('ImageEditor');
|
||||
|
||||
type OnSaveCallback = (svgData: string)=> void;
|
||||
type OnCancelCallback = ()=> void;
|
||||
type LoadInitialSVGCallback = ()=> Promise<string>;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
loadInitialSVGData: LoadInitialSVGCallback|null;
|
||||
onSave: OnSaveCallback;
|
||||
onExit: OnCancelCallback;
|
||||
}
|
||||
|
||||
const useCss = (editorTheme: Theme) => {
|
||||
return useMemo(() => {
|
||||
// Ensure we have contrast between the background and selection. Some themes
|
||||
// have the same backgroundColor and selectionColor2. (E.g. Aritim Dark)
|
||||
let selectionBackgroundColor = editorTheme.selectedColor2;
|
||||
if (selectionBackgroundColor === editorTheme.backgroundColor) {
|
||||
selectionBackgroundColor = editorTheme.selectedColor;
|
||||
}
|
||||
|
||||
return `
|
||||
:root .imageEditorContainer {
|
||||
--background-color-1: ${editorTheme.backgroundColor};
|
||||
--foreground-color-1: ${editorTheme.color};
|
||||
--background-color-2: ${editorTheme.backgroundColor3};
|
||||
--foreground-color-2: ${editorTheme.color3};
|
||||
--background-color-3: ${editorTheme.raisedBackgroundColor};
|
||||
--foreground-color-3: ${editorTheme.raisedColor};
|
||||
|
||||
--selection-background-color: ${editorTheme.backgroundColorHover3};
|
||||
--selection-foreground-color: ${editorTheme.color3};
|
||||
--primary-action-foreground-color: ${editorTheme.color4};
|
||||
|
||||
--primary-shadow-color: ${editorTheme.colorFaded};
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body, html {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Hide the scrollbar. See scrollbar accessibility concerns
|
||||
(https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width#accessibility_concerns)
|
||||
for why this isn't done in js-draw itself. */
|
||||
.toolbar-tool-row::-webkit-scrollbar {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Hide the save/close icons on small screens. This isn't done in the upstream
|
||||
js-draw repository partially beause it isn't as well localized as Joplin
|
||||
(icons can be used to suggest the meaning of a button when a translation is
|
||||
unavailable). */
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--save .toolbar-icon,
|
||||
.toolbar-edge-toolbar:not(.one-row) .toolwidget-tag--exit .toolbar-icon {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
}, [editorTheme]);
|
||||
};
|
||||
|
||||
const ImageEditor = (props: Props) => {
|
||||
const editorTheme: Theme = themeStyle(props.themeId);
|
||||
const webviewRef: MutableRefObject<WebViewControl>|null = useRef(null);
|
||||
const [imageChanged, setImageChanged] = useState(false);
|
||||
|
||||
const onRequestCloseEditor = useCallback((promptIfUnsaved: boolean) => {
|
||||
const discardChangesAndClose = async () => {
|
||||
await clearAutosave();
|
||||
props.onExit();
|
||||
};
|
||||
|
||||
if (!imageChanged || !promptIfUnsaved) {
|
||||
void discardChangesAndClose();
|
||||
return true;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
_('Save changes?'), _('This drawing may have unsaved changes.'), [
|
||||
{
|
||||
text: _('Discard changes'),
|
||||
onPress: discardChangesAndClose,
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: _('Save changes'),
|
||||
onPress: () => {
|
||||
// saveDrawing calls props.onSave(...) which may close the
|
||||
// editor.
|
||||
webviewRef.current.injectJS('window.editorControl.saveThenExit()');
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
return true;
|
||||
}, [webviewRef, props.onExit, imageChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
const hardwareBackPressListener = () => {
|
||||
onRequestCloseEditor(true);
|
||||
return true;
|
||||
};
|
||||
BackHandler.addEventListener('hardwareBackPress', hardwareBackPressListener);
|
||||
|
||||
return () => {
|
||||
BackHandler.removeEventListener('hardwareBackPress', hardwareBackPressListener);
|
||||
};
|
||||
}, [onRequestCloseEditor]);
|
||||
|
||||
const css = useCss(editorTheme);
|
||||
const html = useMemo(() => `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"/>
|
||||
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`, [css]);
|
||||
|
||||
// A set of localization overrides (Joplin is better localized than js-draw).
|
||||
// All localizable strings (some unused?) can be found at
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/main/.github/ISSUE_TEMPLATE/translation-js-draw-new.yml
|
||||
const localizedStrings: LocalizedStrings = useMemo(() => ({
|
||||
save: _('Save'),
|
||||
close: _('Close'),
|
||||
undo: _('Undo'),
|
||||
redo: _('Redo'),
|
||||
}), []);
|
||||
|
||||
const injectedJavaScript = useMemo(() => `
|
||||
window.onerror = (message, source, lineno) => {
|
||||
window.ReactNativeWebView.postMessage(
|
||||
"error: " + message + " in file://" + source + ", line " + lineno
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
}));
|
||||
};
|
||||
|
||||
try {
|
||||
if (window.editorControl === undefined) {
|
||||
${shim.injectedJs('svgEditorBundle')}
|
||||
|
||||
window.editorControl = svgEditorBundle.createJsDrawEditor(
|
||||
{
|
||||
saveDrawing,
|
||||
closeEditor,
|
||||
updateEditorTemplate,
|
||||
setImageHasChanges,
|
||||
},
|
||||
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
|
||||
${JSON.stringify(Setting.value('locale'))},
|
||||
${JSON.stringify(localizedStrings)},
|
||||
);
|
||||
|
||||
// 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(
|
||||
'error: ' + e.message + ': ' + JSON.stringify(e)
|
||||
);
|
||||
}
|
||||
true;
|
||||
`, [localizedStrings]);
|
||||
|
||||
useEffect(() => {
|
||||
webviewRef.current?.injectJS(`
|
||||
if (window.editorControl) {
|
||||
window.editorControl.onThemeUpdate();
|
||||
}
|
||||
`);
|
||||
}, [editorTheme]);
|
||||
|
||||
const onReadyToLoadData = useCallback(async () => {
|
||||
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
|
||||
|
||||
// It can take some time for initialSVGData to be transferred to the WebView.
|
||||
// Thus, do so after the main content has been loaded.
|
||||
webviewRef.current.injectJS(`(async () => {
|
||||
if (window.editorControl) {
|
||||
const initialSVGData = ${JSON.stringify(initialSVGData)};
|
||||
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
|
||||
|
||||
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData);
|
||||
}
|
||||
})();`);
|
||||
}, [webviewRef, props.loadInitialSVGData]);
|
||||
|
||||
const onMessage = useCallback(async (event: WebViewMessageEvent) => {
|
||||
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();
|
||||
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 === '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]);
|
||||
|
||||
const onError = useCallback((event: any) => {
|
||||
logger.error('ImageEditor: WebView error: ', event);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ExtendedWebView
|
||||
themeId={props.themeId}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
onMessage={onMessage}
|
||||
onError={onError}
|
||||
ref={webviewRef}
|
||||
webviewInstanceId={'image-editor-js-draw'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageEditor;
|
||||
@@ -1,26 +0,0 @@
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export const autosaveFilename = 'autosaved-drawing.joplin.svg';
|
||||
|
||||
const logger = Logger.create('ImageEditor/autosave');
|
||||
|
||||
export const getAutosaveFilepath = () => {
|
||||
return `${Setting.value('resourceDir')}/${autosaveFilename}`;
|
||||
};
|
||||
|
||||
export const writeAutosave = async (data: string) => {
|
||||
const filePath = getAutosaveFilepath();
|
||||
logger.info(`Auto-saving drawing to ${JSON.stringify(filePath)}`);
|
||||
|
||||
await shim.fsDriver().writeFile(filePath, data, 'utf8');
|
||||
};
|
||||
|
||||
export const readAutosave = async (): Promise<string|null> => {
|
||||
return await shim.fsDriver().readFile(getAutosaveFilepath());
|
||||
};
|
||||
|
||||
export const clearAutosave = async () => {
|
||||
await shim.fsDriver().remove(getAutosaveFilepath());
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
const isEditableResource = (resourceMime: string) => {
|
||||
return resourceMime === 'image/svg+xml';
|
||||
};
|
||||
|
||||
export default isEditableResource;
|
||||
@@ -1,67 +0,0 @@
|
||||
import { AbstractComponent, Editor, BackgroundComponentBackgroundType, Erase, Vec2, Rect2 } from 'js-draw';
|
||||
|
||||
const applyTemplateToEditor = async (editor: Editor, templateData: string) => {
|
||||
let backgroundComponent: AbstractComponent|null = null;
|
||||
let imageSize = editor.getImportExportRect().size;
|
||||
let autoresize = true;
|
||||
|
||||
try {
|
||||
const templateJSON = JSON.parse(templateData);
|
||||
|
||||
const isEmptyTemplate =
|
||||
!('imageSize' in templateJSON) && !('backgroundData' in templateJSON);
|
||||
|
||||
// If the template is empty, add a default background
|
||||
if (isEmptyTemplate) {
|
||||
templateJSON.backgroundData = {
|
||||
'name': 'image-background',
|
||||
'zIndex': 0,
|
||||
'data': {
|
||||
'mainColor': '#ffffff',
|
||||
'backgroundType': BackgroundComponentBackgroundType.SolidColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if ('backgroundData' in templateJSON) {
|
||||
backgroundComponent = AbstractComponent.deserialize(
|
||||
templateJSON['backgroundData'],
|
||||
);
|
||||
}
|
||||
|
||||
if ('imageSize' in templateJSON) {
|
||||
imageSize = Vec2.ofXY(templateJSON.imageSize);
|
||||
}
|
||||
|
||||
if ('autoresize' in templateJSON) {
|
||||
autoresize = !!templateJSON.autoresize;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Warning: Invalid image template data: ', e);
|
||||
}
|
||||
|
||||
if (backgroundComponent) {
|
||||
// Remove the old background (if any)
|
||||
const previousBackground = editor.image.getBackgroundComponents();
|
||||
if (previousBackground.length > 0) {
|
||||
const removeBackgroundCommand = new Erase(previousBackground);
|
||||
await editor.dispatchNoAnnounce(removeBackgroundCommand, false);
|
||||
}
|
||||
|
||||
// Add the new background
|
||||
const addBackgroundCommand = editor.image.addElement(backgroundComponent);
|
||||
await editor.dispatchNoAnnounce(addBackgroundCommand, false);
|
||||
}
|
||||
|
||||
// Set the image size
|
||||
const imageSizeCommand = editor.setImportExportRect(new Rect2(0, 0, imageSize.x, imageSize.y));
|
||||
await editor.dispatchNoAnnounce(imageSizeCommand, false);
|
||||
|
||||
// Enable/disable autoresize
|
||||
await editor.dispatchNoAnnounce(editor.image.setAutoresizeEnabled(autoresize), false);
|
||||
|
||||
// And zoom to the template (false = don't make undoable)
|
||||
await editor.dispatchNoAnnounce(editor.viewport.zoomTo(editor.getImportExportRect()), false);
|
||||
};
|
||||
|
||||
export default applyTemplateToEditor;
|
||||
@@ -1,109 +0,0 @@
|
||||
/** @jest-environment jsdom */
|
||||
|
||||
// Hide warnings from js-draw.
|
||||
// jsdom doesn't support ResizeObserver and HTMLCanvasElement.getContext.
|
||||
HTMLCanvasElement.prototype.getContext = () => null;
|
||||
window.ResizeObserver = class { public observe() { } } as any;
|
||||
|
||||
import { describe, it, expect, jest } from '@jest/globals';
|
||||
import { Color4, EditorImage, EditorSettings, Path, pathToRenderable, StrokeComponent } from 'js-draw';
|
||||
import { RenderingMode } from 'js-draw';
|
||||
import createJsDrawEditor from './createJsDrawEditor';
|
||||
import { BackgroundComponent } from 'js-draw';
|
||||
import { BackgroundComponentBackgroundType } from 'js-draw';
|
||||
import { ImageEditorCallbacks } from './types';
|
||||
import applyTemplateToEditor from './applyTemplateToEditor';
|
||||
|
||||
|
||||
const createEditorWithCallbacks = (callbacks: Partial<ImageEditorCallbacks>) => {
|
||||
const toolbarState = '';
|
||||
const locale = 'en';
|
||||
|
||||
const allCallbacks: ImageEditorCallbacks = {
|
||||
saveDrawing: () => {},
|
||||
closeEditor: ()=> {},
|
||||
setImageHasChanges: ()=> {},
|
||||
updateEditorTemplate: ()=> {},
|
||||
|
||||
...callbacks,
|
||||
};
|
||||
|
||||
const editorOptions: Partial<EditorSettings> = {
|
||||
// Don't use a CanvasRenderer: jsdom doesn't support DrawingContext2D
|
||||
renderingMode: RenderingMode.DummyRenderer,
|
||||
};
|
||||
|
||||
const localizations = {
|
||||
save: 'Save',
|
||||
close: 'Close',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
};
|
||||
|
||||
return createJsDrawEditor(allCallbacks, toolbarState, locale, localizations, editorOptions);
|
||||
};
|
||||
|
||||
describe('createJsDrawEditor', () => {
|
||||
it('should trigger autosave callback every few minutes', async () => {
|
||||
let calledAutosaveCount = 0;
|
||||
|
||||
jest.useFakeTimers();
|
||||
const editorControl = createEditorWithCallbacks({
|
||||
saveDrawing: (_drawing: SVGElement, isAutosave: boolean) => {
|
||||
if (isAutosave) {
|
||||
calledAutosaveCount ++;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Load no image and an empty template so that autosave can start
|
||||
await editorControl.loadImageOrTemplate(undefined, '{}');
|
||||
|
||||
expect(calledAutosaveCount).toBe(0);
|
||||
|
||||
// Using the synchronous version of advanceTimersByTime seems to not
|
||||
// run the asynchronous code used to autosave drawings in createJsDrawEditor.ts.
|
||||
await jest.advanceTimersByTimeAsync(1000 * 60 * 4);
|
||||
|
||||
const lastAutosaveCount = calledAutosaveCount;
|
||||
expect(calledAutosaveCount).toBeGreaterThanOrEqual(1);
|
||||
expect(calledAutosaveCount).toBeLessThan(10);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(1000 * 60 * 10);
|
||||
|
||||
expect(calledAutosaveCount).toBeGreaterThan(lastAutosaveCount);
|
||||
});
|
||||
|
||||
it('should fire has changes callback on first change', () => {
|
||||
let hasChanges = false;
|
||||
const editorControl = createEditorWithCallbacks({
|
||||
setImageHasChanges: (newHasChanges: boolean) => {
|
||||
hasChanges = newHasChanges;
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasChanges).toBe(false);
|
||||
|
||||
const stroke = new StrokeComponent([
|
||||
// A filled shape
|
||||
pathToRenderable(Path.fromString('m0,0 l10,0 l0,10'), { fill: Color4.red }),
|
||||
]);
|
||||
void editorControl.editor.dispatch(EditorImage.addElement(stroke));
|
||||
|
||||
expect(hasChanges).toBe(true);
|
||||
});
|
||||
|
||||
it('default template should be a white grid background', async () => {
|
||||
const editorControl = createEditorWithCallbacks({});
|
||||
const editor = editorControl.editor;
|
||||
|
||||
await applyTemplateToEditor(editor, '{}');
|
||||
|
||||
expect(editor.image.getBackgroundComponents()).toHaveLength(1);
|
||||
|
||||
// Should have a white, solid background
|
||||
const background = editor.image.getBackgroundComponents()[0] as BackgroundComponent;
|
||||
expect(editor.estimateBackgroundColor().eq(Color4.white)).toBe(true);
|
||||
expect(background.getBackgroundType()).toBe(BackgroundComponentBackgroundType.SolidColor);
|
||||
});
|
||||
});
|
||||
@@ -1,168 +0,0 @@
|
||||
|
||||
import { Editor, AbstractToolbar, EditorEventType, EditorSettings, getLocalizationTable, adjustEditorThemeForContrast, BaseWidget } from 'js-draw';
|
||||
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 startAutosaveLoop from './startAutosaveLoop';
|
||||
|
||||
declare namespace ReactNativeWebView {
|
||||
const postMessage: (data: any)=> void;
|
||||
}
|
||||
|
||||
const restoreToolbarState = (toolbar: AbstractToolbar, state: string) => {
|
||||
if (state) {
|
||||
// deserializeState throws on invalid argument.
|
||||
try {
|
||||
toolbar.deserializeState(state);
|
||||
} catch (e) {
|
||||
console.warn('Error deserializing toolbar state: ', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 createJsDrawEditor = (
|
||||
callbacks: ImageEditorCallbacks,
|
||||
initialToolbarState: string,
|
||||
locale: string,
|
||||
defaultLocalizations: LocalizedStrings,
|
||||
|
||||
// Intended for automated tests.
|
||||
editorSettings: Partial<EditorSettings> = {},
|
||||
) => {
|
||||
const parentElement = document.body;
|
||||
const editor = new Editor(parentElement, {
|
||||
// Try to use the Joplin locale, but fall back to the system locale if
|
||||
// js-draw doesn't support it.
|
||||
localization: {
|
||||
...getLocalizationTable([locale, ...navigator.languages]),
|
||||
...defaultLocalizations,
|
||||
},
|
||||
iconProvider: new MaterialIconProvider(),
|
||||
...editorSettings,
|
||||
});
|
||||
|
||||
const toolbar = editor.addToolbar();
|
||||
|
||||
const maxSpacerSize = '20px';
|
||||
toolbar.addSpacer({
|
||||
grow: 1,
|
||||
maxSize: maxSpacerSize,
|
||||
});
|
||||
|
||||
// Override the default "Exit" label:
|
||||
toolbar.addExitButton(
|
||||
() => callbacks.closeEditor(true), {
|
||||
label: defaultLocalizations.close,
|
||||
},
|
||||
);
|
||||
|
||||
toolbar.addSpacer({
|
||||
grow: 1,
|
||||
maxSize: maxSpacerSize,
|
||||
});
|
||||
|
||||
// saveButton needs to be defined after the following callbacks.
|
||||
// As such, this variable can't be made const.
|
||||
// eslint-disable-next-line prefer-const
|
||||
let saveButton: BaseWidget;
|
||||
|
||||
let lastHadChanges: boolean|null = null;
|
||||
const setImageHasChanges = (hasChanges: boolean) => {
|
||||
if (lastHadChanges !== hasChanges) {
|
||||
saveButton.setDisabled(!hasChanges);
|
||||
callbacks.setImageHasChanges(hasChanges);
|
||||
lastHadChanges = hasChanges;
|
||||
}
|
||||
};
|
||||
|
||||
const saveNow = () => {
|
||||
callbacks.saveDrawing(editor.toSVG({
|
||||
// Grow small images to this minimum size
|
||||
minDimension: 50,
|
||||
}), false);
|
||||
|
||||
// The image is now up-to-date with the resource
|
||||
setImageHasChanges(false);
|
||||
};
|
||||
|
||||
saveButton = toolbar.addSaveButton(saveNow);
|
||||
|
||||
// Load and save toolbar-realated state (e.g. pen sizes/colors).
|
||||
restoreToolbarState(toolbar, initialToolbarState);
|
||||
listenToolbarState(editor, toolbar);
|
||||
|
||||
setImageHasChanges(false);
|
||||
|
||||
editor.notifier.on(EditorEventType.UndoRedoStackUpdated, () => {
|
||||
setImageHasChanges(true);
|
||||
});
|
||||
|
||||
// Disable save (a full save can't be done until the entire image
|
||||
// has been loaded).
|
||||
saveButton.setDisabled(true);
|
||||
|
||||
// Show a loading message until the template is loaded.
|
||||
editor.showLoadingWarning(0);
|
||||
editor.setReadOnly(true);
|
||||
|
||||
const editorControl = {
|
||||
editor,
|
||||
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => {
|
||||
// loadFromSVG shows its own loading message. Hide the original.
|
||||
editor.hideLoadingWarning();
|
||||
|
||||
if (svgData && svgData.length > 0) {
|
||||
await editor.loadFromSVG(svgData);
|
||||
} else {
|
||||
await applyTemplateToEditor(editor, templateData);
|
||||
|
||||
// The editor expects to be saved initially (without
|
||||
// unsaved changes). Save now.
|
||||
saveNow();
|
||||
}
|
||||
|
||||
// We can now edit and save safely (without data loss).
|
||||
editor.setReadOnly(false);
|
||||
|
||||
void startAutosaveLoop(editor, callbacks.saveDrawing);
|
||||
watchEditorForTemplateChanges(editor, templateData, callbacks.updateEditorTemplate);
|
||||
},
|
||||
onThemeUpdate: () => {
|
||||
// Slightly adjusts the given editor's theme colors. This ensures that the colors chosen for
|
||||
// the editor have proper contrast.
|
||||
adjustEditorThemeForContrast(editor);
|
||||
},
|
||||
saveNow,
|
||||
saveThenExit: async () => {
|
||||
saveNow();
|
||||
|
||||
// Don't show a confirmation dialog -- it's possible that
|
||||
// the code outside of the WebView still thinks changes haven't
|
||||
// been saved:
|
||||
const showConfirmation = false;
|
||||
callbacks.closeEditor(showConfirmation);
|
||||
},
|
||||
};
|
||||
|
||||
editorControl.onThemeUpdate();
|
||||
|
||||
return editorControl;
|
||||
};
|
||||
|
||||
|
||||
export default createJsDrawEditor;
|
||||
@@ -1,26 +0,0 @@
|
||||
|
||||
import { Editor } from 'js-draw';
|
||||
import { SaveDrawingCallback } from './types';
|
||||
|
||||
const startAutosaveLoop = async (
|
||||
editor: Editor,
|
||||
saveDrawing: SaveDrawingCallback,
|
||||
) => {
|
||||
// Autosave every two minutes.
|
||||
const delayTime = 1000 * 60 * 2; // ms
|
||||
|
||||
const createAutosave = async () => {
|
||||
const savedSVG = await editor.toSVGAsync();
|
||||
saveDrawing(savedSVG, true);
|
||||
};
|
||||
|
||||
while (true) {
|
||||
await (new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), delayTime);
|
||||
}));
|
||||
|
||||
await createAutosave();
|
||||
}
|
||||
};
|
||||
|
||||
export default startAutosaveLoop;
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
export type SaveDrawingCallback = (svgElement: SVGElement, isAutosave: boolean)=> void;
|
||||
export type UpdateEditorTemplateCallback = (newTemplate: string)=> void;
|
||||
|
||||
export interface ImageEditorCallbacks {
|
||||
saveDrawing: SaveDrawingCallback;
|
||||
updateEditorTemplate: UpdateEditorTemplateCallback;
|
||||
|
||||
closeEditor: (promptIfUnsaved: boolean)=> void;
|
||||
setImageHasChanges: (hasChanges: boolean)=> void;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
save: string;
|
||||
close: string;
|
||||
undo: string;
|
||||
redo: string;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Editor, BackgroundComponent, EditorEventType, Vec2 } from 'js-draw';
|
||||
|
||||
const watchEditorForTemplateChanges = (
|
||||
editor: Editor, initialTemplate: string, updateTemplate: (templateData: string)=> void,
|
||||
) => {
|
||||
const computeTemplate = (): string => {
|
||||
let backgroundSize: Vec2|null = null;
|
||||
|
||||
// Only store the background size if the size isn't determined
|
||||
// by the editor content. In this case, the background always
|
||||
// appears to be full screen.
|
||||
if (!editor.image.getAutoresizeEnabled()) {
|
||||
backgroundSize = editor.getImportExportRect().size;
|
||||
|
||||
// Constrain the size: Don't allow an extremely small or extremely large tempalte.
|
||||
// Map components to constrained components.
|
||||
backgroundSize = backgroundSize.map(component => {
|
||||
const minDimen = 45;
|
||||
const maxDimen = 5000;
|
||||
|
||||
return Math.max(Math.min(component, maxDimen), minDimen);
|
||||
});
|
||||
}
|
||||
|
||||
// Find the topmost background component (if any)
|
||||
let backgroundComponent: BackgroundComponent|null = null;
|
||||
for (const component of editor.image.getBackgroundComponents()) {
|
||||
if (component instanceof BackgroundComponent) {
|
||||
backgroundComponent = component;
|
||||
}
|
||||
}
|
||||
|
||||
const templateData = {
|
||||
imageSize: backgroundSize?.xy,
|
||||
backgroundData: backgroundComponent?.serialize(),
|
||||
autoresize: editor.image.getAutoresizeEnabled(),
|
||||
};
|
||||
return JSON.stringify(templateData);
|
||||
};
|
||||
|
||||
let lastTemplate = initialTemplate;
|
||||
const updateTemplateIfNecessary = () => {
|
||||
const newTemplate = computeTemplate();
|
||||
|
||||
if (newTemplate !== lastTemplate) {
|
||||
updateTemplate(newTemplate);
|
||||
lastTemplate = newTemplate;
|
||||
}
|
||||
};
|
||||
|
||||
// Whenever a command is done/undone, re-calculate the template & save.
|
||||
editor.notifier.on(EditorEventType.CommandDone, () => {
|
||||
updateTemplateIfNecessary();
|
||||
});
|
||||
|
||||
editor.notifier.on(EditorEventType.CommandUndone, () => {
|
||||
updateTemplateIfNecessary();
|
||||
});
|
||||
};
|
||||
|
||||
export default watchEditorForTemplateChanges;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Alert } from 'react-native';
|
||||
import { clearAutosave, getAutosaveFilepath, readAutosave } from './autosave';
|
||||
|
||||
export type RestoreAutosaveCallback = (data: string)=> void;
|
||||
|
||||
const promptRestoreAutosave = async (onRestoreAutosave: RestoreAutosaveCallback) => {
|
||||
const autosavePath = getAutosaveFilepath();
|
||||
|
||||
if (await shim.fsDriver().exists(autosavePath)) {
|
||||
const title: string|null = null;
|
||||
const message = _(
|
||||
'An autosaved drawing was found. Attach a copy of it to the note?',
|
||||
);
|
||||
|
||||
Alert.alert(title, message, [
|
||||
{
|
||||
text: _('Discard'),
|
||||
onPress: async () => {
|
||||
await clearAutosave();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Attach'),
|
||||
onPress: async () => {
|
||||
const autosaveData = await readAutosave();
|
||||
await clearAutosave();
|
||||
|
||||
onRestoreAutosave(autosaveData);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
export default promptRestoreAutosave;
|
||||
@@ -178,7 +178,7 @@ const useEditorControl = (
|
||||
execCommand(EditorCommandType.ToggleNumberedList);
|
||||
},
|
||||
toggleUnorderedList() {
|
||||
execCommand(EditorCommandType.ToggleBulletedList);
|
||||
execCommand(EditorCommandType.ToggleCheckList);
|
||||
},
|
||||
toggleTaskList() {
|
||||
execCommand(EditorCommandType.ToggleCheckList);
|
||||
|
||||
@@ -308,7 +308,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityHint={_('Show/hide the sidebar')}
|
||||
accessibilityRole="button">
|
||||
<View style={styles.sideMenuButton}>
|
||||
<Icon name="menu" style={styles.topIcon} />
|
||||
<Icon name="md-menu" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -324,7 +324,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
accessibilityRole="button">
|
||||
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
|
||||
<Icon
|
||||
name="arrow-back"
|
||||
name="md-arrow-back"
|
||||
style={styles.topIcon}
|
||||
/>
|
||||
</View>
|
||||
@@ -337,7 +337,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
) {
|
||||
if (!show) return null;
|
||||
|
||||
const icon = disabled ? <Icon name="checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
|
||||
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -407,7 +407,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
description={_('Select all')}
|
||||
contentStyle={styles.iconButton}
|
||||
>
|
||||
<Icon name="checkmark-circle-outline" style={styles.topIcon} />
|
||||
<Icon name="md-checkmark-circle-outline" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -421,7 +421,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
themeId={themeId}
|
||||
contentStyle={styles.iconButton}
|
||||
>
|
||||
<Icon name="search" style={styles.topIcon} />
|
||||
<Icon name="md-search" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -439,7 +439,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
|
||||
>
|
||||
<Icon name="trash" style={styles.topIcon} />
|
||||
<Icon name="md-trash" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -457,7 +457,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
}
|
||||
contentStyle={disabled ? styles.iconButtonDisabled : styles.iconButton}
|
||||
>
|
||||
<Icon name="copy" style={styles.topIcon} />
|
||||
<Icon name="md-copy" style={styles.topIcon} />
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
@@ -602,7 +602,7 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
|
||||
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={contextMenuStyle}>
|
||||
<View accessibilityLabel={_('Actions')}>
|
||||
<Icon name="ellipsis-vertical" style={this.styles().contextMenuTrigger} />
|
||||
<Icon name="md-ellipsis-vertical" style={this.styles().contextMenuTrigger} />
|
||||
</View>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
|
||||
@@ -40,7 +40,7 @@ class Checkbox extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconName = this.state.checked ? 'checkbox-outline' : 'square-outline';
|
||||
const iconName = this.state.checked ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
|
||||
const style = this.props.style ? { ...this.props.style } : {};
|
||||
style.justifyContent = 'center';
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FlatList, View, Text, Button, StyleSheet, Platform, Alert } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import { reg } from '@joplin/lib/registry.js';
|
||||
import { ScreenHeader } from '../ScreenHeader';
|
||||
import time from '@joplin/lib/time';
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuOptionType } from '../ScreenHeader';
|
||||
import { AppState } from '../../utils/types';
|
||||
import Share from 'react-native-share';
|
||||
import { writeTextToCacheFile } from '../../utils/ShareUtils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
const logger = Logger.create('LogScreen');
|
||||
|
||||
class LogScreenComponent extends BaseScreenComponent {
|
||||
private readonly menuOptions: MenuOptionType[];
|
||||
|
||||
public static navigationOptions(): any {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
logEntries: [],
|
||||
showErrorsOnly: false,
|
||||
};
|
||||
this.styles_ = {};
|
||||
|
||||
this.menuOptions = [
|
||||
{
|
||||
title: _('Share'),
|
||||
onPress: () => {
|
||||
void this.onSharePress();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async onSharePress() {
|
||||
const limit: number|null = null; // no limit
|
||||
const levels = this.getLogLevels(this.state.showErrorsOnly);
|
||||
const allEntries: any[] = await reg.logger().lastEntries(limit, { levels });
|
||||
const logData = allEntries.map(entry => this.formatLogEntry(entry)).join('\n');
|
||||
|
||||
let fileToShare;
|
||||
try {
|
||||
// Using a .txt file extension causes a "No valid provider found from URL" error
|
||||
// and blank share sheet on iOS for larger log files (around 200 KiB).
|
||||
fileToShare = await writeTextToCacheFile(logData, 'mobile-log.log');
|
||||
|
||||
await Share.open({
|
||||
type: 'text/plain',
|
||||
filename: 'log.txt',
|
||||
url: `file://${fileToShare}`,
|
||||
failOnCancel: false,
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error('Unable to share log data:', e);
|
||||
|
||||
// Display a message to the user (e.g. in the case where the user is out of disk space).
|
||||
Alert.alert(_('Error'), _('Unable to share log data. Reason: %s', e.toString()));
|
||||
} finally {
|
||||
if (fileToShare) {
|
||||
await shim.fsDriver().remove(fileToShare);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public styles() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles: any = {
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
rowText: {
|
||||
fontSize: 10,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
if (Platform.OS !== 'ios') {
|
||||
// Crashes on iOS with error "Unrecognized font family 'monospace'"
|
||||
styles.rowText.fontFamily = 'monospace';
|
||||
}
|
||||
|
||||
styles.rowTextError = { ...styles.rowText };
|
||||
styles.rowTextError.color = theme.colorError;
|
||||
|
||||
styles.rowTextWarn = { ...styles.rowText };
|
||||
styles.rowTextWarn.color = theme.colorWarn;
|
||||
|
||||
this.styles_[this.props.themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.themeId];
|
||||
}
|
||||
|
||||
public UNSAFE_componentWillMount() {
|
||||
void this.resfreshLogEntries();
|
||||
}
|
||||
|
||||
private getLogLevels(showErrorsOnly: boolean) {
|
||||
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
private async resfreshLogEntries(showErrorsOnly: boolean = null) {
|
||||
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
|
||||
|
||||
const levels = this.getLogLevels(showErrorsOnly);
|
||||
|
||||
this.setState({
|
||||
logEntries: await reg.logger().lastEntries(1000, { levels: levels }),
|
||||
showErrorsOnly: showErrorsOnly,
|
||||
});
|
||||
}
|
||||
|
||||
private toggleErrorsOnly() {
|
||||
void this.resfreshLogEntries(!this.state.showErrorsOnly);
|
||||
}
|
||||
|
||||
private formatLogEntry(item: any) {
|
||||
return `${time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss')}: ${item.message}`;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const renderRow = ({ item }: any) => {
|
||||
let textStyle = this.styles().rowText;
|
||||
if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
|
||||
if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
|
||||
|
||||
return (
|
||||
<View style={this.styles().row}>
|
||||
<Text style={textStyle}>{this.formatLogEntry(item)}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader
|
||||
title={_('Log')}
|
||||
menuOptions={this.menuOptions}/>
|
||||
<FlatList
|
||||
data={this.state.logEntries}
|
||||
renderItem={renderRow}
|
||||
keyExtractor={item => { return `${item.id}`; }}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flex: 1, marginRight: 5 }}>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={() => {
|
||||
void this.resfreshLogEntries();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button
|
||||
title={this.state.showErrorsOnly ? _('Show all') : _('Errors only')}
|
||||
onPress={() => {
|
||||
this.toggleErrorsOnly();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogScreen = connect((state: AppState) => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
})(LogScreenComponent as any);
|
||||
|
||||
export default LogScreen;
|
||||
@@ -42,11 +42,8 @@ import { ImagePickerResponse, launchImageLibrary } from 'react-native-image-pick
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
import ShareExtension from '../../utils/ShareExtension.js';
|
||||
import CameraView from '../CameraView';
|
||||
import { NoteEntity, ResourceEntity } from '@joplin/lib/services/database/types';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import ImageEditor from '../NoteEditor/ImageEditor/ImageEditor';
|
||||
import promptRestoreAutosave from '../NoteEditor/ImageEditor/promptRestoreAutosave';
|
||||
import isEditableResource from '../NoteEditor/ImageEditor/isEditableResource';
|
||||
import VoiceTypingDialog from '../voiceTyping/VoiceTypingDialog';
|
||||
import { voskEnabled } from '../../services/voiceTyping/vosk';
|
||||
import { isSupportedLanguage } from '../../services/voiceTyping/vosk.android';
|
||||
@@ -79,9 +76,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
noteTagDialogShown: false,
|
||||
fromShare: false,
|
||||
showCamera: false,
|
||||
showImageEditor: false,
|
||||
loadImageEditorData: null,
|
||||
imageEditorResource: null,
|
||||
noteResources: {},
|
||||
|
||||
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
|
||||
@@ -201,8 +195,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}, 5);
|
||||
} else if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
if (!(await Resource.isReady(item))) throw new Error(_('This attachment is not downloaded or not decrypted yet.'));
|
||||
|
||||
const resourcePath = Resource.fullPath(item);
|
||||
|
||||
logger.info(`Opening resource: ${resourcePath}`);
|
||||
await FileViewer.open(resourcePath);
|
||||
} else {
|
||||
@@ -457,7 +451,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
void ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: any, prevState: any) {
|
||||
public componentDidUpdate(prevProps: any) {
|
||||
if (this.doFocusUpdate_) {
|
||||
this.doFocusUpdate_ = false;
|
||||
this.focusUpdate();
|
||||
@@ -469,22 +463,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
options: this.sideMenuOptions(),
|
||||
});
|
||||
}
|
||||
|
||||
if (prevState.isLoading !== this.state.isLoading && !this.state.isLoading) {
|
||||
// If there's autosave data, prompt the user to restore it.
|
||||
void promptRestoreAutosave((drawingData: string) => {
|
||||
void this.attachNewDrawing(drawingData);
|
||||
});
|
||||
}
|
||||
|
||||
// Disable opening/closing the side menu with touch gestures
|
||||
// when the image editor is open.
|
||||
if (prevState.showImageEditor !== this.state.showImageEditor) {
|
||||
this.props.dispatch({
|
||||
type: 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED',
|
||||
disableSideMenuGestures: this.state.showImageEditor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
@@ -654,10 +632,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return await saveOriginalImage();
|
||||
}
|
||||
|
||||
public async attachFile(pickerResponse: any, fileType: string): Promise<ResourceEntity|null> {
|
||||
public async attachFile(pickerResponse: any, fileType: string) {
|
||||
if (!pickerResponse) {
|
||||
// User has cancelled
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const localFilePath = Platform.select({
|
||||
@@ -684,7 +662,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
reg.logger().info(`Got file: ${localFilePath}`);
|
||||
reg.logger().info(`Got type: ${mimeType}`);
|
||||
|
||||
let resource: ResourceEntity = Resource.new();
|
||||
let resource = Resource.new();
|
||||
resource.id = uuid.create();
|
||||
resource.mime = mimeType;
|
||||
resource.title = pickerResponse.name ? pickerResponse.name : '';
|
||||
@@ -697,11 +675,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
try {
|
||||
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg' || mimeType === 'image/png') {
|
||||
const done = await this.resizeImage(localFilePath, targetPath, mimeType);
|
||||
if (!done) return null;
|
||||
if (!done) return;
|
||||
} else {
|
||||
if (fileType === 'image' && mimeType !== 'image/svg+xml') {
|
||||
if (fileType === 'image') {
|
||||
dialogs.error(this, _('Unsupported image type: %s', mimeType));
|
||||
return null;
|
||||
return;
|
||||
} else {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
const stat = await shim.fsDriver().stat(targetPath);
|
||||
@@ -715,7 +693,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
} catch (error) {
|
||||
reg.logger().warn('Could not attach file:', error);
|
||||
await dialogs.error(this, error.message);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const itDoes = await shim.fsDriver().waitTillExists(targetPath);
|
||||
@@ -757,8 +735,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.refreshResource(resource, newNote.body);
|
||||
|
||||
this.scheduleSave();
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
private async attachPhoto_onPress() {
|
||||
@@ -800,82 +776,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
private async attachNewDrawing(svgData: string) {
|
||||
const filePath = `${Setting.value('resourceDir')}/saved-drawing.joplin.svg`;
|
||||
await shim.fsDriver().writeFile(filePath, svgData, 'utf8');
|
||||
logger.info('Saved new drawing to', filePath);
|
||||
|
||||
return await this.attachFile({
|
||||
uri: filePath,
|
||||
name: _('Drawing'),
|
||||
}, 'image');
|
||||
}
|
||||
|
||||
private drawPicture_onPress = async () => {
|
||||
// Create a new empty drawing and attach it now.
|
||||
const resource = await this.attachNewDrawing('');
|
||||
|
||||
this.setState({
|
||||
showImageEditor: true,
|
||||
loadImageEditorData: null,
|
||||
imageEditorResource: resource,
|
||||
});
|
||||
};
|
||||
|
||||
private async updateDrawing(svgData: string) {
|
||||
let resource: ResourceEntity|null = this.state.imageEditorResource;
|
||||
|
||||
if (!resource) {
|
||||
throw new Error('No resource is loaded in the editor');
|
||||
}
|
||||
|
||||
const resourcePath = Resource.fullPath(resource);
|
||||
|
||||
const filePath = resourcePath;
|
||||
await shim.fsDriver().writeFile(filePath, svgData, 'utf8');
|
||||
logger.info('Saved drawing to', filePath);
|
||||
|
||||
resource = await Resource.save(resource, { isNew: false });
|
||||
await this.refreshResource(resource);
|
||||
}
|
||||
|
||||
private onSaveDrawing = async (svgData: string) => {
|
||||
await this.updateDrawing(svgData);
|
||||
};
|
||||
|
||||
private onCloseDrawing = () => {
|
||||
this.setState({ showImageEditor: false });
|
||||
};
|
||||
|
||||
private async editDrawing(item: BaseItem) {
|
||||
const filePath = Resource.fullPath(item);
|
||||
this.setState({
|
||||
showImageEditor: true,
|
||||
loadImageEditorData: async () => {
|
||||
return await shim.fsDriver().readFile(filePath);
|
||||
},
|
||||
imageEditorResource: item,
|
||||
});
|
||||
}
|
||||
|
||||
private onEditResource = async (message: string) => {
|
||||
const messageData = /^edit:(.*)$/.exec(message);
|
||||
if (!messageData) {
|
||||
throw new Error('onEditResource: Error: Invalid message');
|
||||
}
|
||||
|
||||
const resourceId = messageData[1];
|
||||
|
||||
const resource = await BaseItem.loadItemById(resourceId);
|
||||
await Resource.requireIsReady(resource);
|
||||
|
||||
if (isEditableResource(resource.mime)) {
|
||||
await this.editDrawing(resource);
|
||||
} else {
|
||||
throw new Error(_('Unable to edit resource of type %s', resource.mime));
|
||||
}
|
||||
};
|
||||
|
||||
private async attachFile_onPress() {
|
||||
const response = await this.pickDocuments();
|
||||
for (const asset of response) {
|
||||
@@ -1107,12 +1007,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
}
|
||||
|
||||
output.push({
|
||||
title: _('Draw picture'),
|
||||
onPress: () => this.drawPicture_onPress(),
|
||||
disabled: readOnly,
|
||||
});
|
||||
|
||||
if (isTodo) {
|
||||
output.push({
|
||||
title: _('Set alarm'),
|
||||
@@ -1300,13 +1194,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (this.state.showCamera) {
|
||||
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
|
||||
} else if (this.state.showImageEditor) {
|
||||
return <ImageEditor
|
||||
loadInitialSVGData={this.state.loadImageEditorData}
|
||||
themeId={this.props.themeId}
|
||||
onSave={this.onSaveDrawing}
|
||||
onExit={this.onCloseDrawing}
|
||||
/>;
|
||||
}
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
@@ -1332,7 +1219,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
noteHash={this.props.noteHash}
|
||||
onCheckboxChange={this.onBodyViewerCheckboxChange}
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onRequestEditResource={this.onEditResource}
|
||||
onLoadEnd={this.onBodyViewerLoadEnd}
|
||||
/>
|
||||
);
|
||||
@@ -1406,7 +1292,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
const editButton = {
|
||||
label: _('Edit'),
|
||||
icon: 'create',
|
||||
icon: 'md-create',
|
||||
onPress: () => {
|
||||
this.setState({ mode: 'edit' });
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class NoteTagsDialogComponent extends React.Component {
|
||||
|
||||
this.renderTag = data => {
|
||||
const tag = data.item;
|
||||
const iconName = noteHasTag(tag.id) ? 'checkbox-outline' : 'square-outline';
|
||||
const iconName = noteHasTag(tag.id) ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
return (
|
||||
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
|
||||
<View style={this.styles().tagIconText}>
|
||||
|
||||
@@ -236,7 +236,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'checkbox-outline',
|
||||
icon: 'md-checkbox-outline',
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
@@ -246,7 +246,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
|
||||
void this.newNoteNavigate(buttonFolderId, isTodo);
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'document',
|
||||
icon: 'md-document',
|
||||
});
|
||||
return <ActionButton buttons={buttons}/>;
|
||||
}
|
||||
|
||||
135
packages/app-mobile/components/screens/log.js
Normal file
135
packages/app-mobile/components/screens/log.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const React = require('react');
|
||||
|
||||
const { FlatList, View, Text, Button, StyleSheet, Platform } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('@joplin/lib/registry.js');
|
||||
const { ScreenHeader } = require('../ScreenHeader');
|
||||
const time = require('@joplin/lib/time').default;
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { _ } = require('@joplin/lib/locale');
|
||||
|
||||
class LogScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
logEntries: [],
|
||||
showErrorsOnly: false,
|
||||
};
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
if (this.styles_[this.props.themeId]) return this.styles_[this.props.themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const styles = {
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
rowText: {
|
||||
fontSize: 10,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
if (Platform.OS !== 'ios') {
|
||||
// Crashes on iOS with error "Unrecognized font family 'monospace'"
|
||||
styles.rowText.fontFamily = 'monospace';
|
||||
}
|
||||
|
||||
styles.rowTextError = { ...styles.rowText };
|
||||
styles.rowTextError.color = theme.colorError;
|
||||
|
||||
styles.rowTextWarn = { ...styles.rowText };
|
||||
styles.rowTextWarn.color = theme.colorWarn;
|
||||
|
||||
this.styles_[this.props.themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.themeId];
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.resfreshLogEntries();
|
||||
}
|
||||
|
||||
async resfreshLogEntries(showErrorsOnly = null) {
|
||||
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
|
||||
|
||||
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
|
||||
this.setState({
|
||||
logEntries: await reg.logger().lastEntries(1000, { levels: levels }),
|
||||
showErrorsOnly: showErrorsOnly,
|
||||
});
|
||||
}
|
||||
|
||||
toggleErrorsOnly() {
|
||||
this.resfreshLogEntries(!this.state.showErrorsOnly);
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderRow = ({ item }) => {
|
||||
let textStyle = this.styles().rowText;
|
||||
if (item.level === Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
|
||||
if (item.level === Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
|
||||
|
||||
return (
|
||||
<View style={this.styles().row}>
|
||||
<Text style={textStyle}>{`${time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss')}: ${item.message}`}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.themeId).root}>
|
||||
<ScreenHeader title={_('Log')} />
|
||||
<FlatList
|
||||
data={this.state.logEntries}
|
||||
renderItem={renderRow}
|
||||
keyExtractor={item => { return `${item.id}`; }}
|
||||
/>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flex: 1, marginRight: 5 }}>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={() => {
|
||||
this.resfreshLogEntries();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button
|
||||
title={this.state.showErrorsOnly ? _('Show all') : _('Errors only')}
|
||||
onPress={() => {
|
||||
this.toggleErrorsOnly();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogScreen = connect(state => {
|
||||
return {
|
||||
themeId: state.settings.theme,
|
||||
};
|
||||
})(LogScreenComponent);
|
||||
|
||||
module.exports = { LogScreen };
|
||||
@@ -6,6 +6,7 @@ import ScreenHeader from '../ScreenHeader';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import gotoAnythingStyleQuery from '@joplin/lib/services/searchengine/gotoAnythingStyleQuery';
|
||||
const { NoteItem } = require('../note-item.js');
|
||||
const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
@@ -94,6 +95,8 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
public async refreshSearch(query: string = null) {
|
||||
if (!this.props.visible) return;
|
||||
|
||||
query = gotoAnythingStyleQuery(query);
|
||||
|
||||
let notes = [];
|
||||
|
||||
if (query) {
|
||||
@@ -190,7 +193,7 @@ class SearchScreenComponent extends BaseScreenComponent {
|
||||
onPress={() => this.clearButton_press()}
|
||||
accessibilityLabel={_('Clear')}
|
||||
>
|
||||
<Icon name="close-circle" style={this.styles().clearIcon} />
|
||||
<Icon name="md-close-circle" style={this.styles().clearIcon} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
styles.sideButtonSelected = { ...styles.sideButton, backgroundColor: theme.selectedColor };
|
||||
styles.sideButtonText = { ...styles.buttonText };
|
||||
|
||||
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 21 };
|
||||
styles.emptyFolderIcon = { ...styles.sidebarIcon, marginRight: folderIconRightMargin, width: 20 };
|
||||
|
||||
return StyleSheet.create(styles);
|
||||
}, [props.themeId]);
|
||||
@@ -421,15 +421,15 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
|
||||
items.push(makeDivider('divider_1'));
|
||||
|
||||
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'folder-open', newFolderButton_press));
|
||||
items.push(renderSidebarButton('newFolder_button', _('New Notebook'), 'md-folder-open', newFolderButton_press));
|
||||
|
||||
items.push(renderSidebarButton('tag_button', _('Tags'), 'pricetag', tagButton_press));
|
||||
items.push(renderSidebarButton('tag_button', _('Tags'), 'md-pricetag', tagButton_press));
|
||||
|
||||
if (props.profileConfig && props.profileConfig.profiles.length > 1) {
|
||||
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'people-circle-outline', switchProfileButton_press));
|
||||
items.push(renderSidebarButton('switchProfile_button', _('Switch profile'), 'md-people-circle-outline', switchProfileButton_press));
|
||||
}
|
||||
|
||||
items.push(renderSidebarButton('config_button', _('Configuration'), 'settings', configButton_press));
|
||||
items.push(renderSidebarButton('config_button', _('Configuration'), 'md-settings', configButton_press));
|
||||
|
||||
items.push(makeDivider('divider_2'));
|
||||
|
||||
@@ -451,7 +451,7 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
if (resourceFetcherText) fullReport.push(resourceFetcherText);
|
||||
if (decryptionReportText) fullReport.push(decryptionReportText);
|
||||
|
||||
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'sync', synchronize_press));
|
||||
items.push(renderSidebarButton('synchronize_button', !props.syncStarted ? _('Synchronise') : _('Cancel'), 'md-sync', synchronize_press));
|
||||
|
||||
if (fullReport.length) {
|
||||
items.push(
|
||||
@@ -480,11 +480,11 @@ const SideMenuContentComponent = (props: Props) => {
|
||||
// using padding. So instead creating blank elements for padding bottom and top.
|
||||
items.push(<View style={{ height: theme.marginTop }} key="bottom_top_hack" />);
|
||||
|
||||
items.push(renderSidebarButton('all_notes', _('All notes'), 'document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
|
||||
items.push(renderSidebarButton('all_notes', _('All notes'), 'md-document', allNotesButton_press, props.notesParentType === 'SmartFilter'));
|
||||
|
||||
items.push(makeDivider('divider_all'));
|
||||
|
||||
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'folder'));
|
||||
items.push(renderSidebarButton('folder_header', _('Notebooks'), 'md-folder'));
|
||||
|
||||
if (props.folders.length) {
|
||||
const result = shared.renderFolders(props, renderFolderItem, false);
|
||||
|
||||
@@ -407,9 +407,6 @@
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Regular.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome6_Solid.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
|
||||
@@ -430,9 +427,6 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Regular.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome6_Solid.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
|
||||
|
||||
@@ -288,7 +288,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-get-random-values (1.9.0):
|
||||
- React-Core
|
||||
- react-native-image-picker (5.7.0):
|
||||
- react-native-image-picker (5.6.1):
|
||||
- React-Core
|
||||
- react-native-image-resizer (3.0.7):
|
||||
- React-Core
|
||||
@@ -306,7 +306,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.5.1):
|
||||
- react-native-webview (13.3.1):
|
||||
- React-Core
|
||||
- React-perflogger (0.71.10)
|
||||
- React-RCTActionSheet (0.71.10):
|
||||
@@ -398,9 +398,9 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.5.0):
|
||||
- RNDateTimePicker (7.4.2):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.9.0):
|
||||
- RNDeviceInfo (10.8.0):
|
||||
- React-Core
|
||||
- RNExitApp (2.0.0):
|
||||
- React-Core
|
||||
@@ -414,15 +414,15 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (9.4.1):
|
||||
- RNShare (9.2.4):
|
||||
- React-Core
|
||||
- RNVectorIcons (10.0.0):
|
||||
- RNVectorIcons (9.2.0):
|
||||
- React-Core
|
||||
- RNZipArchive (6.1.0):
|
||||
- RNZipArchive (6.0.9):
|
||||
- React-Core
|
||||
- RNZipArchive/Core (= 6.1.0)
|
||||
- RNZipArchive/Core (= 6.0.9)
|
||||
- SSZipArchive (~> 2.2)
|
||||
- RNZipArchive/Core (6.1.0):
|
||||
- RNZipArchive/Core (6.0.9):
|
||||
- React-Core
|
||||
- SSZipArchive (~> 2.2)
|
||||
- SSZipArchive (2.4.3)
|
||||
@@ -669,7 +669,7 @@ SPEC CHECKSUMS:
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
|
||||
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
|
||||
react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169
|
||||
react-native-image-picker: 5fcac5a5ffcb3737837f0617d43fd767249290de
|
||||
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
@@ -678,7 +678,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
|
||||
react-native-webview: c2b70afb1d910cdd8810375aecc6c2894e2ba061
|
||||
React-perflogger: 217095464d5c4bb70df0742fa86bf2a363693468
|
||||
React-RCTActionSheet: 8deae9b85a4cbc6a2243618ea62a374880a2c614
|
||||
React-RCTAnimation: 59c62353a8b59ce206044786c5d30e4754bffa64
|
||||
@@ -695,17 +695,17 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: b722c23030b744763cf51d80fc22f354653f65e0
|
||||
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
|
||||
RNDateTimePicker: a21bfcc694e3190f8ddb5aacb0dd16f97b6be79f
|
||||
RNDeviceInfo: 5795b418ed3451ebcaf39384e6cf51f60cb931c9
|
||||
RNExitApp: 00036cabe7bacbb413d276d5520bf74ba39afa6a
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNLocalize: dbea38dcb344bf80ff18a1757b1becf11f70cae4
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
|
||||
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
|
||||
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9
|
||||
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
|
||||
RNShare: 255701a138fcdef6c27dc93724548fd122e91f9e
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
RNZipArchive: 68a0c6db4b1c103f846f1559622050df254a3ade
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
Yoga: e7ea9e590e27460d28911403b894722354d73479
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ module.exports = {
|
||||
|
||||
// Do transform most packages in node_modules (transformations correct unrecognized
|
||||
// import syntax)
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/jest', '<rootDir>/node_modules/js-draw'],
|
||||
transformIgnorePatterns: ['<rootDir>/node_modules/jest'],
|
||||
|
||||
slowTestThreshold: 40,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@joplin/renderer": "~2.13",
|
||||
"@joplin/utils": "~2.13",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "7.5.0",
|
||||
"@react-native-community/datetimepicker": "7.4.2",
|
||||
"@react-native-community/geolocation": "3.0.6",
|
||||
"@react-native-community/netinfo": "9.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
@@ -46,7 +46,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.71.10",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.9.0",
|
||||
"react-native-device-info": "10.8.0",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "9.0.1",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
@@ -55,24 +55,24 @@
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-image-picker": "5.7.0",
|
||||
"react-native-image-picker": "5.6.1",
|
||||
"react-native-localize": "3.0.2",
|
||||
"react-native-modal-datetime-picker": "17.1.0",
|
||||
"react-native-paper": "5.10.6",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.7.2",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "9.4.1",
|
||||
"react-native-share": "9.2.4",
|
||||
"react-native-side-menu-updated": "1.3.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-vector-icons": "10.0.0",
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-vector-icons": "9.2.0",
|
||||
"react-native-version-info": "1.1.1",
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "13.5.1",
|
||||
"react-native-zip-archive": "6.1.0",
|
||||
"react-native-webview": "13.3.1",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-redux": "8.1.2",
|
||||
"redux": "4.2.1",
|
||||
"rn-fetch-blob": "0.12.0",
|
||||
@@ -81,24 +81,23 @@
|
||||
"string-natural-compare": "3.0.1",
|
||||
"tar-stream": "3.1.6",
|
||||
"timers": "0.1.1",
|
||||
"url": "0.11.3"
|
||||
"url": "0.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.20.2",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@babel/runtime": "7.20.0",
|
||||
"@joplin/tools": "~2.13",
|
||||
"@js-draw/material-icons": "1.5.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@testing-library/jest-native": "5.4.3",
|
||||
"@testing-library/react-native": "12.3.0",
|
||||
"@testing-library/react-native": "12.2.2",
|
||||
"@tsconfig/react-native": "2.0.2",
|
||||
"@types/fs-extra": "11.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/react-native": "0.70.6",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/tar-stream": "2.2.3",
|
||||
"@types/tar-stream": "2.2.2",
|
||||
"babel-jest": "29.6.4",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"execa": "4.1.0",
|
||||
@@ -107,11 +106,10 @@
|
||||
"jest": "29.6.4",
|
||||
"jest-environment-jsdom": "29.6.4",
|
||||
"jetifier": "2.0.0",
|
||||
"js-draw": "1.5.0",
|
||||
"jsdom": "22.1.0",
|
||||
"md5-file": "5.0.0",
|
||||
"metro-react-native-babel-preset": "0.73.9",
|
||||
"nodemon": "3.0.1",
|
||||
"nodemon": "2.0.22",
|
||||
"react-test-renderer": "18.2.0",
|
||||
"sqlite3": "5.1.6",
|
||||
"ts-jest": "29.1.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"3b957b10f317a91aaea146b3445709dd", files: {
|
||||
hash:"5ff17e40ffa32867404a4e20c6c80099", files: {
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ import NoteScreen from './components/screens/Note';
|
||||
import UpgradeSyncTargetScreen from './components/screens/UpgradeSyncTargetScreen';
|
||||
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||
import PoorManIntervals from '@joplin/lib/PoorManIntervals';
|
||||
import reducer, { NotesParent, parseNotesParent, serializeNotesParent } from '@joplin/lib/reducer';
|
||||
import reducer from '@joplin/lib/reducer';
|
||||
import ShareExtension from './utils/ShareExtension';
|
||||
import handleShared from './utils/shareHandler';
|
||||
import uuid from '@joplin/lib/uuid';
|
||||
@@ -60,7 +60,7 @@ import NotesScreen from './components/screens/Notes';
|
||||
const { TagsScreen } = require('./components/screens/tags.js');
|
||||
import ConfigScreen from './components/screens/ConfigScreen/ConfigScreen';
|
||||
const { FolderScreen } = require('./components/screens/folder.js');
|
||||
import LogScreen from './components/screens/LogScreen';
|
||||
const { LogScreen } = require('./components/screens/log.js');
|
||||
const { StatusScreen } = require('./components/screens/status.js');
|
||||
const { SearchScreen } = require('./components/screens/search.js');
|
||||
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
|
||||
@@ -199,11 +199,6 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
|
||||
|
||||
if (action.type === 'NAV_GO' && action.routeName === 'Notes') {
|
||||
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
||||
const notesParent: NotesParent = {
|
||||
type: action.smartFilterId ? 'SmartFilter' : 'Folder',
|
||||
selectedItemId: action.smartFilterId ? action.smartFilterId : newState.selectedFolderId,
|
||||
};
|
||||
Setting.setValue('notesParent', serializeNotesParent(notesParent));
|
||||
}
|
||||
|
||||
if (action.type === 'SYNC_GOT_ENCRYPTED_ITEM') {
|
||||
@@ -241,9 +236,7 @@ const appDefaultState: AppState = { ...defaultState, sideMenuOpenPercent: 0,
|
||||
route: DEFAULT_ROUTE,
|
||||
noteSelectionEnabled: false,
|
||||
noteSideMenuOptions: null,
|
||||
isOnMobileData: false,
|
||||
disableSideMenuGestures: false,
|
||||
};
|
||||
isOnMobileData: false };
|
||||
|
||||
const appReducer = (state = appDefaultState, action: any) => {
|
||||
let newState = state;
|
||||
@@ -404,11 +397,6 @@ const appReducer = (state = appDefaultState, action: any) => {
|
||||
newState.noteSideMenuOptions = action.options;
|
||||
break;
|
||||
|
||||
case 'SET_SIDE_MENU_TOUCH_GESTURES_DISABLED':
|
||||
newState = { ...state };
|
||||
newState.disableSideMenuGestures = action.disableSideMenuGestures;
|
||||
break;
|
||||
|
||||
case 'MOBILE_DATA_WARNING_UPDATE':
|
||||
|
||||
newState = { ...state };
|
||||
@@ -661,11 +649,7 @@ async function initialize(dispatch: Function) {
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
const notesParent = parseNotesParent(Setting.value('notesParent'), Setting.value('activeFolderId'));
|
||||
|
||||
if (notesParent && notesParent.type === 'SmartFilter') {
|
||||
dispatch(DEFAULT_ROUTE);
|
||||
} else if (!folder) {
|
||||
if (!folder) {
|
||||
dispatch(DEFAULT_ROUTE);
|
||||
} else {
|
||||
dispatch({
|
||||
@@ -1067,7 +1051,6 @@ class AppComponent extends React.Component {
|
||||
openMenuOffset={this.state.sideMenuWidth}
|
||||
menuPosition={menuPosition}
|
||||
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
|
||||
disableGestures={this.props.disableSideMenuGestures}
|
||||
onSliding={(percent: number) => {
|
||||
this.props.dispatch({
|
||||
type: 'SIDE_MENU_OPEN_PERCENT',
|
||||
@@ -1128,7 +1111,6 @@ const mapStateToProps = (state: any) => {
|
||||
routeName: state.route.routeName,
|
||||
themeId: state.settings.theme,
|
||||
noteSideMenuOptions: state.noteSideMenuOptions,
|
||||
disableSideMenuGestures: state.disableSideMenuGestures,
|
||||
biometricsDone: state.biometricsDone,
|
||||
biometricsEnabled: state.settings['security.biometricsEnabled'],
|
||||
};
|
||||
|
||||
@@ -202,10 +202,6 @@ const bundledFiles: BundledFile[] = [
|
||||
'codeMirrorBundle',
|
||||
`${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts`,
|
||||
),
|
||||
new BundledFile(
|
||||
'svgEditorBundle',
|
||||
`${mobileDir}/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts`,
|
||||
),
|
||||
];
|
||||
|
||||
export async function buildInjectedJS() {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { writeTextToCacheFile } from './ShareUtils';
|
||||
|
||||
describe('ShareUtils', () => {
|
||||
test('writeTextFileToCache should write given text to a cache file', async () => {
|
||||
const filePath1 = await writeTextToCacheFile('testing...', 'test1.txt');
|
||||
expect(await pathExists(filePath1)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,18 +6,13 @@ import { CachesDirectoryPath } from 'react-native-fs';
|
||||
// when refactoring this name, make sure to refactor the `SharePackage.java` (in android) as well
|
||||
const DIR_NAME = 'sharedFiles';
|
||||
|
||||
const makeShareCacheDirectory = async () => {
|
||||
const targetDir = `${CachesDirectoryPath}/${DIR_NAME}`;
|
||||
await shim.fsDriver().mkdir(targetDir);
|
||||
|
||||
return targetDir;
|
||||
};
|
||||
|
||||
// Copy a file to be shared to cache, renaming it to its orignal name
|
||||
export async function copyToCache(resource: ResourceEntity): Promise<string> {
|
||||
const filename = Resource.friendlySafeFilename(resource);
|
||||
|
||||
const targetDir = await makeShareCacheDirectory();
|
||||
const targetDir = `${CachesDirectoryPath}/${DIR_NAME}`;
|
||||
await shim.fsDriver().mkdir(targetDir);
|
||||
|
||||
const targetFile = `${targetDir}/${filename}`;
|
||||
|
||||
await shim.fsDriver().copy(Resource.fullPath(resource), targetFile);
|
||||
@@ -25,16 +20,6 @@ export async function copyToCache(resource: ResourceEntity): Promise<string> {
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
// fileName should be unique -- any file with fileName will be overwritten if it already exists.
|
||||
export const writeTextToCacheFile = async (text: string, fileName: string): Promise<string> => {
|
||||
const targetDir = await makeShareCacheDirectory();
|
||||
|
||||
const filePath = `${targetDir}/${fileName}`;
|
||||
await shim.fsDriver().writeFile(filePath, text, 'utf8');
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Clear previously shared files from cache
|
||||
export async function clearSharedFilesCache(): Promise<void> {
|
||||
return shim.fsDriver().remove(`${CachesDirectoryPath}/sharedFiles`);
|
||||
|
||||
@@ -16,7 +16,6 @@ const { setLocale, defaultLocale, closestSupportedLocale } = require('@joplin/li
|
||||
const injectedJs = {
|
||||
webviewLib: require('@joplin/lib/rnInjectedJs/webviewLib'),
|
||||
codeMirrorBundle: require('../lib/rnInjectedJs/CodeMirror.bundle'),
|
||||
svgEditorBundle: require('../lib/rnInjectedJs/createJsDrawEditor.bundle'),
|
||||
};
|
||||
|
||||
function shimInit() {
|
||||
|
||||
@@ -6,6 +6,5 @@ export interface AppState extends State {
|
||||
route: any;
|
||||
smartFilterId: string;
|
||||
noteSideMenuOptions: any;
|
||||
disableSideMenuGestures: boolean;
|
||||
themeId: number;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../testUtil/createTestEditor';
|
||||
import computeSelectionFormatting from './computeSelectionFormatting';
|
||||
|
||||
|
||||
describe('computeSelectionFormatting', () => {
|
||||
// The below tests rely on CodeMirror to correctly parse the document, which
|
||||
// can be buggy (and fail very rarely).
|
||||
jest.retryTimes(2);
|
||||
|
||||
it('should correctly compute formatting for partial links', async () => {
|
||||
// Start with the selection midway through the link
|
||||
const editor = await createTestEditor('A [partial link]', EditorSelection.cursor(4), ['Link']);
|
||||
|
||||
const formatting = computeSelectionFormatting(editor.state, false);
|
||||
expect(formatting.linkData).toMatchObject({
|
||||
linkText: null,
|
||||
linkURL: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,10 +20,7 @@ const computeSelectionFormatting = (state: EditorState, globalSpellcheck: boolea
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
linkText: null,
|
||||
linkURL: null,
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
// Find nodes that overlap/are within the selected region
|
||||
|
||||
@@ -17,17 +17,17 @@
|
||||
"devDependencies": {
|
||||
"@joplin/lib": "~2.13",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/react": "18.0.24",
|
||||
"@types/react-redux": "7.1.26",
|
||||
"@types/styled-components": "5.1.28",
|
||||
"jest": "29.6.3",
|
||||
"jest-environment-jsdom": "29.6.3",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"jest": "29.5.0",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "6.9.1",
|
||||
"@codemirror/autocomplete": "6.9.0",
|
||||
"@codemirror/commands": "6.2.5",
|
||||
"@codemirror/lang-cpp": "6.0.2",
|
||||
"@codemirror/lang-html": "6.4.6",
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.4",
|
||||
"@types/node": "18.17.19",
|
||||
"@typescript-eslint/eslint-plugin": "6.0.0",
|
||||
"@typescript-eslint/parser": "6.0.0",
|
||||
"@types/node": "18.17.14",
|
||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||
"@typescript-eslint/parser": "5.62.0",
|
||||
"coveralls": "3.1.1",
|
||||
"eslint": "8.47.0",
|
||||
"jest": "29.6.4",
|
||||
"prettier": "3.0.0",
|
||||
"prettier": "2.8.8",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
import { Store } from 'redux';
|
||||
import Plugin from '../Plugin';
|
||||
import { ListRenderer } from './noteListType';
|
||||
/**
|
||||
* This API allows you to customise how each note in the note list is rendered.
|
||||
* The renderer you implement follows a unidirectional data flow.
|
||||
*
|
||||
* The app provides the required dependencies whenever a note is updated - you
|
||||
* process these dependencies, and return some props, which are then passed to
|
||||
* your template and rendered. See [[[ListRenderer]]] for a detailed description
|
||||
* of each property of the renderer.
|
||||
*
|
||||
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/note_list_renderer)
|
||||
*
|
||||
* The default list renderer is implemented using the same API, so it worth checking it too:
|
||||
*
|
||||
* [Default list renderer](https://github.com/laurent22/joplin/tree/dev/packages/lib/services/noteList/defaultListRenderer.ts)
|
||||
*/
|
||||
export default class JoplinViewsNoteList {
|
||||
private plugin_;
|
||||
private store_;
|
||||
|
||||
@@ -12,145 +12,16 @@ export interface OnChangeEvent {
|
||||
}
|
||||
export type OnRenderNoteHandler = (props: any) => Promise<RenderNoteView>;
|
||||
export type OnChangeHandler = (event: OnChangeEvent) => Promise<void>;
|
||||
/**
|
||||
* Most of these are the built-in note properties, such as `note.title`,
|
||||
* `note.todo_completed`, etc.
|
||||
*
|
||||
* Additionally, the `item.*` properties are specific to the rendered item. The
|
||||
* most important being `item.selected`, which you can use to display the
|
||||
* selected note in a different way.
|
||||
*
|
||||
* Finally some special properties are provided to make it easier to render
|
||||
* notes. In particular, if possible prefer `note.titleHtml` to `note.title`
|
||||
* since some important processing has already been done on the string, such as
|
||||
* handling the search highlighter and escaping. Since it's HTML and already
|
||||
* escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax,
|
||||
* which disables escaping).
|
||||
*
|
||||
* `notes.tag` gives you the list of tags associated with the note.
|
||||
*
|
||||
* `note.isWatched` tells you if the note is currently opened in an external
|
||||
* editor. In which case you would generally display some indicator.
|
||||
*/
|
||||
export type ListRendererDepependency = ListRendererDatabaseDependency | 'item.size.width' | 'item.size.height' | 'item.selected' | 'note.titleHtml' | 'note.isWatched' | 'note.tags';
|
||||
export interface ListRenderer {
|
||||
/**
|
||||
* It must be unique to your plugin.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Can be top to bottom or left to right. Left to right gives you more
|
||||
* option to set the size of the items since you set both its width and
|
||||
* height.
|
||||
*/
|
||||
flow: ItemFlow;
|
||||
/**
|
||||
* The size of each item must be specified in advance for performance
|
||||
* reasons, and cannot be changed afterwards. If the item flow is top to
|
||||
* bottom, you only need to specificy the item height (the width will be
|
||||
* ignored).
|
||||
*/
|
||||
itemSize: Size;
|
||||
/**
|
||||
* The CSS is relative to the list item container. What will appear in the
|
||||
* page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use
|
||||
* child combinator with guarantee it will only apply to your own items. In
|
||||
* this example, the styling will apply to `.note-list-item > .content`:
|
||||
*
|
||||
* ```css
|
||||
* > .content {
|
||||
* padding: 10px;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In order to get syntax highlighting working here, it's recommended
|
||||
* installing an editor extension such as [es6-string-html VSCode
|
||||
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
|
||||
*/
|
||||
itemCss?: string;
|
||||
/**
|
||||
* List the dependencies that your plugin needs to render the note list
|
||||
* items. Only these will be passed to your `onRenderNote` handler. Ensure
|
||||
* that you do not add more than what you need since there is a performance
|
||||
* penalty for each property.
|
||||
*/
|
||||
dependencies: ListRendererDepependency[];
|
||||
/**
|
||||
* This is the HTML template that will be used to render the note list item.
|
||||
* This is a [Mustache template](https://github.com/janl/mustache.js) and it
|
||||
* will receive the variable you return from `onRenderNote` as tags. For
|
||||
* example, if you return a property named `formattedDate` from
|
||||
* `onRenderNote`, you can insert it in the template using `Created date:
|
||||
* {{formattedDate}}`.
|
||||
*
|
||||
* In order to get syntax highlighting working here, it's recommended
|
||||
* installing an editor extension such as [es6-string-html VSCode
|
||||
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
|
||||
*/
|
||||
itemTemplate: string;
|
||||
/**
|
||||
* This user-facing text is used for example in the View menu, so that your
|
||||
* renderer can be selected.
|
||||
*/
|
||||
label: () => Promise<string>;
|
||||
/**
|
||||
* This is where most of the real-time processing will happen. When a note
|
||||
* is rendered for the first time and every time it changes, this handler
|
||||
* receives the properties specified in the `dependencies` property. You can
|
||||
* then process them, load any additional data you need, and once done you
|
||||
* need to return the properties that are needed in the `itemTemplate` HTML.
|
||||
* Again, to use the formatted date example, you could have such a renderer:
|
||||
*
|
||||
* ```typescript
|
||||
* dependencies: [
|
||||
* 'note.title',
|
||||
* 'note.created_time',
|
||||
* ],
|
||||
*
|
||||
* itemTemplate: // html
|
||||
* `
|
||||
* <div>
|
||||
* Title: {{note.title}}<br/>
|
||||
* Date: {{formattedDate}}
|
||||
* </div>
|
||||
* `,
|
||||
*
|
||||
* onRenderNote: async (props: any) => {
|
||||
* const formattedDate = dayjs(props.note.created_time).format();
|
||||
* return {
|
||||
* // Also return the props, so that note.title is available from the
|
||||
* // template
|
||||
* ...props,
|
||||
* formattedDate,
|
||||
* }
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
onRenderNote: OnRenderNoteHandler;
|
||||
/**
|
||||
* This handler allows adding some interacivity to the note renderer -
|
||||
* whenever an input element within the item is changed (for example, when a
|
||||
* checkbox is clicked, or a text input is changed), this `onChange` handler
|
||||
* is going to be called.
|
||||
*
|
||||
* You can inspect `event.elementId` to know which element had some changes,
|
||||
* and `event.value` to know the new value. `event.noteId` also tells you
|
||||
* what note is affected, so that you can potentially apply changes to it.
|
||||
*
|
||||
* You specify the element ID, by setting a `data-id` attribute on the
|
||||
* input.
|
||||
*
|
||||
* For example, if you have such a template:
|
||||
*
|
||||
* ```html
|
||||
* <div>
|
||||
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* The event handler will receive an event with `elementId` set to
|
||||
* `noteTitleInput`.
|
||||
*/
|
||||
onChange?: OnChangeHandler;
|
||||
}
|
||||
export {};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable multiline-comment-style */
|
||||
|
||||
import { Size } from './types';
|
||||
|
||||
// AUTO-GENERATED by generate-database-type
|
||||
@@ -22,26 +20,6 @@ export interface OnChangeEvent {
|
||||
export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>;
|
||||
export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>;
|
||||
|
||||
/**
|
||||
* Most of these are the built-in note properties, such as `note.title`,
|
||||
* `note.todo_completed`, etc.
|
||||
*
|
||||
* Additionally, the `item.*` properties are specific to the rendered item. The
|
||||
* most important being `item.selected`, which you can use to display the
|
||||
* selected note in a different way.
|
||||
*
|
||||
* Finally some special properties are provided to make it easier to render
|
||||
* notes. In particular, if possible prefer `note.titleHtml` to `note.title`
|
||||
* since some important processing has already been done on the string, such as
|
||||
* handling the search highlighter and escaping. Since it's HTML and already
|
||||
* escaped you would insert it using `{{{titleHtml}}}` (triple-mustache syntax,
|
||||
* which disables escaping).
|
||||
*
|
||||
* `notes.tag` gives you the list of tags associated with the note.
|
||||
*
|
||||
* `note.isWatched` tells you if the note is currently opened in an external
|
||||
* editor. In which case you would generally display some indicator.
|
||||
*/
|
||||
export type ListRendererDepependency =
|
||||
ListRendererDatabaseDependency |
|
||||
'item.size.width' |
|
||||
@@ -52,130 +30,13 @@ export type ListRendererDepependency =
|
||||
'note.tags';
|
||||
|
||||
export interface ListRenderer {
|
||||
/**
|
||||
* It must be unique to your plugin.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Can be top to bottom or left to right. Left to right gives you more
|
||||
* option to set the size of the items since you set both its width and
|
||||
* height.
|
||||
*/
|
||||
flow: ItemFlow;
|
||||
|
||||
/**
|
||||
* The size of each item must be specified in advance for performance
|
||||
* reasons, and cannot be changed afterwards. If the item flow is top to
|
||||
* bottom, you only need to specificy the item height (the width will be
|
||||
* ignored).
|
||||
*/
|
||||
itemSize: Size;
|
||||
|
||||
/**
|
||||
* The CSS is relative to the list item container. What will appear in the
|
||||
* page is essentially `.note-list-item { YOUR_CSS; }`. It means you can use
|
||||
* child combinator with guarantee it will only apply to your own items. In
|
||||
* this example, the styling will apply to `.note-list-item > .content`:
|
||||
*
|
||||
* ```css
|
||||
* > .content {
|
||||
* padding: 10px;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* In order to get syntax highlighting working here, it's recommended
|
||||
* installing an editor extension such as [es6-string-html VSCode
|
||||
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
|
||||
*/
|
||||
itemCss?: string;
|
||||
|
||||
/**
|
||||
* List the dependencies that your plugin needs to render the note list
|
||||
* items. Only these will be passed to your `onRenderNote` handler. Ensure
|
||||
* that you do not add more than what you need since there is a performance
|
||||
* penalty for each property.
|
||||
*/
|
||||
dependencies: ListRendererDepependency[];
|
||||
|
||||
/**
|
||||
* This is the HTML template that will be used to render the note list item.
|
||||
* This is a [Mustache template](https://github.com/janl/mustache.js) and it
|
||||
* will receive the variable you return from `onRenderNote` as tags. For
|
||||
* example, if you return a property named `formattedDate` from
|
||||
* `onRenderNote`, you can insert it in the template using `Created date:
|
||||
* {{formattedDate}}`.
|
||||
*
|
||||
* In order to get syntax highlighting working here, it's recommended
|
||||
* installing an editor extension such as [es6-string-html VSCode
|
||||
* extension](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html)
|
||||
*/
|
||||
itemTemplate: string;
|
||||
|
||||
/**
|
||||
* This user-facing text is used for example in the View menu, so that your
|
||||
* renderer can be selected.
|
||||
*/
|
||||
label: ()=> Promise<string>;
|
||||
|
||||
/**
|
||||
* This is where most of the real-time processing will happen. When a note
|
||||
* is rendered for the first time and every time it changes, this handler
|
||||
* receives the properties specified in the `dependencies` property. You can
|
||||
* then process them, load any additional data you need, and once done you
|
||||
* need to return the properties that are needed in the `itemTemplate` HTML.
|
||||
* Again, to use the formatted date example, you could have such a renderer:
|
||||
*
|
||||
* ```typescript
|
||||
* dependencies: [
|
||||
* 'note.title',
|
||||
* 'note.created_time',
|
||||
* ],
|
||||
*
|
||||
* itemTemplate: // html
|
||||
* `
|
||||
* <div>
|
||||
* Title: {{note.title}}<br/>
|
||||
* Date: {{formattedDate}}
|
||||
* </div>
|
||||
* `,
|
||||
*
|
||||
* onRenderNote: async (props: any) => {
|
||||
* const formattedDate = dayjs(props.note.created_time).format();
|
||||
* return {
|
||||
* // Also return the props, so that note.title is available from the
|
||||
* // template
|
||||
* ...props,
|
||||
* formattedDate,
|
||||
* }
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
onRenderNote: OnRenderNoteHandler;
|
||||
|
||||
/**
|
||||
* This handler allows adding some interacivity to the note renderer -
|
||||
* whenever an input element within the item is changed (for example, when a
|
||||
* checkbox is clicked, or a text input is changed), this `onChange` handler
|
||||
* is going to be called.
|
||||
*
|
||||
* You can inspect `event.elementId` to know which element had some changes,
|
||||
* and `event.value` to know the new value. `event.noteId` also tells you
|
||||
* what note is affected, so that you can potentially apply changes to it.
|
||||
*
|
||||
* You specify the element ID, by setting a `data-id` attribute on the
|
||||
* input.
|
||||
*
|
||||
* For example, if you have such a template:
|
||||
*
|
||||
* ```html
|
||||
* <div>
|
||||
* <input type="text" value="{{note.title}}" data-id="noteTitleInput"/>
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
* The event handler will receive an event with `elementId` set to
|
||||
* `noteTitleInput`.
|
||||
*/
|
||||
onChange?: OnChangeHandler;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ function readManifest(manifestPath) {
|
||||
}
|
||||
|
||||
function createPluginArchive(sourceDir, destPath) {
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true, windowsPathsNoEscape: true })
|
||||
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
|
||||
.map(f => f.substr(sourceDir.length + 1));
|
||||
|
||||
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.0",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"html-entities": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "11.0.2"
|
||||
"@types/fs-extra": "11.0.1"
|
||||
},
|
||||
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
|
||||
}
|
||||
|
||||
@@ -33,15 +33,8 @@ const htmlentities = (s: string): string => {
|
||||
};
|
||||
|
||||
const dataUriEncode = (filePath: string): string => {
|
||||
try {
|
||||
const result = Datauri(filePath);
|
||||
return result.content;
|
||||
} catch (error) {
|
||||
// If the file path is invalid, the Datauri will throw an exception.
|
||||
// Instead, since we can just ignore that particular file.
|
||||
// Fixes https://github.com/laurent22/joplin/issues/8305
|
||||
return '';
|
||||
}
|
||||
const result = Datauri(filePath);
|
||||
return result.content;
|
||||
};
|
||||
|
||||
const attributesHtml = (attr: any) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import Logger, { TargetType, LoggerWrapper } from '@joplin/utils/Logger';
|
||||
import shim from './shim';
|
||||
const { setupProxySettings } = require('./shim-init-node');
|
||||
import BaseService from './services/BaseService';
|
||||
import reducer, { getNotesParent, serializeNotesParent, setStore, State } from './reducer';
|
||||
import reducer, { setStore } from './reducer';
|
||||
import KeychainServiceDriver from './services/keychain/KeychainServiceDriver.node';
|
||||
import KeychainServiceDriverDummy from './services/keychain/KeychainServiceDriver.dummy';
|
||||
import { _, setLocale } from './locale';
|
||||
@@ -544,7 +544,7 @@ export default class BaseApplication {
|
||||
let refreshNotesHash = '';
|
||||
|
||||
await reduxSharedMiddleware(store, next, action);
|
||||
const newState = store.getState() as State;
|
||||
const newState = store.getState();
|
||||
|
||||
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(15 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
@@ -573,10 +573,6 @@ export default class BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
if (['HISTORY_BACKWARD', 'HISTORY_FORWARD', 'FOLDER_SELECT', 'TAG_SELECT', 'SMART_FILTER_SELECT', 'FOLDER_DELETE', 'FOLDER_AND_NOTE_SELECT'].includes(action.type) || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
|
||||
Setting.setValue('notesParent', serializeNotesParent(getNotesParent(newState)));
|
||||
}
|
||||
|
||||
if (this.hasGui() && (action.type === 'NOTE_IS_INSERTING_NOTES' && !action.value)) {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
||||
UseArnRegion: true, // override the request region with the region inferred from requested resource's ARN.
|
||||
forcePathStyle: Setting.value('sync.8.forcePathStyle'), // Older implementations may not support more modern access, so we expose this to allow people the option to toggle.
|
||||
endpoint: Setting.value('sync.8.url'),
|
||||
ignoreTlsErrors: Setting.value('net.ignoreTlsErrors'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,7 +87,6 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
||||
UseArnRegion: true, // override the request region with the region inferred from requested resource's ARN.
|
||||
forcePathStyle: options.forcePathStyle(),
|
||||
endpoint: options.url(),
|
||||
ignoreTlsErrors: options.ignoreTlsErrors(),
|
||||
};
|
||||
|
||||
const api = new S3Client(apiOptions);
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
function absoluteUrl(url: string) {
|
||||
if (!url) return url;
|
||||
const protocol = url.toLowerCase().split(':')[0];
|
||||
if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
|
||||
|
||||
if (url.indexOf('//') === 0) {
|
||||
return location.protocol + url;
|
||||
} else if (url[0] === '/') {
|
||||
return `${location.protocol}//${location.host}${url}`;
|
||||
} else {
|
||||
return `${baseUrl()}/${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
function pageLocationOrigin() {
|
||||
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
|
||||
// but for file:// protocol this is browser dependant and in particular Firefox returns "null"
|
||||
// in this case.
|
||||
|
||||
if (location.protocol === 'file:') {
|
||||
return 'file://';
|
||||
} else {
|
||||
return location.origin;
|
||||
}
|
||||
}
|
||||
|
||||
function baseUrl() {
|
||||
let output = pageLocationOrigin() + location.pathname;
|
||||
if (output[output.length - 1] !== '/') {
|
||||
const output2 = output.split('/');
|
||||
output2.pop();
|
||||
output = output2.join('/');
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function getJoplinClipperSvgClassName(svg: SVGSVGElement) {
|
||||
for (const className of svg.classList) {
|
||||
if (className.indexOf('joplin-clipper-svg-') === 0) return className;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
type ImageObject = {
|
||||
width: number;
|
||||
height: number;
|
||||
naturalWidth?: number;
|
||||
naturalHeight?: number;
|
||||
};
|
||||
|
||||
export function getImageSizes(element: HTMLElement, forceAbsoluteUrls = false) {
|
||||
const output: Record<string, ImageObject[]> = {};
|
||||
|
||||
const images = element.getElementsByTagName('img');
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
if (img.classList && img.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
let src = imageSrc(img);
|
||||
src = forceAbsoluteUrls ? absoluteUrl(src) : src;
|
||||
|
||||
if (!output[src]) output[src] = [];
|
||||
|
||||
output[src].push({
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
naturalWidth: img.naturalWidth,
|
||||
naturalHeight: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
|
||||
const svgs = element.getElementsByTagName('svg');
|
||||
for (let i = 0; i < svgs.length; i++) {
|
||||
const svg = svgs[i];
|
||||
if (svg.classList && svg.classList.contains('joplin-clipper-hidden')) continue;
|
||||
|
||||
const className = getJoplinClipperSvgClassName(svg);// 'joplin-clipper-svg-' + i;
|
||||
|
||||
if (!className) {
|
||||
console.warn('SVG without a Joplin class:', svg);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!svg.classList.contains(className)) {
|
||||
svg.classList.add(className);
|
||||
}
|
||||
|
||||
const rect = svg.getBoundingClientRect();
|
||||
|
||||
if (!output[className]) output[className] = [];
|
||||
|
||||
output[className].push({
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// In general we should use currentSrc because that's the image that's currently displayed,
|
||||
// especially within <picture> tags or with srcset. In these cases there can be multiple
|
||||
// sources and the best one is probably the one being displayed, thus currentSrc.
|
||||
function imageSrc(image: HTMLImageElement) {
|
||||
if (image.currentSrc) return image.currentSrc;
|
||||
return image.src;
|
||||
}
|
||||
|
||||
// Given a document, return a <style> tag that contains all the styles
|
||||
// required to render the page. Not currently used but could be as an
|
||||
// option to clip pages as HTML.
|
||||
// eslint-disable-next-line
|
||||
export function getStyleSheets(doc: Document) {
|
||||
const output = [];
|
||||
for (let i = 0; i < doc.styleSheets.length; i++) {
|
||||
const sheet = doc.styleSheets[i];
|
||||
try {
|
||||
for (const cssRule of sheet.cssRules) {
|
||||
output.push({ type: 'text', value: cssRule.cssText });
|
||||
}
|
||||
} catch (error) {
|
||||
// Calling sheet.cssRules will throw a CORS error on Chrome if the stylesheet is on a different domain.
|
||||
// In that case, we skip it and add it to the list of stylesheet URLs. These URls will be downloaded
|
||||
// by the desktop application, since it doesn't have CORS restrictions.
|
||||
// eslint-disable-next-line
|
||||
console.info('Could not retrieve stylesheet now:', sheet.href);
|
||||
// eslint-disable-next-line
|
||||
console.info('It will downloaded by the main application.');
|
||||
// eslint-disable-next-line
|
||||
console.info(error);
|
||||
output.push({ type: 'url', value: sheet.href });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user