You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
120 Commits
android-v2
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
cac10c4e29 | ||
|
9b348fdc29 | ||
|
ec97dd8c60 | ||
|
f28c1bc6ba | ||
|
e660fafb7a | ||
|
2c49270f38 | ||
|
13c1ae3d39 | ||
|
29550ade49 | ||
|
1b9f74f674 | ||
|
0b69ae371c | ||
|
37ebd21cb3 | ||
|
c996ddaf9d | ||
|
cea1aeac4b | ||
|
13ee1c89ea | ||
|
f01ec941b7 | ||
|
0853521bc9 | ||
|
e484671a08 | ||
|
50253d00e7 | ||
|
5364965a69 | ||
|
50baad3c04 | ||
|
cf219762c9 | ||
|
9e27b0881f | ||
|
44a96f347a | ||
|
cc6620a7e1 | ||
|
29f1abb666 | ||
|
9781a33419 | ||
|
0954794195 | ||
|
a996375b88 | ||
|
129ac1829d | ||
|
44e60bdda9 | ||
|
afc34b44c8 | ||
|
e08c74ae08 | ||
|
e5c669dc7a | ||
|
f4a7f5914e | ||
|
62eee4df56 | ||
|
c16445bc2f | ||
|
e05c5598a0 | ||
|
66c9ee0a1a | ||
|
d07788607c | ||
|
907dc7601b | ||
|
4b9adcde04 | ||
|
9f3a4e0d99 | ||
|
ea14488dc3 | ||
|
f59d29f1c5 | ||
|
0a9e919ac7 | ||
|
f11b6e8fa9 | ||
|
167560ff6f | ||
|
4b4e316bf0 | ||
|
7809228bd3 | ||
|
540fbbc22c | ||
|
2983d4f1a3 | ||
|
f6a8bf9ea2 | ||
|
e3ba02281b | ||
|
295b310079 | ||
|
62346575f8 | ||
|
0a590b7de9 | ||
|
dfd95f8385 | ||
|
6efe8c171a | ||
|
a7cdcaf25f | ||
|
6277958d6a | ||
|
25bd91bed1 | ||
|
7974df98ff | ||
|
e37d980453 | ||
|
597569745c | ||
|
6e6275b1b7 | ||
|
cfba73e938 | ||
|
7e1c34b769 | ||
|
b5b281c276 | ||
|
80906cbdb3 | ||
|
1504cb71ae | ||
|
eb7083d788 | ||
|
e40d733176 | ||
|
170c669e37 | ||
|
24b4b879f2 | ||
|
3942029c90 | ||
|
01f4bb0591 | ||
|
86fbf82d36 | ||
|
1069d7d6fb | ||
|
8d67aefcd5 | ||
|
ff90166b6e | ||
|
6beaaf75bb | ||
|
ebf9a9375c | ||
|
de94c35c0b | ||
|
6a4eb33093 | ||
|
8b91427056 | ||
|
b174fcf17b | ||
|
c6b91cdc5d | ||
|
e784e8c947 | ||
|
6498f94c36 | ||
|
ae300de42f | ||
|
40e682faae | ||
|
92c24c2129 | ||
|
3ec3a37603 | ||
|
ed2a328616 | ||
|
58dc4feee7 | ||
|
0356cbbfab | ||
|
8b06cbf04e | ||
|
fd82758e74 | ||
|
c705ec682c | ||
|
a5e6491cda | ||
|
8ef9804cab | ||
|
09ec77f904 | ||
|
36871d9cb0 | ||
|
b4ece67092 | ||
|
7e8a6dfb54 | ||
|
549095f0e5 | ||
|
313c05732b | ||
|
641b0fa9a2 | ||
|
96982849ce | ||
|
4b8745c875 | ||
|
78f72f33e6 | ||
|
b4aa418276 | ||
|
8d66322c94 | ||
|
6969341745 | ||
|
488f19e3c4 | ||
|
79889facea | ||
|
74f513b082 | ||
|
ab540edacc | ||
|
9dedd88989 | ||
|
be8ebd9fc5 |
121
.eslintignore
121
.eslintignore
@@ -6,6 +6,7 @@ _releases/
|
||||
*.min.js
|
||||
**/commands/index.ts
|
||||
**/node_modules/
|
||||
packages/generator-joplin/generators/app/templates/api/
|
||||
Assets/
|
||||
docs/
|
||||
highlight.pack.js
|
||||
@@ -116,6 +117,9 @@ packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js.map
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.d.ts
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.js
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.js.map
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
@@ -329,6 +333,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@@ -593,6 +600,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@@ -842,6 +852,15 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
|
||||
packages/app-mobile/components/CameraView.d.ts
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CameraView.js.map
|
||||
packages/app-mobile/components/CustomButton.d.ts
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/CustomButton.js.map
|
||||
packages/app-mobile/components/Dropdown.d.ts
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/Dropdown.js.map
|
||||
packages/app-mobile/components/ExtendedWebView.d.ts
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView.js.map
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map
|
||||
@@ -872,12 +891,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
|
||||
@@ -905,6 +921,27 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
@@ -917,9 +954,21 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
|
||||
packages/app-mobile/components/NoteEditor/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteEditor/types.js.map
|
||||
packages/app-mobile/components/ScreenHeader.d.ts
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
packages/app-mobile/components/ScreenHeader.js.map
|
||||
packages/app-mobile/components/SelectDateTimeDialog.d.ts
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js.map
|
||||
packages/app-mobile/components/SideMenu.d.ts
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenu.js.map
|
||||
packages/app-mobile/components/getResponsiveValue.d.ts
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.js.map
|
||||
packages/app-mobile/components/getResponsiveValue.test.d.ts
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js.map
|
||||
packages/app-mobile/components/screens/ConfigScreen.d.ts
|
||||
packages/app-mobile/components/screens/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen.js.map
|
||||
@@ -932,6 +981,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/components/screens/encryption-config.d.ts
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/encryption-config.js.map
|
||||
packages/app-mobile/components/side-menu-content.d.ts
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/side-menu-content.js.map
|
||||
packages/app-mobile/gulpfile.d.ts
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/gulpfile.js.map
|
||||
@@ -974,6 +1026,9 @@ packages/app-mobile/utils/setupNotifications.js.map
|
||||
packages/app-mobile/utils/shareHandler.d.ts
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/shareHandler.js.map
|
||||
packages/app-mobile/utils/types.d.ts
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/utils/types.js.map
|
||||
packages/fork-htmlparser2/src/CollectingHandler.d.ts
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js.map
|
||||
@@ -1538,6 +1593,9 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js.map
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js.map
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.d.ts
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js.map
|
||||
packages/lib/services/interop/types.d.ts
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/types.js.map
|
||||
@@ -1646,6 +1704,12 @@ packages/lib/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/lib/services/plugins/api/types.d.ts
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/api/types.js.map
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.d.ts
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js.map
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.d.ts
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js.map
|
||||
packages/lib/services/plugins/reducer.d.ts
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/reducer.js.map
|
||||
@@ -1952,9 +2016,15 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
packages/pdf-viewer/PdfDocument.d.ts
|
||||
packages/pdf-viewer/PdfDocument.js
|
||||
packages/pdf-viewer/PdfDocument.js.map
|
||||
packages/pdf-viewer/VerticalPages.d.ts
|
||||
packages/pdf-viewer/VerticalPages.js
|
||||
packages/pdf-viewer/VerticalPages.js.map
|
||||
@@ -1964,15 +2034,42 @@ packages/pdf-viewer/hooks/useIsFocused.js.map
|
||||
packages/pdf-viewer/hooks/useIsVisible.d.ts
|
||||
packages/pdf-viewer/hooks/useIsVisible.js
|
||||
packages/pdf-viewer/hooks/useIsVisible.js.map
|
||||
packages/pdf-viewer/hooks/usePdfDocument.d.ts
|
||||
packages/pdf-viewer/hooks/usePdfDocument.js
|
||||
packages/pdf-viewer/hooks/usePdfDocument.js.map
|
||||
packages/pdf-viewer/hooks/useScaledSize.d.ts
|
||||
packages/pdf-viewer/hooks/useScaledSize.js
|
||||
packages/pdf-viewer/hooks/useScaledSize.js.map
|
||||
packages/pdf-viewer/hooks/useScrollSaver.d.ts
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
packages/pdf-viewer/pdfSource.d.ts
|
||||
packages/pdf-viewer/pdfSource.js
|
||||
packages/pdf-viewer/pdfSource.js.map
|
||||
packages/pdf-viewer/pdfSource.test.d.ts
|
||||
packages/pdf-viewer/pdfSource.test.js
|
||||
packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
packages/pdf-viewer/ui/ZoomControls.d.ts
|
||||
packages/pdf-viewer/ui/ZoomControls.js
|
||||
packages/pdf-viewer/ui/ZoomControls.js.map
|
||||
packages/plugin-repo-cli/commands/updateRelease.d.ts
|
||||
packages/plugin-repo-cli/commands/updateRelease.js
|
||||
packages/plugin-repo-cli/commands/updateRelease.js.map
|
||||
@@ -2123,6 +2220,12 @@ packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/bundleDefaultPlugins.d.ts
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
packages/tools/bundleDefaultPlugins.js.map
|
||||
packages/tools/bundleDefaultPlugins.test.d.ts
|
||||
packages/tools/bundleDefaultPlugins.test.js
|
||||
packages/tools/bundleDefaultPlugins.test.js.map
|
||||
packages/tools/checkLibPaths.d.ts
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/checkLibPaths.js.map
|
||||
|
12
.eslintrc.js
12
.eslintrc.js
@@ -83,11 +83,15 @@ module.exports = {
|
||||
// 'complexity': ['warn', { max: 10 }],
|
||||
|
||||
// Checks rules of Hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'@seiyab/react-hooks/rules-of-hooks': 'error',
|
||||
'@seiyab/react-hooks/exhaustive-deps': ['error', { 'ignoreThisDependency': 'props' }],
|
||||
|
||||
// Checks effect dependencies
|
||||
// Disable because of this: https://github.com/facebook/react/issues/16265
|
||||
// "react-hooks/exhaustive-deps": "warn",
|
||||
|
||||
'promise/prefer-await-to-then': 'error',
|
||||
|
||||
// -------------------------------
|
||||
// Formatting
|
||||
// -------------------------------
|
||||
@@ -134,8 +138,12 @@ module.exports = {
|
||||
'plugins': [
|
||||
'react',
|
||||
'@typescript-eslint',
|
||||
'react-hooks',
|
||||
// Need to use a fork of the official rules of hooks because of this bug:
|
||||
// https://github.com/facebook/react/issues/16265
|
||||
'@seiyab/eslint-plugin-react-hooks',
|
||||
// 'react-hooks',
|
||||
'import',
|
||||
'promise',
|
||||
],
|
||||
'overrides': [
|
||||
{
|
||||
|
8
.github/scripts/run_ci.sh
vendored
8
.github/scripts/run_ci.sh
vendored
@@ -57,6 +57,11 @@ echo "Yarn $( yarn -v )"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
yarn install
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
echo "Yarn installation failed. Search for 'exit code 1' in the log for more information."
|
||||
exit $testResult
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Run test units. Only do it for pull requests and dev branch because we don't
|
||||
@@ -170,6 +175,9 @@ cd "$ROOT_DIR/packages/app-desktop"
|
||||
|
||||
if [[ $GIT_TAG_NAME = v* ]]; then
|
||||
echo "Step: Building and publishing desktop application..."
|
||||
# cd "$ROOT_DIR/packages/tools"
|
||||
# node bundleDefaultPlugins.js
|
||||
cd "$ROOT_DIR/packages/app-desktop"
|
||||
USE_HARD_LINKS=false yarn run dist
|
||||
elif [[ $IS_LINUX = 1 ]] && [[ $GIT_TAG_NAME = $SERVER_TAG_PREFIX-* ]]; then
|
||||
echo "Step: Building Docker Image..."
|
||||
|
3
.github/workflows/github-actions-main.yml
vendored
3
.github/workflows/github-actions-main.yml
vendored
@@ -5,7 +5,6 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# Removed windows-2016 for now - discontinued by GitHub
|
||||
os: [macos-latest, ubuntu-latest, windows-2019]
|
||||
steps:
|
||||
|
||||
@@ -76,6 +75,8 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
SERVER_REPOSITORY: joplin/server
|
||||
SERVER_TAG_PREFIX: server
|
||||
run: |
|
||||
"${GITHUB_WORKSPACE}/.github/scripts/run_ci.sh"
|
||||
|
||||
|
123
.gitignore
vendored
123
.gitignore
vendored
@@ -105,6 +105,9 @@ packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js.map
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
|
||||
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.d.ts
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.js
|
||||
packages/app-cli/tests/services/plugins/defaultPluginsUtils.js.map
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js
|
||||
packages/app-cli/tests/services/plugins/sandboxProxy.js.map
|
||||
@@ -318,6 +321,9 @@ packages/app-desktop/gui/MainScreen/commands/openItem.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openNote.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openPdfViewer.js.map
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.d.ts
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js
|
||||
packages/app-desktop/gui/MainScreen/commands/openTag.js.map
|
||||
@@ -582,6 +588,9 @@ packages/app-desktop/gui/OneDriveLoginScreen.js.map
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.d.ts
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js
|
||||
packages/app-desktop/gui/PasswordInput/PasswordInput.js.map
|
||||
packages/app-desktop/gui/PdfViewer.d.ts
|
||||
packages/app-desktop/gui/PdfViewer.js
|
||||
packages/app-desktop/gui/PdfViewer.js.map
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.d.ts
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js
|
||||
packages/app-desktop/gui/ResizableLayout/MoveButtons.js.map
|
||||
@@ -831,6 +840,15 @@ packages/app-mobile/components/BackButtonDialogBox.js.map
|
||||
packages/app-mobile/components/CameraView.d.ts
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CameraView.js.map
|
||||
packages/app-mobile/components/CustomButton.d.ts
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/CustomButton.js.map
|
||||
packages/app-mobile/components/Dropdown.d.ts
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/Dropdown.js.map
|
||||
packages/app-mobile/components/ExtendedWebView.d.ts
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
packages/app-mobile/components/ExtendedWebView.js.map
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.d.ts
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js
|
||||
packages/app-mobile/components/NoteBodyViewer/NoteBodyViewer.js.map
|
||||
@@ -861,12 +879,9 @@ packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleTwice.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.togglingLists.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownCommands.toggleList.test.js.map
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
|
||||
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
|
||||
@@ -894,6 +909,27 @@ packages/app-mobile/components/NoteEditor/CodeMirror/webviewLogger.js.map
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.d.ts
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js
|
||||
packages/app-mobile/components/NoteEditor/EditLinkDialog.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/MarkdownToolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleOverflowButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToggleSpaceButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js.map
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js.map
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js.map
|
||||
@@ -906,9 +942,21 @@ packages/app-mobile/components/NoteEditor/SelectionFormatting.js.map
|
||||
packages/app-mobile/components/NoteEditor/types.d.ts
|
||||
packages/app-mobile/components/NoteEditor/types.js
|
||||
packages/app-mobile/components/NoteEditor/types.js.map
|
||||
packages/app-mobile/components/ScreenHeader.d.ts
|
||||
packages/app-mobile/components/ScreenHeader.js
|
||||
packages/app-mobile/components/ScreenHeader.js.map
|
||||
packages/app-mobile/components/SelectDateTimeDialog.d.ts
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js
|
||||
packages/app-mobile/components/SelectDateTimeDialog.js.map
|
||||
packages/app-mobile/components/SideMenu.d.ts
|
||||
packages/app-mobile/components/SideMenu.js
|
||||
packages/app-mobile/components/SideMenu.js.map
|
||||
packages/app-mobile/components/getResponsiveValue.d.ts
|
||||
packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/getResponsiveValue.js.map
|
||||
packages/app-mobile/components/getResponsiveValue.test.d.ts
|
||||
packages/app-mobile/components/getResponsiveValue.test.js
|
||||
packages/app-mobile/components/getResponsiveValue.test.js.map
|
||||
packages/app-mobile/components/screens/ConfigScreen.d.ts
|
||||
packages/app-mobile/components/screens/ConfigScreen.js
|
||||
packages/app-mobile/components/screens/ConfigScreen.js.map
|
||||
@@ -921,6 +969,9 @@ packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js.map
|
||||
packages/app-mobile/components/screens/encryption-config.d.ts
|
||||
packages/app-mobile/components/screens/encryption-config.js
|
||||
packages/app-mobile/components/screens/encryption-config.js.map
|
||||
packages/app-mobile/components/side-menu-content.d.ts
|
||||
packages/app-mobile/components/side-menu-content.js
|
||||
packages/app-mobile/components/side-menu-content.js.map
|
||||
packages/app-mobile/gulpfile.d.ts
|
||||
packages/app-mobile/gulpfile.js
|
||||
packages/app-mobile/gulpfile.js.map
|
||||
@@ -963,6 +1014,9 @@ packages/app-mobile/utils/setupNotifications.js.map
|
||||
packages/app-mobile/utils/shareHandler.d.ts
|
||||
packages/app-mobile/utils/shareHandler.js
|
||||
packages/app-mobile/utils/shareHandler.js.map
|
||||
packages/app-mobile/utils/types.d.ts
|
||||
packages/app-mobile/utils/types.js
|
||||
packages/app-mobile/utils/types.js.map
|
||||
packages/fork-htmlparser2/src/CollectingHandler.d.ts
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js
|
||||
packages/fork-htmlparser2/src/CollectingHandler.js.map
|
||||
@@ -1527,6 +1581,9 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js.map
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.d.ts
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.js.map
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.d.ts
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js
|
||||
packages/lib/services/interop/InteropService_Importer_Raw.test.js.map
|
||||
packages/lib/services/interop/types.d.ts
|
||||
packages/lib/services/interop/types.js
|
||||
packages/lib/services/interop/types.js.map
|
||||
@@ -1635,6 +1692,12 @@ packages/lib/services/plugins/api/JoplinWorkspace.js.map
|
||||
packages/lib/services/plugins/api/types.d.ts
|
||||
packages/lib/services/plugins/api/types.js
|
||||
packages/lib/services/plugins/api/types.js.map
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.d.ts
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js
|
||||
packages/lib/services/plugins/defaultPlugins/defaultPluginsUtils.js.map
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.d.ts
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js.map
|
||||
packages/lib/services/plugins/reducer.d.ts
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/reducer.js.map
|
||||
@@ -1941,9 +2004,15 @@ packages/lib/uuid.js.map
|
||||
packages/lib/versionInfo.d.ts
|
||||
packages/lib/versionInfo.js
|
||||
packages/lib/versionInfo.js.map
|
||||
packages/pdf-viewer/FullViewer.d.ts
|
||||
packages/pdf-viewer/FullViewer.js
|
||||
packages/pdf-viewer/FullViewer.js.map
|
||||
packages/pdf-viewer/Page.d.ts
|
||||
packages/pdf-viewer/Page.js
|
||||
packages/pdf-viewer/Page.js.map
|
||||
packages/pdf-viewer/PdfDocument.d.ts
|
||||
packages/pdf-viewer/PdfDocument.js
|
||||
packages/pdf-viewer/PdfDocument.js.map
|
||||
packages/pdf-viewer/VerticalPages.d.ts
|
||||
packages/pdf-viewer/VerticalPages.js
|
||||
packages/pdf-viewer/VerticalPages.js.map
|
||||
@@ -1953,15 +2022,42 @@ packages/pdf-viewer/hooks/useIsFocused.js.map
|
||||
packages/pdf-viewer/hooks/useIsVisible.d.ts
|
||||
packages/pdf-viewer/hooks/useIsVisible.js
|
||||
packages/pdf-viewer/hooks/useIsVisible.js.map
|
||||
packages/pdf-viewer/hooks/usePdfDocument.d.ts
|
||||
packages/pdf-viewer/hooks/usePdfDocument.js
|
||||
packages/pdf-viewer/hooks/usePdfDocument.js.map
|
||||
packages/pdf-viewer/hooks/useScaledSize.d.ts
|
||||
packages/pdf-viewer/hooks/useScaledSize.js
|
||||
packages/pdf-viewer/hooks/useScaledSize.js.map
|
||||
packages/pdf-viewer/hooks/useScrollSaver.d.ts
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js
|
||||
packages/pdf-viewer/hooks/useScrollSaver.js.map
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.d.ts
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js
|
||||
packages/pdf-viewer/hooks/useVisibleOnSelect.js.map
|
||||
packages/pdf-viewer/main.d.ts
|
||||
packages/pdf-viewer/main.js
|
||||
packages/pdf-viewer/main.js.map
|
||||
packages/pdf-viewer/messageService.d.ts
|
||||
packages/pdf-viewer/messageService.js
|
||||
packages/pdf-viewer/messageService.js.map
|
||||
packages/pdf-viewer/miniViewer.d.ts
|
||||
packages/pdf-viewer/miniViewer.js
|
||||
packages/pdf-viewer/miniViewer.js.map
|
||||
packages/pdf-viewer/pdfSource.d.ts
|
||||
packages/pdf-viewer/pdfSource.js
|
||||
packages/pdf-viewer/pdfSource.js.map
|
||||
packages/pdf-viewer/pdfSource.test.d.ts
|
||||
packages/pdf-viewer/pdfSource.test.js
|
||||
packages/pdf-viewer/pdfSource.test.js.map
|
||||
packages/pdf-viewer/types.d.ts
|
||||
packages/pdf-viewer/types.js
|
||||
packages/pdf-viewer/types.js.map
|
||||
packages/pdf-viewer/ui/GotoPage.d.ts
|
||||
packages/pdf-viewer/ui/GotoPage.js
|
||||
packages/pdf-viewer/ui/GotoPage.js.map
|
||||
packages/pdf-viewer/ui/IconButtons.d.ts
|
||||
packages/pdf-viewer/ui/IconButtons.js
|
||||
packages/pdf-viewer/ui/IconButtons.js.map
|
||||
packages/pdf-viewer/ui/ZoomControls.d.ts
|
||||
packages/pdf-viewer/ui/ZoomControls.js
|
||||
packages/pdf-viewer/ui/ZoomControls.js.map
|
||||
packages/plugin-repo-cli/commands/updateRelease.d.ts
|
||||
packages/plugin-repo-cli/commands/updateRelease.js
|
||||
packages/plugin-repo-cli/commands/updateRelease.js.map
|
||||
@@ -2112,6 +2208,12 @@ packages/tools/buildServerDocker.js.map
|
||||
packages/tools/buildServerDocker.test.d.ts
|
||||
packages/tools/buildServerDocker.test.js
|
||||
packages/tools/buildServerDocker.test.js.map
|
||||
packages/tools/bundleDefaultPlugins.d.ts
|
||||
packages/tools/bundleDefaultPlugins.js
|
||||
packages/tools/bundleDefaultPlugins.js.map
|
||||
packages/tools/bundleDefaultPlugins.test.d.ts
|
||||
packages/tools/bundleDefaultPlugins.test.js
|
||||
packages/tools/bundleDefaultPlugins.test.js.map
|
||||
packages/tools/checkLibPaths.d.ts
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/checkLibPaths.js.map
|
||||
@@ -2206,3 +2308,6 @@ packages/tools/website/utils/types.d.ts
|
||||
packages/tools/website/utils/types.js
|
||||
packages/tools/website/utils/types.js.map
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-mobile/components/get-responsive-value.test.js
|
||||
packages/app-mobile/components/get-responsive-value.test.js
|
||||
packages/app-mobile/components/get-responsive-value.test.js
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 08 Aug 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Tue, 06 Sep 2022 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><item><title><![CDATA[Joplin interview on Website Planet]]></title><description><![CDATA[<p>Website Planet has recently conducted an interview about Joplin - it may give you some insight on the current status of the project, our priorities, and future plans! More on the article page - <a href="https://www.websiteplanet.com/blog/interview-joplin/">Organise Your Thoughts with Open Source Note-Taking App, Joplin</a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20220906-interview-websiteplanet/</link><guid isPermaLink="false">20220906-interview-websiteplanet</guid><pubDate>Tue, 06 Sep 2022 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Joplin first meetup on 30 August!]]></title><description><![CDATA[<p>We are glad to announce <a href="https://www.meetup.com/joplin/events/287611873/">the first Joplin Meetup</a> that will take place on 30 August 2022 in London!</p>
|
||||
<p>This is an opportunity to meet other Joplin users as well as some of the main contributors, to discuss the apps, or to ask questions and exchange tips and tricks on how to use the app, develop plugins or contribute to the application. Everybody, technical or not, is welcome!</p>
|
||||
<p>We will meet at the Old Thameside Inn next to London Bridge. If the weather allows we will be on the terrace outside, if not inside.</p>
|
||||
<p>More information on the official Meetup page:</p>
|
||||
@@ -258,7 +259,4 @@
|
||||
<p>Also many thanks to everyone who voted and contributed to the tagline discussion! It helped narrow down what the tagline should be, along with the equally important description below. If you have any question or notice any issue with the website let me know!</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210711-095626/</link><guid isPermaLink="false">20210711-095626</guid><pubDate>Sun, 11 Jul 2021 09:56:26 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Poll: What should Joplin tagline be?]]></title><description><![CDATA[<p>Thanks everyone for your tagline suggestions - there were lots of good ideas in there. I've compiled a few of them and create a poll in the forum, so please cast your vote! And if you have any other suggestions on what would make a good tagline, feel free to post over there or here.</p>
|
||||
<p><a href="https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487">https://discourse.joplinapp.org/t/poll-what-should-joplin-tagline-be/18487</a></p>
|
||||
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[Any ideas for a Joplin tagline?]]></title><description><![CDATA[<p>I'm going to update the website front page to better showcase the application. I have most of the sections right, but the part I'm still not sure about is the top tagline, so I'm wondering if anyone had any suggestion about it?</p>
|
||||
<p>From what I can see on Google Keep or Evernote for example it should be something like "Use our app to get X or Y benefit", it should be a sentence that directly speaks to the user essentially.</p>
|
||||
<p>So far I have "Your notes, anywhere you are" but I'm not certain that's particularly inspiring. Any other idea about what tagline could be used?</p>
|
||||
]]></description><link>https://joplinapp.org/news/20210705-094247/</link><guid isPermaLink="false">20210705-094247</guid><pubDate>Mon, 05 Jul 2021 09:42:47 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20210706-140228/</link><guid isPermaLink="false">20210706-140228</guid><pubDate>Tue, 06 Jul 2021 14:02:28 GMT</pubDate><twitter-text></twitter-text></item></channel></rss>
|
@@ -116,16 +116,21 @@
|
||||
});
|
||||
};
|
||||
|
||||
const applyPeriod = (period) => {
|
||||
subscriptionPeriod = period;
|
||||
$('.plan-group').removeClass(period === 'monthly' ? 'plan-prices-yearly' : 'plan-prices-monthly');
|
||||
$('.plan-group').addClass('plan-prices-' + period);
|
||||
$("#pay-" + period + '-radio').prop('checked', true);
|
||||
}
|
||||
|
||||
$(() => {
|
||||
$("input[name='pay-radio']").change(function() {
|
||||
const period = $("input[type='radio'][name='pay-radio']:checked").val();
|
||||
subscriptionPeriod = period;
|
||||
|
||||
$('.plan-group').removeClass(period === 'monthly' ? 'plan-prices-yearly' : 'plan-prices-monthly');
|
||||
$('.plan-group').addClass('plan-prices-' + period);
|
||||
applyPeriod(period);
|
||||
});
|
||||
|
||||
setupBetaHandling(urlQuery);
|
||||
applyPeriod('yearly');
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
@@ -31,7 +31,7 @@ Joplin is available in multiple languages thanks to the help of its users. You c
|
||||
|
||||
If you want to start contributing to the project's code, please follow these guidelines before creating a pull request:
|
||||
|
||||
- Explain WHY you want to add this change. Explain it inside the pull request and you may link to an issue for additional information, but the PR should give a clear overview of why you want to add this.
|
||||
- The top post of the pull request should contain a full, self-contained explanation of the feature: what it does, how it does it, with examples of usage and screenshots. Also explain why you want to add this - what problem does it solve. Do not simply add a text `Implement feature #4345` or link to forum posts, because the information there will most likely be outdated or confusing (multiple discussions and opinions). The pull request needs to be self-contained.
|
||||
- Bug fixes are always welcome. Start by reviewing the [list of bugs](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
- A good way to easily start contributing is to pick and work on a [good first issue](https://github.com/laurent22/joplin/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). We try to make these issues as clear as possible and provide basic info on how the code should be changed, and if something is unclear feel free to ask for more information on the issue.
|
||||
- Before adding a new feature, ask about it in the [Github Issue Tracker](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue) or the [Joplin Forum](https://discourse.joplinapp.org/), or check if existing discussions exist to make sure the new functionality is desired.
|
||||
|
@@ -88,7 +88,8 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/37297218?s=96&v=4"/></br>[Jesssullivan](https://github.com/Jesssullivan) | <img width="50" src="https://avatars2.githubusercontent.com/u/1248504?s=96&v=4"/></br>[joesfer](https://github.com/joesfer) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/1168659?s=96&v=4"/></br>[nicholashead](https://github.com/nicholashead) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/5782817?s=96&v=4"/></br>[piccobit](https://github.com/piccobit) | <img width="50" src="https://avatars2.githubusercontent.com/u/77214738?s=96&v=4"/></br>[Polymathic-Company](https://github.com/Polymathic-Company) | <img width="50" src="https://avatars2.githubusercontent.com/u/47742?s=96&v=4"/></br>[ravenscroftj](https://github.com/ravenscroftj) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/54626606?s=96&v=4"/></br>[skyrunner15](https://github.com/skyrunner15) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/73081837?s=96&v=4"/></br>[thismarty](https://github.com/thismarty) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[thomasbroussard](https://github.com/thomasbroussard) |
|
||||
| | | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
<!-- TOC -->
|
||||
@@ -345,8 +346,8 @@ If you provide a configuration and you receive "success!" on the "check config"
|
||||
- Force Path Style: unchecked
|
||||
|
||||
### Linode
|
||||
- URL: https://<region>.linodeobjects.com
|
||||
- Region: empty
|
||||
- URL: https://regionName.linodeobjects.com (regionName is the region on the URL provided by Linode; this URL is also the same as the URL provided by Linode with the bucket name removed)
|
||||
- Region: Anything you want to type, can't be left empty
|
||||
- Force Path Style: unchecked
|
||||
|
||||
### UpCloud
|
||||
|
@@ -9,3 +9,7 @@ Only the latest version is supported with security updates.
|
||||
Please [contact support](https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/AdresseSupport.png) **with a proof of concept** that shows the security vulnerability. Please do not contact us without this proof of concept, as we cannot fix anything without this.
|
||||
|
||||
For general opinions on what makes an app more or less secure, please use the forum.
|
||||
|
||||
## Bounty
|
||||
|
||||
We **do not** offer a bounty for discovering vulnerabilities, please do not ask. We can however credit you and link to your website in the changelog and release announcement.
|
||||
|
22
package.json
22
package.json
@@ -12,8 +12,8 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"scripts": {
|
||||
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 run build && yarn run tsc",
|
||||
"buildSequential": "yarn workspaces foreach --verbose --interlaced run build && yarn run tsc",
|
||||
"buildParallel": "yarn workspaces foreach --verbose --interlaced --parallel --jobs 2 --topological run build && yarn run tsc",
|
||||
"buildSequential": "yarn workspaces foreach --verbose --interlaced --topological run build && yarn run tsc",
|
||||
"buildApiDoc": "yarn workspace joplin start apidoc ../../readme/api/references/rest_api.md",
|
||||
"buildCommandIndex": "gulp buildCommandIndex",
|
||||
"buildPluginDoc": "typedoc --name 'Joplin Plugin API Documentation' --mode file -theme './Assets/PluginDocTheme/' --readme './Assets/PluginDocTheme/index.md' --excludeNotExported --excludeExternals --excludePrivate --excludeProtected --out ../joplin-website/docs/api/references/plugin_api packages/lib/services/plugins/api/",
|
||||
@@ -31,6 +31,7 @@
|
||||
"linter-ci": "eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-precommit": "eslint --resolve-plugins-relative-to . --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter": "eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-interactive": "eslint-interactive --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"postinstall": "gulp build",
|
||||
"publishAll": "git pull && yarn run buildParallel && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
|
||||
"releaseAndroid": "PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" node packages/tools/release-android.js",
|
||||
@@ -61,13 +62,15 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.6.0",
|
||||
"@typescript-eslint/parser": "^4.6.0",
|
||||
"@seiyab/eslint-plugin-react-hooks": "^4.5.1-alpha.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
||||
"@typescript-eslint/parser": "^5.33.1",
|
||||
"cspell": "^5.20.0",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-react": "^7.18.0",
|
||||
"eslint-plugin-react-hooks": "^2.4.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-interactive": "^10.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.0.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"gulp": "^4.0.2",
|
||||
@@ -76,9 +79,10 @@
|
||||
"lint-staged": "^9.2.1",
|
||||
"madge": "^4.0.2",
|
||||
"typedoc": "^0.17.8",
|
||||
"typescript": "4.0.5"
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"http-server": "^0.12.3",
|
||||
"node-gyp": "^8.4.1",
|
||||
"nodemon": "^2.0.9"
|
||||
|
@@ -375,6 +375,11 @@ class AppGui {
|
||||
this.showNoteMetadata(!this.widget('noteMetadata').shown);
|
||||
}
|
||||
|
||||
toggleFolderIds() {
|
||||
this.widget('folderList').toggleShowIds();
|
||||
this.widget('noteList').toggleShowIds();
|
||||
}
|
||||
|
||||
widget(name) {
|
||||
if (name === 'root') return this.rootWidget_;
|
||||
return this.rootWidget_.childByName(name);
|
||||
@@ -498,6 +503,8 @@ class AppGui {
|
||||
}
|
||||
} else if (cmd === 'toggle_metadata') {
|
||||
this.toggleNoteMetadata();
|
||||
} else if (cmd === 'toggle_ids') {
|
||||
this.toggleFolderIds();
|
||||
} else if (cmd === 'enter_command_line_mode') {
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
|
@@ -332,6 +332,7 @@ class Application extends BaseApplication {
|
||||
{ keys: [' '], command: 'todo toggle $n' },
|
||||
{ keys: ['tc'], type: 'function', command: 'toggle_console' },
|
||||
{ keys: ['tm'], type: 'function', command: 'toggle_metadata' },
|
||||
{ keys: ['ti'], type: 'function', command: 'toggle_ids' },
|
||||
{ keys: ['/'], type: 'prompt', command: 'search ""', cursorPosition: -2 },
|
||||
{ keys: ['mn'], type: 'prompt', command: 'mknote ""', cursorPosition: -2 },
|
||||
{ keys: ['mt'], type: 'prompt', command: 'mktodo ""', cursorPosition: -2 },
|
||||
|
@@ -124,6 +124,7 @@ async function handleAutocompletionPromise(line) {
|
||||
return line;
|
||||
}
|
||||
function handleAutocompletion(str, callback) {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
handleAutocompletionPromise(str).then(function(res) {
|
||||
callback(undefined, res);
|
||||
});
|
||||
|
@@ -7,25 +7,45 @@ const Note = require('@joplin/lib/models/Note').default;
|
||||
|
||||
class Command extends BaseCommand {
|
||||
usage() {
|
||||
return 'mv <note> [notebook]';
|
||||
return 'mv <item> [notebook]';
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Moves the notes matching <note> to [notebook].');
|
||||
return _('Moves the given <item> (notes matching pattern in current notebook or one notebook) to [notebook]. If <item> is subnotebook and [notebook] is "root", will make <item> parent notebook');
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
const pattern = args['note'];
|
||||
const pattern = args['item'];
|
||||
const destination = args['notebook'];
|
||||
let folder = null;
|
||||
|
||||
const folder = await Folder.loadByField('title', destination);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', destination));
|
||||
if (destination !== 'root') {
|
||||
folder = await app().loadItem(BaseModel.TYPE_FOLDER, destination);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', destination));
|
||||
}
|
||||
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const destinationDuplicates = await Folder.search({ titlePattern: destination, limit: 2 });
|
||||
if (destinationDuplicates.length > 1) {
|
||||
throw new Error(_('Ambiguous notebook "%s". Please use short notebook id instead - press "ti" to see the short notebook id' , destination));
|
||||
}
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
await Note.moveToFolder(notes[i].id, folder.id);
|
||||
const itemFolder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (itemFolder) {
|
||||
const sourceDuplicates = await Folder.search({ titlePattern: pattern, limit: 2 });
|
||||
if (sourceDuplicates.length > 1) {
|
||||
throw new Error(_('Ambiguous notebook "%s". Please use notebook id instead - press "ti" to see the short notebook id or use $b for current selected notebook', pattern));
|
||||
}
|
||||
if (destination === 'root') {
|
||||
await Folder.moveToFolder(itemFolder.id, '');
|
||||
} else {
|
||||
await Folder.moveToFolder(itemFolder.id, folder.id);
|
||||
}
|
||||
} else {
|
||||
const notes = await app().loadItems(BaseModel.TYPE_NOTE, pattern);
|
||||
if (notes.length === 0) throw new Error(_('Cannot find "%s".', pattern));
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
await Note.moveToFolder(notes[i].id, folder.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,7 @@ async function createClients() {
|
||||
const client = createClient(clientId);
|
||||
promises.push(fs.remove(client.profileDir));
|
||||
promises.push(
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
execCommand(client, 'config sync.target 2').then(() => {
|
||||
return execCommand(client, `config sync.2.path ${syncDir}`);
|
||||
})
|
||||
@@ -2324,10 +2325,12 @@ async function main() {
|
||||
clients[clientId].activeCommandCount++;
|
||||
|
||||
execRandomCommand(clients[clientId])
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch(error => {
|
||||
logger.info(`Client ${clientId}:`);
|
||||
logger.error(error);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then(r => {
|
||||
if (r) {
|
||||
logger.info(`Client ${clientId}:\n${r.trim()}`);
|
||||
|
@@ -19,13 +19,20 @@ class FolderListWidget extends ListWidget {
|
||||
this.updateIndexFromSelectedFolderId_ = false;
|
||||
this.updateItems_ = false;
|
||||
this.trimItemTitle = false;
|
||||
this.showIds = false;
|
||||
|
||||
this.itemRenderer = item => {
|
||||
const output = [];
|
||||
if (item === '-') {
|
||||
output.push('-'.repeat(this.innerWidth));
|
||||
} else if (item.type_ === Folder.modelType()) {
|
||||
output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
|
||||
output.push(' '.repeat(this.folderDepth(this.folders, item.id)));
|
||||
|
||||
if (this.showIds) {
|
||||
output.push(Folder.shortId(item.id));
|
||||
}
|
||||
output.push(Folder.displayTitle(item));
|
||||
|
||||
if (Setting.value('showNoteCounts')) {
|
||||
let noteCount = item.note_count;
|
||||
// Subtract children note_count from parent folder.
|
||||
@@ -132,6 +139,11 @@ class FolderListWidget extends ListWidget {
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
toggleShowIds() {
|
||||
this.showIds = !this.showIds;
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
folderHasChildren_(folders, folderId) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
|
@@ -5,11 +5,15 @@ class NoteListWidget extends ListWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.selectedNoteId_ = 0;
|
||||
this.showIds = false;
|
||||
|
||||
this.updateIndexFromSelectedNoteId_ = false;
|
||||
|
||||
this.itemRenderer = note => {
|
||||
let label = Note.displayTitle(note); // + ' ' + note.id;
|
||||
let label = Note.displayTitle(note);
|
||||
if (this.showIds) {
|
||||
label = `${Note.shortId(note.id)} ${Note.displayTitle(note)}`;
|
||||
}
|
||||
if (note.is_todo) {
|
||||
label = `[${note.todo_completed ? 'X' : ' '}] ${label}`;
|
||||
}
|
||||
@@ -22,6 +26,11 @@ class NoteListWidget extends ListWidget {
|
||||
this.selectedNoteId_ = v;
|
||||
}
|
||||
|
||||
toggleShowIds() {
|
||||
this.showIds = !this.showIds;
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.updateIndexFromSelectedNoteId_) {
|
||||
const index = this.itemIndexByKey('id', this.selectedNoteId_);
|
||||
|
@@ -50,6 +50,7 @@ export default class PluginRunner extends BasePluginRunner {
|
||||
const callId = `${pluginId}::${path}::${uuid.createNano()}`;
|
||||
this.activeSandboxCalls_[callId] = true;
|
||||
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
promise.finally(() => {
|
||||
delete this.activeSandboxCalls_[callId];
|
||||
});
|
||||
|
269
packages/app-cli/tests/services/plugins/defaultPluginsUtils.ts
Normal file
269
packages/app-cli/tests/services/plugins/defaultPluginsUtils.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { installDefaultPlugins, getDefaultPluginsInstallState, setSettingsForDefaultPlugins, checkPreInstalledDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo, PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const testPluginDir = `${supportDir}/plugins`;
|
||||
|
||||
function newPluginService(appVersion: string = '1.4') {
|
||||
const runner = new PluginRunner();
|
||||
const service = new PluginService();
|
||||
service.initialize(
|
||||
appVersion,
|
||||
{
|
||||
joplin: {},
|
||||
},
|
||||
runner,
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: () => {},
|
||||
}
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
describe('defaultPluginsUtils', function() {
|
||||
|
||||
const pluginsId = ['joplin.plugin.ambrt.backlinksToNote', 'org.joplinapp.plugins.ToggleSidebars'];
|
||||
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should install default plugins with no previous default plugins installed', (async () => {
|
||||
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
||||
Setting.setValue('installedDefaultPlugins', []);
|
||||
|
||||
const service = newPluginService('2.1');
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings);
|
||||
|
||||
const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`;
|
||||
const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`;
|
||||
|
||||
expect(await pathExists(installedPluginPath1)).toBe(true);
|
||||
expect(await pathExists(installedPluginPath2)).toBe(true);
|
||||
|
||||
expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting());
|
||||
expect(newPluginsSettings[pluginsId[1]]).toMatchObject(defaultPluginSetting());
|
||||
|
||||
}));
|
||||
|
||||
it('should install default plugins with previous default plugins installed', (async () => {
|
||||
|
||||
const testPluginDir = `${supportDir}/pluginRepo/plugins`;
|
||||
Setting.setValue('installedDefaultPlugins', ['org.joplinapp.plugins.ToggleSidebars']);
|
||||
|
||||
const service = newPluginService('2.1');
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
const newPluginsSettings = await installDefaultPlugins(service, testPluginDir, pluginsId, pluginSettings);
|
||||
|
||||
const installedPluginPath1 = `${Setting.value('pluginDir')}/${pluginsId[0]}.jpl`;
|
||||
const installedPluginPath2 = `${Setting.value('pluginDir')}/${pluginsId[1]}.jpl`;
|
||||
|
||||
expect(await pathExists(installedPluginPath1)).toBe(true);
|
||||
expect(await pathExists(installedPluginPath2)).toBe(false);
|
||||
|
||||
expect(newPluginsSettings[pluginsId[0]]).toMatchObject(defaultPluginSetting());
|
||||
expect(newPluginsSettings[pluginsId[1]]).toBeUndefined();
|
||||
}));
|
||||
|
||||
it('should get default plugins install state', (async () => {
|
||||
const testCases = [
|
||||
{
|
||||
'installedDefaultPlugins': [''],
|
||||
'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`],
|
||||
'plugin1DefaultState': defaultPluginSetting(),
|
||||
'plugin2DefaultState': defaultPluginSetting(),
|
||||
'installedDefaultPlugins1': true,
|
||||
'installedDefaultPlugins2': true,
|
||||
},
|
||||
{
|
||||
'installedDefaultPlugins': [''],
|
||||
'loadingPlugins': [`${testPluginDir}/simple`],
|
||||
'plugin1DefaultState': defaultPluginSetting(),
|
||||
'plugin2DefaultState': undefined,
|
||||
'installedDefaultPlugins1': true,
|
||||
'installedDefaultPlugins2': false,
|
||||
},
|
||||
{
|
||||
'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'],
|
||||
'loadingPlugins': [`${testPluginDir}/simple`, `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`],
|
||||
'plugin1DefaultState': undefined,
|
||||
'plugin2DefaultState': defaultPluginSetting(),
|
||||
'installedDefaultPlugins1': true,
|
||||
'installedDefaultPlugins2': true,
|
||||
},
|
||||
{
|
||||
'installedDefaultPlugins': ['org.joplinapp.plugins.Simple'],
|
||||
'loadingPlugins': [`${testPluginDir}/simple`],
|
||||
'plugin1DefaultState': undefined,
|
||||
'plugin2DefaultState': undefined,
|
||||
'installedDefaultPlugins1': true,
|
||||
'installedDefaultPlugins2': false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const service = newPluginService();
|
||||
const pluginsId = ['org.joplinapp.plugins.Simple', 'org.joplinapp.FirstJplPlugin'];
|
||||
|
||||
Setting.setValue('installedDefaultPlugins', testCase.installedDefaultPlugins);
|
||||
await service.loadAndRunPlugins(testCase.loadingPlugins, {});
|
||||
|
||||
// setting installedDefaultPlugins state
|
||||
const defaultInstallStates: PluginSettings = getDefaultPluginsInstallState(service, pluginsId);
|
||||
|
||||
expect(defaultInstallStates[pluginsId[0]]).toStrictEqual(testCase.plugin1DefaultState);
|
||||
expect(defaultInstallStates[pluginsId[1]]).toStrictEqual(testCase.plugin2DefaultState);
|
||||
|
||||
|
||||
const installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(testCase.installedDefaultPlugins1);
|
||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(testCase.installedDefaultPlugins2);
|
||||
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
it('should check pre-installed default plugins', (async () => {
|
||||
// with previous pre-installed default plugins
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
let pluginSettings, installedDefaultPlugins;
|
||||
|
||||
pluginSettings = { [pluginsId[0]]: defaultPluginSetting() };
|
||||
checkPreInstalledDefaultPlugins(pluginsId, pluginSettings);
|
||||
|
||||
installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(true);
|
||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false);
|
||||
|
||||
|
||||
// with no previous pre-installed default plugins
|
||||
Setting.setValue('installedDefaultPlugins', ['not-a-default-plugin']);
|
||||
pluginSettings = {};
|
||||
checkPreInstalledDefaultPlugins(pluginsId, pluginSettings);
|
||||
|
||||
installedDefaultPlugins = Setting.value('installedDefaultPlugins');
|
||||
expect(installedDefaultPlugins.includes(pluginsId[0])).toBe(false);
|
||||
expect(installedDefaultPlugins.includes(pluginsId[1])).toBe(false);
|
||||
|
||||
}));
|
||||
|
||||
it('should set initial settings for default plugins', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "io.github.jackgruber.backup",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
path: {
|
||||
value: "initial-path",
|
||||
type: 2,
|
||||
section: "backupSection",
|
||||
public: true,
|
||||
label: "Backup path",
|
||||
},
|
||||
})
|
||||
},
|
||||
});`;
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
version: '1.0.2',
|
||||
settings: {
|
||||
'path': `${Setting.value('profileDir')}`,
|
||||
},
|
||||
},
|
||||
'plugin.calebjohn.rich-markdown': {
|
||||
version: '0.8.3',
|
||||
},
|
||||
};
|
||||
|
||||
// with pre-installed default plugin
|
||||
Setting.setValue('installedDefaultPlugins', ['io.github.jackgruber.backup']);
|
||||
setSettingsForDefaultPlugins(defaultPluginsInfo);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
||||
await service.destroy();
|
||||
|
||||
// with no pre-installed default plugin
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
setSettingsForDefaultPlugins(defaultPluginsInfo);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
it('should not throw error on missing setting key', async () => {
|
||||
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "io.github.jackgruber.backup",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
path: {
|
||||
value: "initial-path",
|
||||
type: 2,
|
||||
section: "backupSection",
|
||||
public: true,
|
||||
label: "Backup path",
|
||||
},
|
||||
})
|
||||
},
|
||||
});`;
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
version: '1.0.2',
|
||||
settings: {
|
||||
'path': `${Setting.value('profileDir')}`,
|
||||
'missing-key1': 'someValue',
|
||||
},
|
||||
},
|
||||
'plugin.calebjohn.rich-markdown': {
|
||||
version: '0.8.3',
|
||||
settings: {
|
||||
'missing-key2': 'someValue',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
expect(checkThrow(() => setSettingsForDefaultPlugins(defaultPluginsInfo))).toBe(false);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
});
|
BIN
packages/app-cli/tests/services/plugins/mockData/mockPlugin.tgz
Normal file
BIN
packages/app-cli/tests/services/plugins/mockData/mockPlugin.tgz
Normal file
Binary file not shown.
@@ -43,6 +43,7 @@ import sidebarCommands from './gui/Sidebar/commands/index';
|
||||
import appCommands from './commands/index';
|
||||
import libCommands from '@joplin/lib/commands/index';
|
||||
import { homedir } from 'os';
|
||||
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
|
||||
const electronContextMenu = require('./services/electron-context-menu');
|
||||
// import populateDatabase from '@joplin/lib/services/debug/populateDatabase';
|
||||
|
||||
@@ -63,6 +64,8 @@ import checkForUpdates from './checkForUpdates';
|
||||
import { AppState } from './app.reducer';
|
||||
import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import path = require('path');
|
||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
||||
|
||||
const pluginClasses = [
|
||||
@@ -260,9 +263,9 @@ class Application extends BaseApplication {
|
||||
const pluginRunner = new PluginRunner();
|
||||
service.initialize(packageInfo.version, PlatformImplementation.instance(), pluginRunner, this.store());
|
||||
service.isSafeMode = Setting.value('isSafeMode');
|
||||
const defaultPluginsId = Object.keys(getDefaultPluginsInfo());
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
let pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
{
|
||||
// Users can add and remove plugins from the config screen at any
|
||||
// time, however we only effectively uninstall the plugin the next
|
||||
@@ -272,7 +275,11 @@ class Application extends BaseApplication {
|
||||
Setting.setValue('plugins.states', newSettings);
|
||||
}
|
||||
|
||||
checkPreInstalledDefaultPlugins(defaultPluginsId, pluginSettings);
|
||||
|
||||
try {
|
||||
const defaultPluginsDir = path.join(bridge().buildDir(), 'defaultPlugins');
|
||||
pluginSettings = await installDefaultPlugins(service, defaultPluginsDir, defaultPluginsId, pluginSettings);
|
||||
if (await shim.fsDriver().exists(Setting.value('pluginDir'))) {
|
||||
await service.loadAndRunPlugins(Setting.value('pluginDir'), pluginSettings);
|
||||
}
|
||||
@@ -320,6 +327,7 @@ class Application extends BaseApplication {
|
||||
type: 'STARTUP_PLUGINS_LOADED',
|
||||
value: true,
|
||||
});
|
||||
setSettingsForDefaultPlugins(getDefaultPluginsInfo());
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
@@ -494,6 +502,7 @@ class Application extends BaseApplication {
|
||||
if (Setting.value('env') === 'dev') {
|
||||
void AlarmService.updateAllNotifications();
|
||||
} else {
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
void reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
|
@@ -246,7 +246,7 @@ export class Bridge {
|
||||
}
|
||||
|
||||
async openItem(fullPath: string) {
|
||||
return require('electron').shell.openPath(fullPath);
|
||||
return require('electron').shell.openPath(toSystemSlashes(fullPath));
|
||||
}
|
||||
|
||||
screen() {
|
||||
|
@@ -45,6 +45,7 @@ class ClipperConfigScreenComponent extends React.Component {
|
||||
if (confirm(_('Are you sure you want to renew the authorisation token?'))) {
|
||||
void EncryptionService.instance()
|
||||
.generateApiToken()
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.then((token) => {
|
||||
Setting.setValue('api.token', token);
|
||||
});
|
||||
|
@@ -15,6 +15,9 @@ import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
import restart from '../../services/restart';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
|
||||
const settingKeyToControl: any = {
|
||||
@@ -66,6 +69,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
this.switchSection(this.props.defaultSection);
|
||||
});
|
||||
}
|
||||
updateDefaultPluginsInstallState(getDefaultPluginsInstallState(PluginService.instance(), Object.keys(getDefaultPluginsInfo())), this);
|
||||
}
|
||||
|
||||
private async handleSettingButton(key: string) {
|
||||
@@ -484,13 +488,19 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
} else {
|
||||
const paths = await bridge().showOpenDialog();
|
||||
if (!paths || !paths.length) return;
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[0] = paths[0];
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
|
||||
if (md.subType === 'file_path') {
|
||||
updateSettingValue(key, paths[0]);
|
||||
} else {
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
cmd[0] = paths[0];
|
||||
updateSettingValue(key, joinCmd(cmd));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cmd = splitCmd(this.state.settings[key]);
|
||||
const path = md.subType === 'file_path_and_args' ? cmd[0] : this.state.settings[key];
|
||||
|
||||
const argComp = md.subType !== 'file_path_and_args' ? null : (
|
||||
<div style={{ ...rowStyle, marginBottom: 5 }}>
|
||||
@@ -526,7 +536,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
onChange={(event: any) => {
|
||||
onPathChange(event);
|
||||
}}
|
||||
value={cmd[0]}
|
||||
value={path}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<Button
|
||||
|
@@ -101,6 +101,7 @@ export default function(props: Props) {
|
||||
|
||||
const pluginSettings = useMemo(() => {
|
||||
return pluginService.unserializePluginSettings(props.value);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.value]);
|
||||
|
||||
const pluginItems = usePluginItems(pluginService.plugins, pluginSettings);
|
||||
@@ -167,6 +168,7 @@ export default function(props: Props) {
|
||||
});
|
||||
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onToggle = useCallback((event: ItemEvent) => {
|
||||
@@ -178,6 +180,7 @@ export default function(props: Props) {
|
||||
});
|
||||
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onInstall = useCallback(async () => {
|
||||
@@ -195,6 +198,7 @@ export default function(props: Props) {
|
||||
});
|
||||
|
||||
props.onChange({ value: pluginService.serializePluginSettings(newSettings) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, props.onChange]);
|
||||
|
||||
const onBrowsePlugins = useCallback(() => {
|
||||
@@ -203,6 +207,7 @@ export default function(props: Props) {
|
||||
|
||||
const onPluginSettingsChange = useCallback((event: OnPluginSettingChangeEvent) => {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const onUpdate = useOnInstallHandler(setUpdatingPluginIds, pluginSettings, repoApi, onPluginSettingsChange, true);
|
||||
@@ -229,6 +234,7 @@ export default function(props: Props) {
|
||||
|
||||
const onSearchPluginSettingsChange = useCallback((event: any) => {
|
||||
props.onChange({ value: pluginService.serializePluginSettings(event.value) });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onChange]);
|
||||
|
||||
function renderCells(items: PluginItem[]) {
|
||||
|
@@ -60,6 +60,7 @@ export default function(props: Props) {
|
||||
setSearchResultCount(r.length);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.searchQuery]);
|
||||
|
||||
const onChange = useCallback((event: OnChangeEvent) => {
|
||||
@@ -70,6 +71,7 @@ export default function(props: Props) {
|
||||
const onSearchButtonClick = useCallback(() => {
|
||||
setSearchStarted(false);
|
||||
props.onSearchQueryChange({ value: '' });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
function installState(pluginId: string): InstallState {
|
||||
|
@@ -57,5 +57,6 @@ export default function(setInstallingPluginIds: Function, pluginSettings: Plugin
|
||||
});
|
||||
|
||||
if (installError) alert(_('Could not install plugin: %s', installError.message));
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [pluginSettings, onPluginSettingsChange]);
|
||||
}
|
||||
|
@@ -16,8 +16,10 @@ export default (props: Props) => {
|
||||
globalKeydownHandlersRef.current.push(elementId);
|
||||
return () => {
|
||||
const idx = globalKeydownHandlersRef.current.findIndex(e => e === elementId);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
globalKeydownHandlersRef.current.splice(idx, 1);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const isTopDialog = () => {
|
||||
@@ -49,6 +51,7 @@ export default (props: Props) => {
|
||||
} else if (event.keyCode === 27) {
|
||||
props.onCancelButtonClick();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onOkButtonClick, props.onCancelButtonClick]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -81,6 +81,7 @@ export default function(props: Props) {
|
||||
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [onClose, folderTitle, folderIcon, props.folderId, props.parentId]);
|
||||
|
||||
const onFolderTitleChange = useCallback((event: any) => {
|
||||
|
@@ -2,15 +2,19 @@ import { FolderIcon, FolderIconType } from '@joplin/lib/services/database/types'
|
||||
|
||||
interface Props {
|
||||
folderIcon: FolderIcon;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export default function(props: Props) {
|
||||
const folderIcon = props.folderIcon;
|
||||
const opacity = 'opacity' in props ? props.opacity : 1;
|
||||
|
||||
if (folderIcon.type === FolderIconType.Emoji) {
|
||||
return <span style={{ fontSize: 20 }}>{folderIcon.emoji}</span>;
|
||||
return <span style={{ fontSize: 20, opacity }}>{folderIcon.emoji}</span>;
|
||||
} else if (folderIcon.type === FolderIconType.DataUrl) {
|
||||
return <img style={{ width: 20, height: 20 }} src={folderIcon.dataUrl} />;
|
||||
return <img style={{ width: 20, height: 20, opacity }} src={folderIcon.dataUrl} />;
|
||||
} else if (folderIcon.type === FolderIconType.FontAwesome) {
|
||||
return <i style={{ fontSize: 18, width: 20, opacity }} className={folderIcon.name}></i>;
|
||||
} else {
|
||||
throw new Error(`Unsupported folder icon type: ${folderIcon.type}`);
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAc
|
||||
onError({ recorderError });
|
||||
setSaveAllowed(false);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [accelerator]);
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
|
@@ -15,6 +15,7 @@ import * as openFolder from './openFolder';
|
||||
import * as openFolderDialog from './openFolderDialog';
|
||||
import * as openItem from './openItem';
|
||||
import * as openNote from './openNote';
|
||||
import * as openPdfViewer from './openPdfViewer';
|
||||
import * as openTag from './openTag';
|
||||
import * as print from './print';
|
||||
import * as renameFolder from './renameFolder';
|
||||
@@ -55,6 +56,7 @@ const index:any[] = [
|
||||
openFolderDialog,
|
||||
openItem,
|
||||
openNote,
|
||||
openPdfViewer,
|
||||
openTag,
|
||||
print,
|
||||
renameFolder,
|
||||
|
@@ -0,0 +1,28 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'openPdfViewer',
|
||||
label: () => _('Open PDF viewer'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, resourceId: string, pageNo: number) => {
|
||||
|
||||
const resource = await Resource.load(resourceId);
|
||||
if (!resource) throw new Error(`No such resource: ${resourceId}`);
|
||||
if (resource.mime !== 'application/pdf') throw new Error(`Not a PDF: ${resource.mime}`);
|
||||
console.log('Opening PDF', resource);
|
||||
context.dispatch({
|
||||
type: 'DIALOG_OPEN',
|
||||
name: 'pdfViewer',
|
||||
props: {
|
||||
resource,
|
||||
pageNo: pageNo,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
@@ -14,21 +14,24 @@ export const declaration: CommandDeclaration = {
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, selectedLanguage: string = null, useSpellChecker: boolean = null) => {
|
||||
selectedLanguage = selectedLanguage === null ? context.state.settings['spellChecker.language'] : selectedLanguage;
|
||||
execute: async (context: CommandContext, selectedLanguages: string[] = null, useSpellChecker: boolean = null) => {
|
||||
selectedLanguages = selectedLanguages === null ? context.state.settings['spellChecker.languages'] : selectedLanguages;
|
||||
useSpellChecker = useSpellChecker === null ? context.state.settings['spellChecker.enabled'] : useSpellChecker;
|
||||
|
||||
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguage, useSpellChecker);
|
||||
const menuItems = SpellCheckerService.instance().spellCheckerConfigMenuItems(selectedLanguages, useSpellChecker);
|
||||
const menu = Menu.buildFromTemplate(menuItems as any);
|
||||
menu.popup(bridge().window());
|
||||
},
|
||||
|
||||
mapStateToTitle(state: AppState): string {
|
||||
if (!state.settings['spellChecker.enabled']) return null;
|
||||
const language = state.settings['spellChecker.language'];
|
||||
if (!language) return null;
|
||||
const s = language.split('-');
|
||||
return s[0];
|
||||
const languages = state.settings['spellChecker.languages'];
|
||||
if (languages.length === 0) return null;
|
||||
const s: string[] = [];
|
||||
languages.forEach((language: string) => {
|
||||
s.push(language.split('-')[0]);
|
||||
});
|
||||
return s.join(', ');
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@@ -38,6 +38,7 @@ export default function(props: Props) {
|
||||
if ([MasterPasswordStatus.NotSet, MasterPasswordStatus.Invalid].includes(status)) return false;
|
||||
if (mode === Mode.Reset) return false;
|
||||
return true;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [status]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
@@ -84,6 +85,7 @@ export default function(props: Props) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [currentPassword, password1, onClose, mode]);
|
||||
|
||||
const needToRepeatPassword = useMemo(() => {
|
||||
|
@@ -122,7 +122,7 @@ interface Props {
|
||||
pluginMenuItems: any[];
|
||||
pluginMenus: any[];
|
||||
['spellChecker.enabled']: boolean;
|
||||
['spellChecker.language']: string;
|
||||
['spellChecker.languages']: string[];
|
||||
plugins: PluginStates;
|
||||
customCss: string;
|
||||
locale: string;
|
||||
@@ -192,12 +192,17 @@ function useMenuStates(menu: any, props: Props) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [
|
||||
props.menuItemProps,
|
||||
props.layoutButtonSequence,
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['notes.sortOrder.field'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['folders.sortOrder.field'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['notes.sortOrder.reverse'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['folders.sortOrder.reverse'],
|
||||
props.showNoteCounts,
|
||||
props.uncompletedTodosOnTop,
|
||||
@@ -276,6 +281,7 @@ function useMenu(props: Props) {
|
||||
}
|
||||
|
||||
void CommandService.instance().execute('hideModalMessage');
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.selectedFolderId]);
|
||||
|
||||
const onMenuItemClickRef = useRef(null);
|
||||
@@ -292,6 +298,7 @@ function useMenu(props: Props) {
|
||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||
props.locale
|
||||
);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [commandNames, pluginCommandNames, props.locale]);
|
||||
|
||||
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
|
||||
@@ -471,7 +478,7 @@ function useMenu(props: Props) {
|
||||
}
|
||||
toolsItems = toolsItems.concat(toolsItemsAll);
|
||||
|
||||
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.language'], props['spellChecker.enabled']));
|
||||
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
|
||||
|
||||
function _checkForUpdates() {
|
||||
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||
@@ -905,13 +912,16 @@ function useMenu(props: Props) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [
|
||||
props.routeName,
|
||||
props.pluginMenuItems,
|
||||
props.pluginMenus,
|
||||
keymapLastChangeTime,
|
||||
modulesLastChangeTime,
|
||||
props['spellChecker.language'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['spellChecker.languages'],
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
props['spellChecker.enabled'],
|
||||
props.customCss,
|
||||
props.locale,
|
||||
@@ -973,7 +983,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
pluginMenuItems: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menuItem') }, 'menuBar.pluginMenuItems'),
|
||||
pluginMenus: stateUtils.selectArrayShallow({ array: pluginUtils.viewsByType(state.pluginService.plugins, 'menu') }, 'menuBar.pluginMenus'),
|
||||
['spellChecker.language']: state.settings['spellChecker.language'],
|
||||
['spellChecker.languages']: state.settings['spellChecker.languages'],
|
||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
|
@@ -70,6 +70,7 @@ export default function NoteContentPropertiesDialog(props: NoteContentProperties
|
||||
useEffect(() => {
|
||||
const strippedText: string = markupToHtml().stripMarkup(props.markupLanguage, props.text);
|
||||
countElements(strippedText, setStrippedWords, setStrippedCharacters, setStrippedCharactersNoSpace, setStrippedLines);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.text]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -259,6 +259,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
return commandOutput;
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
@@ -565,6 +566,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
return () => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.themeId, props.contentMaxWidth]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
@@ -592,6 +594,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onMessage, props.content, setEditorPercentScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -616,6 +619,8 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
mapsToLine: true,
|
||||
// Always using useCustomPdfViewer for now, we can add a new setting for it in future if we need to.
|
||||
useCustomPdfViewer: true,
|
||||
noteId: props.noteId,
|
||||
vendorDir: bridge().vendorDir(),
|
||||
}));
|
||||
|
||||
if (cancelled) return;
|
||||
@@ -635,6 +640,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
cancelled = true;
|
||||
shim.clearTimeout(timeoutId);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.content, props.contentKey, renderedBodyContentKey, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -660,6 +666,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
} else {
|
||||
console.error('Trying to set HTML on an undefined webview ref');
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -683,6 +690,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.searchMarkers, previousSearchMarkers, props.setLocalSearchResultCount, props.content, previousContent, renderedBody, previousRenderedBody, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
@@ -835,10 +843,13 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
return () => {
|
||||
bridge().window().webContents.off('context-menu', onContextMenu);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.plugins]);
|
||||
|
||||
function renderEditor() {
|
||||
|
||||
const matchBracesOptions = Setting.value('editor.autoMatchingBraces') ? { override: true, pairs: '<>()[]{}\'\'""‘’“”()《》「」『』【】〔〕〖〗〘〙〚〛' } : false;
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<Editor
|
||||
@@ -849,7 +860,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
codeMirrorTheme={styles.editor.codeMirrorTheme}
|
||||
style={styles.editor}
|
||||
readOnly={props.visiblePanes.indexOf('editor') < 0}
|
||||
autoMatchBraces={Setting.value('editor.autoMatchingBraces')}
|
||||
autoMatchBraces={matchBracesOptions}
|
||||
keyMap={props.keyboardMode}
|
||||
plugins={props.plugins}
|
||||
onChange={codeMirror_change}
|
||||
|
@@ -86,7 +86,7 @@ export interface EditorProps {
|
||||
style: any;
|
||||
codeMirrorTheme: any;
|
||||
readOnly: boolean;
|
||||
autoMatchBraces: boolean;
|
||||
autoMatchBraces: boolean | object;
|
||||
keyMap: string;
|
||||
plugins: PluginStates;
|
||||
onChange: any;
|
||||
@@ -219,9 +219,11 @@ function Editor(props: EditorProps, ref: any) {
|
||||
cm.off('dragover', editor_drag);
|
||||
cm.off('refresh', editor_resize);
|
||||
cm.off('update', editor_update);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
editorParent.current.removeChild(cm.getWrapperElement());
|
||||
setEditor(null);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -234,36 +236,42 @@ function Editor(props: EditorProps, ref: any) {
|
||||
}
|
||||
editor.setOption('screenReaderLabel', props.value);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('theme', props.codeMirrorTheme);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.codeMirrorTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('mode', props.mode);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('readOnly', props.readOnly);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.readOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('autoCloseBrackets', props.autoMatchBraces);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.autoMatchBraces]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor) {
|
||||
editor.setOption('keyMap', props.keyMap ? props.keyMap : 'default');
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.keyMap]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -7,6 +7,8 @@ import uuid from '@joplin/lib/uuid';
|
||||
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
|
||||
const loadedPluginIdSet = new Set<string>();
|
||||
|
||||
export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) {
|
||||
|
||||
const [options, setOptions] = useState({});
|
||||
@@ -17,6 +19,10 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
|
||||
|
||||
for (const contentScript of contentScripts) {
|
||||
try {
|
||||
if (loadedPluginIdSet.has(contentScript.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mod = contentScript.module;
|
||||
|
||||
if (mod.codeMirrorResources) {
|
||||
@@ -64,11 +70,14 @@ export default function useExternalPlugins(CodeMirror: any, plugins: PluginState
|
||||
if (mod.plugin) {
|
||||
mod.plugin(CodeMirror);
|
||||
}
|
||||
|
||||
loadedPluginIdSet.add(contentScript.id);
|
||||
} catch (error) {
|
||||
reg.logger().error(error.toString());
|
||||
}
|
||||
}
|
||||
setOptions(newOptions);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [plugins]);
|
||||
|
||||
function addInlineCss(cssStrings: string[], id: string) {
|
||||
|
@@ -184,5 +184,6 @@ export default function useKeymap(CodeMirror: any) {
|
||||
|
||||
setupEmacs();
|
||||
setupVim();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
@@ -94,6 +94,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
if (editorRef.current) {
|
||||
scheduleOnScroll({ percent });
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [scheduleOnScroll]);
|
||||
|
||||
const setViewerPercentScroll = useCallback((percent: number) => {
|
||||
@@ -101,6 +102,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
webviewRef.current.wrappedInstance.send('setPercentScroll', percent);
|
||||
scheduleOnScroll({ percent });
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [scheduleOnScroll]);
|
||||
|
||||
const editor_scroll = useCallback(() => {
|
||||
@@ -126,6 +128,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
lastResizeHeight_.current = NaN;
|
||||
lastLinesHeight_.current = NaN;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [setViewerPercentScroll]);
|
||||
|
||||
const resetScroll = useCallback(() => {
|
||||
@@ -134,6 +137,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
editorRef.current.setScrollPercent(0);
|
||||
scrollTopIsUncertain_.current = false;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const editor_resize = useCallback((cm) => {
|
||||
@@ -152,6 +156,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
lastResizeHeight_.current = NaN;
|
||||
lastLinesHeight_.current = NaN;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
// When heights of lines are updated in CodeMirror, 'update' events are raised.
|
||||
@@ -173,6 +178,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
lastResizeHeight_.current = NaN;
|
||||
lastLinesHeight_.current = NaN;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const getLineScrollPercent = useCallback(() => {
|
||||
@@ -183,6 +189,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
} else {
|
||||
return scrollPercent_.current;
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
@@ -280,6 +280,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editor, props.contentMarkupLanguage, props.contentOriginalCss]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
@@ -512,6 +513,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// style and re-applying it on editorReady gives our styles precedence and prevents any flashing
|
||||
//
|
||||
// tl;dr: editorReady is used here because the css needs to be re-applied after TinyMCE init
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editorReady, props.themeId]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
@@ -680,6 +682,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
};
|
||||
|
||||
void loadEditor();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [scriptLoaded]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
@@ -829,6 +832,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editor, props.markupToHtml, props.allAssets, props.content, props.resourceInfos, props.contentKey]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -909,6 +913,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
return () => {
|
||||
void execOnChangeEvent();
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const onChangeHandlerTimeoutRef = useRef<any>(null);
|
||||
@@ -1003,7 +1008,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else { // Paste regular text
|
||||
const pastedHtml = event.clipboardData.getData('text/html');
|
||||
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
|
||||
// which seems to be not supported in editor.insertContent().
|
||||
const pastedHtml = clipboard.readHTML();
|
||||
if (pastedHtml) { // Handles HTML
|
||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
||||
editor.insertContent(modifiedHtml);
|
||||
@@ -1091,6 +1098,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
console.warn('Error removing events', error);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.onWillChange, props.onChange, props.contentMarkupLanguage, props.contentOriginalCss, editor]);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
@@ -59,6 +59,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
const formNote_beforeLoad = useCallback(async (event: OnLoadEvent) => {
|
||||
await saveNoteIfWillChange(event.formNote);
|
||||
setShowRevisions(false);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const formNote_afterLoad = useCallback(async () => {
|
||||
@@ -177,6 +178,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
id: formNote.id,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.isProvisional, formNote.id]);
|
||||
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
@@ -194,6 +196,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
});
|
||||
|
||||
void ResourceEditWatcher.instance().stopWatchingAll();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote.id, previousNoteId]);
|
||||
|
||||
const onFieldChange = useCallback((field: string, value: any, changeId = 0) => {
|
||||
@@ -238,6 +241,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
setFormNote(newNote);
|
||||
scheduleSaveNote(newNote);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [handleProvisionalFlag, formNote, isNewNote, titleHasBeenManuallyChanged]);
|
||||
|
||||
useWindowCommandHandler({
|
||||
@@ -288,6 +292,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
id: formNote.id,
|
||||
status: 'saving',
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote, handleProvisionalFlag]);
|
||||
|
||||
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote);
|
||||
@@ -302,6 +307,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
setFormNote(newFormNote);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote]);
|
||||
|
||||
const onNotePropertyChange = useCallback((event) => {
|
||||
@@ -317,6 +323,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
return newFormNote;
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -350,6 +357,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
noteId: formNoteRef.current.id,
|
||||
percent: event.percent,
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.dispatch, formNote]);
|
||||
|
||||
function renderNoNotes(rootStyle: any) {
|
||||
@@ -413,6 +421,9 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
fontSize: Setting.value('style.editor.fontSize'),
|
||||
contentMaxWidth: props.contentMaxWidth,
|
||||
isSafeMode: props.isSafeMode,
|
||||
// We need it to identify the context for which media is rendered.
|
||||
// It is currently used to remember pdf scroll position for each attacments of each note uniquely.
|
||||
noteId: props.noteId,
|
||||
};
|
||||
|
||||
let editor = null;
|
||||
|
@@ -75,6 +75,7 @@ export interface NoteBodyEditorProps {
|
||||
fontSize: number;
|
||||
contentMaxWidth: number;
|
||||
isSafeMode: boolean;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
export interface FormNote {
|
||||
|
@@ -51,5 +51,6 @@ export default function useDropHandler(dependencies: HookDependencies) {
|
||||
},
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
@@ -138,6 +138,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [prevSyncStarted, syncStarted, formNote]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -188,6 +189,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [noteId, isProvisional, formNote]);
|
||||
|
||||
const onResourceChange = useCallback(async function(event: any = null) {
|
||||
|
@@ -21,6 +21,8 @@ export interface MarkupToHtmlOptions {
|
||||
bodyOnly?: boolean;
|
||||
mapsToLine?: boolean;
|
||||
useCustomPdfViewer?: boolean;
|
||||
noteId?: string;
|
||||
vendorDir?: string;
|
||||
}
|
||||
|
||||
export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
@@ -31,6 +33,7 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
resourceBaseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
customCss: customCss || '',
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [plugins, customCss]);
|
||||
|
||||
return useCallback(async (markupLanguage: number, md: string, options: MarkupToHtmlOptions = null): Promise<any> => {
|
||||
@@ -62,5 +65,6 @@ export default function useMarkupToHtml(deps: HookDependencies) {
|
||||
}, options));
|
||||
|
||||
return result;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [themeId, customCss, markupToHtml]);
|
||||
}
|
||||
|
@@ -52,9 +52,12 @@ export default function useMessageHandler(scrollWhenReady: any, setScrollWhenRea
|
||||
void CommandService.instance().execute(commandName, ...commandArgs);
|
||||
} else if (msg === 'postMessageService.message') {
|
||||
void PostMessageService.instance().postMessage(arg0);
|
||||
} else if (msg === 'openPdfViewer') {
|
||||
await CommandService.instance().execute('openPdfViewer', arg0.resourceId, arg0.pageNo);
|
||||
} else {
|
||||
await CommandService.instance().execute('openItem', msg);
|
||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [dispatch, setLocalSearchResultCount, scrollWhenReady, formNote]);
|
||||
}
|
||||
|
@@ -8,5 +8,6 @@ export default function usePluginServiceRegistration(ref: any) {
|
||||
return () => {
|
||||
PlatformImplementation.instance().unregisterComponent('textEditor');
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
@@ -32,5 +32,6 @@ export default function useSearchMarkers(showLocalSearch: boolean, localSearchMa
|
||||
output.keywords = highlightedWords;
|
||||
|
||||
return output;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [highlightedWords, showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
|
||||
}
|
||||
|
@@ -96,5 +96,6 @@ export default function useWindowCommandHandler(dependencies: HookDependencies)
|
||||
CommandService.instance().unregisterRuntime(command.declaration.name);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [editorRef, setShowLocalSearch, noteSearchBarRef, titleInputRef]);
|
||||
}
|
||||
|
@@ -273,6 +273,7 @@ const NoteListComponent = (props: Props) => {
|
||||
onTitleClick={noteItem_titleClick}
|
||||
onContextMenu={itemContextMenu}
|
||||
/>;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [style, props.themeId, width, itemHeight, dragOverTargetNoteIndex, props.provisionalNoteIds, props.selectedNoteIds, props.watchedNoteFiles,
|
||||
props.notes,
|
||||
props.notesParentType,
|
||||
@@ -305,6 +306,7 @@ const NoteListComponent = (props: Props) => {
|
||||
if (previousVisible !== props.visible) {
|
||||
updateSizeState();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [previousSelectedNoteIds,previousNotes, previousVisible, props.selectedNoteIds, props.notes]);
|
||||
|
||||
const scrollNoteIndex_ = (keyCode: any, ctrlKey: any, metaKey: any, noteIndex: any) => {
|
||||
@@ -439,6 +441,7 @@ const NoteListComponent = (props: Props) => {
|
||||
return () => {
|
||||
props.resizableLayoutEventEmitter.off('resize', resizableLayout_resize);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.resizableLayoutEventEmitter]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -453,6 +456,7 @@ const NoteListComponent = (props: Props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
useEffect(() => {
|
||||
// When a note list item is styled by userchrome.css, its height is reflected.
|
||||
// Ref. https://github.com/laurent22/joplin/pull/6542
|
||||
|
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
101
packages/app-desktop/gui/PdfViewer.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import Resource from '@joplin/lib/models/Resource';
|
||||
import bridge from '../services/bridge';
|
||||
import contextMenu from './NoteEditor/utils/contextMenu';
|
||||
import { ContextMenuItemType, ContextMenuOptions } from './NoteEditor/utils/contextMenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
const Window = styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 999;
|
||||
background-color: ${(props: any) => props.theme.backgroundColor};
|
||||
color: ${(props: any) => props.theme.color};
|
||||
`;
|
||||
|
||||
const IFrame = styled.iframe`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
dispatch: Function;
|
||||
resource: any;
|
||||
pageNo: number;
|
||||
}
|
||||
|
||||
export default function PdfViewer(props: Props) {
|
||||
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
props.dispatch({
|
||||
type: 'DIALOG_CLOSE',
|
||||
name: 'pdfViewer',
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const openExternalViewer = useCallback(async () => {
|
||||
await CommandService.instance().execute('openItem', `joplin://${props.resource.id}`);
|
||||
}, [props.resource.id]);
|
||||
|
||||
const textSelected = useCallback(async (text: string) => {
|
||||
if (!text) return;
|
||||
const itemType = ContextMenuItemType.Text;
|
||||
const menu = await contextMenu({
|
||||
itemType,
|
||||
resourceId: null,
|
||||
filename: null,
|
||||
mime: 'text/plain',
|
||||
textToCopy: text,
|
||||
linkToCopy: null,
|
||||
htmlToCopy: '',
|
||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||
} as ContextMenuOptions, props.dispatch);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage_ = async (event: any) =>{
|
||||
if (!event.data || !event.data.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.name === 'close') {
|
||||
onClose();
|
||||
} else if (event.data.name === 'externalViewer') {
|
||||
await openExternalViewer();
|
||||
} else if (event.data.name === 'textSelected') {
|
||||
await textSelected(event.data.text);
|
||||
} else {
|
||||
console.error('Unknown event received', event.data.name);
|
||||
}
|
||||
};
|
||||
const iframe = iframeRef.current;
|
||||
iframe.contentWindow.addEventListener('message', onMessage_);
|
||||
return () => {
|
||||
iframe.contentWindow.removeEventListener('message', onMessage_);
|
||||
};
|
||||
}, [onClose, openExternalViewer, textSelected]);
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
return (
|
||||
<Window theme={theme}>
|
||||
<IFrame src="./vendor/lib/@joplin/pdf-viewer/index.html" x-url={Resource.fullPath(props.resource)}
|
||||
x-appearance={theme.appearance} ref={iframeRef}
|
||||
x-title={props.resource.title}
|
||||
x-anchorpage={props.pageNo}
|
||||
x-type="full"></IFrame>
|
||||
</Window>
|
||||
);
|
||||
}
|
@@ -13,5 +13,6 @@ export default function useWindowResizeEvent(eventEmitter: any) {
|
||||
window_resize.clear();
|
||||
window.removeEventListener('resize', window_resize);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
}
|
||||
|
@@ -174,9 +174,11 @@ class ResourceScreenComponent extends React.Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
Resource.delete(resource.id)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.catch((error: Error) => {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then -- Old code before rule was applied
|
||||
.finally(() => {
|
||||
void this.reloadResources(this.state.sorting);
|
||||
});
|
||||
|
@@ -22,6 +22,7 @@ import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||
import PdfViewer from './PdfViewer';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
@@ -75,6 +76,11 @@ const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
pdfViewer: {
|
||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||
return <PdfViewer key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
@@ -66,6 +66,7 @@ function useRestartOnDone(upgradeResult: SyncTargetUpgradeResult) {
|
||||
if (upgradeResult.done && !upgradeResult.error) {
|
||||
void restart();
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [upgradeResult.done]);
|
||||
}
|
||||
|
||||
|
@@ -55,6 +55,7 @@ function SearchBar(props: Props) {
|
||||
return () => {
|
||||
debouncedSearch.clear();
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [query, searchStarted]);
|
||||
|
||||
const onExitSearch = useCallback(async (navigateAway = true) => {
|
||||
@@ -80,6 +81,7 @@ function SearchBar(props: Props) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.selectedNoteId]);
|
||||
|
||||
function onChange(event: any) {
|
||||
@@ -129,6 +131,7 @@ function SearchBar(props: Props) {
|
||||
field: 'globalSearch',
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [onExitSearch, props.isFocused, searchStarted]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -150,6 +153,7 @@ function SearchBar(props: Props) {
|
||||
}
|
||||
void onExitSearch(true);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@@ -140,6 +140,7 @@ function ShareFolderDialog(props: Props) {
|
||||
useEffect(() => {
|
||||
const s = props.shares.find(s => s.folder_id === props.folderId);
|
||||
setShare(s);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [props.shares]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||
import { ButtonLevel } from '../Button/Button';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -17,12 +18,14 @@ import Folder from '@joplin/lib/models/Folder';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import { FolderEntity, FolderIcon } from '@joplin/lib/services/database/types';
|
||||
import { FolderEntity, FolderIcon, FolderIconType } from '@joplin/lib/services/database/types';
|
||||
import stateToWhenClauseContext from '../../services/commands/stateToWhenClauseContext';
|
||||
import { store } from '@joplin/lib/reducer';
|
||||
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
|
||||
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
import FolderIconBox from '../FolderIconBox';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { RuntimeProps } from './commands/focusElementSideBar';
|
||||
const { connect } = require('react-redux');
|
||||
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
@@ -50,11 +53,8 @@ interface Props {
|
||||
tags: any[];
|
||||
syncStarted: boolean;
|
||||
plugins: PluginStates;
|
||||
}
|
||||
|
||||
interface State {
|
||||
tagHeaderIsExpanded: boolean;
|
||||
folderHeaderIsExpanded: boolean;
|
||||
tagHeaderIsExpanded: boolean;
|
||||
}
|
||||
|
||||
const commands = [
|
||||
@@ -79,13 +79,21 @@ function ExpandLink(props: any) {
|
||||
}
|
||||
|
||||
const renderFolderIcon = (folderIcon: FolderIcon) => {
|
||||
if (!folderIcon) return null;
|
||||
if (!folderIcon) {
|
||||
const defaultFolderIcon: FolderIcon = {
|
||||
dataUrl: '',
|
||||
emoji: '',
|
||||
name: 'far fa-folder',
|
||||
type: FolderIconType.FontAwesome,
|
||||
};
|
||||
return <div style={{ marginRight: 7, display: 'flex' }}><FolderIconBox opacity={0.7} folderIcon={defaultFolderIcon}/></div>;
|
||||
}
|
||||
|
||||
return <div style={{ marginRight: 5, display: 'flex' }}><FolderIconBox folderIcon={folderIcon}/></div>;
|
||||
return <div style={{ marginRight: 7, display: 'flex' }}><FolderIconBox folderIcon={folderIcon}/></div>;
|
||||
};
|
||||
|
||||
function FolderItem(props: any) {
|
||||
const { hasChildren, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
||||
const { hasChildren, showFolderIcon, isExpanded, parentId, depth, selected, folderId, folderTitle, folderIcon, anchorRef, noteCount, onFolderDragStart_, onFolderDragOver_, onFolderDrop_, itemContextMenu, folderItem_click, onFolderToggleClick_, shareId } = props;
|
||||
|
||||
const noteCountComp = noteCount ? <StyledNoteCount className="note-count-label">{noteCount}</StyledNoteCount> : null;
|
||||
|
||||
@@ -110,7 +118,7 @@ function FolderItem(props: any) {
|
||||
}}
|
||||
onDoubleClick={onFolderToggleClick_}
|
||||
>
|
||||
{renderFolderIcon(folderIcon)}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{showFolderIcon ? renderFolderIcon(folderIcon) : null}<span className="title" style={{ lineHeight: 0 }}>{folderTitle}</span>
|
||||
{shareIcon} {noteCountComp}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
@@ -119,56 +127,88 @@ function FolderItem(props: any) {
|
||||
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
class SidebarComponent extends React.Component<Props, State> {
|
||||
const SidebarComponent = (props: Props) => {
|
||||
|
||||
private folderItemsOrder_: any[] = [];
|
||||
private tagItemsOrder_: any[] = [];
|
||||
private rootRef: any = null;
|
||||
private anchorItemRefs: any = {};
|
||||
private pluginsRef: any;
|
||||
const folderItemsOrder_ = useRef<any[]>();
|
||||
folderItemsOrder_.current = [];
|
||||
const tagItemsOrder_ = useRef<any[]>();
|
||||
tagItemsOrder_.current = [];
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const rootRef = useRef(null);
|
||||
const anchorItemRefs = useRef<Record<string, any>>(null);
|
||||
anchorItemRefs.current = {};
|
||||
|
||||
CommandService.instance().componentRegisterCommands(this, commands);
|
||||
// This whole component is a bit of a mess and rather than passing
|
||||
// a plugins prop around, not knowing how it's going to affect
|
||||
// re-rendering, we just keep a ref to it. Currently that's enough
|
||||
// as plugins are only accessed from context menus. However if want
|
||||
// to do more complex things with plugins in the sidebar, it will
|
||||
// probably have to be refactored using React Hooks first.
|
||||
const pluginsRef = useRef<PluginStates>(null);
|
||||
pluginsRef.current = props.plugins;
|
||||
|
||||
this.state = {
|
||||
tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'),
|
||||
folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'),
|
||||
// If at least one of the folder has an icon, then we display icons for all
|
||||
// folders (those without one will get the default icon). This is so that
|
||||
// visual alignment is correct for all folders, otherwise the folder tree
|
||||
// looks messy.
|
||||
const showFolderIcons = useMemo(() => {
|
||||
return Folder.shouldShowFolderIcons(props.folders);
|
||||
}, [props.folders]);
|
||||
|
||||
const getSelectedItem = useCallback(() => {
|
||||
if (props.notesParentType === 'Folder' && props.selectedFolderId) {
|
||||
return { type: 'folder', id: props.selectedFolderId };
|
||||
} else if (props.notesParentType === 'Tag' && props.selectedTagId) {
|
||||
return { type: 'tag', id: props.selectedTagId };
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [props.notesParentType, props.selectedFolderId, props.selectedTagId]);
|
||||
|
||||
const getFirstAnchorItemRef = useCallback((type: string) => {
|
||||
const refs = anchorItemRefs.current[type];
|
||||
if (!refs) return null;
|
||||
|
||||
const p = type === 'folder' ? props.folders : props.tags;
|
||||
const item = p && p.length ? p[0] : null;
|
||||
if (!item) return null;
|
||||
|
||||
return refs[item.id];
|
||||
}, [anchorItemRefs, props.folders, props.tags]);
|
||||
|
||||
useEffect(() => {
|
||||
const runtimeProps: RuntimeProps = {
|
||||
getSelectedItem,
|
||||
anchorItemRefs,
|
||||
getFirstAnchorItemRef,
|
||||
};
|
||||
|
||||
// This whole component is a bit of a mess and rather than passing
|
||||
// a plugins prop around, not knowing how it's going to affect
|
||||
// re-rendering, we just keep a ref to it. Currently that's enough
|
||||
// as plugins are only accessed from context menus. However if want
|
||||
// to do more complex things with plugins in the sidebar, it will
|
||||
// probably have to be refactored using React Hooks first.
|
||||
this.pluginsRef = React.createRef();
|
||||
CommandService.instance().componentRegisterCommands(runtimeProps, commands);
|
||||
|
||||
this.onFolderToggleClick_ = this.onFolderToggleClick_.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
|
||||
this.header_contextMenu = this.header_contextMenu.bind(this);
|
||||
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this);
|
||||
this.folderItem_click = this.folderItem_click.bind(this);
|
||||
this.itemContextMenu = this.itemContextMenu.bind(this);
|
||||
}
|
||||
return () => {
|
||||
CommandService.instance().componentUnregisterCommands(commands);
|
||||
};
|
||||
}, [
|
||||
getSelectedItem,
|
||||
anchorItemRefs,
|
||||
getFirstAnchorItemRef,
|
||||
]);
|
||||
|
||||
onFolderDragStart_(event: any) {
|
||||
const onFolderDragStart_ = useCallback((event: any) => {
|
||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||
if (!folderId) return;
|
||||
|
||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
|
||||
}
|
||||
}, []);
|
||||
|
||||
onFolderDragOver_(event: any) {
|
||||
const onFolderDragOver_ = useCallback((event: any) => {
|
||||
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
||||
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
async onFolderDrop_(event: any) {
|
||||
const onFolderDrop_ = useCallback(async (event: any) => {
|
||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||
const dt = event.dataTransfer;
|
||||
if (!dt) return;
|
||||
@@ -199,9 +239,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
logger.error(error);
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
async onTagDrop_(event: any) {
|
||||
const onTagDrop_ = useCallback(async (event: any) => {
|
||||
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
||||
const dt = event.dataTransfer;
|
||||
if (!dt) return;
|
||||
@@ -214,22 +254,18 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
await Tag.addNote(tagId, noteIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
async onFolderToggleClick_(event: any) {
|
||||
const onFolderToggleClick_ = useCallback((event: any) => {
|
||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||
|
||||
this.props.dispatch({
|
||||
props.dispatch({
|
||||
type: 'FOLDER_TOGGLE',
|
||||
id: folderId,
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
componentWillUnmount() {
|
||||
CommandService.instance().componentUnregisterCommands(commands);
|
||||
}
|
||||
|
||||
async header_contextMenu() {
|
||||
const header_contextMenu = useCallback(async () => {
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(
|
||||
@@ -237,9 +273,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
}, []);
|
||||
|
||||
async itemContextMenu(event: any) {
|
||||
const itemContextMenu = useCallback(async (event: any) => {
|
||||
const itemId = event.currentTarget.getAttribute('data-id');
|
||||
if (itemId === Folder.conflictFolderId()) return;
|
||||
|
||||
@@ -265,7 +301,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
|
||||
let item = null;
|
||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
item = BaseModel.byId(this.props.folders, itemId);
|
||||
item = BaseModel.byId(props.folders, itemId);
|
||||
}
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||
@@ -289,7 +325,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||
await Tag.untagAll(itemId);
|
||||
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
||||
this.props.dispatch({
|
||||
props.dispatch({
|
||||
type: 'SEARCH_DELETE',
|
||||
id: itemId,
|
||||
});
|
||||
@@ -314,7 +350,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
new MenuItem({
|
||||
label: module.fullLabel(),
|
||||
click: async () => {
|
||||
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId], plugins: this.pluginsRef.current });
|
||||
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: [itemId], plugins: pluginsRef.current });
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -374,7 +410,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem');
|
||||
const pluginViews = pluginUtils.viewsByType(pluginsRef.current, 'menuItem');
|
||||
|
||||
for (const view of pluginViews) {
|
||||
const location = view.location;
|
||||
@@ -389,80 +425,79 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
}, [props.folders, props.dispatch, pluginsRef]);
|
||||
|
||||
folderItem_click(folderId: string) {
|
||||
this.props.dispatch({
|
||||
const folderItem_click = useCallback((folderId: string) => {
|
||||
props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folderId ? folderId : null,
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
tagItem_click(tag: any) {
|
||||
this.props.dispatch({
|
||||
const tagItem_click = useCallback((tag: any) => {
|
||||
props.dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: tag ? tag.id : null,
|
||||
});
|
||||
}
|
||||
}, [props.dispatch]);
|
||||
|
||||
anchorItemRef(type: string, id: string) {
|
||||
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {};
|
||||
if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id];
|
||||
this.anchorItemRefs[type][id] = React.createRef();
|
||||
return this.anchorItemRefs[type][id];
|
||||
}
|
||||
const onHeaderClick_ = useCallback((key: string) => {
|
||||
const isExpanded = key === 'tag' ? props.tagHeaderIsExpanded : props.folderHeaderIsExpanded;
|
||||
Setting.setValue(key === 'tag' ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded', !isExpanded);
|
||||
}, [props.folderHeaderIsExpanded, props.tagHeaderIsExpanded]);
|
||||
|
||||
firstAnchorItemRef(type: string) {
|
||||
const refs = this.anchorItemRefs[type];
|
||||
if (!refs) return null;
|
||||
const onAllNotesClick_ = () => {
|
||||
props.dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: ALL_NOTES_FILTER_ID,
|
||||
});
|
||||
};
|
||||
|
||||
const n = `${type}s`;
|
||||
const p = this.props as any;
|
||||
const item = p[n] && p[n].length ? p[n][0] : null;
|
||||
if (!item) return null;
|
||||
const anchorItemRef = (type: string, id: string) => {
|
||||
if (!anchorItemRefs.current[type]) anchorItemRefs.current[type] = {};
|
||||
if (anchorItemRefs.current[type][id]) return anchorItemRefs.current[type][id];
|
||||
anchorItemRefs.current[type][id] = React.createRef();
|
||||
return anchorItemRefs.current[type][id];
|
||||
};
|
||||
|
||||
return refs[item.id];
|
||||
}
|
||||
|
||||
renderNoteCount(count: number) {
|
||||
const renderNoteCount = (count: number) => {
|
||||
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
|
||||
}
|
||||
};
|
||||
|
||||
renderExpandIcon(isExpanded: boolean, isVisible: boolean = true) {
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
const renderExpandIcon = (theme: any, isExpanded: boolean, isVisible: boolean) => {
|
||||
const style: any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
|
||||
if (!isVisible) style.visibility = 'hidden';
|
||||
return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
|
||||
}
|
||||
};
|
||||
|
||||
renderAllNotesItem(selected: boolean) {
|
||||
const renderAllNotesItem = (theme: Theme, selected: boolean) => {
|
||||
return (
|
||||
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}>
|
||||
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
|
||||
<StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
|
||||
<StyledAllNotesIcon className="icon-notes"/>
|
||||
<StyledListItemAnchor
|
||||
className="list-item"
|
||||
isSpecialItem={true}
|
||||
href="#"
|
||||
selected={selected}
|
||||
onClick={this.onAllNotesClick_}
|
||||
onClick={onAllNotesClick_}
|
||||
>
|
||||
{_('All notes')}
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderFolderItem(folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) {
|
||||
const anchorRef = this.anchorItemRef('folder', folder.id);
|
||||
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
|
||||
const renderFolderItem = (folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) =>{
|
||||
const anchorRef = anchorItemRef('folder', folder.id);
|
||||
const isExpanded = props.collapsedFolderIds.indexOf(folder.id) < 0;
|
||||
let noteCount = (folder as any).note_count;
|
||||
|
||||
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
|
||||
if (isExpanded) {
|
||||
for (let i = 0; i < this.props.folders.length; i++) {
|
||||
if (this.props.folders[i].parent_id === folder.id) {
|
||||
noteCount -= this.props.folders[i].note_count;
|
||||
for (let i = 0; i < props.folders.length; i++) {
|
||||
if (props.folders[i].parent_id === folder.id) {
|
||||
noteCount -= props.folders[i].note_count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,40 +507,41 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
folderId={folder.id}
|
||||
folderTitle={Folder.displayTitle(folder)}
|
||||
folderIcon={Folder.unserializeIcon(folder.icon)}
|
||||
themeId={this.props.themeId}
|
||||
themeId={props.themeId}
|
||||
depth={depth}
|
||||
selected={selected}
|
||||
isExpanded={isExpanded}
|
||||
hasChildren={hasChildren}
|
||||
anchorRef={anchorRef}
|
||||
noteCount={noteCount}
|
||||
onFolderDragStart_={this.onFolderDragStart_}
|
||||
onFolderDragOver_={this.onFolderDragOver_}
|
||||
onFolderDrop_={this.onFolderDrop_}
|
||||
itemContextMenu={this.itemContextMenu}
|
||||
folderItem_click={this.folderItem_click}
|
||||
onFolderToggleClick_={this.onFolderToggleClick_}
|
||||
onFolderDragStart_={onFolderDragStart_}
|
||||
onFolderDragOver_={onFolderDragOver_}
|
||||
onFolderDrop_={onFolderDrop_}
|
||||
itemContextMenu={itemContextMenu}
|
||||
folderItem_click={folderItem_click}
|
||||
onFolderToggleClick_={onFolderToggleClick_}
|
||||
shareId={folder.share_id}
|
||||
parentId={folder.parent_id}
|
||||
showFolderIcon={showFolderIcons}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
|
||||
renderTag(tag: any, selected: boolean) {
|
||||
const anchorRef = this.anchorItemRef('tag', tag.id);
|
||||
const renderTag = (tag: any, selected: boolean) => {
|
||||
const anchorRef = anchorItemRef('tag', tag.id);
|
||||
let noteCount = null;
|
||||
if (Setting.value('showNoteCounts')) {
|
||||
if (Setting.value('showCompletedTodos')) noteCount = this.renderNoteCount(tag.note_count);
|
||||
else noteCount = this.renderNoteCount(tag.note_count - tag.todo_completed_count);
|
||||
if (Setting.value('showCompletedTodos')) noteCount = renderNoteCount(tag.note_count);
|
||||
else noteCount = renderNoteCount(tag.note_count - tag.todo_completed_count);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledListItem selected={selected}
|
||||
className={`list-item-container ${selected ? 'selected' : ''}`}
|
||||
key={tag.id}
|
||||
onDrop={this.onTagDrop_}
|
||||
onDrop={onTagDrop_}
|
||||
data-tag-id={tag.id}
|
||||
>
|
||||
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
|
||||
<StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
|
||||
<StyledListItemAnchor
|
||||
ref={anchorRef}
|
||||
className="list-item"
|
||||
@@ -513,9 +549,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
selected={selected}
|
||||
data-id={tag.id}
|
||||
data-type={BaseModel.TYPE_TAG}
|
||||
onContextMenu={this.itemContextMenu}
|
||||
onContextMenu={itemContextMenu}
|
||||
onClick={() => {
|
||||
this.tagItem_click(tag);
|
||||
tagItem_click(tag);
|
||||
}}
|
||||
>
|
||||
<span className="tag-label">{Tag.displayTitle(tag)}</span>
|
||||
@@ -523,16 +559,12 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
</StyledListItemAnchor>
|
||||
</StyledListItem>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
makeDivider(key: string) {
|
||||
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
|
||||
}
|
||||
|
||||
renderHeader(key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) {
|
||||
const renderHeader = (key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) => {
|
||||
const headerClick = extraProps.onClick || null;
|
||||
delete extraProps.onClick;
|
||||
const ref = this.anchorItemRef('headers', key);
|
||||
const ref = anchorItemRef('headers', key);
|
||||
|
||||
return (
|
||||
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
@@ -545,7 +577,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
if (headerClick) {
|
||||
headerClick(key, event);
|
||||
}
|
||||
this.onHeaderClick_(key);
|
||||
onHeaderClick_(key);
|
||||
}}
|
||||
>
|
||||
<StyledHeaderIcon className={iconName}/>
|
||||
@@ -554,21 +586,11 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SidebarSecondary}/> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
selectedItem() {
|
||||
if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) {
|
||||
return { type: 'folder', id: this.props.selectedFolderId };
|
||||
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
|
||||
return { type: 'tag', id: this.props.selectedTagId };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onKeyDown(event: any) {
|
||||
const onKeyDown = useCallback((event: any) => {
|
||||
const keyCode = event.keyCode;
|
||||
const selectedItem = this.selectedItem();
|
||||
const selectedItem = getSelectedItem();
|
||||
|
||||
if (keyCode === 40 || keyCode === 38) {
|
||||
// DOWN / UP
|
||||
@@ -576,14 +598,14 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
|
||||
const focusItems = [];
|
||||
|
||||
for (let i = 0; i < this.folderItemsOrder_.length; i++) {
|
||||
const id = this.folderItemsOrder_[i];
|
||||
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' });
|
||||
for (let i = 0; i < folderItemsOrder_.current.length; i++) {
|
||||
const id = folderItemsOrder_.current[i];
|
||||
focusItems.push({ id: id, ref: anchorItemRefs.current['folder'][id], type: 'folder' });
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.tagItemsOrder_.length; i++) {
|
||||
const id = this.tagItemsOrder_[i];
|
||||
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' });
|
||||
for (let i = 0; i < tagItemsOrder_.current.length; i++) {
|
||||
const id = tagItemsOrder_.current[i];
|
||||
focusItems.push({ id: id, ref: anchorItemRefs.current['tag'][id], type: 'tag' });
|
||||
}
|
||||
|
||||
let currentIndex = 0;
|
||||
@@ -604,7 +626,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
|
||||
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
|
||||
|
||||
this.props.dispatch({
|
||||
props.dispatch({
|
||||
type: actionName,
|
||||
id: focusItem.id,
|
||||
});
|
||||
@@ -627,7 +649,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
// SPACE
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch({
|
||||
props.dispatch({
|
||||
type: 'FOLDER_TOGGLE',
|
||||
id: selectedItem.id,
|
||||
});
|
||||
@@ -637,24 +659,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
// Ctrl+A key
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}, [getSelectedItem, props.dispatch]);
|
||||
|
||||
onHeaderClick_(key: string) {
|
||||
const toggleKey = `${key}IsExpanded`;
|
||||
const isExpanded = (this.state as any)[toggleKey];
|
||||
const newState: any = { [toggleKey]: !isExpanded };
|
||||
this.setState(newState);
|
||||
Setting.setValue(toggleKey, !isExpanded);
|
||||
}
|
||||
|
||||
onAllNotesClick_() {
|
||||
this.props.dispatch({
|
||||
type: 'SMART_FILTER_SELECT',
|
||||
id: ALL_NOTES_FILTER_ID,
|
||||
});
|
||||
}
|
||||
|
||||
renderSynchronizeButton(type: string) {
|
||||
const renderSynchronizeButton = (type: string) => {
|
||||
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
||||
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
|
||||
|
||||
@@ -670,116 +677,98 @@ class SidebarComponent extends React.Component<Props, State> {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onAddFolderButtonClick() {
|
||||
const onAddFolderButtonClick = useCallback(() => {
|
||||
void CommandService.instance().execute('newFolder');
|
||||
}, []);
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const items = [];
|
||||
|
||||
items.push(
|
||||
renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', header_contextMenu, onAddFolderButtonClick, {
|
||||
onDrop: onFolderDrop_,
|
||||
['data-folder-id']: '',
|
||||
toggleblock: 1,
|
||||
})
|
||||
);
|
||||
|
||||
if (props.folders.length) {
|
||||
const allNotesSelected = props.notesParentType === 'SmartFilter' && props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||
const result = shared.renderFolders(props, renderFolderItem);
|
||||
const folderItems = [renderAllNotesItem(theme, allNotesSelected)].concat(result.items);
|
||||
folderItemsOrder_.current = result.order;
|
||||
items.push(
|
||||
<div
|
||||
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||
key="folder_items"
|
||||
style={{ display: props.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
|
||||
>
|
||||
{folderItems}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// componentDidUpdate(prevProps:any, prevState:any) {
|
||||
// for (const n in prevProps) {
|
||||
// if (prevProps[n] !== (this.props as any)[n]) {
|
||||
// console.info('CHANGED PROPS', n);
|
||||
// }
|
||||
// }
|
||||
items.push(
|
||||
renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
|
||||
toggleblock: 1,
|
||||
})
|
||||
);
|
||||
|
||||
// for (const n in prevState) {
|
||||
// if (prevState[n] !== (this.state as any)[n]) {
|
||||
// console.info('CHANGED STATE', n);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
render() {
|
||||
this.pluginsRef.current = this.props.plugins;
|
||||
|
||||
const theme = themeStyle(this.props.themeId);
|
||||
|
||||
const items = [];
|
||||
if (props.tags.length) {
|
||||
const result = shared.renderTags(props, renderTag);
|
||||
const tagItems = result.items;
|
||||
tagItemsOrder_.current = result.order;
|
||||
|
||||
items.push(
|
||||
this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, {
|
||||
onDrop: this.onFolderDrop_,
|
||||
['data-folder-id']: '',
|
||||
toggleblock: 1,
|
||||
})
|
||||
);
|
||||
|
||||
if (this.props.folders.length) {
|
||||
const allNotesSelected = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||
const result = shared.renderFolders(this.props, this.renderFolderItem.bind(this));
|
||||
const folderItems = [this.renderAllNotesItem(allNotesSelected)].concat(result.items);
|
||||
this.folderItemsOrder_ = result.order;
|
||||
items.push(
|
||||
<div
|
||||
className={`folders ${this.state.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||
key="folder_items"
|
||||
style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
|
||||
>
|
||||
{folderItems}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
this.renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
|
||||
toggleblock: 1,
|
||||
})
|
||||
);
|
||||
|
||||
if (this.props.tags.length) {
|
||||
const result = shared.renderTags(this.props, this.renderTag.bind(this));
|
||||
const tagItems = result.items;
|
||||
this.tagItemsOrder_ = result.order;
|
||||
|
||||
items.push(
|
||||
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
{tagItems}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let decryptionReportText = '';
|
||||
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
|
||||
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
|
||||
}
|
||||
|
||||
let resourceFetcherText = '';
|
||||
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
|
||||
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
|
||||
}
|
||||
|
||||
const lines = Synchronizer.reportToLines(this.props.syncReport);
|
||||
if (resourceFetcherText) lines.push(resourceFetcherText);
|
||||
if (decryptionReportText) lines.push(decryptionReportText);
|
||||
const syncReportText = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
syncReportText.push(
|
||||
<StyledSyncReportText key={i}>
|
||||
{lines[i]}
|
||||
</StyledSyncReportText>
|
||||
);
|
||||
}
|
||||
|
||||
const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
|
||||
|
||||
const syncReportComp = !syncReportText.length ? null : (
|
||||
<StyledSyncReport key="sync_report">
|
||||
{syncReportText}
|
||||
</StyledSyncReport>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="sidebar">
|
||||
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
|
||||
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
||||
{syncReportComp}
|
||||
{syncButton}
|
||||
</div>
|
||||
</StyledRoot>
|
||||
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||
{tagItems}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let decryptionReportText = '';
|
||||
if (props.decryptionWorker && props.decryptionWorker.state !== 'idle' && props.decryptionWorker.itemCount) {
|
||||
decryptionReportText = _('Decrypting items: %d/%d', props.decryptionWorker.itemIndex + 1, props.decryptionWorker.itemCount);
|
||||
}
|
||||
|
||||
let resourceFetcherText = '';
|
||||
if (props.resourceFetcher && props.resourceFetcher.toFetchCount) {
|
||||
resourceFetcherText = _('Fetching resources: %d/%d', props.resourceFetcher.fetchingCount, props.resourceFetcher.toFetchCount);
|
||||
}
|
||||
|
||||
const lines = Synchronizer.reportToLines(props.syncReport);
|
||||
if (resourceFetcherText) lines.push(resourceFetcherText);
|
||||
if (decryptionReportText) lines.push(decryptionReportText);
|
||||
const syncReportText = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
syncReportText.push(
|
||||
<StyledSyncReportText key={i}>
|
||||
{lines[i]}
|
||||
</StyledSyncReportText>
|
||||
);
|
||||
}
|
||||
|
||||
const syncButton = renderSynchronizeButton(props.syncStarted ? 'cancel' : 'sync');
|
||||
|
||||
const syncReportComp = !syncReportText.length ? null : (
|
||||
<StyledSyncReport key="sync_report">
|
||||
{syncReportText}
|
||||
</StyledSyncReport>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRoot ref={rootRef} onKeyDown={onKeyDown} className="sidebar">
|
||||
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
|
||||
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
||||
{syncReportComp}
|
||||
{syncButton}
|
||||
</div>
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
@@ -799,6 +788,8 @@ const mapStateToProps = (state: AppState) => {
|
||||
decryptionWorker: state.decryptionWorker,
|
||||
resourceFetcher: state.resourceFetcher,
|
||||
plugins: state.pluginService.plugins,
|
||||
tagHeaderIsExpanded: state.settings.tagHeaderIsExpanded,
|
||||
folderHeaderIsExpanded: state.settings.folderHeaderIsExpanded,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -9,18 +9,24 @@ export const declaration: CommandDeclaration = {
|
||||
parentLabel: () => _('Focus'),
|
||||
};
|
||||
|
||||
export const runtime = (comp: any): CommandRuntime => {
|
||||
export interface RuntimeProps {
|
||||
getSelectedItem(): any;
|
||||
getFirstAnchorItemRef(type: string): any;
|
||||
anchorItemRefs: any;
|
||||
}
|
||||
|
||||
export const runtime = (props: RuntimeProps): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext) => {
|
||||
const sidebarVisible = layoutItemProp((context.state as AppState).mainLayout, 'sideBar', 'visible');
|
||||
|
||||
if (sidebarVisible) {
|
||||
const item = comp.selectedItem();
|
||||
const item = props.getSelectedItem();
|
||||
if (item) {
|
||||
const anchorRef = comp.anchorItemRefs[item.type][item.id];
|
||||
const anchorRef = props.anchorItemRefs.current[item.type][item.id];
|
||||
if (anchorRef) anchorRef.current.focus();
|
||||
} else {
|
||||
const anchorRef = comp.firstAnchorItemRef('folder');
|
||||
const anchorRef = props.getFirstAnchorItemRef('folder');
|
||||
if (anchorRef) anchorRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
@@ -23,5 +23,6 @@ export default function useEffectDebugger(effectHook: any, dependencies: any, de
|
||||
console.log('[use-effet-debugger] ', changedDeps);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
useEffect(effectHook, dependencies);
|
||||
}
|
||||
|
@@ -23,5 +23,6 @@ export default function useImperativeHandleDebugger(ref: any, effectHook: any, d
|
||||
console.log('[use-imperativeHandler-debugger] ', changedDeps);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
useImperativeHandle(ref, effectHook, dependencies);
|
||||
}
|
||||
|
@@ -579,6 +579,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
ipc.textSelected = function(event) {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'text',
|
||||
textToCopy: event.text,
|
||||
});
|
||||
}
|
||||
|
||||
ipc.openPdfViewer = function(event) {
|
||||
ipcProxySendToHost('openPdfViewer', { resourceId: event.resourceId, mime: 'application/pdf', pageNo: event.pageNo || 1 });
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', webviewLib.logEnabledEventHandler(e => {
|
||||
if (!window.location.hash) return;
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.9.2",
|
||||
"version": "2.9.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -31,7 +31,8 @@
|
||||
"afterSign": "./tools/notarizeMacApp.js",
|
||||
"extraResources": [
|
||||
"build/icons/**",
|
||||
"build/images/**"
|
||||
"build/images/**",
|
||||
"build/defaultPlugins/**"
|
||||
],
|
||||
"afterAllArtifactBuild": "./generateSha512.js",
|
||||
"asar": true,
|
||||
|
@@ -69,6 +69,7 @@ function UserWebview(props: Props, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (isReady && props.onReady) props.onReady();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [isReady]);
|
||||
|
||||
function frameWindow() {
|
||||
|
@@ -33,6 +33,7 @@ export default function(frameWindow: any, htmlHash: string, minWidth: number, mi
|
||||
|
||||
useEffect(() => {
|
||||
updateContentSize(htmlHash);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [htmlHash]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,6 +56,7 @@ export default function(frameWindow: any, htmlHash: string, minWidth: number, mi
|
||||
return () => {
|
||||
clearInterval(updateFrameSizeIID);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [fitToContent, isReady, minWidth, minHeight, htmlHash]);
|
||||
|
||||
return contentSize;
|
||||
|
@@ -45,6 +45,7 @@ export default function(frameWindow: any, isReady: boolean, postMessage: Functio
|
||||
hash: htmlHash,
|
||||
html: html,
|
||||
});
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [html, htmlHash, isReady]);
|
||||
|
||||
return loadedHtmlHash;
|
||||
|
@@ -4,10 +4,12 @@ export default function(postMessage: Function, isReady: boolean, scripts: string
|
||||
useEffect(() => {
|
||||
if (!isReady) return;
|
||||
postMessage('setScripts', { scripts: scripts });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [scripts, isReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !cssFilePath) return;
|
||||
postMessage('setScript', { script: cssFilePath, key: 'themeCss' });
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [isReady, cssFilePath]);
|
||||
}
|
||||
|
@@ -43,8 +43,10 @@ export default function useViewIsReady(viewRef: any) {
|
||||
return () => {
|
||||
viewRef.current.removeEventListener('dom-ready', onIFrameReady);
|
||||
viewRef.current.removeEventListener('load', onIFrameReady);
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
viewRef.current.contentWindow.removeEventListener('message', onMessage);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
return iframeReady && iframeContentReady;
|
||||
|
@@ -10,6 +10,7 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi
|
||||
return () => {
|
||||
PostMessageService.instance().unregisterResponder(ResponderComponentType.UserWebview, viewId);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [viewId]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -39,5 +40,6 @@ export default function(frameWindow: any, isReady: boolean, pluginId: string, vi
|
||||
return () => {
|
||||
frameWindow.removeEventListener('message', onMessage_);
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [frameWindow, isReady, pluginId, viewId]);
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
import SpellCheckerServiceDriverBase from '@joplin/lib/services/spellChecker/SpellCheckerServiceDriverBase';
|
||||
import bridge from '../bridge';
|
||||
import { languageCodeOnly, localesFromLanguageCode } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
|
||||
const logger = Logger.create('SpellCheckerServiceDriverNative');
|
||||
@@ -17,35 +16,17 @@ export default class SpellCheckerServiceDriverNative extends SpellCheckerService
|
||||
return this.session().availableSpellCheckerLanguages;
|
||||
}
|
||||
|
||||
// Language can be set to '' to disable spell-checking
|
||||
public setLanguage(v: string) {
|
||||
// Language can be set to [] to disable spell-checking
|
||||
public setLanguages(v: string[]) {
|
||||
// If we pass an empty array, it disables spell checking
|
||||
// https://github.com/electron/electron/issues/25228
|
||||
if (!v) {
|
||||
if (v.length === 0) {
|
||||
this.session().setSpellCheckerLanguages([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// The below function will throw an error if the provided language is
|
||||
// not supported, so we provide fallbacks.
|
||||
// https://github.com/laurent22/joplin/issues/4146
|
||||
const languagesToTry = [
|
||||
v,
|
||||
languageCodeOnly(v),
|
||||
].concat(localesFromLanguageCode(languageCodeOnly(v), this.availableLanguages));
|
||||
|
||||
for (const toTry of languagesToTry) {
|
||||
try {
|
||||
this.session().setSpellCheckerLanguages([toTry]);
|
||||
logger.info(`Set effective language from "${v}" to "${toTry}"`);
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to set language to "${toTry}". Will try the next one in this list: ${JSON.stringify(languagesToTry)}`);
|
||||
logger.warn('Error was:', error);
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`Could not set language to: ${v}`);
|
||||
this.session().setSpellCheckerLanguages(v);
|
||||
logger.info(`Set effective languages to "${v}"`);
|
||||
}
|
||||
|
||||
public get language(): string {
|
||||
|
@@ -48,20 +48,6 @@ function convertJsx(paths) {
|
||||
});
|
||||
}
|
||||
|
||||
function build(path) {
|
||||
chdir(path);
|
||||
|
||||
const result = spawnSync('yarn', ['run', 'build'], { shell: true });
|
||||
if (result.status !== 0) {
|
||||
const msg = [];
|
||||
if (result.stdout) msg.push(result.stdout.toString());
|
||||
if (result.stderr) msg.push(result.stderr.toString());
|
||||
console.error(msg.join('\n'));
|
||||
if (result.error) console.error(result.error);
|
||||
process.exit(result.status);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function() {
|
||||
convertJsx([
|
||||
`${__dirname}/../gui`,
|
||||
@@ -70,8 +56,6 @@ module.exports = function() {
|
||||
`${__dirname}/../plugins`,
|
||||
]);
|
||||
|
||||
build(`${__dirname}/../../pdf-viewer`);
|
||||
|
||||
const libContent = [
|
||||
fs.readFileSync(`${basePath}/packages/lib/string-utils-common.js`, 'utf8'),
|
||||
fs.readFileSync(`${basePath}/packages/lib/markJsUtils.js`, 'utf8'),
|
||||
|
@@ -146,8 +146,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097669
|
||||
versionName "2.9.1"
|
||||
versionCode 2097672
|
||||
versionName "2.9.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.cozic.joplin"
|
||||
android:installLocation="auto">
|
||||
package="net.cozic.joplin"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
@@ -8,7 +8,7 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<!-- Make these features optional to enable Chromebooks -->
|
||||
<!-- Make these features optional to enable Chromebooks -->
|
||||
<!-- https://github.com/laurent22/joplin/issues/37 -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
@@ -58,14 +58,19 @@
|
||||
This is recommended in the React docs: https://reactnavigation.org/docs/deep-linking. In practice, "singleTask" and "singleTop" are
|
||||
largely similar, but "singleTask" is more strict in preventing multiple instances of the app from being created if another app
|
||||
explicitly requests it.
|
||||
|
||||
2022-08-12: Added `screenLayout` and `smallestScreenSize` to `android:configChanges`.
|
||||
This prevents the application from being re-constructed on
|
||||
screen orientation change/window resizes on some devices.
|
||||
See https://github.com/laurent22/joplin/pull/6737.
|
||||
-->
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -93,6 +98,6 @@
|
||||
</activity>
|
||||
<!-- /SHARE EXTENSION -->
|
||||
|
||||
</application>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
190
packages/app-mobile/components/CustomButton.tsx
Normal file
190
packages/app-mobile/components/CustomButton.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// A button with a long-press action. Long-pressing the button displays a tooltip
|
||||
//
|
||||
|
||||
const React = require('react');
|
||||
import { ReactNode } from 'react';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { useState, useMemo, useCallback, useRef } from 'react';
|
||||
import { View, Text, Pressable, ViewStyle, PressableStateCallbackType, StyleProp, StyleSheet, LayoutChangeEvent, LayoutRectangle, Animated, AccessibilityState, AccessibilityRole } from 'react-native';
|
||||
import { Menu, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu';
|
||||
|
||||
type ButtonClickListener = ()=> void;
|
||||
interface ButtonProps {
|
||||
onPress: ButtonClickListener;
|
||||
|
||||
// Accessibility label and text shown in a tooltip
|
||||
description?: string;
|
||||
|
||||
children: ReactNode;
|
||||
|
||||
themeId: number;
|
||||
|
||||
style?: ViewStyle;
|
||||
pressedStyle?: ViewStyle;
|
||||
contentStyle?: ViewStyle;
|
||||
|
||||
// Additional accessibility information. See View.accessibilityHint
|
||||
accessibilityHint?: string;
|
||||
|
||||
// Role of the button. Defaults to 'button'.
|
||||
accessibilityRole?: AccessibilityRole;
|
||||
accessibilityState?: AccessibilityState;
|
||||
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CustomButton = (props: ButtonProps) => {
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [buttonLayout, setButtonLayout] = useState<LayoutRectangle|null>(null);
|
||||
const tooltipStyles = useTooltipStyles(props.themeId);
|
||||
|
||||
// See https://blog.logrocket.com/react-native-touchable-vs-pressable-components/
|
||||
// for more about animating Pressable buttons.
|
||||
const fadeAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
const animationDuration = 100; // ms
|
||||
const onPressIn = useCallback(() => {
|
||||
// Fade out.
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0.5,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [fadeAnim]);
|
||||
const onPressOut = useCallback(() => {
|
||||
// Fade in.
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: animationDuration,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
|
||||
setTooltipVisible(false);
|
||||
}, [fadeAnim]);
|
||||
const onLongPress = useCallback(() => {
|
||||
setTooltipVisible(true);
|
||||
}, []);
|
||||
|
||||
// Select different user-specified styles if selected/unselected.
|
||||
const onStyleChange = useCallback((state: PressableStateCallbackType): StyleProp<ViewStyle> => {
|
||||
let result = { ...props.style };
|
||||
|
||||
if (state.pressed) {
|
||||
result = {
|
||||
...result,
|
||||
...props.pressedStyle,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}, [props.pressedStyle, props.style]);
|
||||
|
||||
const onButtonLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const layoutEvt = event.nativeEvent.layout;
|
||||
|
||||
// Copy the layout event
|
||||
setButtonLayout({ ...layoutEvt });
|
||||
}, []);
|
||||
|
||||
|
||||
const button = (
|
||||
<Pressable
|
||||
onPress={props.onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
|
||||
style={ onStyleChange }
|
||||
|
||||
disabled={ props.disabled ?? false }
|
||||
onLayout={ onButtonLayout }
|
||||
|
||||
accessibilityLabel={props.description}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityRole={props.accessibilityRole ?? 'button'}
|
||||
accessibilityState={props.accessibilityState}
|
||||
>
|
||||
<Animated.View style={{
|
||||
opacity: fadeAnim,
|
||||
...props.contentStyle,
|
||||
}}>
|
||||
{ props.children }
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const tooltip = (
|
||||
<View
|
||||
// Any information given by the tooltip should also be provided via
|
||||
// [accessibilityLabel]/[accessibilityHint]. As such, we can hide the tooltip
|
||||
// from the screen reader.
|
||||
// On Android:
|
||||
importantForAccessibility='no-hide-descendants'
|
||||
// On iOS:
|
||||
accessibilityElementsHidden={true}
|
||||
|
||||
// Position the menu beneath the button so the tooltip appears in the
|
||||
// correct location.
|
||||
style={{
|
||||
left: buttonLayout?.x,
|
||||
top: buttonLayout?.y,
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
opened={tooltipVisible}
|
||||
renderer={renderers.Popover}
|
||||
rendererProps={{
|
||||
preferredPlacement: 'bottom',
|
||||
anchorStyle: tooltipStyles.anchor,
|
||||
}}>
|
||||
<MenuTrigger
|
||||
// Don't show/hide when pressed (let the Pressable handle opening/closing)
|
||||
disabled={true}
|
||||
style={{
|
||||
// Ensure that the trigger region has the same size as the button.
|
||||
width: buttonLayout?.width ?? 0,
|
||||
height: buttonLayout?.height ?? 0,
|
||||
}}
|
||||
/>
|
||||
<MenuOptions
|
||||
customStyles={{ optionsContainer: tooltipStyles.optionsContainer }}
|
||||
>
|
||||
<Text style={tooltipStyles.text}>
|
||||
{props.description}
|
||||
</Text>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.description ? tooltip : null}
|
||||
{button}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const useTooltipStyles = (themeId: number) => {
|
||||
return useMemo(() => {
|
||||
const themeData: Theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
text: {
|
||||
color: themeData.raisedColor,
|
||||
padding: 4,
|
||||
},
|
||||
anchor: {
|
||||
backgroundColor: themeData.raisedBackgroundColor,
|
||||
},
|
||||
optionsContainer: {
|
||||
backgroundColor: themeData.raisedBackgroundColor,
|
||||
},
|
||||
});
|
||||
}, [themeId]);
|
||||
};
|
||||
|
||||
export default CustomButton;
|
@@ -1,31 +1,60 @@
|
||||
const React = require('react');
|
||||
const { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View } = require('react-native');
|
||||
import { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View, LayoutRectangle, ViewStyle, TextStyle } from 'react-native';
|
||||
import { Component } from 'react';
|
||||
const { ItemList } = require('./ItemList.js');
|
||||
|
||||
class Dropdown extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
type ValueType = string;
|
||||
export interface DropdownListItem {
|
||||
label: string;
|
||||
value: ValueType;
|
||||
}
|
||||
|
||||
this.headerRef_ = null;
|
||||
}
|
||||
export type OnValueChangedListener = (newValue: ValueType)=> void;
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
interface DropdownProps {
|
||||
listItemStyle?: ViewStyle;
|
||||
itemListStyle?: ViewStyle;
|
||||
itemWrapperStyle?: ViewStyle;
|
||||
headerWrapperStyle?: ViewStyle;
|
||||
headerStyle?: TextStyle;
|
||||
itemStyle?: TextStyle;
|
||||
disabled?: boolean;
|
||||
|
||||
labelTransform?: 'trim';
|
||||
items: DropdownListItem[];
|
||||
|
||||
selectedValue: ValueType|null;
|
||||
onValueChange?: OnValueChangedListener;
|
||||
}
|
||||
|
||||
interface DropdownState {
|
||||
headerSize: LayoutRectangle;
|
||||
listVisible: boolean;
|
||||
}
|
||||
|
||||
class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
private headerRef: TouchableOpacity;
|
||||
|
||||
public constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
|
||||
this.headerRef = null;
|
||||
this.state = {
|
||||
headerSize: { x: 0, y: 0, width: 0, height: 0 },
|
||||
listVisible: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
updateHeaderCoordinates() {
|
||||
private updateHeaderCoordinates() {
|
||||
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
|
||||
this.headerRef_.measure((fx, fy, width, height, px, py) => {
|
||||
this.headerRef.measure((_fx, _fy, width, height, px, py) => {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
public render() {
|
||||
const items = this.props.items;
|
||||
const itemHeight = 60;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
@@ -84,23 +113,26 @@ class Dropdown extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
|
||||
if (this.props.labelTransform && this.props.labelTransform === 'trim') {
|
||||
headerLabel = headerLabel.trim();
|
||||
}
|
||||
|
||||
const closeList = () => {
|
||||
this.setState({ listVisible: false });
|
||||
};
|
||||
|
||||
const itemRenderer = item => {
|
||||
const itemRenderer = (item: DropdownListItem) => {
|
||||
const key = item.value ? item.value.toString() : '__null'; // The top item ("Move item to notebook...") has a null value.
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={itemWrapperStyle}
|
||||
key={item.value}
|
||||
key={key}
|
||||
onPress={() => {
|
||||
closeList();
|
||||
if (this.props.onValueChange) this.props.onValueChange(item.value);
|
||||
}}
|
||||
>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={key}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -111,7 +143,7 @@ class Dropdown extends React.Component {
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<TouchableOpacity
|
||||
style={headerWrapperStyle}
|
||||
ref={ref => (this.headerRef_ = ref)}
|
||||
ref={ref => (this.headerRef = ref)}
|
||||
disabled={this.props.disabled}
|
||||
onPress={() => {
|
||||
this.updateHeaderCoordinates();
|
||||
@@ -141,9 +173,7 @@ class Dropdown extends React.Component {
|
||||
style={itemListStyle}
|
||||
items={this.props.items}
|
||||
itemHeight={itemHeight}
|
||||
itemRenderer={item => {
|
||||
return itemRenderer(item);
|
||||
}}
|
||||
itemRenderer={itemRenderer}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -154,4 +184,5 @@ class Dropdown extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Dropdown };
|
||||
export default Dropdown;
|
||||
export { Dropdown };
|
147
packages/app-mobile/components/ExtendedWebView.tsx
Normal file
147
packages/app-mobile/components/ExtendedWebView.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
// Wraps react-native-webview. Allows loading HTML directly.
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
forwardRef, Ref, useEffect, useImperativeHandle, useRef, useState,
|
||||
} from 'react';
|
||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||
import { WebViewErrorEvent, WebViewEvent, WebViewSource } from 'react-native-webview/lib/WebViewTypes';
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { StyleProp, ViewStyle } from 'react-native';
|
||||
|
||||
|
||||
export interface WebViewControl {
|
||||
// Evaluate the given [script] in the context of the page.
|
||||
// Unlike react-native-webview/WebView, this does not need to return true.
|
||||
injectJS(script: string): void;
|
||||
}
|
||||
|
||||
interface SourceFileUpdateEvent {
|
||||
uri: string;
|
||||
baseUrl: string;
|
||||
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
type OnMessageCallback = (event: WebViewMessageEvent)=> void;
|
||||
type OnErrorCallback = (event: WebViewErrorEvent)=> void;
|
||||
type OnLoadEndCallback = (event: WebViewEvent)=> void;
|
||||
type OnFileUpdateCallback = (event: SourceFileUpdateEvent)=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
|
||||
// A name to be associated with the WebView (e.g. NoteEditor)
|
||||
// This name should be unique.
|
||||
webviewInstanceId: string;
|
||||
|
||||
// If HTML is still being loaded, [html] should be an empty string.
|
||||
html: string;
|
||||
|
||||
// Allow a secure origin to load content from any other origin.
|
||||
// Defaults to 'never'.
|
||||
// See react-native-webview's prop with the same name.
|
||||
mixedContentMode?: 'never' | 'always';
|
||||
|
||||
// Initial javascript. Must evaluate to true.
|
||||
injectedJavaScript: string;
|
||||
|
||||
style?: StyleProp<ViewStyle>;
|
||||
onMessage: OnMessageCallback;
|
||||
onError: OnErrorCallback;
|
||||
onLoadEnd?: OnLoadEndCallback;
|
||||
|
||||
// Triggered when the file containing [html] is overwritten with new content.
|
||||
onFileUpdate?: OnFileUpdateCallback;
|
||||
}
|
||||
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const theme: Theme = themeStyle(props.themeId);
|
||||
const webviewRef = useRef(null);
|
||||
const [source, setSource] = useState<WebViewSource|undefined>(undefined);
|
||||
|
||||
useImperativeHandle(ref, (): WebViewControl => {
|
||||
return {
|
||||
injectJS(js: string) {
|
||||
webviewRef.current.injectJavaScript(`
|
||||
try {
|
||||
${js}
|
||||
}
|
||||
catch(e) {
|
||||
logMessage('Error in injected JS:' + e, e);
|
||||
throw e;
|
||||
};
|
||||
|
||||
true;`);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function createHtmlFile() {
|
||||
const tempFile = `${Setting.value('resourceDir')}/${props.webviewInstanceId}.html`;
|
||||
await shim.fsDriver().writeFile(tempFile, props.html, 'utf8');
|
||||
if (cancelled) return;
|
||||
|
||||
// Now that we are sending back a file instead of an HTML string, we're always sending back the
|
||||
// same file. So we add a cache busting query parameter to it, to make sure that the WebView re-renders.
|
||||
//
|
||||
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
|
||||
const newSource = {
|
||||
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
};
|
||||
setSource(newSource);
|
||||
|
||||
props.onFileUpdate?.({
|
||||
...newSource,
|
||||
filePath: tempFile,
|
||||
});
|
||||
}
|
||||
|
||||
if (props.html && props.html.length > 0) {
|
||||
void createHtmlFile();
|
||||
} else {
|
||||
setSource(undefined);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [props.html, props.webviewInstanceId, props.onFileUpdate]);
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
|
||||
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
|
||||
// when an editable region (e.g. a the full-screen NoteEditor) is focused.
|
||||
return (
|
||||
<WebView
|
||||
style={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
...(props.style as any),
|
||||
}}
|
||||
ref={webviewRef}
|
||||
scrollEnabled={false}
|
||||
useWebKit={true}
|
||||
source={source}
|
||||
setSupportMultipleWindows={true}
|
||||
hideKeyboardAccessoryView={true}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
mixedContentMode={props.mixedContentMode}
|
||||
allowFileAccess={true}
|
||||
injectedJavaScript={props.injectedJavaScript}
|
||||
onMessage={props.onMessage}
|
||||
onError={props.onError}
|
||||
onLoadEnd={props.onLoadEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(ExtendedWebView);
|
@@ -1,16 +1,14 @@
|
||||
import { useRef, useCallback } from 'react';
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import useSource from './hooks/useSource';
|
||||
import useOnMessage from './hooks/useOnMessage';
|
||||
import useOnResourceLongPress from './hooks/useOnResourceLongPress';
|
||||
|
||||
const React = require('react');
|
||||
const { View } = require('react-native');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { themeStyle } = require('../global-style.js');
|
||||
import { View } from 'react-native';
|
||||
import BackButtonDialogBox from '../BackButtonDialogBox';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@@ -32,11 +30,9 @@ const webViewStyle = {
|
||||
};
|
||||
|
||||
export default function NoteBodyViewer(props: Props) {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
const dialogBoxRef = useRef(null);
|
||||
|
||||
const { source, injectedJs } = useSource(
|
||||
const { html, injectedJs } = useSource(
|
||||
props.noteBody,
|
||||
props.noteMarkupLanguage,
|
||||
props.themeId,
|
||||
@@ -67,6 +63,8 @@ export default function NoteBodyViewer(props: Props) {
|
||||
reg.logger().error('WebView error');
|
||||
}
|
||||
|
||||
const BackButtonDialogBox_ = BackButtonDialogBox as any;
|
||||
|
||||
// On iOS scalesPageToFit work like this:
|
||||
//
|
||||
// Find the widest image, resize it *and everything else* by x% so that
|
||||
@@ -88,21 +86,15 @@ export default function NoteBodyViewer(props: Props) {
|
||||
// 2020-10-15: As we've now fully switched to WebKit for iOS (useWebKit=true) and
|
||||
// since the WebView package went through many versions it's possible that
|
||||
// the above no longer applies.
|
||||
|
||||
const BackButtonDialogBox_ = BackButtonDialogBox as any;
|
||||
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<WebView
|
||||
theme={theme}
|
||||
useWebKit={true}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
<ExtendedWebView
|
||||
webviewInstanceId='NoteBodyViewer'
|
||||
themeId={props.themeId}
|
||||
style={webViewStyle}
|
||||
source={source}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJs.join('\n')}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
mixedContentMode="always"
|
||||
allowFileAccess={true}
|
||||
onLoadEnd={onLoadEnd}
|
||||
onError={onError}
|
||||
onMessage={onMessage}
|
||||
|
@@ -36,5 +36,6 @@ export default function useOnResourceLongPress(onJoplinLinkClick: Function, dial
|
||||
reg.logger().error('Could not handle link long press', e);
|
||||
ToastAndroid.show('An error occurred, check log for details', ToastAndroid.SHORT);
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [onJoplinLinkClick]);
|
||||
}
|
||||
|
@@ -5,13 +5,9 @@ const { themeStyle } = require('../../global-style.js');
|
||||
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
const { assetsToHeaders } = require('@joplin/renderer');
|
||||
|
||||
interface Source {
|
||||
uri: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
interface UseSourceResult {
|
||||
source: Source;
|
||||
// [html] can be null if the note is still being rendered.
|
||||
html: string|null;
|
||||
injectedJs: string[];
|
||||
}
|
||||
|
||||
@@ -24,7 +20,7 @@ function usePrevious(value: any, initialValue: any = null): any {
|
||||
}
|
||||
|
||||
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult {
|
||||
const [source, setSource] = useState<Source>(undefined);
|
||||
const [html, setHtml] = useState<string>('');
|
||||
const [injectedJs, setInjectedJs] = useState<string[]>([]);
|
||||
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
|
||||
const [isFirstRender, setIsFirstRender] = useState(true);
|
||||
@@ -39,6 +35,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
|
||||
const markupToHtml = useMemo(() => {
|
||||
return markupLanguageUtils.newMarkupToHtml();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [isFirstRender]);
|
||||
|
||||
// To address https://github.com/laurent22/joplin/issues/433
|
||||
@@ -82,7 +79,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
resources: noteResources,
|
||||
codeTheme: theme.codeThemeCss,
|
||||
postMessageSyntax: 'window.joplinPostMessage_',
|
||||
enableLongPress: shim.mobilePlatform() === 'android', // On iOS, there's already a built-on open/share menu
|
||||
enableLongPress: true,
|
||||
};
|
||||
|
||||
// Whenever a resource state changes, for example when it goes from "not downloaded" to "downloaded", the "noteResources"
|
||||
@@ -168,20 +165,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
</html>
|
||||
`;
|
||||
|
||||
const tempFile = `${Setting.value('resourceDir')}/NoteBodyViewer.html`;
|
||||
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
// Now that we are sending back a file instead of an HTML string, we're always sending back the
|
||||
// same file. So we add a cache busting query parameter to it, to make sure that the WebView re-renders.
|
||||
//
|
||||
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
|
||||
setSource({
|
||||
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
|
||||
setHtml(html);
|
||||
setInjectedJs(js);
|
||||
}
|
||||
|
||||
@@ -193,7 +177,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
|
||||
if (isFirstRender) {
|
||||
setIsFirstRender(false);
|
||||
setSource(undefined);
|
||||
setHtml('');
|
||||
setInjectedJs([]);
|
||||
} else {
|
||||
void renderNote();
|
||||
@@ -202,7 +186,8 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, effectDependencies);
|
||||
|
||||
return { source, injectedJs };
|
||||
return { html, injectedJs };
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
|
||||
import { indentOnInput, indentUnit, syntaxTree } from '@codemirror/language';
|
||||
import {
|
||||
openSearchPanel, closeSearchPanel, SearchQuery, setSearchQuery, getSearchQuery,
|
||||
highlightSelectionMatches, search, findNext, findPrevious, replaceAll, replaceNext,
|
||||
/* highlightSelectionMatches, */ search, findNext, findPrevious, replaceAll, replaceNext,
|
||||
} from '@codemirror/search';
|
||||
|
||||
import {
|
||||
@@ -291,7 +291,7 @@ export function initCodeMirror(
|
||||
}),
|
||||
drawSelection(),
|
||||
highlightSpecialChars(),
|
||||
highlightSelectionMatches(),
|
||||
// highlightSelectionMatches(),
|
||||
indentOnInput(),
|
||||
|
||||
// By default, indent with four spaces
|
||||
@@ -302,7 +302,10 @@ export function initCodeMirror(
|
||||
decoratorExtension,
|
||||
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({ autocapitalize: 'sentence' }),
|
||||
EditorView.contentAttributes.of({
|
||||
autocapitalize: 'sentence',
|
||||
spellcheck: settings.spellcheckEnabled ? 'true' : 'false',
|
||||
}),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
notifyDocChanged(viewUpdate);
|
||||
notifySelectionChange(viewUpdate);
|
||||
@@ -339,6 +342,27 @@ export function initCodeMirror(
|
||||
parent: parentElement,
|
||||
});
|
||||
|
||||
// HACK: 09/02/22: Work around https://github.com/laurent22/joplin/issues/6802 by creating a copy mousedown
|
||||
// event to prevent the Editor's .preventDefault from making the context menu not appear.
|
||||
// TODO: Track the upstream issue at https://github.com/codemirror/dev/issues/935 and remove this workaround
|
||||
// when the upstream bug is fixed.
|
||||
document.body.addEventListener('mousedown', (evt) => {
|
||||
if (!evt.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Walk up the tree -- is evt.target or any of its parent nodes the editor's input region?
|
||||
for (let current: Record<string, any> = evt.target; current; current = current.parentElement) {
|
||||
if (current === editor.contentDOM) {
|
||||
evt.stopPropagation();
|
||||
|
||||
const copyEvent = new Event('mousedown', evt);
|
||||
editor.contentDOM.dispatchEvent(copyEvent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
|
||||
const updateSearchQuery = (newState: SearchState) => {
|
||||
const query = new SearchQuery({
|
||||
search: newState.searchText,
|
||||
@@ -381,10 +405,6 @@ export function initCodeMirror(
|
||||
closeSearchPanel(editor);
|
||||
}
|
||||
},
|
||||
setSpellcheckEnabled: (enabled: boolean) => {
|
||||
editor.contentDOM.spellcheck = enabled;
|
||||
notifySelectionFormattingChange();
|
||||
},
|
||||
|
||||
// Formatting
|
||||
toggleBolded: () => { toggleBolded(editor); },
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
|
||||
import { indentUnit } from '@codemirror/language';
|
||||
import { forceParsing, indentUnit } from '@codemirror/language';
|
||||
import { SelectionRange, EditorSelection, EditorState } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { MarkdownMathExtension } from './markdownMathParser';
|
||||
|
||||
// Creates and returns a minimal editor with markdown extensions
|
||||
const createEditor = (initialText: string, initialSelection: SelectionRange): EditorView => {
|
||||
return new EditorView({
|
||||
const editor = new EditorView({
|
||||
doc: initialText,
|
||||
selection: EditorSelection.create([initialSelection]),
|
||||
extensions: [
|
||||
@@ -18,6 +18,9 @@ const createEditor = (initialText: string, initialSelection: SelectionRange): Ed
|
||||
EditorState.tabSize.of(4),
|
||||
],
|
||||
});
|
||||
|
||||
forceParsing(editor);
|
||||
return editor;
|
||||
};
|
||||
|
||||
export default createEditor;
|
||||
|
@@ -42,7 +42,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
codeMirrorBundle.initCodeMirror(parent, initialText, settings);
|
||||
window.cm = codeMirrorBundle.initCodeMirror(parent, initialText, settings);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -55,6 +55,24 @@ describe('markdownCommands', () => {
|
||||
expect(editor.state.doc.toString()).toBe('Testing...');
|
||||
});
|
||||
|
||||
it('for a cursor, bolding, then italicizing, should produce a bold-italic region', () => {
|
||||
const initialDocText = '';
|
||||
const editor = createEditor(
|
||||
initialDocText, EditorSelection.cursor(0)
|
||||
);
|
||||
|
||||
toggleBolded(editor);
|
||||
toggleItalicized(editor);
|
||||
expect(editor.state.doc.toString()).toBe('******');
|
||||
|
||||
editor.dispatch(editor.state.replaceSelection('Test'));
|
||||
expect(editor.state.doc.toString()).toBe('***Test***');
|
||||
|
||||
toggleItalicized(editor);
|
||||
editor.dispatch(editor.state.replaceSelection(' Test'));
|
||||
expect(editor.state.doc.toString()).toBe('***Test*** Test');
|
||||
});
|
||||
|
||||
it('toggling math should both create and navigate out of math regions', () => {
|
||||
const initialDocText = 'Testing... ';
|
||||
const editor = createEditor(initialDocText, EditorSelection.cursor(initialDocText.length));
|
||||
|
@@ -24,13 +24,58 @@ export const toggleBolded: Command = (view: EditorView): boolean => {
|
||||
};
|
||||
|
||||
export const toggleItalicized: Command = (view: EditorView): boolean => {
|
||||
const changes = toggleInlineFormatGlobally(view.state, {
|
||||
nodeName: 'Emphasis',
|
||||
let handledBoldItalicRegion = false;
|
||||
|
||||
template: { start: '*', end: '*' },
|
||||
matcher: { start: /[_*]/g, end: /[_*]/g },
|
||||
});
|
||||
view.dispatch(changes);
|
||||
// Bold-italic regions' starting and ending patterns are similar to italicized regions.
|
||||
// Thus, we need additional logic to convert bold regions to bold-italic regions.
|
||||
view.dispatch(view.state.changeByRange((sel: SelectionRange) => {
|
||||
const changes: ChangeSpec[] = [];
|
||||
|
||||
// Only handle cursors (empty selections)
|
||||
if (sel.empty) {
|
||||
const doc = view.state.doc;
|
||||
const selLine = doc.lineAt(sel.from);
|
||||
|
||||
const selStartLineIdx = sel.from - selLine.from;
|
||||
const selEndLineIdx = sel.to - selLine.from;
|
||||
const beforeSel = selLine.text.substring(0, selStartLineIdx);
|
||||
const afterSel = selLine.text.substring(selEndLineIdx);
|
||||
|
||||
const isBolded = beforeSel.endsWith('**') && afterSel.startsWith('**');
|
||||
|
||||
// If at the end of a bold-italic region, exit the region.
|
||||
if (afterSel.startsWith('***')) {
|
||||
sel = EditorSelection.cursor(sel.to + 3);
|
||||
handledBoldItalicRegion = true;
|
||||
} else if (isBolded) {
|
||||
// Create a bold-italic region.
|
||||
changes.push({
|
||||
from: sel.from,
|
||||
to: sel.to,
|
||||
insert: '**',
|
||||
});
|
||||
|
||||
// Move to the center of the bold-italic region (**|**** -> ***|***)
|
||||
sel = EditorSelection.cursor(sel.to + 1);
|
||||
handledBoldItalicRegion = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
changes,
|
||||
range: sel,
|
||||
};
|
||||
}));
|
||||
|
||||
if (!handledBoldItalicRegion) {
|
||||
const changes = toggleInlineFormatGlobally(view.state, {
|
||||
nodeName: 'Emphasis',
|
||||
|
||||
template: { start: '*', end: '*' },
|
||||
matcher: { start: /[_*]/g, end: /[_*]/g },
|
||||
});
|
||||
view.dispatch(changes);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@@ -7,8 +7,6 @@ export interface CodeMirrorControl {
|
||||
select(anchor: number, head: number): void;
|
||||
insertText(text: string): void;
|
||||
|
||||
setSpellcheckEnabled(enabled: boolean): void;
|
||||
|
||||
// Toggle whether we're in a type of region.
|
||||
toggleBolded(): void;
|
||||
toggleItalicized(): void;
|
||||
|
@@ -20,7 +20,7 @@ interface LinkDialogProps {
|
||||
|
||||
const EditLinkDialog = (props: LinkDialogProps) => {
|
||||
// The content of the link selected in the editor (if any)
|
||||
const editorLinkData = props.selectionState.linkData;
|
||||
const editorLinkData = props.selectionState.linkData ?? {};
|
||||
const [linkLabel, setLinkLabel] = useState('');
|
||||
const [linkURL, setLinkURL] = useState('');
|
||||
|
||||
|
@@ -0,0 +1,363 @@
|
||||
// A toolbar for the markdown editor.
|
||||
|
||||
const React = require('react');
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
|
||||
// See https://oblador.github.io/react-native-vector-icons/ for a list of
|
||||
// available icons.
|
||||
const AntIcon = require('react-native-vector-icons/AntDesign').default;
|
||||
const FontAwesomeIcon = require('react-native-vector-icons/FontAwesome5').default;
|
||||
const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default;
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import time from '@joplin/lib/time';
|
||||
import { useEffect } from 'react';
|
||||
import { Keyboard, ViewStyle } from 'react-native';
|
||||
import { EditorControl, EditorSettings, ListType, SearchState } from '../types';
|
||||
import SelectionFormatting from '../SelectionFormatting';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
import Toolbar from './Toolbar';
|
||||
import { buttonSize } from './ToolbarButton';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import ToggleSpaceButton from './ToggleSpaceButton';
|
||||
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
interface MarkdownToolbarProps {
|
||||
editorControl: EditorControl;
|
||||
selectionState: SelectionFormatting;
|
||||
searchState: SearchState;
|
||||
editorSettings: EditorSettings;
|
||||
onAttach: OnAttachCallback;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const MarkdownToolbar = (props: MarkdownToolbarProps) => {
|
||||
const themeData = props.editorSettings.themeData;
|
||||
const styles = useStyles(props.style, themeData);
|
||||
const selState = props.selectionState;
|
||||
const editorControl = props.editorControl;
|
||||
|
||||
const headerButtons: ButtonSpec[] = [];
|
||||
for (let level = 1; level <= 5; level++) {
|
||||
const active = selState.headerLevel === level;
|
||||
let label;
|
||||
if (!active) {
|
||||
label = _('Create header level %d', level);
|
||||
} else {
|
||||
label = _('Remove level %d header', level);
|
||||
}
|
||||
|
||||
headerButtons.push({
|
||||
icon: `H${level}`,
|
||||
description: label,
|
||||
active,
|
||||
|
||||
// We only call addHeaderButton 5 times and in the same order, so
|
||||
// the linter error is safe to ignore.
|
||||
// eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleHeaderLevel(level);
|
||||
}, [editorControl, level]),
|
||||
|
||||
// Make it likely for the first three header buttons to show, less likely for
|
||||
// the others.
|
||||
priority: level < 3 ? 2 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
const listButtons: ButtonSpec[] = [];
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="list-ul" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inUnorderedList ? _('Remove unordered list') : _('Create unordered list'),
|
||||
active: selState.inUnorderedList,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.UnorderedList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="list-ol" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inOrderedList ? _('Remove ordered list') : _('Create ordered list'),
|
||||
active: selState.inOrderedList,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.OrderedList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="tasks" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inChecklist ? _('Remove task list') : _('Create task list'),
|
||||
active: selState.inChecklist,
|
||||
onPress: useCallback(() => {
|
||||
editorControl.toggleList(ListType.CheckList);
|
||||
}, [editorControl]),
|
||||
|
||||
priority: -2,
|
||||
});
|
||||
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<AntIcon name="indent-left" style={styles.text}/>
|
||||
),
|
||||
description: _('Decrease indent level'),
|
||||
onPress: editorControl.decreaseIndent,
|
||||
|
||||
priority: -1,
|
||||
});
|
||||
|
||||
listButtons.push({
|
||||
icon: (
|
||||
<AntIcon name="indent-right" style={styles.text}/>
|
||||
),
|
||||
description: _('Increase indent level'),
|
||||
onPress: editorControl.increaseIndent,
|
||||
|
||||
priority: -1,
|
||||
});
|
||||
|
||||
|
||||
// Inline formatting
|
||||
const inlineFormattingBtns: ButtonSpec[] = [];
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="bold" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.bolded ? _('Unbold') : _('Bold text'),
|
||||
active: selState.bolded,
|
||||
onPress: editorControl.toggleBolded,
|
||||
|
||||
priority: 3,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="italic" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.italicized ? _('Unitalicize') : _('Italicize'),
|
||||
active: selState.italicized,
|
||||
onPress: editorControl.toggleItalicized,
|
||||
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: '{;}',
|
||||
description:
|
||||
selState.inCode ? _('Remove code formatting') : _('Format as code'),
|
||||
active: selState.inCode,
|
||||
onPress: editorControl.toggleCode,
|
||||
|
||||
priority: 2,
|
||||
});
|
||||
|
||||
if (props.editorSettings.katexEnabled) {
|
||||
inlineFormattingBtns.push({
|
||||
icon: '∑',
|
||||
description:
|
||||
selState.inMath ? _('Remove TeX region') : _('Create TeX region'),
|
||||
active: selState.inMath,
|
||||
onPress: editorControl.toggleMath,
|
||||
|
||||
priority: 1,
|
||||
});
|
||||
}
|
||||
|
||||
inlineFormattingBtns.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="link" style={styles.text}/>
|
||||
),
|
||||
description:
|
||||
selState.inLink ? _('Edit link') : _('Create link'),
|
||||
active: selState.inLink,
|
||||
onPress: editorControl.showLinkDialog,
|
||||
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
|
||||
// Actions
|
||||
const actionButtons: ButtonSpec[] = [];
|
||||
actionButtons.push({
|
||||
icon: (
|
||||
<FontAwesomeIcon name="calendar-plus" style={styles.text}/>
|
||||
),
|
||||
description: _('Insert time'),
|
||||
onPress: useCallback(() => {
|
||||
editorControl.insertText(time.formatDateToLocal(new Date()));
|
||||
}, [editorControl]),
|
||||
});
|
||||
|
||||
const onDismissKeyboard = useCallback(() => {
|
||||
// Keyboard.dismiss() doesn't dismiss the keyboard if it's editing the WebView.
|
||||
Keyboard.dismiss();
|
||||
|
||||
// As such, dismiss the keyboard by sending a message to the View.
|
||||
editorControl.hideKeyboard();
|
||||
}, [editorControl]);
|
||||
|
||||
actionButtons.push({
|
||||
icon: (
|
||||
<MaterialIcon name="attachment" style={styles.text}/>
|
||||
),
|
||||
description: _('Attach'),
|
||||
onPress: useCallback(() => {
|
||||
onDismissKeyboard();
|
||||
props.onAttach();
|
||||
}, [props.onAttach, onDismissKeyboard]),
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: (
|
||||
<MaterialIcon name="search" style={styles.text}/>
|
||||
),
|
||||
description: (
|
||||
props.searchState.dialogVisible ? _('Close find and replace') : _('Find and replace')
|
||||
),
|
||||
active: props.searchState.dialogVisible,
|
||||
onPress: useCallback(() => {
|
||||
if (props.searchState.dialogVisible) {
|
||||
editorControl.searchControl.hideSearch();
|
||||
} else {
|
||||
editorControl.searchControl.showSearch();
|
||||
}
|
||||
}, [editorControl, props.searchState.dialogVisible]),
|
||||
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
||||
const [hasSoftwareKeyboard, setHasSoftwareKeyboard] = useState(false);
|
||||
useEffect(() => {
|
||||
const showListener = Keyboard.addListener('keyboardDidShow', () => {
|
||||
setKeyboardVisible(true);
|
||||
setHasSoftwareKeyboard(true);
|
||||
});
|
||||
const hideListener = Keyboard.addListener('keyboardDidHide', () => {
|
||||
setKeyboardVisible(false);
|
||||
});
|
||||
|
||||
return (() => {
|
||||
showListener.remove();
|
||||
hideListener.remove();
|
||||
});
|
||||
});
|
||||
|
||||
actionButtons.push({
|
||||
icon: (
|
||||
<MaterialIcon name="keyboard-hide" style={styles.text}/>
|
||||
),
|
||||
description: _('Hide keyboard'),
|
||||
disabled: !keyboardVisible,
|
||||
visible: hasSoftwareKeyboard && Platform.OS === 'ios',
|
||||
onPress: onDismissKeyboard,
|
||||
|
||||
priority: -3,
|
||||
});
|
||||
|
||||
const styleData: StyleSheetData = {
|
||||
styles: styles,
|
||||
themeId: props.editorSettings.themeId,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToggleSpaceButton
|
||||
spaceApplicable={ Platform.OS === 'ios' && keyboardVisible }
|
||||
themeId={props.editorSettings.themeId}
|
||||
style={styles.container}
|
||||
>
|
||||
<Toolbar
|
||||
styleSheet={styleData}
|
||||
buttons={[
|
||||
{
|
||||
title: _('Formatting'),
|
||||
items: inlineFormattingBtns,
|
||||
},
|
||||
{
|
||||
title: _('Headers'),
|
||||
items: headerButtons,
|
||||
},
|
||||
{
|
||||
title: _('Lists'),
|
||||
items: listButtons,
|
||||
},
|
||||
{
|
||||
title: _('Actions'),
|
||||
items: actionButtons,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ToggleSpaceButton>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = (styleProps: any, theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
...styleProps,
|
||||
},
|
||||
button: {
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
buttonDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
buttonDisabledContent: {
|
||||
},
|
||||
buttonActive: {
|
||||
backgroundColor: theme.backgroundColor3,
|
||||
color: theme.color3,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.color3,
|
||||
borderRadius: 6,
|
||||
},
|
||||
buttonActiveContent: {
|
||||
color: theme.color3,
|
||||
},
|
||||
text: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
toolbarRow: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
justifyContent: 'center',
|
||||
|
||||
// Add a small amount of additional padding for button borders
|
||||
height: buttonSize + 6,
|
||||
},
|
||||
toolbarContainer: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
toolbarContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
}, [styleProps, theme]);
|
||||
};
|
||||
|
||||
export default MarkdownToolbar;
|
@@ -0,0 +1,34 @@
|
||||
const React = require('react');
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
const MaterialIcon = require('react-native-vector-icons/MaterialIcons').default;
|
||||
|
||||
type OnToggleOverflowCallback = ()=> void;
|
||||
interface ToggleOverflowButtonProps {
|
||||
overflowVisible: boolean;
|
||||
onToggleOverflowVisible: OnToggleOverflowCallback;
|
||||
styleSheet: StyleSheetData;
|
||||
}
|
||||
|
||||
// Button that shows/hides the overflow menu.
|
||||
const ToggleOverflowButton = (props: ToggleOverflowButtonProps) => {
|
||||
const spec: ButtonSpec = {
|
||||
icon: (
|
||||
<MaterialIcon name="more-horiz" style={props.styleSheet.styles.text}/>
|
||||
),
|
||||
description:
|
||||
props.overflowVisible ? _('Hide more actions') : _('Show more actions'),
|
||||
active: props.overflowVisible,
|
||||
onPress: props.onToggleOverflowVisible,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default ToggleOverflowButton;
|
@@ -0,0 +1,96 @@
|
||||
|
||||
// On some devices, the SafeAreaView conflicts with the KeyboardAvoidingView, creating
|
||||
// additional (or a lack of additional) space at the bottom of the screen. Because this
|
||||
// is different on different devices, this button allows toggling additional space a the bottom
|
||||
// of the screen to compensate.
|
||||
|
||||
// Works around https://github.com/facebook/react-native/issues/13393 by adding additional
|
||||
// space below the given component when the keyboard is visible unless a button is pressed.
|
||||
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ReactNode, useCallback, useState, useEffect } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import CustomButton from '../../CustomButton';
|
||||
|
||||
const AntIcon = require('react-native-vector-icons/AntDesign').default;
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
spaceApplicable: boolean;
|
||||
themeId: number;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
const ToggleSpaceButton = (props: Props) => {
|
||||
const [additionalSpace, setAdditionalSpace] = useState(0);
|
||||
const [decreaseSpaceBtnVisible, setDecreaseSpaceBtnVisible] = useState(true);
|
||||
|
||||
// Some devices need space added, others need space removed.
|
||||
const additionalPositiveSpace = 14;
|
||||
const additionalNegativeSpace = -14;
|
||||
|
||||
// Switch from adding +14px to -14px.
|
||||
const onDecreaseSpace = useCallback(() => {
|
||||
setAdditionalSpace(additionalNegativeSpace);
|
||||
setDecreaseSpaceBtnVisible(false);
|
||||
Setting.setValue('editor.mobile.removeSpaceBelowToolbar', true);
|
||||
}, [setAdditionalSpace, setDecreaseSpaceBtnVisible, additionalNegativeSpace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (Setting.value('editor.mobile.removeSpaceBelowToolbar')) {
|
||||
onDecreaseSpace();
|
||||
}
|
||||
}, [onDecreaseSpace]);
|
||||
|
||||
const theme: Theme = themeStyle(props.themeId);
|
||||
|
||||
const decreaseSpaceButton = (
|
||||
<>
|
||||
<View style={{
|
||||
height: additionalPositiveSpace,
|
||||
zIndex: -2,
|
||||
}} />
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
description={'Move toolbar to bottom of screen'}
|
||||
style={{
|
||||
height: additionalPositiveSpace,
|
||||
width: '100%',
|
||||
|
||||
// Ensure that the icon is near the bottom of the screen,
|
||||
// and thus invisible on devices where it isn't necessary.
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
|
||||
// Don't show the button on top of views with content.
|
||||
zIndex: -1,
|
||||
|
||||
alignItems: 'center',
|
||||
}}
|
||||
onPress={onDecreaseSpace}
|
||||
>
|
||||
<AntIcon name='down' style={{
|
||||
color: theme.color,
|
||||
}}/>
|
||||
</CustomButton>
|
||||
</>
|
||||
);
|
||||
|
||||
const style: ViewStyle = {
|
||||
marginBottom: props.spaceApplicable ? additionalSpace : 0,
|
||||
...props.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
{props.children}
|
||||
{ decreaseSpaceBtnVisible && props.spaceApplicable ? decreaseSpaceButton : null }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToggleSpaceButton;
|
@@ -0,0 +1,122 @@
|
||||
const React = require('react');
|
||||
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { AccessibilityInfo, LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import ToolbarOverflowRows from './ToolbarOverflowRows';
|
||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
interface ToolbarProps {
|
||||
buttons: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
style?: ViewStyle;
|
||||
}
|
||||
|
||||
// Displays a list of buttons with an overflow menu.
|
||||
const Toolbar = (props: ToolbarProps) => {
|
||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||
|
||||
const allButtonSpecs = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
|
||||
// Sort from highest priority to lowest
|
||||
allButtonSpecs.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
|
||||
const allButtonComponents: ReactElement[] = [];
|
||||
let key = 0;
|
||||
for (const spec of allButtonSpecs) {
|
||||
key++;
|
||||
allButtonComponents.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={spec}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerWidth = event.nativeEvent.layout.width;
|
||||
const maxButtonsTotal = Math.floor(containerWidth / buttonSize);
|
||||
setMaxButtonsEachSide(Math.floor(
|
||||
Math.min((maxButtonsTotal - 1) / 2, allButtonSpecs.length / 2)
|
||||
));
|
||||
}, [allButtonSpecs.length]);
|
||||
|
||||
const onToggleOverflowVisible = useCallback(() => {
|
||||
AccessibilityInfo.announceForAccessibility(
|
||||
!overflowButtonsVisible
|
||||
? _('Opened toolbar overflow menu')
|
||||
: _('Closed toolbar overflow menu')
|
||||
);
|
||||
setOverflowPopupVisible(!overflowButtonsVisible);
|
||||
}, [overflowButtonsVisible]);
|
||||
|
||||
const toggleOverflowButton = (
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={overflowButtonsVisible}
|
||||
onToggleOverflowVisible={onToggleOverflowVisible}
|
||||
/>
|
||||
);
|
||||
|
||||
const mainButtons: ReactElement[] = [];
|
||||
if (maxButtonsEachSide < allButtonComponents.length) {
|
||||
// We want the menu to look something like this:
|
||||
// B I (…) 🔍 ⌨
|
||||
// where (…) shows/hides overflow.
|
||||
// Add from the left and right of [allButtonComponents] to ensure that
|
||||
// the (…) button is in the center:
|
||||
mainButtons.push(...allButtonComponents.slice(0, maxButtonsEachSide));
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
||||
} else {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
}
|
||||
|
||||
const styles = props.styleSheet.styles;
|
||||
const mainButtonRow = (
|
||||
<View style={styles.toolbarRow}>
|
||||
{ mainButtons }
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
...styles.toolbarContainer,
|
||||
|
||||
// The number of buttons displayed is based on the width of the
|
||||
// container. As such, we can't base the container's width on the
|
||||
// size of its content.
|
||||
width: '100%',
|
||||
|
||||
...props.style,
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
<ScrollView>
|
||||
<ToolbarOverflowRows
|
||||
buttonGroups={props.buttons}
|
||||
styleSheet={props.styleSheet}
|
||||
visible={overflowButtonsVisible}
|
||||
onToggleOverflow={onToggleOverflowVisible}
|
||||
/>
|
||||
</ScrollView>
|
||||
{ !overflowButtonsVisible ? mainButtonRow : null }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default Toolbar;
|
@@ -0,0 +1,64 @@
|
||||
import React = require('react');
|
||||
import { useCallback } from 'react';
|
||||
import { Text, TextStyle } from 'react-native';
|
||||
import { ButtonSpec, StyleSheetData } from './types';
|
||||
import CustomButton from '../../CustomButton';
|
||||
|
||||
export const buttonSize = 54;
|
||||
|
||||
interface ToolbarButtonProps {
|
||||
styleSheet: StyleSheetData;
|
||||
style?: TextStyle;
|
||||
spec: ButtonSpec;
|
||||
onActionComplete?: ()=> void;
|
||||
}
|
||||
|
||||
const ToolbarButton = ({ styleSheet, spec, onActionComplete, style }: ToolbarButtonProps) => {
|
||||
const visible = spec.visible ?? true;
|
||||
const disabled = (spec.disabled ?? false) && visible;
|
||||
const styles = styleSheet.styles;
|
||||
|
||||
// Additional styles if activated
|
||||
const activatedStyle = spec.active ? styles.buttonActive : {};
|
||||
const activatedTextStyle = spec.active ? styles.buttonActiveContent : {};
|
||||
const disabledStyle = disabled ? styles.buttonDisabled : {};
|
||||
const disabledTextStyle = disabled ? styles.buttonDisabledContent : {};
|
||||
|
||||
let content;
|
||||
|
||||
if (typeof spec.icon === 'string') {
|
||||
content = (
|
||||
<Text style={{ ...styles.text, ...activatedTextStyle, ...disabledTextStyle }}>
|
||||
{spec.icon}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
content = spec.icon;
|
||||
}
|
||||
|
||||
const sourceOnPress = spec.onPress;
|
||||
const onPress = useCallback(() => {
|
||||
if (!disabled) {
|
||||
sourceOnPress();
|
||||
onActionComplete?.();
|
||||
}
|
||||
}, [disabled, sourceOnPress, onActionComplete]);
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
style={{
|
||||
...styles.button, ...activatedStyle, ...disabledStyle, ...style,
|
||||
...(!visible ? { opacity: 0 } : null),
|
||||
}}
|
||||
themeId={styleSheet.themeId}
|
||||
onPress={onPress}
|
||||
description={ spec.description }
|
||||
accessibilityRole="button"
|
||||
disabled={ disabled }
|
||||
>
|
||||
{ content }
|
||||
</CustomButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
@@ -0,0 +1,123 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
import { ButtonGroup, ButtonSpec, StyleSheetData } from './types';
|
||||
|
||||
const React = require('react');
|
||||
|
||||
type OnToggleOverflowCallback = ()=> void;
|
||||
interface OverflowPopupProps {
|
||||
buttonGroups: ButtonGroup[];
|
||||
styleSheet: StyleSheetData;
|
||||
visible: boolean;
|
||||
|
||||
// Should be created using useCallback
|
||||
onToggleOverflow: OnToggleOverflowCallback;
|
||||
}
|
||||
|
||||
// Contains buttons that overflow the available space.
|
||||
// Displays all buttons in [props.buttonGroups] if [props.visible].
|
||||
// Otherwise, displays nothing.
|
||||
const ToolbarOverflowRows = (props: OverflowPopupProps) => {
|
||||
const overflowRows: ReactElement[] = [];
|
||||
|
||||
let key = 0;
|
||||
for (let i = 0; i < props.buttonGroups.length; i++) {
|
||||
key++;
|
||||
const row: ReactElement[] = [];
|
||||
|
||||
const group = props.buttonGroups[i];
|
||||
for (let j = 0; j < group.items.length; j++) {
|
||||
key++;
|
||||
|
||||
const buttonSpec = group.items[j];
|
||||
row.push(
|
||||
<ToolbarButton
|
||||
key={key.toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
spec={buttonSpec}
|
||||
|
||||
// After invoking this button's action, hide the overflow menu
|
||||
onActionComplete={props.onToggleOverflow}
|
||||
/>
|
||||
);
|
||||
|
||||
// Show the "hide overflow" button if in the center of the last row
|
||||
const isLastRow = i === props.buttonGroups.length - 1;
|
||||
const isCenterOfRow = j + 1 === Math.floor(group.items.length / 2);
|
||||
if (isLastRow && isCenterOfRow) {
|
||||
row.push(
|
||||
<ToggleOverflowButton
|
||||
key={(++key).toString()}
|
||||
styleSheet={props.styleSheet}
|
||||
overflowVisible={true}
|
||||
onToggleOverflowVisible={props.onToggleOverflow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
overflowRows.push(
|
||||
<View
|
||||
key={key.toString()}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
contentContainerStyle={props.styleSheet.styles.toolbarContent}
|
||||
>
|
||||
{row}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const [hasSpaceForCloseBtn, setHasSpaceForCloseBtn] = useState(true);
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
if (props.buttonGroups.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add 1 to account for the close button
|
||||
const totalButtonCount = props.buttonGroups[0].items.length + 1;
|
||||
|
||||
const newWidth = event.nativeEvent.layout.width;
|
||||
setHasSpaceForCloseBtn(newWidth > totalButtonCount * buttonSize);
|
||||
}, [setHasSpaceForCloseBtn, props.buttonGroups]);
|
||||
|
||||
const closeButtonSpec: ButtonSpec = {
|
||||
icon: '⨉',
|
||||
description: _('Close'),
|
||||
onPress: props.onToggleOverflow,
|
||||
};
|
||||
const closeButton = (
|
||||
<ToolbarButton
|
||||
styleSheet={props.styleSheet}
|
||||
spec={closeButtonSpec}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: props.buttonGroups.length * buttonSize,
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
onLayout={onContainerLayout}
|
||||
>
|
||||
{hasSpaceForCloseBtn ? closeButton : null}
|
||||
{overflowRows}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
export default ToolbarOverflowRows;
|
@@ -0,0 +1,35 @@
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export type OnPressListener = ()=> void;
|
||||
|
||||
export interface ButtonSpec {
|
||||
// Either text that will be shown in place of an icon or a component.
|
||||
icon: string | ReactElement;
|
||||
|
||||
// Tooltip/accessibility label
|
||||
description: string;
|
||||
onPress: OnPressListener;
|
||||
|
||||
// Priority for showing the button in the main toolbar.
|
||||
// Higher priority => more likely to be shown on the left of the toolbar
|
||||
// Lower (negative) priority => more likely to be shown on the right side of the
|
||||
// toolbar.
|
||||
priority?: number;
|
||||
|
||||
// True if the button is connected to an enabled action.
|
||||
// E.g. the cursor is in a header and the button is a header button.
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface ButtonGroup {
|
||||
title: string;
|
||||
items: ButtonSpec[];
|
||||
}
|
||||
|
||||
export interface StyleSheetData {
|
||||
themeId: number;
|
||||
styles: any;
|
||||
}
|
@@ -3,43 +3,43 @@ import shim from '@joplin/lib/shim';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import EditLinkDialog from './EditLinkDialog';
|
||||
import { defaultSearchState, SearchPanel } from './SearchPanel';
|
||||
import ExtendedWebView from '../ExtendedWebView';
|
||||
|
||||
const React = require('react');
|
||||
const { forwardRef, useImperativeHandle } = require('react');
|
||||
const { useEffect, useMemo, useState, useCallback, useRef } = require('react');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { View } = require('react-native');
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
import {
|
||||
EditorSettings,
|
||||
EditorControl,
|
||||
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent,
|
||||
ListType,
|
||||
SearchState,
|
||||
EditorSettings, EditorControl,
|
||||
ChangeEvent, UndoRedoDepthChangeEvent, Selection, SelectionChangeEvent, ListType, SearchState,
|
||||
} from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar';
|
||||
|
||||
type ChangeEventHandler = (event: ChangeEvent)=> void;
|
||||
type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void;
|
||||
type SelectionChangeEventHandler = (event: SelectionChangeEvent)=> void;
|
||||
type OnAttachCallback = ()=> void;
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
initialText: string;
|
||||
initialSelection?: Selection;
|
||||
style: any;
|
||||
style: ViewStyle;
|
||||
contentStyle?: ViewStyle;
|
||||
|
||||
onChange: ChangeEventHandler;
|
||||
onSelectionChange: SelectionChangeEventHandler;
|
||||
onUndoRedoDepthChange: UndoRedoDepthChangeHandler;
|
||||
onAttach: OnAttachCallback;
|
||||
}
|
||||
|
||||
function fontFamilyFromSettings() {
|
||||
const f = editorFont(Setting.value('style.editor.fontFamily'));
|
||||
return [f, 'sans-serif'].join(', ');
|
||||
const font = editorFont(Setting.value('style.editor.fontFamily'));
|
||||
return font ? `${font}, sans-serif` : 'sans-serif';
|
||||
}
|
||||
|
||||
function useCss(themeId: number): string {
|
||||
@@ -106,8 +106,107 @@ function editorTheme(themeId: number) {
|
||||
};
|
||||
}
|
||||
|
||||
type OnInjectJSCallback = (js: string)=> void;
|
||||
type OnSetVisibleCallback = (visible: boolean)=> void;
|
||||
type OnSearchStateChangeCallback = (state: SearchState)=> void;
|
||||
const useEditorControl = (
|
||||
injectJS: OnInjectJSCallback, setLinkDialogVisible: OnSetVisibleCallback,
|
||||
setSearchState: OnSearchStateChangeCallback, searchStateRef: RefObject<SearchState>
|
||||
): EditorControl => {
|
||||
return useMemo(() => {
|
||||
return {
|
||||
undo() {
|
||||
injectJS('cm.undo();');
|
||||
},
|
||||
redo() {
|
||||
injectJS('cm.redo();');
|
||||
},
|
||||
select(anchor: number, head: number) {
|
||||
injectJS(
|
||||
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
|
||||
);
|
||||
},
|
||||
insertText(text: string) {
|
||||
injectJS(`cm.insertText(${JSON.stringify(text)});`);
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
injectJS('cm.toggleBolded();');
|
||||
},
|
||||
toggleItalicized() {
|
||||
injectJS('cm.toggleItalicized();');
|
||||
},
|
||||
toggleList(listType: ListType) {
|
||||
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
|
||||
},
|
||||
toggleCode() {
|
||||
injectJS('cm.toggleCode();');
|
||||
},
|
||||
toggleMath() {
|
||||
injectJS('cm.toggleMath();');
|
||||
},
|
||||
toggleHeaderLevel(level: number) {
|
||||
injectJS(`cm.toggleHeaderLevel(${level});`);
|
||||
},
|
||||
increaseIndent() {
|
||||
injectJS('cm.increaseIndent();');
|
||||
},
|
||||
decreaseIndent() {
|
||||
injectJS('cm.decreaseIndent();');
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
injectJS(`cm.updateLink(
|
||||
${JSON.stringify(label)},
|
||||
${JSON.stringify(url)}
|
||||
);`);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
injectJS('cm.scrollSelectionIntoView();');
|
||||
},
|
||||
showLinkDialog() {
|
||||
setLinkDialogVisible(true);
|
||||
},
|
||||
hideLinkDialog() {
|
||||
setLinkDialogVisible(false);
|
||||
},
|
||||
hideKeyboard() {
|
||||
injectJS('document.activeElement?.blur();');
|
||||
},
|
||||
searchControl: {
|
||||
findNext() {
|
||||
injectJS('cm.searchControl.findNext();');
|
||||
},
|
||||
findPrevious() {
|
||||
injectJS('cm.searchControl.findPrevious();');
|
||||
},
|
||||
replaceCurrent() {
|
||||
injectJS('cm.searchControl.replaceCurrent();');
|
||||
},
|
||||
replaceAll() {
|
||||
injectJS('cm.searchControl.replaceAll();');
|
||||
},
|
||||
setSearchState(state: SearchState) {
|
||||
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
|
||||
setSearchState(state);
|
||||
},
|
||||
showSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: true,
|
||||
});
|
||||
},
|
||||
hideSearch() {
|
||||
setSearchState({
|
||||
...searchStateRef.current,
|
||||
dialogVisible: false,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}, [injectJS, searchStateRef, setLinkDialogVisible, setSearchState]);
|
||||
};
|
||||
|
||||
function NoteEditor(props: Props, ref: any) {
|
||||
const [source, setSource] = useState(undefined);
|
||||
const webviewRef = useRef(null);
|
||||
|
||||
const setInitialSelectionJS = props.initialSelection ? `
|
||||
@@ -115,8 +214,10 @@ function NoteEditor(props: Props, ref: any) {
|
||||
` : '';
|
||||
|
||||
const editorSettings: EditorSettings = {
|
||||
themeId: props.themeId,
|
||||
themeData: editorTheme(props.themeId),
|
||||
katexEnabled: Setting.value('markdown.plugin.katex') as boolean,
|
||||
katexEnabled: Setting.value('markdown.plugin.katex'),
|
||||
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
|
||||
};
|
||||
|
||||
const injectedJavaScript = `
|
||||
@@ -170,141 +271,31 @@ function NoteEditor(props: Props, ref: any) {
|
||||
const css = useCss(props.themeId);
|
||||
const html = useHtml(css);
|
||||
const [selectionState, setSelectionState] = useState(new SelectionFormatting());
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
const [linkDialogVisible, setLinkDialogVisible] = useState(false);
|
||||
const [searchState, setSearchState] = useState(defaultSearchState);
|
||||
|
||||
// Having a [searchStateRef] allows [editorControl] to not be re-created
|
||||
// whenever [searchState] changes.
|
||||
const searchStateRef = useRef(defaultSearchState);
|
||||
|
||||
// Keep the reference and the [searchState] in sync
|
||||
useEffect(() => {
|
||||
searchStateRef.current = searchState;
|
||||
}, [searchState]);
|
||||
|
||||
// / Runs [js] in the context of the CodeMirror frame.
|
||||
const injectJS = (js: string) => {
|
||||
webviewRef.current.injectJavaScript(`
|
||||
try {
|
||||
${js}
|
||||
}
|
||||
catch(e) {
|
||||
logMessage('Error in injected JS:' + e, e);
|
||||
throw e;
|
||||
};
|
||||
|
||||
true;`);
|
||||
webviewRef.current.injectJS(js);
|
||||
};
|
||||
|
||||
|
||||
const editorControl: EditorControl = {
|
||||
undo() {
|
||||
injectJS('cm.undo();');
|
||||
},
|
||||
redo() {
|
||||
injectJS('cm.redo();');
|
||||
},
|
||||
select(anchor: number, head: number) {
|
||||
injectJS(
|
||||
`cm.select(${JSON.stringify(anchor)}, ${JSON.stringify(head)});`
|
||||
);
|
||||
},
|
||||
insertText(text: string) {
|
||||
injectJS(`cm.insertText(${JSON.stringify(text)});`);
|
||||
},
|
||||
|
||||
toggleBolded() {
|
||||
injectJS('cm.toggleBolded();');
|
||||
},
|
||||
toggleItalicized() {
|
||||
injectJS('cm.toggleItalicized();');
|
||||
},
|
||||
toggleList(listType: ListType) {
|
||||
injectJS(`cm.toggleList(${JSON.stringify(listType)});`);
|
||||
},
|
||||
toggleCode() {
|
||||
injectJS('cm.toggleCode();');
|
||||
},
|
||||
toggleMath() {
|
||||
injectJS('cm.toggleMath();');
|
||||
},
|
||||
toggleHeaderLevel(level: number) {
|
||||
injectJS(`cm.toggleHeaderLevel(${level});`);
|
||||
},
|
||||
increaseIndent() {
|
||||
injectJS('cm.increaseIndent();');
|
||||
},
|
||||
decreaseIndent() {
|
||||
injectJS('cm.decreaseIndent();');
|
||||
},
|
||||
updateLink(label: string, url: string) {
|
||||
injectJS(`cm.updateLink(
|
||||
${JSON.stringify(label)},
|
||||
${JSON.stringify(url)}
|
||||
);`);
|
||||
},
|
||||
scrollSelectionIntoView() {
|
||||
injectJS('cm.scrollSelectionIntoView();');
|
||||
},
|
||||
showLinkDialog() {
|
||||
setLinkDialogVisible(true);
|
||||
},
|
||||
hideLinkDialog() {
|
||||
setLinkDialogVisible(false);
|
||||
},
|
||||
hideKeyboard() {
|
||||
injectJS('document.activeElement?.blur();');
|
||||
},
|
||||
setSpellcheckEnabled(enabled: boolean) {
|
||||
injectJS(`cm.setSpellcheckEnabled(${enabled ? 'true' : 'false'});`);
|
||||
},
|
||||
searchControl: {
|
||||
findNext() {
|
||||
injectJS('cm.searchControl.findNext();');
|
||||
},
|
||||
findPrevious() {
|
||||
injectJS('cm.searchControl.findPrevious();');
|
||||
},
|
||||
replaceCurrent() {
|
||||
injectJS('cm.searchControl.replaceCurrent();');
|
||||
},
|
||||
replaceAll() {
|
||||
injectJS('cm.searchControl.replaceAll();');
|
||||
},
|
||||
setSearchState(state: SearchState) {
|
||||
injectJS(`cm.searchControl.setSearchState(${JSON.stringify(state)})`);
|
||||
setSearchState(state);
|
||||
},
|
||||
showSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = true;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
},
|
||||
hideSearch() {
|
||||
const newSearchState: SearchState = Object.assign({}, searchState);
|
||||
newSearchState.dialogVisible = false;
|
||||
|
||||
setSearchState(newSearchState);
|
||||
},
|
||||
},
|
||||
};
|
||||
const editorControl = useEditorControl(
|
||||
injectJS, setLinkDialogVisible, setSearchState, searchStateRef
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return editorControl;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function createHtmlFile() {
|
||||
const tempFile = `${Setting.value('resourceDir')}/NoteEditor.html`;
|
||||
await shim.fsDriver().writeFile(tempFile, html, 'utf8');
|
||||
if (cancelled) return;
|
||||
|
||||
setSource({
|
||||
uri: `file://${tempFile}?r=${Math.round(Math.random() * 100000000)}`,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
});
|
||||
}
|
||||
|
||||
void createHtmlFile();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [html]);
|
||||
|
||||
const onMessage = useCallback((event: any) => {
|
||||
const data = event.nativeEvent.data;
|
||||
|
||||
@@ -359,17 +350,12 @@ function NoteEditor(props: Props, ref: any) {
|
||||
} else {
|
||||
console.info('Unsupported CodeMirror message:', msg);
|
||||
}
|
||||
}, [props.onChange]);
|
||||
}, [props.onSelectionChange, props.onUndoRedoDepthChange, props.onChange, editorControl]);
|
||||
|
||||
const onError = useCallback(() => {
|
||||
console.error('NoteEditor: webview error');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// - `setSupportMultipleWindows` must be `true` for security reasons:
|
||||
// https://github.com/react-native-webview/react-native-webview/releases/tag/v11.0.0
|
||||
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
|
||||
// when the editor is focused.
|
||||
return (
|
||||
<View style={{
|
||||
...props.style,
|
||||
@@ -384,21 +370,14 @@ function NoteEditor(props: Props, ref: any) {
|
||||
<View style={{
|
||||
flexGrow: 1,
|
||||
flexShrink: 0,
|
||||
minHeight: '40%',
|
||||
minHeight: '30%',
|
||||
...props.contentStyle,
|
||||
}}>
|
||||
<WebView
|
||||
style={{
|
||||
backgroundColor: editorSettings.themeData.backgroundColor,
|
||||
}}
|
||||
<ExtendedWebView
|
||||
webviewInstanceId='NoteEditor'
|
||||
themeId={props.themeId}
|
||||
ref={webviewRef}
|
||||
scrollEnabled={false}
|
||||
useWebKit={true}
|
||||
source={source}
|
||||
setSupportMultipleWindows={true}
|
||||
hideKeyboardAccessoryView={true}
|
||||
allowingReadAccessToURL={`file://${Setting.value('resourceDir')}`}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
allowFileAccess={true}
|
||||
html={html}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
onMessage={onMessage}
|
||||
onError={onError}
|
||||
@@ -410,6 +389,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchControl={editorControl.searchControl}
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
<MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
// for it:
|
||||
flexShrink: 1,
|
||||
}}
|
||||
editorSettings={editorSettings}
|
||||
editorControl={editorControl}
|
||||
selectionState={selectionState}
|
||||
searchState={searchState}
|
||||
onAttach={props.onAttach}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@@ -1,15 +1,14 @@
|
||||
// Displays a find/replace dialog
|
||||
|
||||
const React = require('react');
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { TextInput, View, Text, TouchableOpacity } = require('react-native');
|
||||
const { useMemo, useState, useEffect } = require('react');
|
||||
const MaterialCommunityIcon = require('react-native-vector-icons/MaterialCommunityIcons').default;
|
||||
|
||||
import { SearchControl, SearchState, EditorSettings } from './types';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { BackHandler } from 'react-native';
|
||||
import { BackHandler, TextInput, View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import CustomButton from '../CustomButton';
|
||||
|
||||
const buttonSize = 48;
|
||||
|
||||
@@ -33,6 +32,7 @@ export interface SearchPanelProps {
|
||||
|
||||
interface ActionButtonProps {
|
||||
styles: any;
|
||||
themeId: number;
|
||||
iconName: string;
|
||||
title: string;
|
||||
onPress: Callback;
|
||||
@@ -42,30 +42,32 @@ const ActionButton = (
|
||||
props: ActionButtonProps
|
||||
) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
style={props.styles.button}
|
||||
onPress={props.onPress}
|
||||
|
||||
accessibilityLabel={props.title}
|
||||
accessibilityRole='button'
|
||||
description={props.title}
|
||||
>
|
||||
<MaterialCommunityIcon name={props.iconName} style={props.styles.buttonText}/>
|
||||
</TouchableOpacity>
|
||||
</CustomButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface ToggleButtonProps {
|
||||
styles: any;
|
||||
themeId: number;
|
||||
iconName: string;
|
||||
title: string;
|
||||
active: boolean;
|
||||
onToggle: Callback;
|
||||
}
|
||||
|
||||
const ToggleButton = (props: ToggleButtonProps) => {
|
||||
const active = props.active;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<CustomButton
|
||||
themeId={props.themeId}
|
||||
style={{
|
||||
...props.styles.toggleButton,
|
||||
...(active ? props.styles.toggleButtonActive : {}),
|
||||
@@ -75,20 +77,20 @@ const ToggleButton = (props: ToggleButtonProps) => {
|
||||
accessibilityState={{
|
||||
checked: props.active,
|
||||
}}
|
||||
accessibilityLabel={props.title}
|
||||
description={props.title}
|
||||
accessibilityRole='switch'
|
||||
>
|
||||
<MaterialCommunityIcon name={props.iconName} style={
|
||||
active ? props.styles.activeButtonText : props.styles.buttonText
|
||||
}/>
|
||||
</TouchableOpacity>
|
||||
</CustomButton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const useStyles = (theme: Theme) => {
|
||||
return useMemo(() => {
|
||||
const buttonStyle = {
|
||||
const buttonStyle: ViewStyle = {
|
||||
width: buttonSize,
|
||||
height: buttonSize,
|
||||
backgroundColor: theme.backgroundColor4,
|
||||
@@ -136,8 +138,9 @@ const useStyles = (theme: Theme) => {
|
||||
};
|
||||
|
||||
export const SearchPanel = (props: SearchPanelProps) => {
|
||||
const placeholderColor = props.editorSettings.themeData.color3;
|
||||
const styles = useStyles(props.editorSettings.themeData);
|
||||
const theme = props.editorSettings.themeData;
|
||||
const placeholderColor = theme.color3;
|
||||
const styles = useStyles(theme);
|
||||
|
||||
const [showingAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
@@ -181,12 +184,14 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
});
|
||||
|
||||
return () => backListener.remove();
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [state.dialogVisible]);
|
||||
|
||||
|
||||
|
||||
const themeId = props.editorSettings.themeId;
|
||||
const closeButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="close"
|
||||
onPress={control.hideSearch}
|
||||
@@ -196,6 +201,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const showDetailsButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="menu-down"
|
||||
onPress={() => setShowAdvanced(true)}
|
||||
@@ -205,6 +211,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const hideDetailsButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="menu-up"
|
||||
onPress={() => setShowAdvanced(false)}
|
||||
@@ -254,6 +261,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const toNextButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="menu-right"
|
||||
onPress={control.findNext}
|
||||
@@ -263,6 +271,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const toPrevButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="menu-left"
|
||||
onPress={control.findPrevious}
|
||||
@@ -272,6 +281,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const replaceButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="swap-horizontal"
|
||||
onPress={control.replaceCurrent}
|
||||
@@ -281,6 +291,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const replaceAllButton = (
|
||||
<ActionButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="reply-all"
|
||||
onPress={control.replaceAll}
|
||||
@@ -290,6 +301,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const regexpButton = (
|
||||
<ToggleButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="regex"
|
||||
onToggle={() => {
|
||||
@@ -304,6 +316,7 @@ export const SearchPanel = (props: SearchPanelProps) => {
|
||||
|
||||
const caseSensitiveButton = (
|
||||
<ToggleButton
|
||||
themeId={themeId}
|
||||
styles={styles}
|
||||
iconName="format-letter-case"
|
||||
onToggle={() => {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// Types related to the NoteEditor
|
||||
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { CodeMirrorControl } from './CodeMirror/types';
|
||||
|
||||
// Controls for the entire editor (including dialogs)
|
||||
@@ -10,8 +11,14 @@ export interface EditorControl extends CodeMirrorControl {
|
||||
}
|
||||
|
||||
export interface EditorSettings {
|
||||
themeData: any;
|
||||
// EditorSettings objects are deserialized within WebViews, where
|
||||
// [themeStyle(themeId: number)] doesn't work. As such, we need both
|
||||
// the [themeId] and [themeData].
|
||||
themeId: number;
|
||||
themeData: Theme;
|
||||
|
||||
katexEnabled: boolean;
|
||||
spellcheckEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ChangeEvent {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user