1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-05 20:56:22 +02:00

Compare commits

..

94 Commits

Author SHA1 Message Date
Laurent Cozic
9bad668cc5 CLI v2.13.2 2023-11-30 19:12:07 +01:00
Laurent Cozic
c18c31ab7f Lock file 2023-11-30 19:10:57 +01:00
Laurent Cozic
7c24a2f4be Releasing sub-packages 2023-11-30 19:10:02 +01:00
Laurent Cozic
56438ea644 iOS 12.13.9 2023-11-30 18:56:49 +01:00
Laurent Cozic
7f9bc1e15c Android 2.13.9 2023-11-30 18:56:17 +01:00
Henry Heino
b1c8cb5632 Mobile: Fixes #9374: Fix tooltips don't disappear on some devices (upgrade to js-draw 1.13.2) (#9401) 2023-11-29 15:17:29 +01:00
Henry Heino
f0a1b41794 Mobile: Resolves #9377: Don't attach empty drawings when a user exits without saving (#9386) 2023-11-27 20:14:04 +01:00
Laurent Cozic
02982464a6 iOS 12.13.8 2023-11-26 13:55:26 +01:00
Laurent Cozic
62e317db05 lock file 2023-11-26 13:49:00 +01:00
Laurent Cozic
e0795748a9 Android 2.13.8 2023-11-26 13:40:59 +01:00
Laurent Cozic
67070ed3d5 Desktop release v2.13.7 2023-11-26 12:38:28 +01:00
Laurent Cozic
fec8c6131c Mobile: Fixes #9376: Sidebar is not dismissed when creating a note 2023-11-26 12:37:45 +01:00
pedr
24ed5bda63 Mobile: #9361: Fix to-dos options toggle don't toggle a rerender in (#9364) 2023-11-24 14:48:41 +01:00
Henry Heino
dbb354ad10 Mobile: Fixes #9328: Fix new note/to-do buttons not visible on app startup in some cases (#9329) 2023-11-19 10:43:57 +00:00
Laurent Cozic
92dccbe98d Merge branch 'release-2.13' into dev 2023-11-16 15:11:39 +00:00
Laurent Cozic
9b775d77f6 iOS 12.13.7 2023-11-16 13:37:15 +00:00
Laurent Cozic
4fd6937d05 lock file 2023-11-16 13:36:45 +00:00
Laurent Cozic
7230f0e698 iOS 12.13.6 2023-11-16 13:28:27 +00:00
Laurent Cozic
ada82538ee Android 2.13.7 2023-11-16 13:26:58 +00:00
Laurent Cozic
e7dd981db6 Desktop release v2.13.6 2023-11-16 13:02:07 +00:00
Laurent Cozic
767bf9f002 Server: Increase number of items that are returned during sync 2023-11-16 12:20:07 +00:00
Laurent Cozic
f698068587 Server: Fix severe performance issue for certain delta calls 2023-11-16 12:20:06 +00:00
Henry Heino
d0955b4ca2 Mobile: Fixes #9321: Restore scroll position when returning to the note viewer from the editor or camera (#9324) 2023-11-16 12:19:48 +00:00
Henry Heino
18e86a7ba3 Mobile: Resolves #9294: Implement settings search (#9320) 2023-11-16 12:17:03 +00:00
renovate[bot]
f9a1ab4d40 Update dependency react-native-vector-icons to v10.0.1 (#9316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-16 01:02:28 +00:00
renovate[bot]
062d0898a0 Update dependency sass to v1.69.5 (#9317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 23:23:22 +00:00
pedr
3b51b4fd72 Tools: Resolves #9265: Add no-constant-binary-expression to eslint rules (#9319) 2023-11-15 20:18:31 +00:00
Helmut K. C. Tessarek
1a78ff4398 All: Translation: Update it_IT.po (thanks Pietro Campanella) 2023-11-15 18:56:00 +01:00
Laurent Cozic
544af8d118 Server v2.13.4 2023-11-15 15:31:09 +00:00
pedr
2616c377a9 Tools: Create vscode launch option for debugging lib project (#9318) 2023-11-15 15:29:45 +00:00
Henry Heino
4a63331306 Plugin Repo: Resolves #9280: Allow marking specific NPM packages as superseded (#9302) 2023-11-15 13:44:09 +00:00
renovate[bot]
48621443ec Update dependency sass to v1.69.0 (#9310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 13:38:48 +00:00
pedr
79fd66b94c All: Fixes #9151: Import of inter-linked md files has incorrect notebook structure (#9269) 2023-11-15 13:33:20 +00:00
Henry Heino
6a6c8c1d83 Mobile: Fixes #9312: Fix settings save confirmation not shown when navigating to encryption/profile/log screens (#9313) 2023-11-15 13:31:36 +00:00
Henry Heino
cf19dacbaf Mobile: Fixes #9308: Disable notebook list side menu in config screen (#9311) 2023-11-15 13:31:26 +00:00
renovate[bot]
50925abc40 Update dependency @types/react to v18.2.33 (#9306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 10:54:33 +00:00
renovate[bot]
c80cbaa32f Update dependency react-native-safe-area-context to v4.7.4 (#9307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 00:45:41 +00:00
Henry Heino
f7cb1aef4b iOS: Fixes #9271: Allow showing dropdowns in landscape mode (#9309) 2023-11-15 00:42:27 +00:00
Joplin Bot
96d5d1dfab Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-15 00:35:50 +00:00
renovate[bot]
98d608fec5 Update dependency @types/node to v18.18.7 (#9305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-14 22:21:00 +00:00
Laurent Cozic
1af46b0246 Update translations 2023-11-14 19:00:52 +00:00
Henry Heino
1e530b74d4 Chore: Plugins: Allow absolute paths and URLs in screenshot srcs (#9254) 2023-11-14 18:49:45 +00:00
Henry Heino
e61c4acce5 Desktop: Resolves #9136: Install script: Work around unprivlidged user namespace restrictions by adding the --no-sandbox flag to the launcher (#9137) 2023-11-14 18:49:25 +00:00
Mohammad Ashouri
184499711d Full translation support for Farsi/Persian (#9244) 2023-11-14 18:48:32 +00:00
Henry Heino
2c0181d097 Desktop, Cli: Fixes #8788: Work around WebDAV sync issues over ipv6 (#9286) 2023-11-14 18:47:52 +00:00
Laurent Cozic
06ea12adb3 Doc: Update sponsors 2023-11-14 18:06:56 +00:00
Laurent Cozic
9923e5c821 Desktop: Resolves #9293: Preserve nested tables in RTE 2023-11-14 11:45:38 +00:00
renovate[bot]
9a06e59cfe Update dependency @testing-library/react-native to v12.3.1 (#9298)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 21:54:42 +00:00
renovate[bot]
80a2cd91f4 Update dependency @types/markdown-it to v13.0.5 (#9290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-13 18:36:02 +00:00
Laurent Cozic
df9ed3e487 Update BUG_REPORT.yml 2023-11-13 14:01:49 +00:00
Laurent Cozic
368d0130f6 Update BUG_REPORT.yml 2023-11-13 14:00:44 +00:00
Laurent Cozic
824e1b44dd Update BUG_REPORT.yml 2023-11-13 13:56:45 +00:00
Laurent Cozic
ccf1c8ee31 Desktop: Improve toolbar button wrapping on RTE 2023-11-13 13:52:37 +00:00
Laurent Cozic
d5f6d83f6d Update BUG_REPORT.yml 2023-11-13 11:58:52 +00:00
Laurent Cozic
5d422f85c8 lock file 2023-11-12 18:12:53 +00:00
renovate[bot]
78aeb46d56 Update dependency nodemailer to v6.9.7 (#9277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 17:28:00 +00:00
renovate[bot]
091bf45149 Update postgres Docker tag to v16 (#9289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 16:16:04 +00:00
Laurent Cozic
8d9d24740b Doc: Suggest installing yo@4.3.1 2023-11-12 15:52:01 +00:00
Laurent Cozic
21e5f88cb2 Plugin Generator release v2.13.2 2023-11-12 15:40:37 +00:00
Laurent Cozic
5d4259d064 Lock file 2023-11-12 15:39:36 +00:00
Laurent Cozic
9ac03ec33a Releasing sub-packages 2023-11-12 15:38:06 +00:00
Laurent Cozic
e760276341 Chore: Fixes #9282: Plugin generator fails when updating plugin 2023-11-12 15:32:39 +00:00
Henry Heino
206f35ffe5 Mobile: Resolves #9258: Add more space between settings title and description (#9270) 2023-11-12 15:08:52 +00:00
Henry Heino
ddf716479d Chore: Partially resolves #9262: Mobile build: Convert bundle tasks to proper gulp tasks and increase output verbosity (#9266) 2023-11-12 15:07:30 +00:00
Henry Heino
ec7f94df25 Chore: Resolves #9274: Desktop: Fix end-to-end tests when the first window is the devtools window (#9275) 2023-11-12 15:06:32 +00:00
Henry Heino
bcbba0973f Mobile: Improve image editor load performance (#9281) 2023-11-12 15:06:16 +00:00
Henry Heino
bd1ddb8522 Mobile: Resolves #9195: Update js-draw to version 1.11.2 (#9120) 2023-11-12 15:04:55 +00:00
Laurent Cozic
fb47398554 lock files 2023-11-12 15:02:44 +00:00
Henry Heino
10356f4009 Chore: Fixes #9284: Desktop: Fix warning on opening settings screen (#9287) 2023-11-12 15:01:14 +00:00
renovate[bot]
ba83fca47a Update dependency mermaid to v10.5.1 (#9279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-12 12:57:00 +00:00
Laurent Cozic
b01295f0fd Update BUG_REPORT.yml 2023-11-12 12:51:49 +00:00
renovate[bot]
b928e614cc Update dependency mermaid to v10.5.0 (#9278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-11 18:22:46 +00:00
Laurent Cozic
973b9c354c Delete .github/ISSUE_TEMPLATE/bug_report.md 2023-11-11 18:20:48 +00:00
Wladimir Kirianov
c12444d6e8 Doc: Improved github issue template, added direct links to support and feature requests (#8974)
Co-authored-by: Henry Heino <46334387+personalizedrefrigerator@users.noreply.github.com>
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-11 18:18:59 +00:00
Joplin Bot
1401d28f82 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-11 18:13:30 +00:00
Laurent Cozic
335269f92d Tools: Fail more cleanly when docker-compose randomly fails to load Postgres 2023-11-11 17:50:15 +00:00
Laurent Cozic
6211606a22 Desktop: Fixed import error report 2023-11-11 17:41:08 +00:00
Laurent Cozic
5f7d438ac1 Doc: Update sponsors 2023-11-11 17:41:07 +00:00
Daeraxa
ee2df96cfb Docs: Fix BUILD.md in contributing instructions (#9273) 2023-11-11 17:17:46 +00:00
Henry Heino
692e925997 Mobile: Fixes #9259: Config screen: Fix section list scroll (#9267) 2023-11-11 17:09:34 +00:00
Henry Heino
39803f53a0 Mobile: Resolves #9260: Fade settings screen icons (#9268) 2023-11-11 17:08:58 +00:00
renovate[bot]
b3591808b7 Update dependency @types/zxcvbn to v4.4.3 (#9247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-11 17:01:00 +00:00
github-actions[bot]
2427677fd5 @Daeraxa has signed the CLA in laurent22/joplin#9273 2023-11-11 16:18:11 +00:00
Laurent Cozic
9a051effcd Update bug_report.md 2023-11-11 13:24:00 +00:00
Henry Heino
e6e9f92e01 Mobile: Fixes #9123: Fix encryption when a resource doesn't have an associated file (#9222)
Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
2023-11-10 14:22:26 +00:00
Laurent Cozic
ca6762c891 iOS 12.13.5 2023-11-10 14:13:43 +00:00
Laurent Cozic
76d07beb27 lock file 2023-11-10 14:13:42 +00:00
Henry Heino
f3daa7f0e4 Desktop: Resolves #9250: Make settings tabs focusable by keyboard (#9253) 2023-11-10 14:00:59 +00:00
Laurent Cozic
041ad22443 Update bug_report.md 2023-11-10 13:42:06 +00:00
Laurent Cozic
6cd0938ee4 iOS 12.13.5 2023-11-10 13:21:02 +00:00
Laurent Cozic
c3dc30ee5d lock file 2023-11-10 13:19:52 +00:00
renovate[bot]
37c925dcf2 Update dependency react-native-safe-area-context to v4.7.3 (#9248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 12:05:54 +00:00
renovate[bot]
05bd51f85c Update dependency @types/yargs to v17.0.29 (#9246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-10 12:05:41 +00:00
Joplin Bot
cfbc37df8d Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-10 00:41:46 +00:00
201 changed files with 26384 additions and 21215 deletions

View File

@@ -383,6 +383,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js packages/app-desktop/plugins/GotoAnything.js
@@ -508,7 +510,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/utils/ShareExtension.js packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js packages/app-mobile/utils/ShareUtils.js

View File

@@ -157,6 +157,8 @@ module.exports = {
// In user-facing text, it should be "notebook". // In user-facing text, it should be "notebook".
'id-denylist': ['error', 'err', 'notebook', 'notebooks'], 'id-denylist': ['error', 'err', 'notebook', 'notebooks'],
'prefer-arrow-callback': ['error'], 'prefer-arrow-callback': ['error'],
'no-constant-binary-expression': ['error'],
}, },
'plugins': [ 'plugins': [
'react', 'react',

62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: Bug Report
description: Report a reproducible bug or regression in Joplin.
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: dropdown
id: os
attributes:
label: "Operating system"
multiple: false
options:
- "Windows"
- "macOS"
- "Linux"
- "Android"
- "iOS"
validations:
required: true
- type: input
id: version
attributes:
label: "Joplin version"
placeholder: "For example 2.3.6"
description:
validations:
required: true
- type: textarea
id: desktop-about-content
attributes:
label: "Desktop version info"
description: "If this issue is about the **desktop app**, please open the \"About\" dialog under the \"Help\" or \"Joplin\" menu and copy its content here."
placeholder: "Joplin 2.13.5 (dev, darwin)\n\nClient ID: ..."
- type: textarea
id: current
attributes:
label: Current behaviour
description: What did Joplin do? Include screenshots and video recordings for UI problems if needed. If you are reporting a clipper bug, please include an example URL that shows the issue.
placeholder: |
1. This
2. Then that
3. Then this
4. Etc.
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What did you expect Joplin to do?
- type: textarea
id: logs
attributes:
label: Logs
description: "If relevant, please provide a log file as described here: https://joplinapp.org/help/apps/debugging"

View File

@@ -1,52 +0,0 @@
---
name: "\U0001F41B Bug Report"
about: Report a reproducible bug or regression in Joplin.
title: ''
labels: bug
assignees: ''
---
<!--
Please provide a clear and concise description of what the bug is. (In the section Steps To Reproduce.)
Include screenshots for UI problems if needed.
DO NOT create screenshots of text !!! Copy and paste the text into a code block.
Please test using the latest Joplin release to make sure your issue has not already been fixed.
-->
<!--
IMPORTANT: If you are reporting a clipper bug, please include an example URL that shows the issue.
Without the URL the issue is likely to be closed.
-->
## Environment
Joplin version:
Platform:
OS specifics:
<!--
Platform can be one of: macOS, Linux, Windows, Android, iOS, terminal (or a combination)
OS specifics: e.g. OS version, Linux distribution, Android/iOS version...
-->
## Steps to reproduce
1.
2.
3.
<!--
Issues without reproduction steps are likely to stall.
-->
## Describe what you expected to happen
## Logfile
<!--
Please attach a debug log. Issues without a debug log are likely to stall.
For information on how to collect a log file: https://joplinapp.org/help/apps/debugging/
-->

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: "\U0001F914 Feature requests and support" - name: Feature Requests
url: https://discourse.joplinapp.org/ url: https://discourse.joplinapp.org/c/features/
about: I have a question or feature request … about: Discuss ideas for new features or changes
- name: Support
url: https://discourse.joplinapp.org/c/support/
about: Please ask for help here

View File

@@ -75,6 +75,10 @@ if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
if [ "$IS_LINUX" == "1" ]; then if [ "$IS_LINUX" == "1" ]; then
echo "Running Joplin Server tests using PostgreSQL..." echo "Running Joplin Server tests using PostgreSQL..."
sudo docker-compose --file docker-compose.db-dev.yml up -d sudo docker-compose --file docker-compose.db-dev.yml up -d
cmdResult=$?
if [ $cmdResult -ne 0 ]; then
exit $cmdResult
fi
export JOPLIN_TESTS_SERVER_DB=pg export JOPLIN_TESTS_SERVER_DB=pg
else else
echo "Running Joplin Server tests using SQLite..." echo "Running Joplin Server tests using SQLite..."

7
.gitignore vendored
View File

@@ -365,6 +365,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js packages/app-desktop/plugins/GotoAnything.js
@@ -490,7 +492,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.js packages/app-mobile/services/voiceTyping/vosk.ios.js
packages/app-mobile/setupQuickActions.js packages/app-mobile/setupQuickActions.js
packages/app-mobile/tools/buildInjectedJs.js packages/app-mobile/tools/buildInjectedJs/BundledFile.js
packages/app-mobile/tools/buildInjectedJs/constants.js
packages/app-mobile/tools/buildInjectedJs/copyJs.js
packages/app-mobile/tools/buildInjectedJs/gulpTasks.js
packages/app-mobile/utils/ShareExtension.js packages/app-mobile/utils/ShareExtension.js
packages/app-mobile/utils/ShareUtils.test.js packages/app-mobile/utils/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js packages/app-mobile/utils/ShareUtils.js

View File

@@ -205,9 +205,16 @@ if command -v lsb_release &> /dev/null; then
# Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem. # Check for "The SUID sandbox helper binary was found, but is not configured correctly" problem.
# It is present in Debian 1X. A (temporary) patch will be applied at .desktop file # It is present in Debian 1X. A (temporary) patch will be applied at .desktop file
# Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling. # Linux Mint 4 Debbie is based on Debian 10 and requires the same param handling.
if [[ $DISTVER =~ Debian1. ]] || [ "$DISTVER" = "Linuxmint4" ] && [ "$DISTCODENAME" = "debbie" ] || [ "$DISTVER" = "CentOS" ] && [[ "$DISTMAJOR" =~ 6|7 ]] #
# This also works around Ubuntu 23.10+'s restrictions on unprivileged user namespaces. Electron
# uses these to sandbox processes. Unfortunately, it doesn't look like we can get around this
# without writing the AppImage to a non-user-writable location (without invalidating other security
# controls). See https://discourse.joplinapp.org/t/possible-future-requirement-for-no-sandbox-flag-for-ubuntu-23-10/.
if [[ $DISTVER = "Ubuntu23.10" || $DISTVER =~ Debian1. || ( "$DISTVER" = "Linuxmint4" && "$DISTCODENAME" = "debbie" ) || ( "$DISTVER" = "CentOS" && "$DISTMAJOR" =~ 6|7 ) ]]
then then
SANDBOXPARAM="--no-sandbox" SANDBOXPARAM="--no-sandbox"
print "${COLOR_YELLOW}WARNING${COLOR_RESET} Electron sandboxing disabled."
print " See https://discourse.joplinapp.org/t/32160/5 for details."
fi fi
fi fi

View File

@@ -42,8 +42,8 @@ Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/read
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) | | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) | | <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) |
| <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/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/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/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/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <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/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/4560672?s=96&v=4"/></br>[mu88](https://github.com/mu88) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) |
| <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/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | | | <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/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | |
<!-- SPONSORS-GITHUB --> <!-- SPONSORS-GITHUB -->
# Community # Community

View File

@@ -6,7 +6,7 @@ version: '3'
services: services:
db: db:
image: postgres:15 image: postgres:16
command: postgres -c work_mem=100000 command: postgres -c work_mem=100000
ports: ports:
- "5432:5432" - "5432:5432"

View File

@@ -18,7 +18,7 @@ services:
- POSTGRES_PORT=5432 - POSTGRES_PORT=5432
- POSTGRES_HOST=localhost - POSTGRES_HOST=localhost
db: db:
image: postgres:15 image: postgres:16
ports: ports:
- "5432:5432" - "5432:5432"
environment: environment:

View File

@@ -19,7 +19,7 @@ version: '3'
services: services:
db: db:
image: postgres:15 image: postgres:16
volumes: volumes:
- ./data/postgres:/var/lib/postgresql/data - ./data/postgres:/var/lib/postgresql/data
ports: ports:

View File

@@ -367,6 +367,12 @@
"type": "shell", "type": "shell",
"command": "cd ${workspaceFolder}/packages/server && yarn tsc", "command": "cd ${workspaceFolder}/packages/server && yarn tsc",
"group": "build", "group": "build",
},
{
"label": "transpile-lib",
"type": "shell",
"command": "cd ${workspaceFolder}/packages/lib && yarn tsc",
"group": "build",
} }
] ]
}, },
@@ -395,6 +401,19 @@
"APP_BASE_URL": "http://joplincloud.local:22300", "APP_BASE_URL": "http://joplincloud.local:22300",
"API_BASE_URL": "http://api.joplincloud.local:22300", "API_BASE_URL": "http://api.joplincloud.local:22300",
} }
},
{
"type": "node",
"request": "launch",
"name": "lib: debug test file",
"preLaunchTask": "transpile-lib",
"program": "${workspaceFolder}/packages/lib/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"--config",
"packages/lib/jest.config.js",
],
"console": "integratedTerminal",
} }
] ]
} }

View File

@@ -35,7 +35,7 @@
], ],
"owner": "Laurent Cozic" "owner": "Laurent Cozic"
}, },
"version": "2.13.1", "version": "2.13.2",
"bin": "./main.js", "bin": "./main.js",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
@@ -73,7 +73,7 @@
"@joplin/tools": "~2.13", "@joplin/tools": "~2.13",
"@types/fs-extra": "11.0.3", "@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/node": "18.18.6", "@types/node": "18.18.7",
"@types/proper-lockfile": "^4.1.2", "@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2", "gulp": "4.0.2",
"jest": "29.7.0", "jest": "29.7.0",

View File

@@ -36,6 +36,10 @@ describe('HtmlToMd', () => {
htmlToMdOptions.preserveImageTagsWithSize = true; htmlToMdOptions.preserveImageTagsWithSize = true;
} }
if (htmlFilename.indexOf('preserve_nested_tables') === 0) {
htmlToMdOptions.preserveNestedTables = true;
}
const html = await readFile(htmlPath, 'utf8'); const html = await readFile(htmlPath, 'utf8');
let expectedMd = await readFile(mdPath, 'utf8'); let expectedMd = await readFile(mdPath, 'utf8');

View File

@@ -0,0 +1,19 @@
<body>
<table border="5px" bordercolor="#8707B0">
<tr>
<td>Left side of the main table</td>
<td>
<table border="5px" bordercolor="#F35557">
<h4 align="center">Nested Table</h4>
<tr>
<td>nested table C1</td>
<td>nested table C2</td>
</tr>
<tr>
<td>nested table</td>
<td>nested table</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1 @@
<table border="5px" bordercolor="#8707B0"><tbody><tr><td>Left side of the main table</td><td><h4 align="center">Nested Table</h4><table border="5px" bordercolor="#F35557"><tbody><tr><td>nested table C1</td><td>nested table C2</td></tr><tr><td>nested table</td><td>nested table</td></tr></tbody></table></td></tr></tbody></table>

View File

@@ -26,7 +26,7 @@ const { FoldersScreenUtils } = require('@joplin/lib/folders-screen-utils.js');
import Folder from '@joplin/lib/models/Folder'; import Folder from '@joplin/lib/models/Folder';
import Tag from '@joplin/lib/models/Tag'; import Tag from '@joplin/lib/models/Tag';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
const packageInfo = require('./packageInfo.js'); const packageInfo: PackageInfo = require('./packageInfo.js');
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker'; import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
import ClipperServer from '@joplin/lib/ClipperServer'; import ClipperServer from '@joplin/lib/ClipperServer';
const { webFrame } = require('electron'); const { webFrame } = require('electron');
@@ -68,6 +68,7 @@ import path = require('path');
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils'; import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher'; import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseNotesParent } from '@joplin/lib/reducer'; import { parseNotesParent } from '@joplin/lib/reducer';
import { PackageInfo } from '@joplin/lib/versionInfo';
const pluginClasses = [ const pluginClasses = [
require('./plugins/GotoAnything').default, require('./plugins/GotoAnything').default,

View File

@@ -5,8 +5,9 @@ import bridge from './services/bridge';
import KvStore from '@joplin/lib/services/KvStore'; import KvStore from '@joplin/lib/services/KvStore';
import * as ArrayUtils from '@joplin/lib/ArrayUtils'; import * as ArrayUtils from '@joplin/lib/ArrayUtils';
import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils'; import { CheckForUpdateOptions, extractVersionInfo, GitHubRelease } from './utils/checkForUpdatesUtils';
const packageInfo = require('./packageInfo.js'); import { PackageInfo } from '@joplin/lib/versionInfo';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
const packageInfo: PackageInfo = require('./packageInfo.js');
const logger = Logger.create('checkForUpdates'); const logger = Logger.create('checkForUpdates');

View File

@@ -92,7 +92,15 @@ export default function Sidebar(props: Props) {
function renderButton(section: any) { function renderButton(section: any) {
const selected = props.selection === section.name; const selected = props.selection === section.name;
return ( return (
<StyledListItem key={section.name} isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }}> <StyledListItem
key={section.name}
href='#'
role='tab'
aria-selected={selected}
isSubSection={Setting.isSubSection(section.name)}
selected={selected}
onClick={() => { props.onSelectionChange({ section: section }); }}
>
<StyledListItemIcon <StyledListItemIcon
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)} className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
/> />
@@ -123,7 +131,7 @@ export default function Sidebar(props: Props) {
} }
return ( return (
<StyledRoot> <StyledRoot role='tablist'>
{buttons} {buttons}
</StyledRoot> </StyledRoot>
); );

View File

@@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import versionInfo from '@joplin/lib/versionInfo'; import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
import PluginService, { Plugins } from '@joplin/lib/services/plugins/PluginService'; import PluginService, { Plugins } from '@joplin/lib/services/plugins/PluginService';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import restart from '../services/restart'; import restart from '../services/restart';
const packageInfo = require('../packageInfo.js'); const packageInfo: PackageInfo = require('../packageInfo.js');
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
interface ErrorInfo { interface ErrorInfo {

View File

@@ -8,7 +8,7 @@ import KeymapService from '@joplin/lib/services/KeymapService';
import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer'; import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins/reducer';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting'; import Setting from '@joplin/lib/models/Setting';
import versionInfo from '@joplin/lib/versionInfo'; import versionInfo, { PackageInfo } from '@joplin/lib/versionInfo';
import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl'; import makeDiscourseDebugUrl from '@joplin/lib/makeDiscourseDebugUrl';
import { ImportModule } from '@joplin/lib/services/interop/Module'; import { ImportModule } from '@joplin/lib/services/interop/Module';
import InteropServiceHelper from '../InteropServiceHelper'; import InteropServiceHelper from '../InteropServiceHelper';
@@ -25,7 +25,7 @@ import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService'; import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers'; import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
const packageInfo = require('../packageInfo.js'); const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron'); const { clipboard } = require('electron');
const Menu = bridge().Menu; const Menu = bridge().Menu;

View File

@@ -559,11 +559,21 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
const toolbarPluginButtons = pluginCommandNames.length ? ` | ${pluginCommandNames.join(' ')}` : ''; const toolbarPluginButtons = pluginCommandNames.length ? ` | ${pluginCommandNames.join(' ')}` : '';
// The toolbar is going to wrap based on groups of buttons
// (delimited by |). It means that if we leave large groups of
// buttons towards the end of the toolbar it's going to needlessly
// hide many buttons even when there is space. So this is why below,
// we create small groups of just one button towards the end.
const toolbar = [ const toolbar = [
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|', 'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|',
'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|', 'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|',
'bullist', 'numlist', 'joplinChecklist', '|', 'bullist', 'numlist', 'joplinChecklist', '|',
'h1', 'h2', 'h3', 'hr', 'blockquote', 'table', `joplinInsertDateTime${toolbarPluginButtons}`, 'h1', 'h2', 'h3', '|',
'hr', '|',
'blockquote', '|',
'table', '|',
`joplinInsertDateTime${toolbarPluginButtons}`,
]; ];
const editors = await (window as any).tinymce.init({ const editors = await (window as any).tinymce.init({

View File

@@ -9,7 +9,10 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) { if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) {
const htmlToMd = new HtmlToMd(); const htmlToMd = new HtmlToMd();
newBody = htmlToMd.parse(html, { preserveImageTagsWithSize: true }); newBody = htmlToMd.parse(html, {
preserveImageTagsWithSize: true,
preserveNestedTables: true,
});
newBody = await Note.replaceResourceExternalToInternalLinks(newBody, { useAbsolutePaths: true }); newBody = await Note.replaceResourceExternalToInternalLinks(newBody, { useAbsolutePaths: true });
} else { } else {
newBody = await Note.replaceResourceExternalToInternalLinks(html, { useAbsolutePaths: true }); newBody = await Note.replaceResourceExternalToInternalLinks(html, { useAbsolutePaths: true });

View File

@@ -5,6 +5,8 @@ import SettingsScreen from './models/SettingsScreen';
import { _electron as electron } from '@playwright/test'; import { _electron as electron } from '@playwright/test';
import { writeFile } from 'fs-extra'; import { writeFile } from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import createStartupArgs from './util/createStartupArgs';
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
test.describe('main', () => { test.describe('main', () => {
@@ -130,11 +132,9 @@ test.describe('main', () => {
// We need to write to the force-safe-mode file before opening the Electron app. // We need to write to the force-safe-mode file before opening the Electron app.
// Open the app ourselves: // Open the app ourselves:
const startupArgs = [ const startupArgs = createStartupArgs(profileDirectory);
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const electronApp = await electron.launch({ args: startupArgs }); const electronApp = await electron.launch({ args: startupArgs });
const mainWindow = await electronApp.firstWindow(); const mainWindow = await firstNonDevToolsWindow(electronApp);
const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart'); const safeModeDisableLink = mainWindow.getByText('Disable safe mode and restart');
await safeModeDisableLink.waitFor(); await safeModeDisableLink.waitFor();

View File

@@ -0,0 +1,9 @@
const createStartupArgs = (profileDirectory: string) => {
// We need to run with --env dev to disable the single instance check.
return [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
};
export default createStartupArgs;

View File

@@ -0,0 +1,43 @@
import { ElectronApplication, Page } from '@playwright/test';
const isDevTools = async (page: Page) => {
// It seems that the developer tools window can have titles in different
// formats (e.g. DevTools, Developer Tools).
return (await page.title()).match(/Dev(eloper)?\s*Tools/i);
};
const firstNonDevToolsWindow = async (electronApp: ElectronApplication) => {
// Wait for the window event as soon as possible -- it's possible that
// the window we want will be shown while doing other async checks.
const nextNonDevToolsPage = electronApp.waitForEvent('window', {
predicate: async page => {
return !(await isDevTools(page));
},
});
// First use firstWindow -- it's possible that the first window
// has already been shown.
let mainWindow = await electronApp.firstWindow();
if (await isDevTools(mainWindow)) {
for (const window of electronApp.windows()) {
if (!(await isDevTools(window))) {
mainWindow = window;
break;
}
}
if (await isDevTools(mainWindow)) {
mainWindow = await nextNonDevToolsPage;
}
}
// waitForEvent will throw if no additional windows are created.
// Ignore.
// eslint-disable-next-line promise/prefer-await-to-then
nextNonDevToolsPage.catch(_error => {});
return mainWindow;
};
export default firstNonDevToolsWindow;

View File

@@ -2,6 +2,8 @@ import { resolve, join, dirname } from 'path';
import { remove, mkdirp } from 'fs-extra'; import { remove, mkdirp } from 'fs-extra';
import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test'; import { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
import uuid from '@joplin/lib/uuid'; import uuid from '@joplin/lib/uuid';
import createStartupArgs from './createStartupArgs';
import firstNonDevToolsWindow from './firstNonDevToolsWindow';
@@ -32,9 +34,7 @@ export const test = base.extend<JoplinFixtures>({
}, },
electronApp: async ({ profileDirectory }, use) => { electronApp: async ({ profileDirectory }, use) => {
const startupArgs = [ const startupArgs = createStartupArgs(profileDirectory);
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const electronApp = await electron.launch({ args: startupArgs }); const electronApp = await electron.launch({ args: startupArgs });
await use(electronApp); await use(electronApp);
@@ -44,8 +44,8 @@ export const test = base.extend<JoplinFixtures>({
}, },
mainWindow: async ({ electronApp }, use) => { mainWindow: async ({ electronApp }, use) => {
const window = await electronApp.firstWindow(); const mainWindow = await firstNonDevToolsWindow(electronApp);
await use(window); await use(mainWindow);
}, },
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@joplin/app-desktop", "name": "@joplin/app-desktop",
"version": "2.13.5", "version": "2.13.7",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"private": true, "private": true,
@@ -120,8 +120,8 @@
"@playwright/test": "1.38.1", "@playwright/test": "1.38.1",
"@testing-library/react-hooks": "8.0.1", "@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/node": "18.18.6", "@types/node": "18.18.7",
"@types/react": "18.2.31", "@types/react": "18.2.33",
"@types/react-redux": "7.1.28", "@types/react-redux": "7.1.28",
"@types/styled-components": "5.1.29", "@types/styled-components": "5.1.29",
"electron": "26.5.0", "electron": "26.5.0",

View File

@@ -110,8 +110,8 @@ android {
applicationId "net.cozic.joplin" applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097726 versionCode 2097729
versionName "2.13.6" versionName "2.13.9"
ndk { ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
} }

View File

@@ -1,10 +1,12 @@
const React = require('react'); const React = require('react');
import { useState, useCallback, useMemo } from 'react'; import { useState, useCallback, useMemo } from 'react';
const Icon = require('react-native-vector-icons/Ionicons').default;
import { FAB, Portal } from 'react-native-paper'; import { FAB, Portal } from 'react-native-paper';
import { _ } from '@joplin/lib/locale'; import { _ } from '@joplin/lib/locale';
import { Dispatch } from 'redux';
const Icon = require('react-native-vector-icons/Ionicons').default;
// eslint-disable-next-line no-undef -- Don't know why it says React is undefined when it's defined above
type FABGroupProps = React.ComponentProps<typeof FAB.Group>;
type OnButtonPress = ()=> void; type OnButtonPress = ()=> void;
interface ButtonSpec { interface ButtonSpec {
@@ -19,6 +21,7 @@ interface ActionButtonProps {
// If not given, an "add" button will be used. // If not given, an "add" button will be used.
mainButton?: ButtonSpec; mainButton?: ButtonSpec;
dispatch: Dispatch;
} }
const defaultOnPress = () => {}; const defaultOnPress = () => {};
@@ -36,10 +39,12 @@ const useIcon = (iconName: string) => {
const ActionButton = (props: ActionButtonProps) => { const ActionButton = (props: ActionButtonProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const onMenuToggled = useCallback( const onMenuToggled: FABGroupProps['onStateChange'] = useCallback(state => {
(state: { open: boolean }) => setOpen(state.open) props.dispatch({
, [setOpen]); type: 'SIDE_MENU_CLOSE',
});
setOpen(state.open);
}, [setOpen, props.dispatch]);
const actions = useMemo(() => (props.buttons ?? []).map(button => { const actions = useMemo(() => (props.buttons ?? []).map(button => {
return { return {

View File

@@ -178,6 +178,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
onRequestClose={() => { onRequestClose={() => {
closeList(); closeList();
}} }}
supportedOrientations={['landscape', 'portrait']}
> >
<TouchableWithoutFeedback <TouchableWithoutFeedback
accessibilityElementsHidden={true} accessibilityElementsHidden={true}

View File

@@ -48,6 +48,8 @@ interface Props {
// See react-native-webview's prop with the same name. // See react-native-webview's prop with the same name.
mixedContentMode?: 'never' | 'always'; mixedContentMode?: 'never' | 'always';
allowFileAccessFromJs?: boolean;
// Initial javascript. Must evaluate to true. // Initial javascript. Must evaluate to true.
injectedJavaScript: string; injectedJavaScript: string;
@@ -143,6 +145,7 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
originWhitelist={['file://*', './*', 'http://*', 'https://*']} originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode={props.mixedContentMode} mixedContentMode={props.mixedContentMode}
allowFileAccess={true} allowFileAccess={true}
allowFileAccessFromFileURLs={props.allowFileAccessFromJs}
injectedJavaScript={props.injectedJavaScript} injectedJavaScript={props.injectedJavaScript}
onMessage={props.onMessage} onMessage={props.onMessage}
onError={props.onError} onError={props.onError}

View File

@@ -1,14 +1,14 @@
import { useRef, useCallback } from 'react'; import { useRef, useCallback } from 'react';
import useSource from './hooks/useSource'; import useSource from './hooks/useSource';
import useOnMessage, { HandleMessageCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage'; import useOnMessage, { HandleMessageCallback, HandleScrollCallback, OnMarkForDownloadCallback } from './hooks/useOnMessage';
import useOnResourceLongPress from './hooks/useOnResourceLongPress'; import useOnResourceLongPress from './hooks/useOnResourceLongPress';
const React = require('react'); const React = require('react');
import { View } from 'react-native'; import { View } from 'react-native';
import BackButtonDialogBox from '../BackButtonDialogBox'; import BackButtonDialogBox from '../BackButtonDialogBox';
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
import ExtendedWebView from '../ExtendedWebView'; import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
interface Props { interface Props {
themeId: number; themeId: number;
@@ -18,11 +18,13 @@ interface Props {
highlightedKeywords: string[]; highlightedKeywords: string[];
noteResources: any; noteResources: any;
paddingBottom: number; paddingBottom: number;
initialScroll: number|null;
noteHash: string; noteHash: string;
onJoplinLinkClick: HandleMessageCallback; onJoplinLinkClick: HandleMessageCallback;
onCheckboxChange?: HandleMessageCallback; onCheckboxChange?: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback; onRequestEditResource?: HandleMessageCallback;
onMarkForDownload?: OnMarkForDownloadCallback; onMarkForDownload?: OnMarkForDownloadCallback;
onScroll: HandleScrollCallback;
onLoadEnd?: ()=> void; onLoadEnd?: ()=> void;
} }
@@ -32,6 +34,7 @@ const webViewStyle = {
export default function NoteBodyViewer(props: Props) { export default function NoteBodyViewer(props: Props) {
const dialogBoxRef = useRef(null); const dialogBoxRef = useRef(null);
const webviewRef = useRef<WebViewControl>(null);
const { html, injectedJs } = useSource( const { html, injectedJs } = useSource(
props.noteBody, props.noteBody,
@@ -41,6 +44,7 @@ export default function NoteBodyViewer(props: Props) {
props.noteResources, props.noteResources,
props.paddingBottom, props.paddingBottom,
props.noteHash, props.noteHash,
props.initialScroll,
); );
const onResourceLongPress = useOnResourceLongPress( const onResourceLongPress = useOnResourceLongPress(
@@ -59,6 +63,7 @@ export default function NoteBodyViewer(props: Props) {
onJoplinLinkClick: props.onJoplinLinkClick, onJoplinLinkClick: props.onJoplinLinkClick,
onRequestEditResource: props.onRequestEditResource, onRequestEditResource: props.onRequestEditResource,
onResourceLongPress, onResourceLongPress,
onMainContainerScroll: props.onScroll,
}, },
); );
@@ -96,6 +101,7 @@ export default function NoteBodyViewer(props: Props) {
return ( return (
<View style={props.style}> <View style={props.style}>
<ExtendedWebView <ExtendedWebView
ref={webviewRef}
webviewInstanceId='NoteBodyViewer' webviewInstanceId='NoteBodyViewer'
themeId={props.themeId} themeId={props.themeId}
style={webViewStyle} style={webViewStyle}

View File

@@ -3,6 +3,7 @@ import shared from '@joplin/lib/components/shared/note-screen-shared';
export type HandleMessageCallback = (message: string)=> void; export type HandleMessageCallback = (message: string)=> void;
export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void; export type OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
export type HandleScrollCallback = (scrollTop: number)=> void;
interface MessageCallbacks { interface MessageCallbacks {
onMarkForDownload?: OnMarkForDownloadCallback; onMarkForDownload?: OnMarkForDownloadCallback;
@@ -10,6 +11,7 @@ interface MessageCallbacks {
onResourceLongPress: HandleMessageCallback; onResourceLongPress: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback; onRequestEditResource?: HandleMessageCallback;
onCheckboxChange: HandleMessageCallback; onCheckboxChange: HandleMessageCallback;
onMainContainerScroll: HandleScrollCallback;
} }
export default function useOnMessage( export default function useOnMessage(
@@ -24,6 +26,7 @@ export default function useOnMessage(
// Thus, useCallback should depend on each callback individually. // Thus, useCallback should depend on each callback individually.
const { const {
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick, onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
onMainContainerScroll,
} = callbacks; } = callbacks;
return useCallback((event: any) => { return useCallback((event: any) => {
@@ -35,10 +38,23 @@ export default function useOnMessage(
// https://github.com/laurent22/joplin/issues/4494 // https://github.com/laurent22/joplin/issues/4494
const msg = event.nativeEvent.data; const msg = event.nativeEvent.data;
// eslint-disable-next-line no-console const isScrollMessage = msg.startsWith('onscroll:');
console.info('Got IPC message: ', msg);
if (msg.indexOf('checkboxclick:') === 0) { // Scroll messages are very frequent so we avoid logging them.
if (!isScrollMessage) {
// eslint-disable-next-line no-console
console.info('Got IPC message: ', msg);
}
if (isScrollMessage) {
const eventData = JSON.parse(msg.substring(msg.indexOf(':') + 1));
if (typeof eventData.scrollTop !== 'number') {
throw new Error(`Invalid scroll message, ${msg}`);
}
onMainContainerScroll?.(eventData.scrollTop);
} else if (msg.indexOf('checkboxclick:') === 0) {
const newBody = shared.toggleCheckbox(msg, noteBody); const newBody = shared.toggleCheckbox(msg, noteBody);
onCheckboxChange?.(newBody); onCheckboxChange?.(newBody);
} else if (msg.indexOf('markForDownload:') === 0) { } else if (msg.indexOf('markForDownload:') === 0) {
@@ -63,5 +79,6 @@ export default function useOnMessage(
onJoplinLinkClick, onJoplinLinkClick,
onResourceLongPress, onResourceLongPress,
onRequestEditResource, onRequestEditResource,
onMainContainerScroll,
]); ]);
} }

View File

@@ -40,7 +40,16 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
return true; return true;
}; };
export default function useSource(noteBody: string, noteMarkupLanguage: number, themeId: number, highlightedKeywords: string[], noteResources: any, paddingBottom: number, noteHash: string): UseSourceResult { export default function useSource(
noteBody: string,
noteMarkupLanguage: number,
themeId: number,
highlightedKeywords: string[],
noteResources: any,
paddingBottom: number,
noteHash: string,
initialScroll: number|null,
): UseSourceResult {
const [html, setHtml] = useState<string>(''); const [html, setHtml] = useState<string>('');
const [injectedJs, setInjectedJs] = useState<string[]>([]); const [injectedJs, setInjectedJs] = useState<string[]>([]);
const [resourceLoadedTime, setResourceLoadedTime] = useState(0); const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
@@ -142,6 +151,12 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode'); const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
// On iOS, the root container has slow inertial scroll, which feels very different from
// the native scroll in other apps. This is not the case, however, when a child (e.g. a div)
// scrolls the content instead.
// Use a div to scroll on iOS instead of the main container:
const scrollRenderedMdContainer = shim.mobilePlatform() === 'ios';
const js = []; const js = [];
js.push('try {'); js.push('try {');
js.push(shim.injectedJs('webviewLib')); js.push(shim.injectedJs('webviewLib'));
@@ -149,15 +164,46 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
// the ReactNativeWebView actually supports only one, so the second arg is ignored (and currently not needed for the mobile app). // the ReactNativeWebView actually supports only one, so the second arg is ignored (and currently not needed for the mobile app).
js.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };'); js.push('window.joplinPostMessage_ = (msg, args) => { return window.ReactNativeWebView.postMessage(msg); };');
js.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });'); js.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
js.push(`
const scrollingElement =
${scrollRenderedMdContainer ? 'document.querySelector("#rendered-md")' : 'document.scrollingElement'};
let lastScrollTop;
const onMainContentScroll = () => {
const newScrollTop = scrollingElement.scrollTop;
if (lastScrollTop !== newScrollTop) {
const eventData = { scrollTop: newScrollTop };
window.ReactNativeWebView.postMessage('onscroll:' + JSON.stringify(eventData));
}
};
// Listen for events on both scrollingElement and window
// - On Android, scrollingElement.addEventListener('scroll', callback) doesn't call callback on
// scroll. However, window.addEventListener('scroll', callback) does.
// - iOS needs a listener to be added to scrollingElement -- events aren't received when
// the listener is added to window with window.addEventListener('scroll', ...).
scrollingElement.addEventListener('scroll', onMainContentScroll);
window.addEventListener('scroll', onMainContentScroll);
const scrollContentToPosition = (position) => {
scrollingElement.scrollTop = position;
};
`);
js.push(` js.push(`
const readyStateCheckInterval = setInterval(function() { const readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") { if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval); clearInterval(readyStateCheckInterval);
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload(); if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
const hash = "${noteHash}"; const hash = "${noteHash}";
// Gives it a bit of time before scrolling to the anchor const initialScroll = ${JSON.stringify(initialScroll)};
// so that images are loaded.
if (hash) { // Don't scroll to a hash if we're given initial scroll (initial scroll
// overrides scrolling to a hash).
if ((initialScroll ?? null) !== null) {
scrollContentToPosition(initialScroll);
} else if (hash) {
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.
setTimeout(() => { setTimeout(() => {
const e = document.getElementById(hash); const e = document.getElementById(hash);
if (!e) { if (!e) {
@@ -171,6 +217,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
}, 10); }, 10);
`); `);
js.push('} catch (e) {'); js.push('} catch (e) {');
js.push(' console.error(e);');
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))'); js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
js.push(' true;'); js.push(' true;');
js.push('}'); js.push('}');
@@ -186,10 +233,11 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
} }
} }
/* :root > body {
iOS seems to increase inertial scrolling friction when the WebView body/root elements padding: 0;
scroll. Scroll the main container instead. }
*/ `;
const scrollRenderedMdContainerCss = `
body > #rendered-md { body > #rendered-md {
width: 100vw; width: 100vw;
overflow: auto; overflow: auto;
@@ -197,10 +245,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
padding-bottom: ${paddingBottom}px; padding-bottom: ${paddingBottom}px;
padding-top: ${paddingTop}; padding-top: ${paddingTop};
} }
:root > body {
padding: 0;
}
`; `;
const defaultCss = ` const defaultCss = `
code { code {
@@ -219,6 +263,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
<style> <style>
${defaultCss} ${defaultCss}
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''} ${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
${scrollRenderedMdContainer ? scrollRenderedMdContainerCss : ''}
${editPopupCss} ${editPopupCss}
</style> </style>
${assetsToHeaders(result.pluginAssets, { asHtml: true })} ${assetsToHeaders(result.pluginAssets, { asHtml: true })}

View File

@@ -11,18 +11,17 @@ import { WebViewMessageEvent } from 'react-native-webview';
import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView'; import ExtendedWebView, { WebViewControl } from '../../ExtendedWebView';
import { clearAutosave, writeAutosave } from './autosave'; import { clearAutosave, writeAutosave } from './autosave';
import { LocalizedStrings } from './js-draw/types'; import { LocalizedStrings } from './js-draw/types';
import VersionInfo from 'react-native-version-info';
const logger = Logger.create('ImageEditor'); const logger = Logger.create('ImageEditor');
type OnSaveCallback = (svgData: string)=> void; type OnSaveCallback = (svgData: string)=> void;
type OnCancelCallback = ()=> void; type OnCancelCallback = ()=> void;
// Returns the empty string to load from a template.
type LoadInitialSVGCallback = ()=> Promise<string>;
interface Props { interface Props {
themeId: number; themeId: number;
loadInitialSVGData: LoadInitialSVGCallback; resourceFilename: string|null;
onSave: OnSaveCallback; onSave: OnSaveCallback;
onExit: OnCancelCallback; onExit: OnCancelCallback;
} }
@@ -166,10 +165,23 @@ const ImageEditor = (props: Props) => {
redo: _('Redo'), redo: _('Redo'),
}), []); }), []);
const appInfo = useMemo(() => {
return {
name: 'Joplin',
description: `v${VersionInfo.appVersion}`,
};
}, []);
const injectedJavaScript = useMemo(() => ` const injectedJavaScript = useMemo(() => `
window.onerror = (message, source, lineno) => { window.onerror = (message, source, lineno) => {
window.ReactNativeWebView.postMessage( window.ReactNativeWebView.postMessage(
"error: " + message + " in file://" + source + ", line " + lineno "error: " + message + " in file://" + source + ", line " + lineno,
);
};
window.onunhandledrejection = (error) => {
window.ReactNativeWebView.postMessage(
"error: " + error.reason,
); );
}; };
@@ -229,6 +241,7 @@ const ImageEditor = (props: Props) => {
${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))}, ${JSON.stringify(Setting.value('imageeditor.jsdrawToolbar'))},
${JSON.stringify(Setting.value('locale'))}, ${JSON.stringify(Setting.value('locale'))},
${JSON.stringify(localizedStrings)}, ${JSON.stringify(localizedStrings)},
${JSON.stringify({ appInfo })},
); );
// Start loading the SVG file (if present) after loading the editor. // Start loading the SVG file (if present) after loading the editor.
@@ -242,7 +255,7 @@ const ImageEditor = (props: Props) => {
); );
} }
true; true;
`, [localizedStrings]); `, [localizedStrings, appInfo]);
useEffect(() => { useEffect(() => {
webviewRef.current?.injectJS(` webviewRef.current?.injectJS(`
@@ -255,19 +268,17 @@ const ImageEditor = (props: Props) => {
}, [css]); }, [css]);
const onReadyToLoadData = useCallback(async () => { const onReadyToLoadData = useCallback(async () => {
const initialSVGData = await props.loadInitialSVGData?.() ?? '';
// It can take some time for initialSVGData to be transferred to the WebView. // It can take some time for initialSVGData to be transferred to the WebView.
// Thus, do so after the main content has been loaded. // Thus, do so after the main content has been loaded.
webviewRef.current.injectJS(`(async () => { webviewRef.current.injectJS(`(async () => {
if (window.editorControl) { if (window.editorControl) {
const initialSVGData = ${JSON.stringify(initialSVGData)}; const initialSVGPath = ${JSON.stringify(props.resourceFilename)};
const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))}; const initialTemplateData = ${JSON.stringify(Setting.value('imageeditor.imageTemplate'))};
editorControl.loadImageOrTemplate(initialSVGData, initialTemplateData); editorControl.loadImageOrTemplate(initialSVGPath, initialTemplateData);
} }
})();`); })();`);
}, [webviewRef, props.loadInitialSVGData]); }, [webviewRef, props.resourceFilename]);
const onMessage = useCallback(async (event: WebViewMessageEvent) => { const onMessage = useCallback(async (event: WebViewMessageEvent) => {
const data = event.nativeEvent.data; const data = event.nativeEvent.data;
@@ -306,6 +317,7 @@ const ImageEditor = (props: Props) => {
themeId={props.themeId} themeId={props.themeId}
html={html} html={html}
injectedJavaScript={injectedJavaScript} injectedJavaScript={injectedJavaScript}
allowFileAccessFromJs={true}
onMessage={onMessage} onMessage={onMessage}
onError={onError} onError={onError}
ref={webviewRef} ref={webviewRef}

View File

@@ -57,7 +57,7 @@ describe('createJsDrawEditor', () => {
}); });
// Load no image and an empty template so that autosave can start // Load no image and an empty template so that autosave can start
await editorControl.loadImageOrTemplate(undefined, '{}'); await editorControl.loadImageOrTemplate('', '{}');
expect(calledAutosaveCount).toBe(0); expect(calledAutosaveCount).toBe(0);

View File

@@ -120,20 +120,44 @@ export const createJsDrawEditor = (
editor.showLoadingWarning(0); editor.showLoadingWarning(0);
editor.setReadOnly(true); editor.setReadOnly(true);
const fetchInitialSvgData = (resourceUrl: string) => {
return new Promise<string>((resolve, reject) => {
if (!resourceUrl) {
resolve('');
}
// fetch seems to be unable to request file:// URLs.
// https://github.com/react-native-webview/react-native-webview/issues/1560#issuecomment-1783611805
const request = new XMLHttpRequest();
const onError = () => {
reject(`Failed to load initial SVG data: ${request.status}, ${request.statusText}, ${request.responseText}`);
};
request.addEventListener('load', _ => {
resolve(request.responseText);
});
request.addEventListener('error', onError);
request.addEventListener('abort', onError);
request.open('GET', resourceUrl);
request.send();
});
};
const editorControl = { const editorControl = {
editor, editor,
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => { loadImageOrTemplate: async (resourceUrl: string, templateData: string) => {
// loadFromSVG shows its own loading message. Hide the original. // loadFromSVG shows its own loading message. Hide the original.
editor.hideLoadingWarning(); editor.hideLoadingWarning();
if (svgData && svgData.length > 0) { const svgData = await fetchInitialSvgData(resourceUrl);
await editor.loadFromSVG(svgData);
} else {
await applyTemplateToEditor(editor, templateData);
// The editor expects to be saved initially (without // Load from a template if no initial data
// unsaved changes). Save now. if (svgData === '') {
saveNow(); await applyTemplateToEditor(editor, templateData);
} else {
await editor.loadFromSVG(svgData);
} }
// We can now edit and save safely (without data loss). // We can now edit and save safely (without data loss).

View File

@@ -40,9 +40,7 @@ interface ActionButtonProps {
onPress: Callback; onPress: Callback;
} }
const ActionButton = ( const ActionButton = (props: ActionButtonProps) => {
props: ActionButtonProps,
) => {
return ( return (
<CustomButton <CustomButton
themeId={props.themeId} themeId={props.themeId}

View File

@@ -70,6 +70,7 @@ interface ScreenHeaderProps {
onRedoButtonPress: OnPressCallback; onRedoButtonPress: OnPressCallback;
onSaveButtonPress: OnPressCallback; onSaveButtonPress: OnPressCallback;
sortButton_press?: OnPressCallback; sortButton_press?: OnPressCallback;
onSearchButtonPress?: OnPressCallback;
showSideMenuButton?: boolean; showSideMenuButton?: boolean;
showSearchButton?: boolean; showSearchButton?: boolean;
@@ -242,7 +243,11 @@ class ScreenHeaderComponent extends PureComponent<ScreenHeaderProps, ScreenHeade
} }
private searchButton_press() { private searchButton_press() {
void NavService.go('Search'); if (this.props.onSearchButtonPress) {
this.props.onSearchButtonPress();
} else {
void NavService.go('Search');
}
} }
private async duplicateButton_press() { private async duplicateButton_press() {

View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native'; import { Platform, Linking, View, Switch, ScrollView, Text, TouchableOpacity, Alert, PermissionsAndroid, Dimensions, AccessibilityInfo } from 'react-native';
import Setting, { AppType } from '@joplin/lib/models/Setting'; import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import NavService from '@joplin/lib/services/NavService'; import NavService from '@joplin/lib/services/NavService';
import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine'; import SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
import checkPermissions from '../../../utils/checkPermissions'; import checkPermissions from '../../../utils/checkPermissions';
@@ -18,21 +18,25 @@ import * as shared from '@joplin/lib/components/shared/config/config-shared';
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import biometricAuthenticate from '../../biometrics/biometricAuthenticate'; import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles'; import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton'; import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './NoteExportSection/NoteExportButton';
import SettingsButton from './SettingsButton'; import SettingsButton from './SettingsButton';
import Clipboard from '@react-native-community/clipboard'; import Clipboard from '@react-native-community/clipboard';
import { ReactNode } from 'react'; import { ReactElement, ReactNode } from 'react';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import SectionHeader from './SectionHeader'; import SectionHeader from './SectionHeader';
import ExportProfileButton from './NoteExportSection/ExportProfileButton'; import ExportProfileButton, { exportProfileButtonTitle } from './NoteExportSection/ExportProfileButton';
import SettingComponent from './SettingComponent'; import SettingComponent from './SettingComponent';
import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton'; import ExportDebugReportButton, { exportDebugReportTitle } from './NoteExportSection/ExportDebugReportButton';
import SectionSelector from './SectionSelector'; import SectionSelector from './SectionSelector';
import { TextInput } from 'react-native-paper';
interface ConfigScreenState { interface ConfigScreenState {
settings: any; settings: any;
changedSettingKeys: string[]; changedSettingKeys: string[];
searchQuery: string;
searching: boolean;
fixingSearchIndex: boolean; fixingSearchIndex: boolean;
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null; checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
showAdvancedSettings: boolean; showAdvancedSettings: boolean;
@@ -66,6 +70,8 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
selectedSectionName: null, selectedSectionName: null,
fixingSearchIndex: false, fixingSearchIndex: false,
sidebarWidth: 100, sidebarWidth: 100,
searchQuery: '',
searching: false,
}; };
this.scrollViewRef_ = React.createRef<ScrollView>(); this.scrollViewRef_ = React.createRef<ScrollView>();
@@ -115,10 +121,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
}; };
private manageProfilesButtonPress_ = () => { private manageProfilesButtonPress_ = () => {
this.props.dispatch({ void NavService.go('ProfileSwitcher');
type: 'NAV_GO',
routeName: 'ProfileSwitcher',
});
}; };
private fixSearchEngineIndexButtonPress_ = async () => { private fixSearchEngineIndexButtonPress_ = async () => {
@@ -131,6 +134,21 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
void NavService.go('Log'); void NavService.go('Log');
}; };
private setShowSearch_(searching: boolean) {
if (searching !== this.state.searching) {
this.setState({ searching });
AccessibilityInfo.announceForAccessibility(searching ? _('Search shown') : _('Search hidden'));
}
}
private onSearchButtonPress_ = () => {
this.setShowSearch_(!this.state.searching);
};
private onSearchUpdate_ = (newQuery: string) => {
this.setState({ searchQuery: newQuery });
};
private updateSidebarWidth = () => { private updateSidebarWidth = () => {
const windowWidth = Dimensions.get('window').width; const windowWidth = Dimensions.get('window').width;
@@ -153,10 +171,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return this.state.sidebarWidth > windowWidth / 2; return this.state.sidebarWidth > windowWidth / 2;
} }
private switchSectionPress_ = (section: string) => { private onJumpToSection_ = (section: string) => {
const label = Setting.sectionNameToLabel(section); const label = Setting.sectionNameToLabel(section);
AccessibilityInfo.announceForAccessibility(_('Opening section %s', label)); AccessibilityInfo.announceForAccessibility(_('Opening section %s', label));
this.setState({ selectedSectionName: section }); this.setState({
selectedSectionName: section,
searching: false,
});
}; };
private showSectionNavigation_ = () => { private showSectionNavigation_ = () => {
@@ -206,36 +227,65 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return 0; return 0;
} }
private hasUnsavedChanges() {
return this.state.changedSettingKeys.length > 0;
}
private promptSaveChanges(): Promise<void> {
return new Promise(resolve => {
if (this.hasUnsavedChanges()) {
const dialogTitle: string|null = null;
Alert.alert(
dialogTitle,
_('There are unsaved changes.'),
[{
text: _('Save changes'),
onPress: async () => {
await this.saveButton_press();
resolve();
},
},
{
text: _('Discard changes'),
onPress: () => resolve(),
}],
);
} else {
resolve();
}
});
}
private handleNavigateToNewScren = async (): Promise<boolean> => {
await this.promptSaveChanges();
// Continue navigation
return false;
};
private handleBackButtonPress = (): boolean => { private handleBackButtonPress = (): boolean => {
const goBack = async () => { const goBack = async () => {
BackButtonService.removeHandler(this.handleBackButtonPress); BackButtonService.removeHandler(this.handleBackButtonPress);
await BackButtonService.back(); await BackButtonService.back();
}; };
// Cancel search on back
if (this.state.searching) {
this.setShowSearch_(false);
return true;
}
// Show navigation when pressing "back" (unless always visible). // Show navigation when pressing "back" (unless always visible).
if (this.state.selectedSectionName && this.navigationFillsScreen()) { if (this.state.selectedSectionName && this.navigationFillsScreen()) {
this.showSectionNavigation_(); this.showSectionNavigation_();
return true; return true;
} }
if (this.state.changedSettingKeys.length > 0) { if (this.hasUnsavedChanges()) {
const dialogTitle: string|null = null; void (async () => {
Alert.alert( await this.promptSaveChanges();
dialogTitle, await goBack();
_('There are unsaved changes.'), })();
[{
text: _('Save changes'),
onPress: async () => {
await this.saveButton_press();
await goBack();
},
},
{
text: _('Discard changes'),
onPress: goBack,
}],
);
return true; return true;
} }
@@ -255,12 +305,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
} }
BackButtonService.addHandler(this.handleBackButtonPress); BackButtonService.addHandler(this.handleBackButtonPress);
NavService.addHandler(this.handleNavigateToNewScren);
Dimensions.addEventListener('change', this.updateSidebarWidth); Dimensions.addEventListener('change', this.updateSidebarWidth);
this.updateSidebarWidth(); this.updateSidebarWidth();
} }
public componentWillUnmount() { public componentWillUnmount() {
BackButtonService.removeHandler(this.handleBackButtonPress); BackButtonService.removeHandler(this.handleBackButtonPress);
NavService.removeHandler(this.handleNavigateToNewScren);
} }
private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) { private renderButton(key: string, title: string, clickHandler: ()=> void, options: any = null) {
@@ -276,10 +328,73 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
); );
} }
public sectionToComponent(key: string, section: any, settings: any, isSelected: boolean) { public sectionToComponent(key: string, section: SettingMetadataSection, settings: any, isSelected: boolean) {
const settingComps = []; const settingComps: ReactElement[] = [];
const headerTitle = Setting.sectionNameToLabel(section.name);
const matchesSearchQuery = (relatedText: string|string[]) => {
let searchThrough;
if (Array.isArray(relatedText)) {
searchThrough = relatedText.join('\n');
} else {
searchThrough = relatedText;
}
searchThrough = searchThrough.toLocaleLowerCase();
const searchQuery = this.state.searchQuery.toLocaleLowerCase().trim();
const hasSearchMatches =
headerTitle.toLocaleLowerCase() === searchQuery
|| searchThrough.includes(searchQuery);
// Don't show results when the search input is empty
return this.state.searchQuery.length > 0 && hasSearchMatches;
};
const addSettingComponent = (component: ReactElement, relatedText: string|string[]) => {
const hiddenBySearch = this.state.searching && !matchesSearchQuery(relatedText);
if (component && !hiddenBySearch) {
settingComps.push(component);
}
};
const addSettingButton = (key: string, title: string, clickHandler: ()=> void, options: any = null) => {
const relatedText = [title];
if (typeof options === 'object' && options?.description) {
relatedText.push(options.description);
}
addSettingComponent(this.renderButton(key, title, clickHandler, options), relatedText);
};
const styleSheet = this.styles().styleSheet; const styleSheet = this.styles().styleSheet;
const addSettingLink = (key: string, title: string, target: string) => {
const component = (
<View key={key} style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL(target);
}}
accessibilityRole='link'
>
<Text key="label" style={styleSheet.linkText}>
{title}
</Text>
</TouchableOpacity>
</View>
);
addSettingComponent(component, title);
};
const addSettingText = (key: string, text: string) => {
addSettingComponent(
<View key={key} style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{text}</Text>
</View>,
text,
);
};
for (let i = 0; i < section.metadatas.length; i++) { for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i]; const md = section.metadatas[i];
@@ -300,24 +415,29 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</View> </View>
); );
settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp })); addSettingButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp });
} }
} }
const settingComp = this.settingToComponent(md.key, settings[md.key]); const settingComp = this.settingToComponent(md.key, settings[md.key]);
settingComps.push(settingComp); const relatedText = [md.label?.() ?? '', md.description?.() ?? ''];
addSettingComponent(
settingComp,
relatedText,
);
} }
if (section.name === 'sync') { if (section.name === 'sync') {
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_)); addSettingButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_);
} }
if (section.name === 'joplinCloud') { if (section.name === 'joplinCloud') {
const label = _('Email to note');
const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook'); const description = _('Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook');
settingComps.push( addSettingComponent(
<View key="joplinCloud"> <View key="joplinCloud">
<View style={this.styles().styleSheet.settingContainerNoBottomBorder}> <View style={this.styles().styleSheet.settingContainerNoBottomBorder}>
<Text style={this.styles().styleSheet.settingText}>{_('Email to note')}</Text> <Text style={this.styles().styleSheet.settingText}>{label}</Text>
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text> <Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
</View> </View>
{ {
@@ -329,20 +449,30 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
) )
} }
</View>, </View>,
[label, description],
); );
} }
if (section.name === 'tools') { if (section.name === 'tools') {
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_)); addSettingButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_);
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_)); addSettingButton('status_button', _('Sync Status'), this.syncStatusButtonPress_);
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_)); addSettingButton('log_button', _('Log'), this.logButtonPress_);
settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') })); addSettingButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') });
} }
if (section.name === 'export') { if (section.name === 'export') {
settingComps.push(<NoteExportButton key='export_as_jex_button' styles={this.styles()} />); addSettingComponent(
settingComps.push(<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>); <NoteExportButton key='export_as_jex_button' styles={this.styles()} />,
settingComps.push(<ExportProfileButton key='export_data' styles={this.styles()}/>); [exportButtonTitle(), exportButtonDescription()],
);
addSettingComponent(
<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>,
exportDebugReportTitle(),
);
addSettingComponent(
<ExportProfileButton key='export_data' styles={this.styles()}/>,
exportProfileButtonTitle(),
);
} }
if (section.name === 'moreInfo') { if (section.name === 'moreInfo') {
@@ -350,7 +480,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually // Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again // set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
settingComps.push( addSettingComponent(
<View key="permission_info" style={styleSheet.settingContainer}> <View key="permission_info" style={styleSheet.settingContainer}>
<View key="permission_info_wrapper"> <View key="permission_info_wrapper">
<Text key="perm1a" style={styleSheet.settingText}> <Text key="perm1a" style={styleSheet.settingText}>
@@ -367,95 +497,60 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</Text> </Text>
</View> </View>
</View>, </View>,
'',
); );
} }
settingComps.push( addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
<View key="donate_link" style={styleSheet.settingContainer}> addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
<TouchableOpacity addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Make a donation')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push( addSettingText('version_info_app', `Joplin ${VersionInfo.appVersion}`);
<View key="website_link" style={styleSheet.settingContainer}> addSettingText('version_info_db', _('Database v%s', reg.db().version()));
<TouchableOpacity addSettingText('version_info_fts', _('FTS enabled: %d', this.props.settings['db.ftsEnabled']));
onPress={() => { addSettingText('version_info_hermes', _('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0));
void Linking.openURL('https://joplinapp.org/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Joplin website')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="privacy_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/privacy/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Privacy Policy')}
</Text>
</TouchableOpacity>
</View>,
);
settingComps.push(
<View key="version_info_app" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
</View>,
);
settingComps.push(
<View key="version_info_db" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Database v%s', reg.db().version())}</Text>
</View>,
);
settingComps.push(
<View key="version_info_fts" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
</View>,
);
settingComps.push(
<View key="version_info_hermes" style={styleSheet.settingContainer}>
<Text style={styleSheet.settingText}>{_('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0)}</Text>
</View>,
);
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile); const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) { if (featureFlagKeys.length) {
const headerKey = 'featureFlags'; const headerKey = 'featureFlags';
settingComps.push(<SectionHeader const featureFlagsTitle = _('Feature flags');
key={headerKey} addSettingComponent(
styles={this.styles().styleSheet} <SectionHeader
title={_('Feature flags')} key={headerKey}
onLayout={event => this.onHeaderLayout(headerKey, event)} styles={this.styles().styleSheet}
/>); title={featureFlagsTitle}
onLayout={event => this.onHeaderLayout(headerKey, event)}
/>,
_('Feature flags'),
);
settingComps.push(<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>); addSettingComponent(
<View key="featureFlagsContainer">{this.renderFeatureFlags(settings, featureFlagKeys)}</View>,
featureFlagsTitle,
);
} }
} }
if (!settingComps.length) return null; if (!settingComps.length) return null;
if (!isSelected) return null; if (!isSelected && !this.state.searching) return null;
const headerComponent = (
<TouchableOpacity onPress={() => {
this.onJumpToSection_(section.name);
}}>
<SectionHeader
styles={styleSheet}
title={headerTitle}
/>
</TouchableOpacity>
);
return ( return (
<View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}> <View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}>
<View>{settingComps}</View> <View>
{this.state.searching ? headerComponent : null}
{settingComps}
</View>
</View> </View>
); );
} }
@@ -541,18 +636,22 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
currentSectionName = 'general'; currentSectionName = 'general';
} }
if (this.state.searching) {
currentSectionName = null;
}
const sectionSelector = ( const sectionSelector = (
<SectionSelector <SectionSelector
selectedSectionName={currentSectionName} selectedSectionName={currentSectionName}
styles={this.styles()} styles={this.styles()}
settings={settings} settings={settings}
openSection={this.switchSectionPress_} openSection={this.onJumpToSection_}
width={this.state.sidebarWidth} width={this.state.sidebarWidth}
/> />
); );
let currentSection: ReactNode; let currentSection: ReactNode;
if (currentSectionName) { if (currentSectionName || this.state.searching) {
const settingComps = shared.settingsToComponents2( const settingComps = shared.settingsToComponents2(
this, AppType.Mobile, settings, currentSectionName, this, AppType.Mobile, settings, currentSectionName,
@@ -560,11 +659,20 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// of React in lib/ and app-mobile/ // of React in lib/ and app-mobile/
) as ReactNode[]; ) as ReactNode[];
const searchInput = <TextInput
value={this.state.searchQuery}
label={_('Search')}
placeholder={_('Search...')}
onChangeText={this.onSearchUpdate_}
autoFocus={true}
/>;
currentSection = ( currentSection = (
<ScrollView <ScrollView
ref={this.scrollViewRef_} ref={this.scrollViewRef_}
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
> >
{this.state.searching ? searchInput : null}
{settingComps} {settingComps}
</ScrollView> </ScrollView>
); );
@@ -598,10 +706,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
<ScreenHeader <ScreenHeader
title={screenHeadingText} title={screenHeadingText}
showSaveButton={true} showSaveButton={true}
showSearchButton={false} showSearchButton={true}
showSideMenuButton={false} showSideMenuButton={false}
saveButtonDisabled={!this.state.changedSettingKeys.length} saveButtonDisabled={!this.hasUnsavedChanges()}
onSaveButtonPress={this.saveButton_press} onSaveButtonPress={this.saveButton_press}
onSearchButtonPress={this.onSearchButtonPress_}
/> />
{mainComponent} {mainComponent}
</View> </View>

View File

@@ -11,6 +11,8 @@ interface Props {
styles: ConfigScreenStyles; styles: ConfigScreenStyles;
} }
export const exportDebugReportTitle = () => _('Export Debug Report');
const ExportDebugReportButton = (props: Props) => { const ExportDebugReportButton = (props: Props) => {
const [creatingReport, setCreatingReport] = useState(false); const [creatingReport, setCreatingReport] = useState(false);
@@ -24,7 +26,7 @@ const ExportDebugReportButton = (props: Props) => {
const exportDebugReportButton = ( const exportDebugReportButton = (
<SettingsButton <SettingsButton
title={creatingReport ? _('Creating report...') : _('Export Debug Report')} title={creatingReport ? _('Creating report...') : exportDebugReportTitle()}
clickHandler={exportDebugButtonPress} clickHandler={exportDebugButtonPress}
styles={props.styles} styles={props.styles}
disabled={creatingReport} disabled={creatingReport}

View File

@@ -13,6 +13,8 @@ interface Props {
styles: ConfigScreenStyles; styles: ConfigScreenStyles;
} }
export const exportProfileButtonTitle = () => _('Export profile');
const ExportProfileButton = (props: Props) => { const ExportProfileButton = (props: Props) => {
const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle'); const [profileExportStatus, setProfileExportStatus] = useState<'idle'|'prompt'|'exporting'>('idle');
const [profileExportPath, setProfileExportPath] = useState<string>(''); const [profileExportPath, setProfileExportPath] = useState<string>('');
@@ -31,7 +33,7 @@ const ExportProfileButton = (props: Props) => {
const exportProfileButton = ( const exportProfileButton = (
<SettingsButton <SettingsButton
styles={props.styles} styles={props.styles}
title={profileExportStatus === 'exporting' ? _('Exporting profile...') : _('Export profile')} title={profileExportStatus === 'exporting' ? _('Exporting profile...') : exportProfileButtonTitle()}
clickHandler={exportProfileButtonPress} clickHandler={exportProfileButtonPress}
description={_('For debugging purpose only: export your profile to an external SD card.')} description={_('For debugging purpose only: export your profile to an external SD card.')}
disabled={profileExportStatus === 'exporting'} disabled={profileExportStatus === 'exporting'}

View File

@@ -24,6 +24,9 @@ enum ExportStatus {
Exported, Exported,
} }
export const exportButtonTitle = () => _('Export all notes as JEX');
export const exportButtonDescription = () => _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const NoteExportButton: FunctionComponent<Props> = props => { const NoteExportButton: FunctionComponent<Props> = props => {
const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted); const [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
const [exportProgress, setExportProgress] = useState<number|undefined>(0); const [exportProgress, setExportProgress] = useState<number|undefined>(0);
@@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent<Props> = props => {
indeterminate={exportProgress === undefined} indeterminate={exportProgress === undefined}
progress={exportProgress}/> progress={exportProgress}/>
); );
const descriptionText = _('Share a copy of all notes in a file format that can be imported by Joplin on a computer.');
const startOrCancelExportButton = ( const startOrCancelExportButton = (
<SettingsButton <SettingsButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')} title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
disabled={exportStatus === ExportStatus.Exporting} disabled={exportStatus === ExportStatus.Exporting}
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null} description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
statusComponent={progressComponent} statusComponent={progressComponent}
clickHandler={startExport} clickHandler={startExport}
styles={props.styles} styles={props.styles}

View File

@@ -3,7 +3,7 @@ import * as React from 'react';
import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting'; import Setting, { AppType, SettingMetadataSection } from '@joplin/lib/models/Setting';
import { FunctionComponent, useEffect, useMemo, useState } from 'react'; import { FunctionComponent, useEffect, useMemo, useState } from 'react';
import { ConfigScreenStyles } from './configScreenStyles'; import { ConfigScreenStyles } from './configScreenStyles';
import { FlatList, Text, Pressable, View } from 'react-native'; import { FlatList, Text, Pressable, View, ViewStyle } from 'react-native';
import { settingsSections } from '@joplin/lib/components/shared/config/config-shared'; import { settingsSections } from '@joplin/lib/components/shared/config/config-shared';
import Icon from '../../Icon'; import Icon from '../../Icon';
@@ -53,7 +53,7 @@ const SectionSelector: FunctionComponent<Props> = props => {
</Text> </Text>
<Text <Text
style={styles.sidebarButtonDescriptionText} style={styles.sidebarButtonDescriptionText}
numberOfLines={2} numberOfLines={1}
ellipsizeMode='tail' ellipsizeMode='tail'
> >
{shortDescription ?? ''} {shortDescription ?? ''}
@@ -82,8 +82,15 @@ const SectionSelector: FunctionComponent<Props> = props => {
} }
}, [props.selectedSectionName, flatListRef, sections]); }, [props.selectedSectionName, flatListRef, sections]);
const containerStyle: ViewStyle = useMemo(() => ({
width: props.width,
maxWidth: props.width,
minWidth: props.width,
flex: 1,
}), [props.width]);
return ( return (
<View style={{ width: props.width, flexDirection: 'column' }}> <View style={containerStyle}>
<FlatList <FlatList
role='tablist' role='tablist'
ref={setFlatListRef} ref={setFlatListRef}

View File

@@ -86,11 +86,13 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
fontSize: theme.fontSize, fontSize: theme.fontSize,
}; };
const fadedOpacity = 0.75;
const sidebarButtonDescriptionText: TextStyle = { const sidebarButtonDescriptionText: TextStyle = {
...sidebarButtonMainText, ...sidebarButtonMainText,
fontSize: theme.fontSizeSmaller, fontSize: theme.fontSizeSmaller,
color: theme.color, color: theme.color,
opacity: 0.75, opacity: fadedOpacity,
paddingTop: 3,
}; };
@@ -185,6 +187,7 @@ const configScreenStyles = (themeId: number): ConfigScreenStyles => {
textAlign: 'center', textAlign: 'center',
fontSize: 18, fontSize: 18,
width: sidebarButtonHeight * 0.8, width: sidebarButtonHeight * 0.8,
opacity: fadedOpacity,
}, },
sidebarSelectedButtonText: { sidebarSelectedButtonText: {
...sidebarButtonMainText, ...sidebarButtonMainText,

View File

@@ -61,6 +61,9 @@ const emptyArray: any[] = [];
const logger = Logger.create('screens/Note'); const logger = Logger.create('screens/Note');
class NoteScreenComponent extends BaseScreenComponent { class NoteScreenComponent extends BaseScreenComponent {
// This isn't in this.state because we don't want changing scroll to trigger
// a re-render.
private lastBodyScroll: number|undefined = undefined;
public static navigationOptions(): any { public static navigationOptions(): any {
return { header: null }; return { header: null };
@@ -81,7 +84,6 @@ class NoteScreenComponent extends BaseScreenComponent {
fromShare: false, fromShare: false,
showCamera: false, showCamera: false,
showImageEditor: false, showImageEditor: false,
loadImageEditorData: null,
imageEditorResource: null, imageEditorResource: null,
noteResources: {}, noteResources: {},
@@ -747,7 +749,11 @@ class NoteScreenComponent extends BaseScreenComponent {
if (this.useEditorBeta()) { if (this.useEditorBeta()) {
// The beta editor needs to be explicitly informed of changes // The beta editor needs to be explicitly informed of changes
// to the note's body // to the note's body
this.editorRef.current.insertText(newText); if (this.editorRef.current) {
this.editorRef.current.insertText(newText);
} else {
logger.error(`Tried to attach resource ${resource.id} to the note when the editor is not visible!`);
}
} }
} else { } else {
newNote.body += `\n${resourceTag}`; newNote.body += `\n${resourceTag}`;
@@ -812,31 +818,34 @@ class NoteScreenComponent extends BaseScreenComponent {
}, 'image'); }, 'image');
} }
private drawPicture_onPress = async () => {
// Create a new empty drawing and attach it now.
const resource = await this.attachNewDrawing('');
await this.editDrawing(resource);
};
private async updateDrawing(svgData: string) { private async updateDrawing(svgData: string) {
let resource: ResourceEntity|null = this.state.imageEditorResource; let resource: ResourceEntity|null = this.state.imageEditorResource;
if (!resource) { if (!resource) {
throw new Error('No resource is loaded in the editor'); resource = await this.attachNewDrawing(svgData);
// Set resouce and file path to allow
// 1. subsequent saves to update the resource
// 2. the editor to load from the resource's filepath (can happen
// if the webview is reloaded).
this.setState({
imageEditorResourceFilepath: Resource.fullPath(resource),
imageEditorResource: resource,
});
} else {
logger.info('Saving drawing to resource', resource.id);
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
resource = await Resource.updateResourceBlobContent(
resource.id,
tempFilePath,
);
await shim.fsDriver().remove(tempFilePath);
await this.refreshResource(resource);
} }
logger.info('Saving drawing to resource', resource.id);
const tempFilePath = join(Setting.value('tempDir'), uuid.createNano());
await shim.fsDriver().writeFile(tempFilePath, svgData, 'utf8');
resource = await Resource.updateResourceBlobContent(
resource.id,
tempFilePath,
);
await shim.fsDriver().remove(tempFilePath);
await this.refreshResource(resource);
} }
private onSaveDrawing = async (svgData: string) => { private onSaveDrawing = async (svgData: string) => {
@@ -847,13 +856,28 @@ class NoteScreenComponent extends BaseScreenComponent {
this.setState({ showImageEditor: false }); this.setState({ showImageEditor: false });
}; };
private drawPicture_onPress = async () => {
if (this.state.mode === 'edit') {
// Create a new empty drawing and attach it now, before the image editor is opened.
// With the present structure of Note.tsx, the we can't use this.editorRef while
// the image editor is open, and thus can't attach drawings at the cursor locaiton.
const resource = await this.attachNewDrawing('');
await this.editDrawing(resource);
} else {
logger.info('Showing image editor...');
this.setState({
showImageEditor: true,
imageEditorResourceFilepath: null,
imageEditorResource: null,
});
}
};
private async editDrawing(item: BaseItem) { private async editDrawing(item: BaseItem) {
const filePath = Resource.fullPath(item); const filePath = Resource.fullPath(item);
this.setState({ this.setState({
showImageEditor: true, showImageEditor: true,
loadImageEditorData: async () => { imageEditorResourceFilepath: filePath,
return await shim.fsDriver().readFile(filePath);
},
imageEditorResource: item, imageEditorResource: item,
}); });
} }
@@ -1260,6 +1284,10 @@ class NoteScreenComponent extends BaseScreenComponent {
}, 5); }, 5);
} }
private onBodyViewerScroll = (scrollTop: number) => {
this.lastBodyScroll = scrollTop;
};
public onBodyViewerCheckboxChange(newBody: string) { public onBodyViewerCheckboxChange(newBody: string) {
void this.saveOneProperty('body', newBody); void this.saveOneProperty('body', newBody);
} }
@@ -1302,7 +1330,7 @@ class NoteScreenComponent extends BaseScreenComponent {
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />; return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
} else if (this.state.showImageEditor) { } else if (this.state.showImageEditor) {
return <ImageEditor return <ImageEditor
loadInitialSVGData={this.state.loadImageEditorData} resourceFilename={this.state.imageEditorResourceFilepath}
themeId={this.props.themeId} themeId={this.props.themeId}
onSave={this.onSaveDrawing} onSave={this.onSaveDrawing}
onExit={this.onCloseDrawing} onExit={this.onCloseDrawing}
@@ -1334,6 +1362,8 @@ class NoteScreenComponent extends BaseScreenComponent {
onMarkForDownload={this.onMarkForDownload} onMarkForDownload={this.onMarkForDownload}
onRequestEditResource={this.onEditResource} onRequestEditResource={this.onEditResource}
onLoadEnd={this.onBodyViewerLoadEnd} onLoadEnd={this.onBodyViewerLoadEnd}
onScroll={this.onBodyViewerScroll}
initialScroll={this.lastBodyScroll}
/> />
); );
} else { } else {
@@ -1416,7 +1446,7 @@ class NoteScreenComponent extends BaseScreenComponent {
if (this.state.mode === 'edit') return null; if (this.state.mode === 'edit') return null;
return <ActionButton mainButton={editButton} />; return <ActionButton mainButton={editButton} dispatch={this.props.dispatch} />;
}; };
// Save button is not really needed anymore with the improved save logic // Save button is not really needed anymore with the improved save logic

View File

@@ -16,6 +16,7 @@ const DialogBox = require('react-native-dialogbox').default;
const { BaseScreenComponent } = require('../base-screen'); const { BaseScreenComponent } = require('../base-screen');
const { BackButtonService } = require('../../services/back-button.js'); const { BackButtonService } = require('../../services/back-button.js');
import { AppState } from '../../utils/types'; import { AppState } from '../../utils/types';
const { ALL_NOTES_FILTER_ID } = require('@joplin/lib/reserved-ids.js');
class NotesScreenComponent extends BaseScreenComponent<any> { class NotesScreenComponent extends BaseScreenComponent<any> {
@@ -108,7 +109,7 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
} }
public async componentDidUpdate(prevProps: any) { public async componentDidUpdate(prevProps: any) {
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType) { if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId !== this.props.selectedFolderId || prevProps.selectedTagId !== this.props.selectedTagId || prevProps.selectedSmartFilterId !== this.props.selectedSmartFilterId || prevProps.notesParentType !== this.props.notesParentType || prevProps.uncompletedTodosOnTop !== this.props.uncompletedTodosOnTop || prevProps.showCompletedTodos !== this.props.showCompletedTodos) {
await this.refreshNotes(this.props); await this.refreshNotes(this.props);
} }
} }
@@ -223,17 +224,32 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null; let buttonFolderId = this.props.selectedFolderId !== Folder.conflictFolderId() ? this.props.selectedFolderId : null;
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId; if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
const addFolderNoteButtons = !!buttonFolderId; const isAllNotes =
this.props.notesParentType === 'SmartFilter'
&& this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
// Usually, when showing all notes, activeFolderId/selectedFolderId is set to the last
// active folder.
// If the app starts showing all notes, activeFolderId/selectedFolderId are
// empty or null. As such, we need a special case to show the buttons:
const addFolderNoteButtons = !!buttonFolderId || isAllNotes;
const thisComp = this; const thisComp = this;
const makeActionButtonComp = () => { const makeActionButtonComp = () => {
const getTargetFolderId = async () => {
if (!buttonFolderId && isAllNotes) {
return (await Folder.defaultFolder()).id;
}
return buttonFolderId;
};
if (addFolderNoteButtons && this.props.folders.length > 0) { if (addFolderNoteButtons && this.props.folders.length > 0) {
const buttons = []; const buttons = [];
buttons.push({ buttons.push({
label: _('New to-do'), label: _('New to-do'),
onPress: () => { onPress: async () => {
const folderId = await getTargetFolderId();
const isTodo = true; const isTodo = true;
void this.newNoteNavigate(buttonFolderId, isTodo); void this.newNoteNavigate(folderId, isTodo);
}, },
color: '#9b59b6', color: '#9b59b6',
icon: 'checkbox-outline', icon: 'checkbox-outline',
@@ -241,14 +257,15 @@ class NotesScreenComponent extends BaseScreenComponent<any> {
buttons.push({ buttons.push({
label: _('New note'), label: _('New note'),
onPress: () => { onPress: async () => {
const folderId = await getTargetFolderId();
const isTodo = false; const isTodo = false;
void this.newNoteNavigate(buttonFolderId, isTodo); void this.newNoteNavigate(folderId, isTodo);
}, },
color: '#9b59b6', color: '#9b59b6',
icon: 'document', icon: 'document',
}); });
return <ActionButton buttons={buttons}/>; return <ActionButton buttons={buttons} dispatch={this.props.dispatch}/>;
} }
return null; return null;
}; };

View File

@@ -1,17 +1,13 @@
const gulp = require('gulp'); const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils'); const utils = require('@joplin/tools/gulp/utils');
import { buildInjectedJS, watchInjectedJS } from './tools/buildInjectedJs';
import gulpTasks from './tools/buildInjectedJs/gulpTasks';
const tasks = { const tasks = {
encodeAssets: { encodeAssets: {
fn: require('./tools/encodeAssets'), fn: require('./tools/encodeAssets'),
}, },
buildInjectedJs: { ...gulpTasks,
fn: buildInjectedJS,
},
watchInjectedJs: {
fn: watchInjectedJS,
},
podInstall: { podInstall: {
fn: require('./tools/podInstall'), fn: require('./tools/podInstall'),
}, },
@@ -19,6 +15,22 @@ const tasks = {
utils.registerGulpTasks(gulp, tasks); utils.registerGulpTasks(gulp, tasks);
gulp.task('buildInjectedJs', gulp.series(
'beforeBundle',
'buildCodeMirrorEditor',
'buildJsDrawEditor',
'copyWebviewLib',
));
gulp.task('watchInjectedJs', gulp.series(
'beforeBundle',
'copyWebviewLib',
gulp.parallel(
'watchCodeMirrorEditor',
'watchJsDrawEditor',
),
));
gulp.task('build', gulp.series( gulp.task('build', gulp.series(
'buildInjectedJs', 'buildInjectedJs',
'encodeAssets', 'encodeAssets',

View File

@@ -523,13 +523,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 101; CURRENT_PROJECT_VERSION = 106;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist; INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.13.4; MARKETING_VERSION = 12.13.9;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@@ -552,12 +552,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements; CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
CURRENT_PROJECT_VERSION = 101; CURRENT_PROJECT_VERSION = 106;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist; INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 12.13.4; MARKETING_VERSION = 12.13.9;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@@ -704,14 +704,14 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 101; CURRENT_PROJECT_VERSION = 106;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.13.4; MARKETING_VERSION = 12.13.9;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
@@ -735,14 +735,14 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 101; CURRENT_PROJECT_VERSION = 106;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = A9BXAFS6CT; DEVELOPMENT_TEAM = A9BXAFS6CT;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = ShareExtension/Info.plist; INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
MARKETING_VERSION = 12.13.4; MARKETING_VERSION = 12.13.9;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -359,9 +359,9 @@ PODS:
- React-Core - React-Core
- react-native-rsa-native (2.0.5): - react-native-rsa-native (2.0.5):
- React - React
- react-native-saf-x (2.13.0): - react-native-saf-x (2.13.3):
- React-Core - React-Core
- react-native-safe-area-context (4.7.2): - react-native-safe-area-context (4.7.4):
- React-Core - React-Core
- react-native-slider (4.4.3): - react-native-slider (4.4.3):
- React-Core - React-Core
@@ -479,7 +479,8 @@ PODS:
- React - React
- RNShare (9.4.1): - RNShare (9.4.1):
- React-Core - React-Core
- RNVectorIcons (10.0.0): - RNVectorIcons (10.0.1):
- RCT-Folly (= 2021.07.22.00)
- React-Core - React-Core
- RNZipArchive (6.1.0): - RNZipArchive (6.1.0):
- React-Core - React-Core
@@ -786,8 +787,8 @@ SPEC CHECKSUMS:
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5 react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
react-native-saf-x: b758d1b38a96a96b8179a16e96ed966a507ca5c2 react-native-saf-x: 0f7531c9f8bdbb62bbd55ceb7433de7bb756cd73
react-native-safe-area-context: 7aa8e6d9d0f3100a820efb1a98af68aa747f9284 react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4 react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261 react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9 react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
@@ -817,7 +818,7 @@ SPEC CHECKSUMS:
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93 RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6 RNShare: 32e97adc8d8c97d4a26bcdd3c45516882184f8b6
RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9 RNVectorIcons: ace237de89f1574ef3c963ae9d5da3bd6fbeb02a
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801 RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
@@ -826,4 +827,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 3b2cace838120977b5b54871752c9dddf5a11cea PODFILE CHECKSUM: 3b2cace838120977b5b54871752c9dddf5a11cea
COCOAPODS: 1.14.2 COCOAPODS: 1.12.1

View File

@@ -62,13 +62,13 @@
"react-native-popup-menu": "0.16.1", "react-native-popup-menu": "0.16.1",
"react-native-quick-actions": "0.3.13", "react-native-quick-actions": "0.3.13",
"react-native-rsa-native": "2.0.5", "react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "4.7.2", "react-native-safe-area-context": "4.7.4",
"react-native-securerandom": "1.0.1", "react-native-securerandom": "1.0.1",
"react-native-share": "9.4.1", "react-native-share": "9.4.1",
"react-native-side-menu-updated": "1.3.2", "react-native-side-menu-updated": "1.3.2",
"react-native-sqlite-storage": "6.0.1", "react-native-sqlite-storage": "6.0.1",
"react-native-url-polyfill": "2.0.0", "react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.0.0", "react-native-vector-icons": "10.0.1",
"react-native-version-info": "1.1.1", "react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12", "react-native-vosk": "0.1.12",
"react-native-webview": "13.6.2", "react-native-webview": "13.6.2",
@@ -88,14 +88,14 @@
"@babel/preset-env": "7.20.2", "@babel/preset-env": "7.20.2",
"@babel/runtime": "7.20.0", "@babel/runtime": "7.20.0",
"@joplin/tools": "~2.13", "@joplin/tools": "~2.13",
"@js-draw/material-icons": "1.5.0", "@js-draw/material-icons": "1.13.2",
"@lezer/highlight": "1.1.4", "@lezer/highlight": "1.1.4",
"@testing-library/jest-native": "5.4.3", "@testing-library/jest-native": "5.4.3",
"@testing-library/react-native": "12.3.0", "@testing-library/react-native": "12.3.1",
"@tsconfig/react-native": "2.0.2", "@tsconfig/react-native": "2.0.2",
"@types/fs-extra": "11.0.3", "@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/react": "18.2.31", "@types/react": "18.2.33",
"@types/react-native": "0.70.6", "@types/react-native": "0.70.6",
"@types/react-redux": "7.1.28", "@types/react-redux": "7.1.28",
"@types/tar-stream": "2.2.3", "@types/tar-stream": "2.2.3",
@@ -106,7 +106,7 @@
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0", "jetifier": "2.0.0",
"js-draw": "1.5.0", "js-draw": "1.13.2",
"jsdom": "22.1.0", "jsdom": "22.1.0",
"metro-react-native-babel-preset": "0.73.9", "metro-react-native-babel-preset": "0.73.9",
"nodemon": "3.0.1", "nodemon": "3.0.1",

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
hash:"7d3976ee03fc0f6880dd54c78d1a325b", files: { hash:"0f14c5392fd01275e9aec0841c384482", files: {
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' }, 'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' }, 'highlight.js/atom-one-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' }, 'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

File diff suppressed because one or more lines are too long

View File

@@ -317,6 +317,7 @@ const appReducer = (state = appDefaultState, action: any) => {
if ('smartFilterId' in action) { if ('smartFilterId' in action) {
newState.smartFilterId = action.smartFilterId; newState.smartFilterId = action.smartFilterId;
newState.selectedSmartFilterId = action.smartFilterId;
newState.notesParentType = 'SmartFilter'; newState.notesParentType = 'SmartFilter';
} }
@@ -1032,10 +1033,13 @@ class AppComponent extends React.Component {
let sideMenuContent: ReactNode = null; let sideMenuContent: ReactNode = null;
let menuPosition: SideMenuPosition = 'left'; let menuPosition: SideMenuPosition = 'left';
let disableSideMenuGestures = this.props.disableSideMenuGestures;
if (this.props.routeName === 'Note') { if (this.props.routeName === 'Note') {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>; sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
menuPosition = 'right'; menuPosition = 'right';
} else if (this.props.routeName === 'Config') {
disableSideMenuGestures = true;
} else { } else {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>; sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
} }
@@ -1076,7 +1080,7 @@ class AppComponent extends React.Component {
openMenuOffset={this.state.sideMenuWidth} openMenuOffset={this.state.sideMenuWidth}
menuPosition={menuPosition} menuPosition={menuPosition}
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)} onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
disableGestures={this.props.disableSideMenuGestures} disableGestures={disableSideMenuGestures}
onSliding={(percent: number) => { onSliding={(percent: number) => {
this.props.dispatch({ this.props.dispatch({
type: 'SIDE_MENU_OPEN_PERCENT', type: 'SIDE_MENU_OPEN_PERCENT',

View File

@@ -3,30 +3,14 @@
// files: First here we convert the JS file to a plain string, and that string // files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView. // is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
import { mkdirp, readFile, writeFile } from 'fs-extra';
import { dirname, extname, basename } from 'path'; import { dirname, extname, basename } from 'path';
// We need this to be transpiled to `const webpack = require('webpack')`. // We need this to be transpiled to `const webpack = require('webpack')`.
// As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop // As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import copyJs from './copyJs';
const rootDir = dirname(dirname(dirname(__dirname))); export default class BundledFile {
const mobileDir = `${rootDir}/packages/app-mobile`;
const outputDir = `${mobileDir}/lib/rnInjectedJs`;
// Stores the contents of the file at [filePath] as an importable string.
// [name] should be the name (excluding the .js extension) of the output file that will contain
// the JSON-ified file content.
async function copyJs(name: string, filePath: string) {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
const js = await readFile(filePath, 'utf-8');
const json = `module.exports = ${JSON.stringify(js)};`;
await writeFile(outputPath, json);
}
class BundledFile {
private readonly bundleOutputPath: string; private readonly bundleOutputPath: string;
private readonly bundleBaseName: string; private readonly bundleBaseName: string;
private readonly rootFileDirectory: string; private readonly rootFileDirectory: string;
@@ -82,17 +66,23 @@ class BundledFile {
return config; return config;
} }
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
private async copyToImportableFile() {
await copyJs(`${this.bundleBaseName}.bundle`, this.bundleOutputPath);
}
private handleErrors(error: Error | undefined | null, stats: webpack.Stats | undefined): boolean { private handleErrors(error: Error | undefined | null, stats: webpack.Stats | undefined): boolean {
let failed = false; let failed = false;
if (error) { if (error) {
console.error(`Error: ${error.name}`, error.message, error.stack); console.error(`Error (${this.bundleName}): ${error.name}`, error.message, error.stack);
failed = true; failed = true;
} else if (stats?.hasErrors() || stats?.hasWarnings()) { } else if (stats?.hasErrors() || stats?.hasWarnings()) {
const data = stats.toJson(); const data = stats.toJson();
if (data.warnings && data.warningsCount) { if (data.warnings && data.warningsCount) {
console.warn('Warnings: ', data.warningsCount); console.warn(`Warnings (${this.bundleName}): `, data.warningsCount);
for (const warning of data.warnings) { for (const warning of data.warnings) {
// Stack contains the message // Stack contains the message
if (warning.stack) { if (warning.stack) {
@@ -103,7 +93,7 @@ class BundledFile {
} }
} }
if (data.errors && data.errorsCount) { if (data.errors && data.errorsCount) {
console.error('Errors: ', data.errorsCount); console.error(`Errors (${this.bundleName}): `, data.errorsCount);
for (const error of data.errors) { for (const error of data.errors) {
if (error.stack) { if (error.stack) {
console.error(error.stack); console.error(error.stack);
@@ -127,19 +117,34 @@ class BundledFile {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
console.info(`Building bundle: ${this.bundleName}...`); console.info(`Building bundle: ${this.bundleName}...`);
compiler.run((error, stats) => { compiler.run((buildError, stats) => {
let failed = this.handleErrors(error, stats); // Always output stats, even on success
console.log(`Bundle ${this.bundleName} built: `, stats?.toString());
let failed = this.handleErrors(buildError, stats);
// Clean up. // Clean up.
compiler.close(async (error) => { compiler.close(async (closeError) => {
if (error) { if (closeError) {
console.error('Error cleaning up:', error); console.error('Error cleaning up:', closeError);
failed = true; failed = true;
} }
let copyError;
if (!failed) {
try {
await this.copyToImportableFile();
} catch (error) {
console.error('Error copying', error);
failed = true;
copyError = error;
}
}
if (!failed) { if (!failed) {
resolve(); resolve();
} else { } else {
reject(); reject(closeError ?? buildError ?? copyError);
} }
}); });
}); });
@@ -160,44 +165,4 @@ class BundledFile {
} }
}); });
} }
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
public async copyToImportableFile() {
await copyJs(`${this.bundleBaseName}.bundle`, this.bundleOutputPath);
}
} }
const bundledFiles: BundledFile[] = [
new BundledFile(
'codeMirrorBundle',
`${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts`,
),
new BundledFile(
'svgEditorBundle',
`${mobileDir}/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts`,
),
];
export async function buildInjectedJS() {
await mkdirp(outputDir);
// Build all in parallel
await Promise.all(bundledFiles.map(async file => {
await file.build();
await file.copyToImportableFile();
}));
await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`);
}
export async function watchInjectedJS() {
// Watch for changes
for (const file of bundledFiles) {
file.startWatching();
}
}

View File

@@ -0,0 +1,6 @@
import { dirname } from 'path';
export const mobileDir = dirname(dirname(__dirname));
export const outputDir = `${mobileDir}/lib/rnInjectedJs`;
export const rootDir = dirname(dirname(mobileDir));

View File

@@ -0,0 +1,16 @@
import { readFile, writeFile } from 'fs-extra';
import { outputDir } from './constants';
// Stores the contents of the file at [filePath] as an importable string.
// [name] should be the name (excluding the .js extension) of the output file that will contain
// the JSON-ified file content.
const copyJs = async (name: string, filePath: string) => {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
const js = await readFile(filePath, 'utf-8');
const json = `module.exports = ${JSON.stringify(js)};`;
await writeFile(outputPath, json);
};
export default copyJs;

View File

@@ -0,0 +1,38 @@
import BundledFile from './BundledFile';
import { mkdirp } from 'fs-extra';
import { mobileDir, outputDir } from './constants';
import copyJs from './copyJs';
const codeMirrorBundle = new BundledFile(
'codeMirrorBundle',
`${mobileDir}/components/NoteEditor/CodeMirror/CodeMirror.ts`,
);
const jsDrawBundle = new BundledFile(
'svgEditorBundle',
`${mobileDir}/components/NoteEditor/ImageEditor/js-draw/createJsDrawEditor.ts`,
);
const gulpTasks = {
beforeBundle: {
fn: () => mkdirp(outputDir),
},
buildCodeMirrorEditor: {
fn: () => codeMirrorBundle.build(),
},
buildJsDrawEditor: {
fn: () => jsDrawBundle.build(),
},
watchCodeMirrorEditor: {
fn: () => codeMirrorBundle.startWatching(),
},
watchJsDrawEditor: {
fn: () => jsDrawBundle.startWatching(),
},
copyWebviewLib: {
fn: () => copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`),
},
};
export default gulpTasks;

View File

@@ -9,6 +9,7 @@ import * as tar from 'tar-stream';
import { resolve } from 'path'; import { resolve } from 'path';
import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import JoplinError from '@joplin/lib/JoplinError';
const logger = Logger.create('fs-driver-rn'); const logger = Logger.create('fs-driver-rn');
@@ -285,6 +286,10 @@ export default class FsDriverRN extends FsDriverBase {
} }
public async readFileChunk(handle: any, length: number, rawEncoding = 'base64') { public async readFileChunk(handle: any, length: number, rawEncoding = 'base64') {
if (!handle?.stat) {
throw new JoplinError('File does not exist (reading file chunk).', 'ENOENT');
}
const encoding = normalizeEncoding(rawEncoding); const encoding = normalizeEncoding(rawEncoding);
if (handle.offset + length > handle.stat.size) { if (handle.offset + length > handle.stat.size) {

View File

@@ -167,6 +167,18 @@ const testReadFileChunkUtf8 = async (tempDir: string) => {
await fsDriver.close(filePath); await fsDriver.close(filePath);
} }
// Should throw when the file doesn't exist
let readData = undefined;
try {
const handle = await fsDriver.open(`${filePath}.noexist`, 'r');
readData = await fsDriver.readFileChunk(handle, 1, 'utf8');
} catch (error) {
await expectToBe(error.code, 'ENOENT');
}
// Should not have read any data
await expectToBe(readData, undefined);
}; };
const testTarCreate = async (tempDir: string) => { const testTarCreate = async (tempDir: string) => {

View File

@@ -17,7 +17,7 @@
"@joplin/lib": "~2.13", "@joplin/lib": "~2.13",
"@testing-library/react-hooks": "8.0.1", "@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/react": "18.2.31", "@types/react": "18.2.33",
"@types/react-redux": "7.1.28", "@types/react-redux": "7.1.28",
"@types/styled-components": "5.1.29", "@types/styled-components": "5.1.29",
"jest": "29.7.0", "jest": "29.7.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@joplin/fork-htmlparser2", "name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser", "description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.48", "version": "4.1.50",
"author": "Felix Boehm <me@feedic.com>", "author": "Felix Boehm <me@feedic.com>",
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@@ -46,7 +46,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/node": "18.18.6", "@types/node": "18.18.7",
"@typescript-eslint/eslint-plugin": "6.7.2", "@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.7.2", "@typescript-eslint/parser": "6.7.2",
"coveralls": "3.1.1", "coveralls": "3.1.1",

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@joplin/fork-uslug", "name": "@joplin/fork-uslug",
"version": "1.0.13", "version": "1.0.15",
"description": "A permissive slug generator that works with unicode.", "description": "A permissive slug generator that works with unicode.",
"author": "Jeremy Selier <jerem.selier@gmail.com>", "author": "Jeremy Selier <jerem.selier@gmail.com>",
"publishConfig": { "publishConfig": {

View File

@@ -12,17 +12,6 @@ module.exports = class extends Generator {
this.option('silent'); this.option('silent');
this.option('update'); this.option('update');
// This appears to be deprecated and undocumented
// Maybe need this instead?
// https://github.com/yeoman/generator/issues/1294#issuecomment-841668595
// this.option('nodePackageManager', 'npm');
if (this.options.update) {
// When updating, overwrite files without prompting
this.conflicter.force = true;
}
} }
async prompting() { async prompting() {
@@ -165,6 +154,15 @@ module.exports = class extends Generator {
}, },
}, },
); );
} else if (this.options.update) {
this.fs.copy(
this.templatePath(file),
destFilePath, {
process: (sourceBuffer) => {
return sourceBuffer.toString();
},
},
);
} else { } else {
this.fs.copyTpl( this.fs.copyTpl(
this.templatePath(file), this.templatePath(file),

View File

@@ -7,7 +7,7 @@ This documentation describes how to create a plugin, and how to work with the pl
First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)).
```bash ```bash
npm install -g yo npm install -g yo@4.3.1
npm install -g generator-joplin npm install -g generator-joplin
``` ```

View File

@@ -2,7 +2,7 @@ import { ModelType } from '../../../BaseModel';
import Plugin from '../Plugin'; import Plugin from '../Plugin';
import { Path } from './types'; import { Path } from './types';
/** /**
* This module provides access to the Joplin data API: https://joplinapp.org/api/references/rest_api/ * This module provides access to the Joplin data API: https://joplinapp.org/help/api/references/rest_api
* This is the main way to retrieve data, such as notes, notebooks, tags, etc. * This is the main way to retrieve data, such as notes, notebooks, tags, etc.
* or to update them or delete them. * or to update them or delete them.
* *
@@ -18,7 +18,7 @@ import { Path } from './types';
* * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder. * * `data`: (Optional) Applies to PUT and POST calls only. The request body contains the data you want to create or modify, for example the content of a note or folder.
* * `files`: (Optional) Used to create new resources and associate them with files. * * `files`: (Optional) Used to create new resources and associate them with files.
* *
* Please refer to the [Joplin API documentation](https://joplinapp.org/api/references/rest_api/) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API. * Please refer to the [Joplin API documentation](https://joplinapp.org/help/api/references/rest_api) for complete details about each call. As the plugin runs within the Joplin application **you do not need an authorisation token** to use this API.
* *
* For example: * For example:
* *

View File

@@ -9,7 +9,7 @@ import { ExportModule, ImportModule } from './types';
* *
* See the documentation of the [[ExportModule]] and [[ImportModule]] for more information. * See the documentation of the [[ExportModule]] and [[ImportModule]] for more information.
* *
* You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/api/references/rest_api/ * You may also want to refer to the Joplin API documentation to see the list of properties for each item (note, notebook, etc.) - https://joplinapp.org/help/api/references/rest_api
*/ */
export default class JoplinInterop { export default class JoplinInterop {
registerExportModule(module: ExportModule): Promise<void>; registerExportModule(module: ExportModule): Promise<void>;

View File

@@ -524,13 +524,13 @@ export interface ContentScriptModuleLoadedEvent {
} }
export interface ContentScriptModule { export interface ContentScriptModule {
onLoaded?: (event:ContentScriptModuleLoadedEvent) => void; onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
plugin: () => any; plugin: ()=> any;
assets?: () => void; assets?: ()=> void;
} }
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> { export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (markdownIt:any, options:any) => any; plugin: (markdownIt: any, options: any)=> any;
} }
export enum ContentScriptType { export enum ContentScriptType {
@@ -552,7 +552,7 @@ export enum ContentScriptType {
* } * }
* } * }
* ``` * ```
* *
* See [the * See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script) * demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* for a simple Markdown-it plugin example. * for a simple Markdown-it plugin example.

View File

@@ -5,7 +5,7 @@
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive", "dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
"prepare": "npm run dist", "prepare": "npm run dist",
"updateVersion": "webpack --env joplin-plugin-config=updateVersion", "updateVersion": "webpack --env joplin-plugin-config=updateVersion",
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update" "update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force"
}, },
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -93,19 +93,24 @@ function validateCategories(categories) {
function validateScreenshots(screenshots) { function validateScreenshots(screenshots) {
if (!screenshots) return null; if (!screenshots) return null;
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied for (const screenshot of screenshots) {
screenshots.forEach(screenshot => {
if (!screenshot.src) throw new Error('You must specify a src for each screenshot'); if (!screenshot.src) throw new Error('You must specify a src for each screenshot');
// Avoid attempting to download and verify URL screenshots.
if (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) {
continue;
}
const screenshotType = screenshot.src.split('.').pop(); const screenshotType = screenshot.src.split('.').pop();
if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`); if (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \n${allPossibleScreenshotsType}\n`);
const screenshotPath = path.resolve(srcDir, screenshot.src); const screenshotPath = path.resolve(rootDir, screenshot.src);
// Max file size is 1MB // Max file size is 1MB
const fileMaxSize = 1024; const fileMaxSize = 1024;
const fileSize = fs.statSync(screenshotPath).size / 1024; const fileSize = fs.statSync(screenshotPath).size / 1024;
if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`); if (fileSize > fileMaxSize) throw new Error(`Max screenshot file size is ${fileMaxSize}KB. ${screenshotPath} is ${fileSize}KB`);
}); }
} }
function readManifest(manifestPath) { function readManifest(manifestPath) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "generator-joplin", "name": "generator-joplin",
"version": "2.13.1", "version": "2.13.2",
"description": "Scaffolds out a new Joplin plugin", "description": "Scaffolds out a new Joplin plugin",
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin", "homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
"author": { "author": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@joplin/htmlpack", "name": "@joplin/htmlpack",
"version": "2.13.2", "version": "2.13.4",
"description": "Pack an HTML file and all its linked resources into a single HTML file", "description": "Pack an HTML file and all its linked resources into a single HTML file",
"main": "dist/index.js", "main": "dist/index.js",
"types": "src/index.ts", "types": "src/index.ts",
@@ -14,7 +14,7 @@
"author": "Laurent Cozic", "author": "Laurent Cozic",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@joplin/fork-htmlparser2": "^4.1.48", "@joplin/fork-htmlparser2": "^4.1.50",
"css": "3.0.0", "css": "3.0.0",
"datauri": "4.1.0", "datauri": "4.1.0",
"fs-extra": "11.1.1", "fs-extra": "11.1.1",

View File

@@ -26,6 +26,7 @@ import time from './time';
import BaseSyncTarget from './BaseSyncTarget'; import BaseSyncTarget from './BaseSyncTarget';
const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware'); const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware');
const os = require('os'); const os = require('os');
import dns = require('dns');
import fs = require('fs-extra'); import fs = require('fs-extra');
const EventEmitter = require('events'); const EventEmitter = require('events');
const syswidecas = require('./vendor/syswide-cas'); const syswidecas = require('./vendor/syswide-cas');
@@ -171,6 +172,13 @@ export default class BaseApplication {
this.showStackTraces_ = true; this.showStackTraces_ = true;
} }
// Work around issues with ipv6 resolution -- default to ipv4first.
// (possibly incorrect URL serialization see https://github.com/mswjs/msw/issues/1388#issuecomment-1241180921).
// See also https://github.com/node-fetch/node-fetch/issues/1624#issuecomment-1407717012
if (flags.matched.allowOverridingDnsResultOrder) {
dns.setDefaultResultOrder('ipv4first');
}
return { return {
matched: flags.matched, matched: flags.matched,
argv: flags.argv, argv: flags.argv,

View File

@@ -7,6 +7,7 @@ const pdfUrlRegex = /[\s\S]*?\.pdf$/i;
export interface ParseOptions { export interface ParseOptions {
anchorNames?: string[]; anchorNames?: string[];
preserveImageTagsWithSize?: boolean; preserveImageTagsWithSize?: boolean;
preserveNestedTables?: boolean;
baseUrl?: string; baseUrl?: string;
disableEscapeContent?: boolean; disableEscapeContent?: boolean;
convertEmbeddedPdfsToLinks?: boolean; convertEmbeddedPdfsToLinks?: boolean;
@@ -20,6 +21,7 @@ export default class HtmlToMd {
anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [], anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [],
codeBlockStyle: 'fenced', codeBlockStyle: 'fenced',
preserveImageTagsWithSize: !!options.preserveImageTagsWithSize, preserveImageTagsWithSize: !!options.preserveImageTagsWithSize,
preserveNestedTables: !!options.preserveNestedTables,
bulletListMarker: '-', bulletListMarker: '-',
emDelimiter: '*', emDelimiter: '*',
strongDelimiter: '**', strongDelimiter: '**',

View File

@@ -224,6 +224,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"يتوفر تحديث ، هل تريد تنزيله الآن؟" "يتوفر تحديث ، هل تريد تنزيله الآن؟"
], ],
@@ -1254,6 +1257,9 @@
"جارٍ إنشاء الروابط...", "جارٍ إنشاء الروابط...",
"جارٍ إنشاء الروابط..." "جارٍ إنشاء الروابط..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"احصل عليها الآن:" "احصل عليها الآن:"
], ],
@@ -1589,7 +1595,7 @@
"Logout": [ "Logout": [
"تسجيل الخروج" "تسجيل الخروج"
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1643,6 +1649,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing Master Keys": [ "Missing Master Keys": [
"المفاتيح الرئيسة مفقودة" "المفاتيح الرئيسة مفقودة"
], ],
@@ -1895,6 +1904,9 @@
"Open...": [ "Open...": [
"فتح..." "فتح..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"ألغيت العملية" "ألغيت العملية"
], ],
@@ -2972,6 +2984,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"عند إنشاء قائمة جديدة للمهام:" "عند إنشاء قائمة جديدة للمهام:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"كلمات" "كلمات"
], ],

View File

@@ -188,6 +188,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Има обновление, искате ли да го свалите сега?" "Има обновление, искате ли да го свалите сега?"
], ],
@@ -929,6 +932,9 @@
"General": [ "General": [
"Общи" "Общи"
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"" ""
], ],
@@ -1196,7 +1202,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1232,6 +1238,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing Master Keys": [ "Missing Master Keys": [
"Липсващи главни ключове" "Липсващи главни ключове"
], ],
@@ -1430,6 +1439,9 @@
"Open...": [ "Open...": [
"Отвори..." "Отвори..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Действието бе отменено" "Действието бе отменено"
], ],
@@ -2393,6 +2405,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Когато се създава нова задача:" "Когато се създава нова задача:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -191,6 +191,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Nova je verzija dostupna. Želite li je preuzeti?" "Nova je verzija dostupna. Želite li je preuzeti?"
], ],
@@ -938,6 +941,9 @@
"General": [ "General": [
"Opće" "Opće"
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Preuzmite sada:" "Preuzmite sada:"
], ],
@@ -1202,7 +1208,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1238,6 +1244,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing Master Keys": [ "Missing Master Keys": [
"Nedostaju glavni ključevi" "Nedostaju glavni ključevi"
], ],
@@ -1424,6 +1433,9 @@
"Open...": [ "Open...": [
"Otvori..." "Otvori..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Radnja je prekinuta" "Radnja je prekinuta"
], ],
@@ -2387,6 +2399,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Prilikom kreiranja novog zadatka:" "Prilikom kreiranja novog zadatka:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Riječi" "Riječi"
], ],

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Hi ha disponible una actualització. Voleu descarregar-la ara?" "Hi ha disponible una actualització. Voleu descarregar-la ara?"
], ],
@@ -1267,6 +1270,9 @@
"Generant enllaç...", "Generant enllaç...",
"Generant enllaços..." "Generant enllaços..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Obteniu-lo ara:" "Obteniu-lo ara:"
], ],
@@ -1622,8 +1628,8 @@
"Logout": [ "Logout": [
"Desconnecta" "Desconnecta"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Registres" ""
], ],
"Make a donation": [ "Make a donation": [
"Donatius" "Donatius"
@@ -1685,6 +1691,9 @@
"Max Total Size": [ "Max Total Size": [
"Mida màxima total" "Mida màxima total"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Claus que falten" "Claus que falten"
], ],
@@ -1949,6 +1958,9 @@
"Open...": [ "Open...": [
"Obre..." "Obre..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"L'operació s'ha cancel·lat" "L'operació s'ha cancel·lat"
], ],
@@ -3167,6 +3179,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"En crear un llistat de tasques pendents:" "En crear un llistat de tasques pendents:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Paraules" "Paraules"
], ],

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Je k dispozici aktualizace, chcete ji stáhnout?" "Je k dispozici aktualizace, chcete ji stáhnout?"
], ],
@@ -1236,6 +1239,9 @@
"Vytváření odkazů...", "Vytváření odkazů...",
"Vytváření odkazů..." "Vytváření odkazů..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Získat hned:" "Získat hned:"
], ],
@@ -1586,8 +1592,8 @@
"Logout": [ "Logout": [
"Odhlásit se" "Odhlásit se"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Logy" ""
], ],
"Make a donation": [ "Make a donation": [
"Přispět" "Přispět"
@@ -1649,6 +1655,9 @@
"Max Total Size": [ "Max Total Size": [
"Maximální celková velikost" "Maximální celková velikost"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Chybějící klíče" "Chybějící klíče"
], ],
@@ -1913,6 +1922,9 @@
"Open...": [ "Open...": [
"Otevřít..." "Otevřít..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operace zrušena" "Operace zrušena"
], ],
@@ -3110,6 +3122,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Při vytváření nového úkolu:" "Při vytváření nového úkolu:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Slova" "Slova"
], ],

View File

@@ -74,6 +74,9 @@
"%s: %s": [ "%s: %s": [
"%s: %s" "%s: %s"
], ],
"%s: Missing password.": [
"%s: Manglende adgangskode."
],
"&Edit": [ "&Edit": [
"&Redigér" "&Redigér"
], ],
@@ -108,7 +111,7 @@
"(wysiwyg: %s)" "(wysiwyg: %s)"
], ],
"(You may disable this prompt in the options)": [ "(You may disable this prompt in the options)": [
"" "(Du kan deaktivere denne prompt i indstillingerne)"
], ],
"- Camera: to allow taking a picture and attaching it to a note.": [ "- Camera: to allow taking a picture and attaching it to a note.": [
"- Kamera: Tilladelse til fotografering og vedhæftning af et billede til en note." "- Kamera: Tilladelse til fotografering og vedhæftning af et billede til en note."
@@ -218,6 +221,12 @@
"Always": [ "Always": [
"Altid" "Altid"
], ],
"Always ask": [
"Spørg altid"
],
"Always resize": [
"Ændr altid størrelsen"
],
"Ambiguous notebook \"%s\". Please use notebook id instead - press \"ti\" to see the short notebook id or use $b for current selected notebook": [ "Ambiguous notebook \"%s\". Please use notebook id instead - press \"ti\" to see the short notebook id or use $b for current selected notebook": [
"Tvetydig notesbog \"%s\". Brug venligst notesbog-id i stedet - tryk på \"ti\" for at se det korte notebogs-id eller brug $b for den aktuelle valgte notesbog" "Tvetydig notesbog \"%s\". Brug venligst notesbog-id i stedet - tryk på \"ti\" for at se det korte notebogs-id eller brug $b for den aktuelle valgte notesbog"
], ],
@@ -225,13 +234,16 @@
"Tvetydig notesbog \"%s\". Brug venligst kort notesbog-id i stedet - tryk på \"ti\" for at se det korte notebogs-id" "Tvetydig notesbog \"%s\". Brug venligst kort notesbog-id i stedet - tryk på \"ti\" for at se det korte notebogs-id"
], ],
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"Der blev fundet en automatisk gemt tegning. Vedhæfter en kopi af den til noten?"
],
"An error occurred: %s": [
"" ""
], ],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Opdatering er tilgængelig, vil du hente den nu?" "Opdatering er tilgængelig, vil du hente den nu?"
], ],
"Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook": [ "Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook": [
"Enhver e-mail, der sendes til denne adresse, bliver konverteret til en note og føjet til din samling. Noten gemmes i indbakkens notesbog." "Enhver e-mail, der sendes til denne adresse, bliver konverteret til en note og føjet til din samling. Noten gemmes i indbakkens notesbog"
], ],
"Appearance": [ "Appearance": [
"Udseende" "Udseende"
@@ -464,6 +476,9 @@
"Code View": [ "Code View": [
"Kodevisning" "Kodevisning"
], ],
"Collaborate on a notebook with others": [
"Samarbejd med andre om en notesbog"
],
"Collaborate on notebooks with others": [ "Collaborate on notebooks with others": [
"Samarbejd med andre om notesbøger" "Samarbejd med andre om notesbøger"
], ],
@@ -488,6 +503,9 @@
"Command palette...": [ "Command palette...": [
"Kommando-udvalg..." "Kommando-udvalg..."
], ],
"Compact": [
"Kompakt"
],
"Completed": [ "Completed": [
"Fuldført" "Fuldført"
], ],
@@ -651,6 +669,12 @@
"Creates a new to-do.": [ "Creates a new to-do.": [
"Opretter en ny opgave." "Opretter en ny opgave."
], ],
"Creating new note...": [
"Opretter ny note..."
],
"Creating new to-do...": [
"Opretter nye gøremål..."
],
"Creating report...": [ "Creating report...": [
"Opretter rapport..." "Opretter rapport..."
], ],
@@ -673,7 +697,7 @@
"Brugerdefinerede TLS certifikater" "Brugerdefinerede TLS certifikater"
], ],
"Customise the note publishing banner": [ "Customise the note publishing banner": [
"" "Tilpas banneret til udgivelse af noter"
], ],
"Cut": [ "Cut": [
"Klip" "Klip"
@@ -793,7 +817,7 @@
"Destinationsformat: %s" "Destinationsformat: %s"
], ],
"Detailed": [ "Detailed": [
"" "Detailed"
], ],
"Directory": [ "Directory": [
"Indeks" "Indeks"
@@ -819,6 +843,9 @@
"Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?": [ "Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?": [
"Slås kryptering fra vil *alle* dine noter og vedhæftninger blive re-synkroniseret og sendt ukrypteret til synk-modtageren. Vil du fortsætte?" "Slås kryptering fra vil *alle* dine noter og vedhæftninger blive re-synkroniseret og sendt ukrypteret til synk-modtageren. Vil du fortsætte?"
], ],
"Discard": [
"Kassér"
],
"Discard changes": [ "Discard changes": [
"Kassér ændringer" "Kassér ændringer"
], ],
@@ -892,10 +919,10 @@
"Dracula" "Dracula"
], ],
"Draw picture": [ "Draw picture": [
"" "Tegn billede"
], ],
"Drawing": [ "Drawing": [
"" "Tegner"
], ],
"Drop notes or files here": [ "Drop notes or files here": [
"Slip noter eller filer her" "Slip noter eller filer her"
@@ -1098,6 +1125,9 @@
"Enter notebook title": [ "Enter notebook title": [
"Indtast notesbogstitel" "Indtast notesbogstitel"
], ],
"Enter password": [
"Indtast adgangskode"
],
"Enum": [ "Enum": [
"Enum" "Enum"
], ],
@@ -1267,6 +1297,9 @@
"Opretter link...", "Opretter link...",
"Opretter links..." "Opretter links..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Få den nu:" "Få den nu:"
], ],
@@ -1334,7 +1367,7 @@
"HTML Indeks" "HTML Indeks"
], ],
"HTML document": [ "HTML document": [
"" "HTML-dokument"
], ],
"HTML File": [ "HTML File": [
"HTML fil" "HTML fil"
@@ -1553,7 +1586,7 @@
"Indstil knap-rækkefølge" "Indstil knap-rækkefølge"
], ],
"Leave it blank to download the language files from the default website": [ "Leave it blank to download the language files from the default website": [
"Lad den være tom for at downloade sprogfilerne fra standardwebstedet." "Lad den være tom for at downloade sprogfilerne fra standardwebstedet"
], ],
"Leave notebook...": [ "Leave notebook...": [
"Forlad notesbog..." "Forlad notesbog..."
@@ -1622,8 +1655,8 @@
"Logout": [ "Logout": [
"Log ud" "Log ud"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Logfiler" ""
], ],
"Make a donation": [ "Make a donation": [
"Giv en donation" "Giv en donation"
@@ -1685,6 +1718,9 @@
"Max Total Size": [ "Max Total Size": [
"Maks samlet størrelse" "Maks samlet størrelse"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Manglende nøgler" "Manglende nøgler"
], ],
@@ -1731,7 +1767,7 @@
"N" "N"
], ],
"Never resize": [ "Never resize": [
"" "Ændr aldrig størrelse"
], ],
"New note": [ "New note": [
"Ny note" "Ny note"
@@ -1862,6 +1898,9 @@
"Note list growth factor": [ "Note list growth factor": [
"Noteliste-vækstfaktor" "Noteliste-vækstfaktor"
], ],
"Note list style": [
"Notatliste-stil"
],
"Note properties": [ "Note properties": [
"Noteegenskaber" "Noteegenskaber"
], ],
@@ -1949,6 +1988,9 @@
"Open...": [ "Open...": [
"Åbn..." "Åbn..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Udførelse annulleret" "Udførelse annulleret"
], ],
@@ -2238,7 +2280,7 @@
"Nulstil hovedadgangskode" "Nulstil hovedadgangskode"
], ],
"Resize large images:": [ "Resize large images:": [
"" "Ændr størrelsen på store billeder:"
], ],
"Resources: %d.": [ "Resources: %d.": [
"Ressourcer: %d." "Ressourcer: %d."
@@ -2312,6 +2354,9 @@
"Save changes": [ "Save changes": [
"Gem ændringer" "Gem ændringer"
], ],
"Save changes?": [
"Gem ændringer?"
],
"Save geo-location with notes": [ "Save geo-location with notes": [
"Gem geo-lokation i noter" "Gem geo-lokation i noter"
], ],
@@ -2387,6 +2432,9 @@
"Share a copy of all notes in a file format that can be imported by Joplin on a computer.": [ "Share a copy of all notes in a file format that can be imported by Joplin on a computer.": [
"Del en kopi af alle noter i et filformat, der kan importeres af Joplin på en computer." "Del en kopi af alle noter i et filformat, der kan importeres af Joplin på en computer."
], ],
"Share a notebook with others": [
"Del en notesbog med andre"
],
"Share Notebook": [ "Share Notebook": [
"Del notesbog" "Del notesbog"
], ],
@@ -2394,7 +2442,7 @@
"Del notesbog..." "Del notesbog..."
], ],
"Share permissions": [ "Share permissions": [
"" "Deletilladelser"
], ],
"Sharing notebook...": [ "Sharing notebook...": [
"Deler notesbog..." "Deler notesbog..."
@@ -2436,10 +2484,13 @@
"Vis/skjul sidebjælken" "Vis/skjul sidebjælken"
], ],
"Shrink large images before adding them to notes.": [ "Shrink large images before adding them to notes.": [
"" "Formindsk store billeder, før du tilføjer dem til noter."
],
"Side menu closed": [
"Sidemenu lukket"
], ],
"Side menu opened": [ "Side menu opened": [
"" "Sidemenu åbnet"
], ],
"Sidebar": [ "Sidebar": [
"Sidebjælke" "Sidebjælke"
@@ -2672,6 +2723,9 @@
"Teams": [ "Teams": [
"Teams" "Teams"
], ],
"Text document": [
"Tekstdokument"
],
"Text editor command": [ "Text editor command": [
"Tekstredigeringskomando" "Tekstredigeringskomando"
], ],
@@ -2765,6 +2819,9 @@
"The sync target needs to be upgraded. Press this banner to proceed.": [ "The sync target needs to be upgraded. Press this banner to proceed.": [
"Synk-mål skal opgraderes! Tryk på denne banner for at fortsætte." "Synk-mål skal opgraderes! Tryk på denne banner for at fortsætte."
], ],
"The synchronisation password is missing.": [
"Adgangskoden til synkronisering mangler."
],
"The tag \"%s\" already exists. Please choose a different name.": [ "The tag \"%s\" already exists. Please choose a different name.": [
"Etiketten \"%s\" eksisterer allerede. Vælg venligst et andet navn." "Etiketten \"%s\" eksisterer allerede. Vælg venligst et andet navn."
], ],
@@ -2814,7 +2871,7 @@
"Disse udvidelser forbedrer Markdown-fortolkeren med yderligere funktioner. Vær opmærksom på, at selvom disse funktioner kan være nyttige, er de ikke standard Markdown og derfor vil de fleste kun virke i Joplin. Derudover er nogle af dem ikke kompatible med WYSIWYG editoren. Hvis du åbner en note, som bruger en af disse udvidelser i den editor, vil du miste udvidelsesformatteringen. Det angives nedenfor, hvilke udvidelser der er kompatible eller ikke med WYSIWYG editoren." "Disse udvidelser forbedrer Markdown-fortolkeren med yderligere funktioner. Vær opmærksom på, at selvom disse funktioner kan være nyttige, er de ikke standard Markdown og derfor vil de fleste kun virke i Joplin. Derudover er nogle af dem ikke kompatible med WYSIWYG editoren. Hvis du åbner en note, som bruger en af disse udvidelser i den editor, vil du miste udvidelsesformatteringen. Det angives nedenfor, hvilke udvidelser der er kompatible eller ikke med WYSIWYG editoren."
], ],
"This allows another user to share a notebook with you, and you can then both collaborate on it. It does not however allow you to share a notebook with someone else, unless you have the feature \"%s\".": [ "This allows another user to share a notebook with you, and you can then both collaborate on it. It does not however allow you to share a notebook with someone else, unless you have the feature \"%s\".": [
"" "Dette giver en anden bruger mulighed for at dele en notesbog med dig, og I kan så begge samarbejde om den. Det giver dig dog ikke mulighed for at dele en notesbog med en anden, medmindre du har funktionen \"%s\"."
], ],
"This attachment is not downloaded or not decrypted yet": [ "This attachment is not downloaded or not decrypted yet": [
"Denne vedhæftning er ikke downloadet eller ikke dekrypteret endnu" "Denne vedhæftning er ikke downloadet eller ikke dekrypteret endnu"
@@ -2825,6 +2882,9 @@
"This authorisation token is only needed to allow third-party applications to access Joplin.": [ "This authorisation token is only needed to allow third-party applications to access Joplin.": [
"Denne autoriseringstoken er kun nødvendig for at tillade tredjepartsprogrammer at tilgå Joplin." "Denne autoriseringstoken er kun nødvendig for at tillade tredjepartsprogrammer at tilgå Joplin."
], ],
"This drawing may have unsaved changes.": [
"Denne tegning kan have ændringer, der ikke er gemt."
],
"This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.": [ "This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.": [
"Dette er et avanceret værktøj til at vise de vedhæftninger, som er knyttet til dine noter. Vær forsigtig med at slette dem, da de ikke kan gendannes bagefter." "Dette er et avanceret værktøj til at vise de vedhæftninger, som er knyttet til dine noter. Vær forsigtig med at slette dem, da de ikke kan gendannes bagefter."
], ],
@@ -2981,9 +3041,15 @@
"Type: %s.": [ "Type: %s.": [
"Tast: %s." "Tast: %s."
], ],
"Unable to edit resource of type %s": [
"Kan ikke redigere ressource af typen %s"
],
"Unable to export or share data. Reason: %s": [ "Unable to export or share data. Reason: %s": [
"Kan ikke eksportere eller dele data. Årsag: %s" "Kan ikke eksportere eller dele data. Årsag: %s"
], ],
"Unable to share log data. Reason: %s": [
"Kan ikke dele logdata. Årsag: %s"
],
"Uncompleted to-dos on top": [ "Uncompleted to-dos on top": [
"Ufærdige opgaver øverst" "Ufærdige opgaver øverst"
], ],
@@ -3161,6 +3227,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Ved oprettelse af ny opgave:" "Ved oprettelse af ny opgave:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Ord" "Ord"
], ],

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Es ist eine Aktualisierung verfügbar. Soll sie jetzt heruntergeladen werden?" "Es ist eine Aktualisierung verfügbar. Soll sie jetzt heruntergeladen werden?"
], ],
@@ -1267,6 +1270,9 @@
"Link wird erzeugt ...", "Link wird erzeugt ...",
"Links werden erzeugt ..." "Links werden erzeugt ..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Hole es jetzt:" "Hole es jetzt:"
], ],
@@ -1622,8 +1628,8 @@
"Logout": [ "Logout": [
"Abmelden" "Abmelden"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Protokolle" ""
], ],
"Make a donation": [ "Make a donation": [
"Spenden" "Spenden"
@@ -1685,6 +1691,9 @@
"Max Total Size": [ "Max Total Size": [
"Maximale Gesamtgröße" "Maximale Gesamtgröße"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Fehlende Schlüssel" "Fehlende Schlüssel"
], ],
@@ -1949,6 +1958,9 @@
"Open...": [ "Open...": [
"Öffnen ..." "Öffnen ..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Vorgang abgebrochen" "Vorgang abgebrochen"
], ],
@@ -3158,6 +3170,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Wenn eine neue Aufgabe erstellt wird:" "Wenn eine neue Aufgabe erstellt wird:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Wörter" "Wörter"
], ],

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Υπάρχει διαθέσιμη μια ενημέρωση, θέλετε να την κατεβάσετε τώρα;" "Υπάρχει διαθέσιμη μια ενημέρωση, θέλετε να την κατεβάσετε τώρα;"
], ],
@@ -1243,6 +1246,9 @@
"Δημιουργία συνδέσμου...", "Δημιουργία συνδέσμου...",
"Δημιουργία νεου %s..." "Δημιουργία νεου %s..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Λήψη τώρα:" "Λήψη τώρα:"
], ],
@@ -1598,8 +1604,8 @@
"Logout": [ "Logout": [
"Αποσύνδεση" "Αποσύνδεση"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Αρχεία Καταγραφής" ""
], ],
"Make a donation": [ "Make a donation": [
"Κάντε μια δωρεά" "Κάντε μια δωρεά"
@@ -1661,6 +1667,9 @@
"Max Total Size": [ "Max Total Size": [
"Μέγιστο συνολικό μέγεθος" "Μέγιστο συνολικό μέγεθος"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Απουσία κλειδιών" "Απουσία κλειδιών"
], ],
@@ -1925,6 +1934,9 @@
"Open...": [ "Open...": [
"Άνοιγμα..." "Άνοιγμα..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Η λειτουργία ακυρώθηκε" "Η λειτουργία ακυρώθηκε"
], ],
@@ -3128,6 +3140,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Κατά τη δημιουργία ενός νέου to-do:" "Κατά τη δημιουργία ενός νέου to-do:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Λέξεις" "Λέξεις"
], ],

View File

@@ -236,6 +236,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"" ""
], ],
@@ -885,6 +888,9 @@
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [ "Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
"" ""
], ],
"Donate, website": [
""
],
"Done": [ "Done": [
"" ""
], ],
@@ -999,6 +1005,9 @@
"Email to Note": [ "Email to Note": [
"" ""
], ],
"Email To Note, login information": [
""
],
"Emails": [ "Emails": [
"" ""
], ],
@@ -1173,6 +1182,9 @@
"Export profile": [ "Export profile": [
"" ""
], ],
"Export your data": [
""
],
"Exported successfully!": [ "Exported successfully!": [
"" ""
], ],
@@ -1294,6 +1306,9 @@
"", "",
"" ""
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"" ""
], ],
@@ -1567,6 +1582,9 @@
"Language": [ "Language": [
"" ""
], ],
"Language, date format": [
""
],
"Last error: %s": [ "Last error: %s": [
"" ""
], ],
@@ -1649,7 +1667,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1712,6 +1730,9 @@
"Max Total Size": [ "Max Total Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"" ""
], ],
@@ -1979,6 +2000,9 @@
"Open...": [ "Open...": [
"" ""
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"" ""
], ],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [ "Restart and upgrade": [
"" ""
], ],
"Restart in safe mode": [
""
],
"Restart now": [ "Restart now": [
"" ""
], ],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [ "Select parent notebook": [
"" ""
], ],
"Send bug report": [
""
],
"Server is already running on port %d": [ "Server is already running on port %d": [
"" ""
], ],
@@ -2654,6 +2684,9 @@
"Sync your notes": [ "Sync your notes": [
"" ""
], ],
"Sync, encryption, proxy": [
""
],
"Synchronisation": [ "Synchronisation": [
"" ""
], ],
@@ -2831,6 +2864,9 @@
"Theme": [ "Theme": [
"" ""
], ],
"Themes, editor font": [
""
],
"There are currently no notes. Create one by clicking on the (+) button.": [ "There are currently no notes. Create one by clicking on the (+) button.": [
"" ""
], ],
@@ -2981,6 +3017,9 @@
"Toggle external editing": [ "Toggle external editing": [
"" ""
], ],
"Toggle note history, keep notes for": [
""
],
"Toggle note list": [ "Toggle note list": [
"" ""
], ],
@@ -3215,6 +3254,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"" ""
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -236,6 +236,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"" ""
], ],
@@ -885,6 +888,9 @@
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [ "Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below." "Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below."
], ],
"Donate, website": [
""
],
"Done": [ "Done": [
"" ""
], ],
@@ -999,6 +1005,9 @@
"Email to Note": [ "Email to Note": [
"" ""
], ],
"Email To Note, login information": [
""
],
"Emails": [ "Emails": [
"" ""
], ],
@@ -1173,6 +1182,9 @@
"Export profile": [ "Export profile": [
"" ""
], ],
"Export your data": [
""
],
"Exported successfully!": [ "Exported successfully!": [
"" ""
], ],
@@ -1294,6 +1306,9 @@
"", "",
"" ""
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"" ""
], ],
@@ -1567,6 +1582,9 @@
"Language": [ "Language": [
"" ""
], ],
"Language, date format": [
""
],
"Last error: %s": [ "Last error: %s": [
"" ""
], ],
@@ -1649,7 +1667,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1712,6 +1730,9 @@
"Max Total Size": [ "Max Total Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"" ""
], ],
@@ -1979,6 +2000,9 @@
"Open...": [ "Open...": [
"" ""
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"" ""
], ],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [ "Restart and upgrade": [
"" ""
], ],
"Restart in safe mode": [
""
],
"Restart now": [ "Restart now": [
"" ""
], ],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [ "Select parent notebook": [
"" ""
], ],
"Send bug report": [
""
],
"Server is already running on port %d": [ "Server is already running on port %d": [
"" ""
], ],
@@ -2654,6 +2684,9 @@
"Sync your notes": [ "Sync your notes": [
"" ""
], ],
"Sync, encryption, proxy": [
""
],
"Synchronisation": [ "Synchronisation": [
"Synchronization" "Synchronization"
], ],
@@ -2828,6 +2861,9 @@
"Theme": [ "Theme": [
"" ""
], ],
"Themes, editor font": [
""
],
"There are currently no notes. Create one by clicking on the (+) button.": [ "There are currently no notes. Create one by clicking on the (+) button.": [
"" ""
], ],
@@ -2978,6 +3014,9 @@
"Toggle external editing": [ "Toggle external editing": [
"" ""
], ],
"Toggle note history, keep notes for": [
""
],
"Toggle note list": [ "Toggle note list": [
"" ""
], ],
@@ -3212,6 +3251,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"" ""
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -194,6 +194,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"" ""
], ],
@@ -770,6 +773,9 @@
"Email to Note": [ "Email to Note": [
"" ""
], ],
"Email To Note, login information": [
""
],
"Emails": [ "Emails": [
"" ""
], ],
@@ -1010,6 +1016,9 @@
"General": [ "General": [
"Ĝenerala" "Ĝenerala"
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Akiri ĝin nun:" "Akiri ĝin nun:"
], ],
@@ -1296,7 +1305,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1335,6 +1344,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"" ""
], ],
@@ -1539,6 +1551,9 @@
"Open...": [ "Open...": [
"Malfermi..." "Malfermi..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operacio nuligita" "Operacio nuligita"
], ],
@@ -1857,6 +1872,9 @@
"Select all": [ "Select all": [
"Elekti ĉiujn" "Elekti ĉiujn"
], ],
"Send bug report": [
""
],
"Server is already running on port %d": [ "Server is already running on port %d": [
"" ""
], ],
@@ -2073,6 +2091,9 @@
"Sync to provided target (defaults to sync.target config value)": [ "Sync to provided target (defaults to sync.target config value)": [
"" ""
], ],
"Sync, encryption, proxy": [
""
],
"Synchronisation interval": [ "Synchronisation interval": [
"" ""
], ],
@@ -2357,6 +2378,9 @@
"to-do": [ "to-do": [
"tasko" "tasko"
], ],
"Toggle note history, keep notes for": [
""
],
"Toggle sidebar": [ "Toggle sidebar": [
"" ""
], ],
@@ -2537,6 +2561,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"" ""
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -224,6 +224,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Está disponible una actualización. ¿Quiere descargarla ahora?" "Está disponible una actualización. ¿Quiere descargarla ahora?"
], ],
@@ -1225,6 +1228,9 @@
"Creando enlace...", "Creando enlace...",
"Creando enlaces..." "Creando enlaces..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Obtenla ahora en:" "Obtenla ahora en:"
], ],
@@ -1574,8 +1580,8 @@
"Logout": [ "Logout": [
"Cerrar sesión" "Cerrar sesión"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Registros" ""
], ],
"Make a donation": [ "Make a donation": [
"Hacer una donación" "Hacer una donación"
@@ -1637,6 +1643,9 @@
"Max Total Size": [ "Max Total Size": [
"Tamaño original" "Tamaño original"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Claves faltantes" "Claves faltantes"
], ],
@@ -1898,6 +1907,9 @@
"Open...": [ "Open...": [
"Abrir..." "Abrir..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operación cancelada" "Operación cancelada"
], ],
@@ -3092,6 +3104,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Al crear una tarea nueva:" "Al crear una tarea nueva:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Palabras" "Palabras"
], ],

View File

@@ -191,6 +191,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Saadaval on värskendus, kas soovite selle kohe alla laadida?" "Saadaval on värskendus, kas soovite selle kohe alla laadida?"
], ],
@@ -868,6 +871,9 @@
"Lingi loomine...", "Lingi loomine...",
"Linkide loomine..." "Linkide loomine..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Võta nüüd:" "Võta nüüd:"
], ],
@@ -1124,7 +1130,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1154,6 +1160,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing Master Keys": [ "Missing Master Keys": [
"Puuduvad juhtklahvid" "Puuduvad juhtklahvid"
], ],
@@ -1337,6 +1346,9 @@
"Open...": [ "Open...": [
"Avatud..." "Avatud..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Toiming tühistati" "Toiming tühistati"
], ],
@@ -2219,6 +2231,9 @@
"When creating a new note:": [ "When creating a new note:": [
"Uue märkme loomisel:" "Uue märkme loomisel:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -176,6 +176,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"" ""
], ],
@@ -627,6 +630,9 @@
"Do not ask for confirmation.": [ "Do not ask for confirmation.": [
"Ez galdetu berresteko." "Ez galdetu berresteko."
], ],
"Donate, website": [
""
],
"Done": [ "Done": [
"" ""
], ],
@@ -909,6 +915,9 @@
"Full name": [ "Full name": [
"" ""
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"" ""
], ],
@@ -1174,7 +1183,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Manage your plugins": [ "Manage your plugins": [
@@ -1207,6 +1216,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing required argument: %s": [ "Missing required argument: %s": [
"Beharrezko argumentua faltan: %s" "Beharrezko argumentua faltan: %s"
], ],
@@ -1366,6 +1378,9 @@
"Open...": [ "Open...": [
"" ""
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
" Eragiketa utzita" " Eragiketa utzita"
], ],
@@ -2092,6 +2107,9 @@
"To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions": [ "To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions": [
"" ""
], ],
"Toggle note history, keep notes for": [
""
],
"Toggle sidebar": [ "Toggle sidebar": [
"" ""
], ],
@@ -2254,6 +2272,9 @@
"Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.": [ "Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.": [
"Ongi etorri Joplin-era!\n\nIdatz `:help shortcuts` lasterbideak ikusteko, edo soilik `:help`erabilerako informaziorako.\n\nEsate baterako, koadernoa sortzeko sakatu `mb`: oharra sortzeko sakatu `mn`" "Ongi etorri Joplin-era!\n\nIdatz `:help shortcuts` lasterbideak ikusteko, edo soilik `:help`erabilerako informaziorako.\n\nEsate baterako, koadernoa sortzeko sakatu `mb`: oharra sortzeko sakatu `mn`"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

File diff suppressed because it is too large Load Diff

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Päivitys on saatavilla. Haluatko ladata sen nyt?" "Päivitys on saatavilla. Haluatko ladata sen nyt?"
], ],
@@ -1234,6 +1237,9 @@
"Luodaan linkki...", "Luodaan linkki...",
"Luodaan linkkejä..." "Luodaan linkkejä..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Hae se nyt:" "Hae se nyt:"
], ],
@@ -1583,8 +1589,8 @@
"Logout": [ "Logout": [
"Uloskirjaus" "Uloskirjaus"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Lokit" ""
], ],
"Make a donation": [ "Make a donation": [
"Tee lahjoitus" "Tee lahjoitus"
@@ -1646,6 +1652,9 @@
"Max Total Size": [ "Max Total Size": [
"Suurin kokonaiskoko" "Suurin kokonaiskoko"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Puuttuvat avaimet" "Puuttuvat avaimet"
], ],
@@ -1910,6 +1919,9 @@
"Open...": [ "Open...": [
"Avaa..." "Avaa..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Toiminto peruutettu" "Toiminto peruutettu"
], ],
@@ -3107,6 +3119,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Kun luot uuden tehtävän:" "Kun luot uuden tehtävän:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Sanat" "Sanat"
], ],

View File

@@ -236,6 +236,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"Un dessin automatiquement sauvegardé a été trouvé. Le joindre à la note ?" "Un dessin automatiquement sauvegardé a été trouvé. Le joindre à la note ?"
], ],
"An error occurred: %s": [
"Erreur : %s"
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?" "Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
], ],
@@ -885,6 +888,9 @@
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [ "Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
"Ne perdez pas le mot de passe car, par sécurité, ce sera la seule façon de déchiffrer les données. Pour activer le chiffrement, veuillez entrer votre mot de passe ci-dessous." "Ne perdez pas le mot de passe car, par sécurité, ce sera la seule façon de déchiffrer les données. Pour activer le chiffrement, veuillez entrer votre mot de passe ci-dessous."
], ],
"Donate, website": [
"Donations, site web"
],
"Done": [ "Done": [
"Terminé" "Terminé"
], ],
@@ -999,6 +1005,9 @@
"Email to Note": [ "Email to Note": [
"Conversion email en note" "Conversion email en note"
], ],
"Email To Note, login information": [
"\"Email to note\", info de connexion"
],
"Emails": [ "Emails": [
"Emails" "Emails"
], ],
@@ -1173,6 +1182,9 @@
"Export profile": [ "Export profile": [
"Exporter le profil" "Exporter le profil"
], ],
"Export your data": [
"Exporter vos données"
],
"Exported successfully!": [ "Exported successfully!": [
"Exporté avec succès !" "Exporté avec succès !"
], ],
@@ -1294,6 +1306,9 @@
"Génération du lien…", "Génération du lien…",
"Génération des liens…" "Génération des liens…"
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
"Géolocalisation, vérification orthographique, barre d'outils, taille des images"
],
"Get it now:": [ "Get it now:": [
"L'obtenir maintenant :" "L'obtenir maintenant :"
], ],
@@ -1567,6 +1582,9 @@
"Language": [ "Language": [
"Langue" "Langue"
], ],
"Language, date format": [
"Langue, format de la date"
],
"Last error: %s": [ "Last error: %s": [
"Dernière erreur : %s" "Dernière erreur : %s"
], ],
@@ -1649,8 +1667,8 @@
"Logout": [ "Logout": [
"Se déconnecter" "Se déconnecter"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Journal" "Logs, profils, info synchronisation"
], ],
"Make a donation": [ "Make a donation": [
"Faire un don" "Faire un don"
@@ -1712,6 +1730,9 @@
"Max Total Size": [ "Max Total Size": [
"Taille max totale" "Taille max totale"
], ],
"Media player, math, diagrams, table of contents": [
"Lecteur média, math, diagrammes, sommaire"
],
"Missing keys": [ "Missing keys": [
"Clefs manquantes" "Clefs manquantes"
], ],
@@ -1979,6 +2000,9 @@
"Open...": [ "Open...": [
"Ouvrir…" "Ouvrir…"
], ],
"Opening section %s": [
"Ouverture section %s"
],
"Operation cancelled": [ "Operation cancelled": [
"Opération annulée" "Opération annulée"
], ],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [ "Restart and upgrade": [
"Redémarrer et mettre à jour" "Redémarrer et mettre à jour"
], ],
"Restart in safe mode": [
"Redémarrer en mode sans échec"
],
"Restart now": [ "Restart now": [
"Redémarrer maintenant" "Redémarrer maintenant"
], ],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [ "Select parent notebook": [
"Sélectionner le carnet parent" "Sélectionner le carnet parent"
], ],
"Send bug report": [
"Envoyer rapport de débogage"
],
"Server is already running on port %d": [ "Server is already running on port %d": [
"Le serveur tourne déjà sur le port %d" "Le serveur tourne déjà sur le port %d"
], ],
@@ -2654,6 +2684,9 @@
"Sync your notes": [ "Sync your notes": [
"Synchroniser vos notes" "Synchroniser vos notes"
], ],
"Sync, encryption, proxy": [
"Synchronisation, chiffrement, proxy"
],
"Synchronisation": [ "Synchronisation": [
"Synchronisation" "Synchronisation"
], ],
@@ -2831,6 +2864,9 @@
"Theme": [ "Theme": [
"Apparence" "Apparence"
], ],
"Themes, editor font": [
"Thèmes, police de l'éditeur"
],
"There are currently no notes. Create one by clicking on the (+) button.": [ "There are currently no notes. Create one by clicking on the (+) button.": [
"Ce carnet ne contient aucune note. Créez‑en une en appuyant sur le bouton (+)." "Ce carnet ne contient aucune note. Créez‑en une en appuyant sur le bouton (+)."
], ],
@@ -2981,6 +3017,9 @@
"Toggle external editing": [ "Toggle external editing": [
"Basculer l'édition externe" "Basculer l'édition externe"
], ],
"Toggle note history, keep notes for": [
"Activation historique des notes"
],
"Toggle note list": [ "Toggle note list": [
"Basculer liste de notes" "Basculer liste de notes"
], ],
@@ -3215,6 +3254,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Lors de la création d'une tâche :" "Lors de la création d'une tâche :"
], ],
"Window unresponsive.": [
"La fenêtre ne répond pas."
],
"Words": [ "Words": [
"Mots" "Mots"
], ],

View File

@@ -173,6 +173,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Hai unha actualización dispoñíbel, desexa descargala agora?" "Hai unha actualización dispoñíbel, desexa descargala agora?"
], ],
@@ -900,6 +903,9 @@
"Full name": [ "Full name": [
"" ""
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"" ""
], ],
@@ -1168,7 +1174,7 @@
"Logout": [ "Logout": [
"" ""
], ],
"Logs": [ "Logs, profiles, sync status": [
"" ""
], ],
"Make a donation": [ "Make a donation": [
@@ -1204,6 +1210,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing Master Keys": [ "Missing Master Keys": [
"Faltan as chaves mestras" "Faltan as chaves mestras"
], ],
@@ -1381,6 +1390,9 @@
"Open...": [ "Open...": [
"Abrir…" "Abrir…"
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operación cancelada" "Operación cancelada"
], ],
@@ -2137,6 +2149,9 @@
"Toggle editor layout": [ "Toggle editor layout": [
"Cambiar a disposición do editor" "Cambiar a disposición do editor"
], ],
"Toggle note history, keep notes for": [
""
],
"Toggle sidebar": [ "Toggle sidebar": [
"" ""
], ],
@@ -2317,6 +2332,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Cando se crea unha nova tarefa:" "Cando se crea unha nova tarefa:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"" ""
], ],

View File

@@ -227,6 +227,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Dostupna je nova verzija. Želiš li je sada preuzeti?" "Dostupna je nova verzija. Želiš li je sada preuzeti?"
], ],
@@ -1269,6 +1272,9 @@
"Generiranje poveznica …", "Generiranje poveznica …",
"Generiranje poveznica …" "Generiranje poveznica …"
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Nabavi sada:" "Nabavi sada:"
], ],
@@ -1625,8 +1631,8 @@
"Logout": [ "Logout": [
"Odjava" "Odjava"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Dnevnici" ""
], ],
"Make a donation": [ "Make a donation": [
"Doniraj" "Doniraj"
@@ -1688,6 +1694,9 @@
"Max Total Size": [ "Max Total Size": [
"Maksimalna ukupna veličina" "Maksimalna ukupna veličina"
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Nedostajući ključevi" "Nedostajući ključevi"
], ],
@@ -1952,6 +1961,9 @@
"Open...": [ "Open...": [
"Otvori …" "Otvori …"
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operacija je prekinuta" "Operacija je prekinuta"
], ],
@@ -3167,6 +3179,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Prilikom stvaranja novog zadatka:" "Prilikom stvaranja novog zadatka:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Broj riječi" "Broj riječi"
], ],

View File

@@ -209,6 +209,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Egy frissítés elérhető, le szeretné tölteni most?" "Egy frissítés elérhető, le szeretné tölteni most?"
], ],
@@ -1159,6 +1162,9 @@
"Link létrehozása...", "Link létrehozása...",
"Linkek létrehozása..." "Linkek létrehozása..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Szerezze be most:" "Szerezze be most:"
], ],
@@ -1478,8 +1484,8 @@
"Logout": [ "Logout": [
"Kilépés" "Kilépés"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Log-ok" ""
], ],
"Make a donation": [ "Make a donation": [
"Adakozzon" "Adakozzon"
@@ -1526,6 +1532,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Hiányzó kulcsok" "Hiányzó kulcsok"
], ],
@@ -1775,6 +1784,9 @@
"Open...": [ "Open...": [
"Megnyitás..." "Megnyitás..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Művelet megszakítva" "Művelet megszakítva"
], ],
@@ -2897,6 +2909,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Amikor egy új to-do-t hoz létre:" "Amikor egy új to-do-t hoz létre:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Szavak" "Szavak"
], ],

View File

@@ -218,6 +218,9 @@
"An autosaved drawing was found. Attach a copy of it to the note?": [ "An autosaved drawing was found. Attach a copy of it to the note?": [
"" ""
], ],
"An error occurred: %s": [
""
],
"An update is available, do you want to download it now?": [ "An update is available, do you want to download it now?": [
"Pembaruan tersedia, apakah Anda ingin mengunduhnya sekarang?" "Pembaruan tersedia, apakah Anda ingin mengunduhnya sekarang?"
], ],
@@ -1178,6 +1181,9 @@
"Generating link...": [ "Generating link...": [
"Menghasilkan tautan..." "Menghasilkan tautan..."
], ],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [ "Get it now:": [
"Dapatkan sekarang:" "Dapatkan sekarang:"
], ],
@@ -1511,8 +1517,8 @@
"Logout": [ "Logout": [
"Keluar" "Keluar"
], ],
"Logs": [ "Logs, profiles, sync status": [
"Log" ""
], ],
"Make a donation": [ "Make a donation": [
"Beri donasi" "Beri donasi"
@@ -1562,6 +1568,9 @@
"Max Item Size": [ "Max Item Size": [
"" ""
], ],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [ "Missing keys": [
"Kunci yang Hilang" "Kunci yang Hilang"
], ],
@@ -1811,6 +1820,9 @@
"Open...": [ "Open...": [
"Buka..." "Buka..."
], ],
"Opening section %s": [
""
],
"Operation cancelled": [ "Operation cancelled": [
"Operasi dibatalkan" "Operasi dibatalkan"
], ],
@@ -2966,6 +2978,9 @@
"When creating a new to-do:": [ "When creating a new to-do:": [
"Ketika membuat tugas baru:" "Ketika membuat tugas baru:"
], ],
"Window unresponsive.": [
""
],
"Words": [ "Words": [
"Kata" "Kata"
], ],

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