1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

96 Commits

Author SHA1 Message Date
Laurent Cozic
214f9916d9 Server v2.13.5 2023-11-19 09:34:30 +00:00
Laurent Cozic
87aeffa160 Server: Fixed issue with sync not immediately returning all items in certain cases 2023-11-19 09:24:20 +00:00
Henry Heino
14a2d2d795 Chore: Mobile: Don't build twice on postinstall (#9340) 2023-11-19 08:55:52 +00:00
Joplin Bot
3560bc62a2 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-18 18:13:50 +00:00
Joplin Bot
0729d1db27 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-18 12:16:13 +00:00
renovate[bot]
5f27d425bf Update dependency highlight.js to v11.9.0 (#9341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-18 10:53:36 +00:00
Joplin Bot
e2956c391d Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-18 06:14:34 +00:00
Joplin Bot
bdc8f30705 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-17 18:14:34 +00:00
Laurent Cozic
2c9bf9f03a Desktop: Fixed copying and pasting an image from Chrome in RTE 2023-11-17 18:11:17 +00:00
Henry Heino
60c2964acd Chore: Tests: Fix vscode doesn't recognize Jest types in some test files (#9337) 2023-11-17 16:04:36 +00:00
Joplin Bot
97248035b1 Doc: Auto-update documentation
Auto-updated using release-website.sh
2023-11-17 12:17:33 +00:00
Laurent Cozic
35c79a2cfb Doc: Fixes #9323: Fixed donation link 2023-11-17 12:01:45 +00:00
Henry Heino
9b9762f940 Chore: Mobile: Fix "MenuContext is deprecated" warning (#9330) 2023-11-17 11:43:01 +00:00
renovate[bot]
e186fe8936 Update dependency ts-loader to v9.5.0 (#9327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-17 08:23:55 +00:00
renovate[bot]
213cd419f0 Update dependency node-gyp to v9.4.1 (#9333)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-17 02:20:51 +00:00
renovate[bot]
b89b5fef65 Update dependency ldapts to v7.0.6 (#9332)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-16 22:06:40 +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
206 changed files with 26655 additions and 21228 deletions

View File

@@ -249,6 +249,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
@@ -383,6 +385,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
@@ -508,7 +512,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.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/ShareUtils.test.js
packages/app-mobile/utils/ShareUtils.js

View File

@@ -157,6 +157,8 @@ module.exports = {
// In user-facing text, it should be "notebook".
'id-denylist': ['error', 'err', 'notebook', 'notebooks'],
'prefer-arrow-callback': ['error'],
'no-constant-binary-expression': ['error'],
},
'plugins': [
'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
contact_links:
- name: "\U0001F914 Feature requests and support"
url: https://discourse.joplinapp.org/
about: I have a question or feature request …
- name: Feature Requests
url: https://discourse.joplinapp.org/c/features/
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
echo "Running Joplin Server tests using PostgreSQL..."
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
else
echo "Running Joplin Server tests using SQLite..."

9
.gitignore vendored
View File

@@ -231,6 +231,8 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/openEditDialog.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.test.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
@@ -365,6 +367,8 @@ packages/app-desktop/integration-tests/models/MainScreen.js
packages/app-desktop/integration-tests/models/NoteEditorScreen.js
packages/app-desktop/integration-tests/models/SettingsScreen.js
packages/app-desktop/integration-tests/util/activateMainMenuItem.js
packages/app-desktop/integration-tests/util/createStartupArgs.js
packages/app-desktop/integration-tests/util/firstNonDevToolsWindow.js
packages/app-desktop/integration-tests/util/test.js
packages/app-desktop/playwright.config.js
packages/app-desktop/plugins/GotoAnything.js
@@ -490,7 +494,10 @@ packages/app-mobile/services/profiles/index.js
packages/app-mobile/services/voiceTyping/vosk.android.js
packages/app-mobile/services/voiceTyping/vosk.ios.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/ShareUtils.test.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.
# 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.
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
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

View File

@@ -26,7 +26,7 @@ For more information about the applications, see the [full Joplin documentation]
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/readme/about/donate.md) for information on how to support the development of Joplin.
Please see the [donation page](https://github.com/laurent22/joplin/blob/dev/readme/donate.md) for information on how to support the development of Joplin.
# Sponsors
@@ -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/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/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/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/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/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 -->
# Community

View File

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

View File

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

View File

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

View File

@@ -367,6 +367,12 @@
"type": "shell",
"command": "cd ${workspaceFolder}/packages/server && yarn tsc",
"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",
"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

@@ -96,7 +96,7 @@
"@types/fs-extra": "11.0.3",
"eslint-plugin-github": "4.10.0",
"http-server": "14.1.1",
"node-gyp": "9.4.0",
"node-gyp": "9.4.1",
"nodemon": "3.0.1"
},
"packageManager": "yarn@3.6.4",

View File

@@ -73,7 +73,7 @@
"@joplin/tools": "~2.13",
"@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5",
"@types/node": "18.18.6",
"@types/node": "18.18.7",
"@types/proper-lockfile": "^4.1.2",
"gulp": "4.0.2",
"jest": "29.7.0",

View File

@@ -36,6 +36,10 @@ describe('HtmlToMd', () => {
htmlToMdOptions.preserveImageTagsWithSize = true;
}
if (htmlFilename.indexOf('preserve_nested_tables') === 0) {
htmlToMdOptions.preserveNestedTables = true;
}
const html = await readFile(htmlPath, '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 Tag from '@joplin/lib/models/Tag';
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 ClipperServer from '@joplin/lib/ClipperServer';
const { webFrame } = require('electron');
@@ -68,6 +68,7 @@ import path = require('path');
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
import { parseNotesParent } from '@joplin/lib/reducer';
import { PackageInfo } from '@joplin/lib/versionInfo';
const pluginClasses = [
require('./plugins/GotoAnything').default,

View File

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

View File

@@ -92,7 +92,15 @@ export default function Sidebar(props: Props) {
function renderButton(section: any) {
const selected = props.selection === section.name;
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
className={Setting.sectionNameToIcon(section.name, AppType.Desktop)}
/>
@@ -123,7 +131,7 @@ export default function Sidebar(props: Props) {
}
return (
<StyledRoot>
<StyledRoot role='tablist'>
{buttons}
</StyledRoot>
);

View File

@@ -1,9 +1,9 @@
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 Setting from '@joplin/lib/models/Setting';
import restart from '../services/restart';
const packageInfo = require('../packageInfo.js');
const packageInfo: PackageInfo = require('../packageInfo.js');
const ipcRenderer = require('electron').ipcRenderer;
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 shim from '@joplin/lib/shim';
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 { ImportModule } from '@joplin/lib/services/interop/Module';
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 { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
const packageInfo = require('../packageInfo.js');
const packageInfo: PackageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;

View File

@@ -27,6 +27,7 @@ import bridge from '../../../../services/bridge';
import { TinyMceEditorEvents } from './utils/types';
import type { Editor } from 'tinymce';
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
import shouldPasteResources from './utils/shouldPasteResources';
const { clipboard } = require('electron');
const supportedLocales = require('./supportedLocales');
@@ -559,11 +560,21 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
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 = [
'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|',
'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|',
'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({
@@ -1075,15 +1086,9 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// formatted text.
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
// We should only process the images if there is no plain text or
// HTML text in the clipboard. This is because certain applications,
// such as Word, are going to add multiple versions of the copied
// data to the clipboard - one with the text formatted as HTML, and
// one with the text as an image. In that case, we need to ignore
// the image and only process the HTML.
const resourceMds = await getResourcesFromPasteEvent(event);
if (!pastedText && !pastedHtml) {
const resourceMds = await getResourcesFromPasteEvent(event);
if (shouldPasteResources(pastedText, pastedHtml, resourceMds)) {
if (resourceMds.length) {
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);

View File

@@ -0,0 +1,47 @@
import shouldPasteResources from './shouldPasteResources';
describe('shouldPasteResources', () => {
test.each([
[
'',
'',
[],
true,
],
[
'some text',
'',
[],
false,
],
[
'',
'<b>some html<b>',
[],
false,
],
[
'',
'<img src="https://example.com/img.png"/>',
[],
false,
],
[
'some text',
'<img src="https://example.com/img.png"/>',
[],
false,
],
[
'',
'<img src="https://example.com/img.png"/><p>Some text</p>',
[],
false,
],
])('should tell if clipboard content should be processed as resources', (pastedText, pastedHtml, resourceMds, expected) => {
const actual = shouldPasteResources(pastedText, pastedHtml, resourceMds);
expect(actual).toBe(expected);
});
});

View File

@@ -0,0 +1,49 @@
import { htmlDocIsImageOnly } from '@joplin/renderer/htmlUtils';
import Logger from '@joplin/utils/Logger';
const logger = Logger.create('shouldPasteResources');
// We should only process the images if there is no plain text or HTML text in
// the clipboard. This is because certain applications, such as Word, are going
// to add multiple versions of the copied data to the clipboard - one with the
// text formatted as HTML, and one with the text as an image. In that case, we
// need to ignore the image and only process the HTML.
//
// Additional source of troubles is that when copying an image from Chrome, the
// clipboard will contain two elements: The actual image (type=image), and an
// HTML fragment with a link to the image. Most of the time getting the image
// from the HTML will work... except if some authentication is required to
// access the image. In that case we'll end up with dead link in the RTE. For
// that reason, when there's only an image in the HTML document, we process
// instead the clipboard resources, which will contain the actual image.
//
// We have a lot of log statements so that if someone reports a bug we can ask
// them to check the console and give us the messages they have.
export default (pastedText: string, pastedHtml: string, resourceMds: string[]) => {
logger.info('Pasted text:', pastedText);
logger.info('Pasted HTML:', pastedHtml);
logger.info('Resources:', resourceMds);
if (pastedText) {
logger.info('Not pasting resources because the clipboard contains plain text');
return false;
}
if (pastedHtml) {
if (!htmlDocIsImageOnly(pastedHtml)) {
logger.info('Not pasting resources because the clipboard contains HTML, which contains more than just one image');
return false;
} else {
logger.info('Not pasting HTML because it only contains one image.');
}
if (!resourceMds.length) {
logger.info('Not pasting resources because there isn\'t any');
return false;
}
}
logger.info('Pasting resources');
return true;
};

View File

@@ -9,7 +9,10 @@ export async function htmlToMarkdown(markupLanguage: number, html: string, origi
if (markupLanguage === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN) {
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 });
} else {
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 { writeFile } from 'fs-extra';
import { join } from 'path';
import createStartupArgs from './util/createStartupArgs';
import firstNonDevToolsWindow from './util/firstNonDevToolsWindow';
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.
// Open the app ourselves:
const startupArgs = [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const startupArgs = createStartupArgs(profileDirectory);
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');
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 { _electron as electron, Page, ElectronApplication, test as base } from '@playwright/test';
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) => {
const startupArgs = [
'main.js', '--env', 'dev', '--profile', profileDirectory,
];
const startupArgs = createStartupArgs(profileDirectory);
const electronApp = await electron.launch({ args: startupArgs });
await use(electronApp);
@@ -44,8 +44,8 @@ export const test = base.extend<JoplinFixtures>({
},
mainWindow: async ({ electronApp }, use) => {
const window = await electronApp.firstWindow();
await use(window);
const mainWindow = await firstNonDevToolsWindow(electronApp);
await use(mainWindow);
},
});

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.13.5",
"version": "2.13.6",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,
@@ -120,8 +120,8 @@
"@playwright/test": "1.38.1",
"@testing-library/react-hooks": "8.0.1",
"@types/jest": "29.5.5",
"@types/node": "18.18.6",
"@types/react": "18.2.31",
"@types/node": "18.18.7",
"@types/react": "18.2.33",
"@types/react-redux": "7.1.28",
"@types/styled-components": "5.1.29",
"electron": "26.5.0",
@@ -159,7 +159,7 @@
"electron-window-state": "5.0.3",
"formatcoords": "1.1.3",
"fs-extra": "11.1.1",
"highlight.js": "11.8.0",
"highlight.js": "11.9.0",
"immer": "7.0.15",
"keytar": "7.9.0",
"mark.js": "8.11.1",

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
import { useRef, useCallback } from 'react';
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';
const React = require('react');
import { View } from 'react-native';
import BackButtonDialogBox from '../BackButtonDialogBox';
import { reg } from '@joplin/lib/registry';
import ExtendedWebView from '../ExtendedWebView';
import ExtendedWebView, { WebViewControl } from '../ExtendedWebView';
interface Props {
themeId: number;
@@ -18,11 +18,13 @@ interface Props {
highlightedKeywords: string[];
noteResources: any;
paddingBottom: number;
initialScroll: number|null;
noteHash: string;
onJoplinLinkClick: HandleMessageCallback;
onCheckboxChange?: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback;
onMarkForDownload?: OnMarkForDownloadCallback;
onScroll: HandleScrollCallback;
onLoadEnd?: ()=> void;
}
@@ -32,6 +34,7 @@ const webViewStyle = {
export default function NoteBodyViewer(props: Props) {
const dialogBoxRef = useRef(null);
const webviewRef = useRef<WebViewControl>(null);
const { html, injectedJs } = useSource(
props.noteBody,
@@ -41,6 +44,7 @@ export default function NoteBodyViewer(props: Props) {
props.noteResources,
props.paddingBottom,
props.noteHash,
props.initialScroll,
);
const onResourceLongPress = useOnResourceLongPress(
@@ -59,6 +63,7 @@ export default function NoteBodyViewer(props: Props) {
onJoplinLinkClick: props.onJoplinLinkClick,
onRequestEditResource: props.onRequestEditResource,
onResourceLongPress,
onMainContainerScroll: props.onScroll,
},
);
@@ -96,6 +101,7 @@ export default function NoteBodyViewer(props: Props) {
return (
<View style={props.style}>
<ExtendedWebView
ref={webviewRef}
webviewInstanceId='NoteBodyViewer'
themeId={props.themeId}
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 OnMarkForDownloadCallback = (resource: { resourceId: string })=> void;
export type HandleScrollCallback = (scrollTop: number)=> void;
interface MessageCallbacks {
onMarkForDownload?: OnMarkForDownloadCallback;
@@ -10,6 +11,7 @@ interface MessageCallbacks {
onResourceLongPress: HandleMessageCallback;
onRequestEditResource?: HandleMessageCallback;
onCheckboxChange: HandleMessageCallback;
onMainContainerScroll: HandleScrollCallback;
}
export default function useOnMessage(
@@ -24,6 +26,7 @@ export default function useOnMessage(
// Thus, useCallback should depend on each callback individually.
const {
onMarkForDownload, onResourceLongPress, onCheckboxChange, onRequestEditResource, onJoplinLinkClick,
onMainContainerScroll,
} = callbacks;
return useCallback((event: any) => {
@@ -35,10 +38,23 @@ export default function useOnMessage(
// https://github.com/laurent22/joplin/issues/4494
const msg = event.nativeEvent.data;
// eslint-disable-next-line no-console
console.info('Got IPC message: ', msg);
const isScrollMessage = msg.startsWith('onscroll:');
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);
onCheckboxChange?.(newBody);
} else if (msg.indexOf('markForDownload:') === 0) {
@@ -63,5 +79,6 @@ export default function useOnMessage(
onJoplinLinkClick,
onResourceLongPress,
onRequestEditResource,
onMainContainerScroll,
]);
}

View File

@@ -40,7 +40,16 @@ const onlyCheckboxHasChangedHack = (previousBody: string, newBody: string) => {
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 [injectedJs, setInjectedJs] = useState<string[]>([]);
const [resourceLoadedTime, setResourceLoadedTime] = useState(0);
@@ -142,6 +151,12 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
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 = [];
js.push('try {');
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).
js.push('window.joplinPostMessage_ = (msg, args) => { 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(`
const readyStateCheckInterval = setInterval(function() {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
const hash = "${noteHash}";
// Gives it a bit of time before scrolling to the anchor
// so that images are loaded.
if (hash) {
const initialScroll = ${JSON.stringify(initialScroll)};
// 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(() => {
const e = document.getElementById(hash);
if (!e) {
@@ -171,6 +217,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
}, 10);
`);
js.push('} catch (e) {');
js.push(' console.error(e);');
js.push(' window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))');
js.push(' true;');
js.push('}');
@@ -186,10 +233,11 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
}
}
/*
iOS seems to increase inertial scrolling friction when the WebView body/root elements
scroll. Scroll the main container instead.
*/
:root > body {
padding: 0;
}
`;
const scrollRenderedMdContainerCss = `
body > #rendered-md {
width: 100vw;
overflow: auto;
@@ -197,10 +245,6 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
padding-bottom: ${paddingBottom}px;
padding-top: ${paddingTop};
}
:root > body {
padding: 0;
}
`;
const defaultCss = `
code {
@@ -219,6 +263,7 @@ export default function useSource(noteBody: string, noteMarkupLanguage: number,
<style>
${defaultCss}
${shim.mobilePlatform() === 'ios' ? iOSSpecificCss : ''}
${scrollRenderedMdContainer ? scrollRenderedMdContainerCss : ''}
${editPopupCss}
</style>
${assetsToHeaders(result.pluginAssets, { asHtml: true })}

View File

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

View File

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

View File

@@ -120,20 +120,48 @@ export const createJsDrawEditor = (
editor.showLoadingWarning(0);
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 = {
editor,
loadImageOrTemplate: async (svgData: string|undefined, templateData: string) => {
loadImageOrTemplate: async (resourceUrl: string, templateData: string) => {
// loadFromSVG shows its own loading message. Hide the original.
editor.hideLoadingWarning();
if (svgData && svgData.length > 0) {
await editor.loadFromSVG(svgData);
} else {
const svgData = await fetchInitialSvgData(resourceUrl);
// Load from a template if no initial data
if (svgData === '') {
await applyTemplateToEditor(editor, templateData);
// The editor expects to be saved initially (without
// unsaved changes). Save now.
saveNow();
} else {
await editor.loadFromSVG(svgData);
}
// We can now edit and save safely (without data loss).

View File

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

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
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 SearchEngine from '@joplin/lib/services/searchengine/SearchEngine';
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 biometricAuthenticate from '../../biometrics/biometricAuthenticate';
import configScreenStyles, { ConfigScreenStyles } from './configScreenStyles';
import NoteExportButton from './NoteExportSection/NoteExportButton';
import NoteExportButton, { exportButtonDescription, exportButtonTitle } from './NoteExportSection/NoteExportButton';
import SettingsButton from './SettingsButton';
import Clipboard from '@react-native-community/clipboard';
import { ReactNode } from 'react';
import { ReactElement, ReactNode } from 'react';
import { Dispatch } from 'redux';
import SectionHeader from './SectionHeader';
import ExportProfileButton from './NoteExportSection/ExportProfileButton';
import ExportProfileButton, { exportProfileButtonTitle } from './NoteExportSection/ExportProfileButton';
import SettingComponent from './SettingComponent';
import ExportDebugReportButton from './NoteExportSection/ExportDebugReportButton';
import ExportDebugReportButton, { exportDebugReportTitle } from './NoteExportSection/ExportDebugReportButton';
import SectionSelector from './SectionSelector';
import { TextInput } from 'react-native-paper';
interface ConfigScreenState {
settings: any;
changedSettingKeys: string[];
searchQuery: string;
searching: boolean;
fixingSearchIndex: boolean;
checkSyncConfigResult: { ok: boolean; errorMessage: string }|'checking'|null;
showAdvancedSettings: boolean;
@@ -66,6 +70,8 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
selectedSectionName: null,
fixingSearchIndex: false,
sidebarWidth: 100,
searchQuery: '',
searching: false,
};
this.scrollViewRef_ = React.createRef<ScrollView>();
@@ -115,10 +121,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
};
private manageProfilesButtonPress_ = () => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'ProfileSwitcher',
});
void NavService.go('ProfileSwitcher');
};
private fixSearchEngineIndexButtonPress_ = async () => {
@@ -131,6 +134,21 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
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 = () => {
const windowWidth = Dimensions.get('window').width;
@@ -153,10 +171,13 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
return this.state.sidebarWidth > windowWidth / 2;
}
private switchSectionPress_ = (section: string) => {
private onJumpToSection_ = (section: string) => {
const label = Setting.sectionNameToLabel(section);
AccessibilityInfo.announceForAccessibility(_('Opening section %s', label));
this.setState({ selectedSectionName: section });
this.setState({
selectedSectionName: section,
searching: false,
});
};
private showSectionNavigation_ = () => {
@@ -206,36 +227,65 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
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 => {
const goBack = async () => {
BackButtonService.removeHandler(this.handleBackButtonPress);
await BackButtonService.back();
};
// Cancel search on back
if (this.state.searching) {
this.setShowSearch_(false);
return true;
}
// Show navigation when pressing "back" (unless always visible).
if (this.state.selectedSectionName && this.navigationFillsScreen()) {
this.showSectionNavigation_();
return true;
}
if (this.state.changedSettingKeys.length > 0) {
const dialogTitle: string|null = null;
Alert.alert(
dialogTitle,
_('There are unsaved changes.'),
[{
text: _('Save changes'),
onPress: async () => {
await this.saveButton_press();
await goBack();
},
},
{
text: _('Discard changes'),
onPress: goBack,
}],
);
if (this.hasUnsavedChanges()) {
void (async () => {
await this.promptSaveChanges();
await goBack();
})();
return true;
}
@@ -255,12 +305,14 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
}
BackButtonService.addHandler(this.handleBackButtonPress);
NavService.addHandler(this.handleNavigateToNewScren);
Dimensions.addEventListener('change', this.updateSidebarWidth);
this.updateSidebarWidth();
}
public componentWillUnmount() {
BackButtonService.removeHandler(this.handleBackButtonPress);
NavService.removeHandler(this.handleNavigateToNewScren);
}
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) {
const settingComps = [];
public sectionToComponent(key: string, section: SettingMetadataSection, settings: any, isSelected: boolean) {
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 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++) {
const md = section.metadatas[i];
@@ -300,24 +415,29 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</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]);
settingComps.push(settingComp);
const relatedText = [md.label?.() ?? '', md.description?.() ?? ''];
addSettingComponent(
settingComp,
relatedText,
);
}
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') {
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');
settingComps.push(
addSettingComponent(
<View key="joplinCloud">
<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>
</View>
{
@@ -329,20 +449,30 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
)
}
</View>,
[label, description],
);
}
if (section.name === 'tools') {
settingComps.push(this.renderButton('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_));
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
settingComps.push(this.renderButton('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('profiles_buttons', _('Manage profiles'), this.manageProfilesButtonPress_);
addSettingButton('status_button', _('Sync Status'), this.syncStatusButtonPress_);
addSettingButton('log_button', _('Log'), this.logButtonPress_);
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') {
settingComps.push(<NoteExportButton key='export_as_jex_button' styles={this.styles()} />);
settingComps.push(<ExportDebugReportButton key='export_report_button' styles={this.styles()}/>);
settingComps.push(<ExportProfileButton key='export_data' styles={this.styles()}/>);
addSettingComponent(
<NoteExportButton key='export_as_jex_button' 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') {
@@ -350,7 +480,7 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// 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
settingComps.push(
addSettingComponent(
<View key="permission_info" style={styleSheet.settingContainer}>
<View key="permission_info_wrapper">
<Text key="perm1a" style={styleSheet.settingText}>
@@ -367,95 +497,60 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
</Text>
</View>
</View>,
'',
);
}
settingComps.push(
<View key="donate_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
void Linking.openURL('https://joplinapp.org/donate/');
}}
>
<Text key="label" style={styleSheet.linkText}>
{_('Make a donation')}
</Text>
</TouchableOpacity>
</View>,
);
addSettingLink('donate_link', _('Make a donation'), 'https://joplinapp.org/donate/');
addSettingLink('website_link', _('Joplin website'), 'https://joplinapp.org/');
addSettingLink('privacy_link', _('Privacy Policy'), 'https://joplinapp.org/privacy/');
settingComps.push(
<View key="website_link" style={styleSheet.settingContainer}>
<TouchableOpacity
onPress={() => {
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>,
);
addSettingText('version_info_app', `Joplin ${VersionInfo.appVersion}`);
addSettingText('version_info_db', _('Database v%s', reg.db().version()));
addSettingText('version_info_fts', _('FTS enabled: %d', this.props.settings['db.ftsEnabled']));
addSettingText('version_info_hermes', _('Hermes enabled: %d', (global as any).HermesInternal ? 1 : 0));
const featureFlagKeys = Setting.featureFlagKeys(AppType.Mobile);
if (featureFlagKeys.length) {
const headerKey = 'featureFlags';
settingComps.push(<SectionHeader
key={headerKey}
styles={this.styles().styleSheet}
title={_('Feature flags')}
onLayout={event => this.onHeaderLayout(headerKey, event)}
/>);
const featureFlagsTitle = _('Feature flags');
addSettingComponent(
<SectionHeader
key={headerKey}
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 (!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 (
<View key={key} onLayout={(event: any) => this.onSectionLayout(key, event)}>
<View>{settingComps}</View>
<View>
{this.state.searching ? headerComponent : null}
{settingComps}
</View>
</View>
);
}
@@ -541,18 +636,22 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
currentSectionName = 'general';
}
if (this.state.searching) {
currentSectionName = null;
}
const sectionSelector = (
<SectionSelector
selectedSectionName={currentSectionName}
styles={this.styles()}
settings={settings}
openSection={this.switchSectionPress_}
openSection={this.onJumpToSection_}
width={this.state.sidebarWidth}
/>
);
let currentSection: ReactNode;
if (currentSectionName) {
if (currentSectionName || this.state.searching) {
const settingComps = shared.settingsToComponents2(
this, AppType.Mobile, settings, currentSectionName,
@@ -560,11 +659,20 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
// of React in lib/ and app-mobile/
) as ReactNode[];
const searchInput = <TextInput
value={this.state.searchQuery}
label={_('Search')}
placeholder={_('Search...')}
onChangeText={this.onSearchUpdate_}
autoFocus={true}
/>;
currentSection = (
<ScrollView
ref={this.scrollViewRef_}
style={{ flexGrow: 1 }}
>
{this.state.searching ? searchInput : null}
{settingComps}
</ScrollView>
);
@@ -598,10 +706,11 @@ class ConfigScreenComponent extends BaseScreenComponent<ConfigScreenProps, Confi
<ScreenHeader
title={screenHeadingText}
showSaveButton={true}
showSearchButton={false}
showSearchButton={true}
showSideMenuButton={false}
saveButtonDisabled={!this.state.changedSettingKeys.length}
saveButtonDisabled={!this.hasUnsavedChanges()}
onSaveButtonPress={this.saveButton_press}
onSearchButtonPress={this.onSearchButtonPress_}
/>
{mainComponent}
</View>

View File

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

View File

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

View File

@@ -24,6 +24,9 @@ enum ExportStatus {
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 [exportStatus, setExportStatus] = useState<ExportStatus>(ExportStatus.NotStarted);
const [exportProgress, setExportProgress] = useState<number|undefined>(0);
@@ -80,13 +83,12 @@ const NoteExportButton: FunctionComponent<Props> = props => {
indeterminate={exportProgress === undefined}
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 = (
<SettingsButton
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : _('Export all notes as JEX')}
title={exportStatus === ExportStatus.Exporting ? _('Exporting...') : exportButtonTitle()}
disabled={exportStatus === ExportStatus.Exporting}
description={exportStatus === ExportStatus.NotStarted ? descriptionText : null}
description={exportStatus === ExportStatus.NotStarted ? exportButtonDescription() : null}
statusComponent={progressComponent}
clickHandler={startExport}
styles={props.styles}

View File

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

View File

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

View File

@@ -61,6 +61,9 @@ const emptyArray: any[] = [];
const logger = Logger.create('screens/Note');
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 {
return { header: null };
@@ -81,7 +84,6 @@ class NoteScreenComponent extends BaseScreenComponent {
fromShare: false,
showCamera: false,
showImageEditor: false,
loadImageEditorData: null,
imageEditorResource: null,
noteResources: {},
@@ -851,9 +853,7 @@ class NoteScreenComponent extends BaseScreenComponent {
const filePath = Resource.fullPath(item);
this.setState({
showImageEditor: true,
loadImageEditorData: async () => {
return await shim.fsDriver().readFile(filePath);
},
imageEditorResourceFilepath: filePath,
imageEditorResource: item,
});
}
@@ -1260,6 +1260,10 @@ class NoteScreenComponent extends BaseScreenComponent {
}, 5);
}
private onBodyViewerScroll = (scrollTop: number) => {
this.lastBodyScroll = scrollTop;
};
public onBodyViewerCheckboxChange(newBody: string) {
void this.saveOneProperty('body', newBody);
}
@@ -1302,7 +1306,7 @@ class NoteScreenComponent extends BaseScreenComponent {
return <CameraView themeId={this.props.themeId} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
} else if (this.state.showImageEditor) {
return <ImageEditor
loadInitialSVGData={this.state.loadImageEditorData}
resourceFilename={this.state.imageEditorResourceFilepath}
themeId={this.props.themeId}
onSave={this.onSaveDrawing}
onExit={this.onCloseDrawing}
@@ -1334,6 +1338,8 @@ class NoteScreenComponent extends BaseScreenComponent {
onMarkForDownload={this.onMarkForDownload}
onRequestEditResource={this.onEditResource}
onLoadEnd={this.onBodyViewerLoadEnd}
onScroll={this.onBodyViewerScroll}
initialScroll={this.lastBodyScroll}
/>
);
} else {

View File

@@ -1,17 +1,13 @@
const gulp = require('gulp');
const utils = require('@joplin/tools/gulp/utils');
import { buildInjectedJS, watchInjectedJS } from './tools/buildInjectedJs';
import gulpTasks from './tools/buildInjectedJs/gulpTasks';
const tasks = {
encodeAssets: {
fn: require('./tools/encodeAssets'),
},
buildInjectedJs: {
fn: buildInjectedJS,
},
watchInjectedJs: {
fn: watchInjectedJS,
},
...gulpTasks,
podInstall: {
fn: require('./tools/podInstall'),
},
@@ -19,6 +15,22 @@ const 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(
'buildInjectedJs',
'encodeAssets',

View File

@@ -45,9 +45,6 @@ LogBox.ignoreLogs([
// Apparently it can be safely ignored:
// https://github.com/react-native-webview/react-native-webview/issues/124
'Did not receive response to shouldStartLoad in time, defaulting to YES',
// Emitted by react-native-popup-menu
'MenuContext is deprecated and it might be removed in future releases, use MenuProvider instead.',
]);
AppRegistry.registerComponent('Joplin', () => Root);

View File

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

View File

@@ -359,9 +359,9 @@ PODS:
- React-Core
- react-native-rsa-native (2.0.5):
- React
- react-native-saf-x (2.13.0):
- react-native-saf-x (2.13.2):
- React-Core
- react-native-safe-area-context (4.7.2):
- react-native-safe-area-context (4.7.3):
- React-Core
- react-native-slider (4.4.3):
- React-Core
@@ -786,8 +786,8 @@ SPEC CHECKSUMS:
react-native-image-resizer: 681f7607418b97c084ba2d0999b153b103040d8a
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
react-native-saf-x: b758d1b38a96a96b8179a16e96ed966a507ca5c2
react-native-safe-area-context: 7aa8e6d9d0f3100a820efb1a98af68aa747f9284
react-native-saf-x: a93121b21f9d5ec84d5e7fc99fdeebfbf232920a
react-native-safe-area-context: 238cd8b619e05cb904ccad97ef42e84d1b5ae6ec
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
@@ -826,4 +826,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 3b2cace838120977b5b54871752c9dddf5a11cea
COCOAPODS: 1.14.2
COCOAPODS: 1.12.1

View File

@@ -15,7 +15,7 @@
"test": "jest",
"test-ci": "yarn test",
"watchInjectedJs": "gulp watchInjectedJs",
"postinstall": "jetify && yarn run build"
"postinstall": "jetify"
},
"dependencies": {
"@bam.tech/react-native-image-resizer": "3.0.7",
@@ -62,13 +62,13 @@
"react-native-popup-menu": "0.16.1",
"react-native-quick-actions": "0.3.13",
"react-native-rsa-native": "2.0.5",
"react-native-safe-area-context": "4.7.2",
"react-native-safe-area-context": "4.7.4",
"react-native-securerandom": "1.0.1",
"react-native-share": "9.4.1",
"react-native-side-menu-updated": "1.3.2",
"react-native-sqlite-storage": "6.0.1",
"react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.0.0",
"react-native-vector-icons": "10.0.1",
"react-native-version-info": "1.1.1",
"react-native-vosk": "0.1.12",
"react-native-webview": "13.6.2",
@@ -88,14 +88,14 @@
"@babel/preset-env": "7.20.2",
"@babel/runtime": "7.20.0",
"@joplin/tools": "~2.13",
"@js-draw/material-icons": "1.5.0",
"@js-draw/material-icons": "1.11.2",
"@lezer/highlight": "1.1.4",
"@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",
"@types/fs-extra": "11.0.3",
"@types/jest": "29.5.5",
"@types/react": "18.2.31",
"@types/react": "18.2.33",
"@types/react-native": "0.70.6",
"@types/react-redux": "7.1.28",
"@types/tar-stream": "2.2.3",
@@ -106,14 +106,14 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jetifier": "2.0.0",
"js-draw": "1.5.0",
"js-draw": "1.11.2",
"jsdom": "22.1.0",
"metro-react-native-babel-preset": "0.73.9",
"nodemon": "3.0.1",
"react-test-renderer": "18.2.0",
"sqlite3": "5.1.6",
"ts-jest": "29.1.1",
"ts-loader": "9.4.4",
"ts-loader": "9.5.0",
"ts-node": "10.9.1",
"typescript": "5.2.2",
"uglify-js": "3.17.4",

View File

@@ -1,5 +1,5 @@
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-light.css': { data: require('./highlight.js/atom-one-light.css.base64.js'), mime: 'text/css', encoding: 'base64' },
'katex/fonts/KaTeX_AMS-Regular.woff2': { data: require('./katex/fonts/KaTeX_AMS-Regular.woff2.base64.js'), mime: 'application/octet-stream', encoding: 'base64' },

File diff suppressed because one or more lines are too long

View File

@@ -66,7 +66,7 @@ const { SearchScreen } = require('./components/screens/search.js');
const { OneDriveLoginScreen } = require('./components/screens/onedrive-login.js');
import EncryptionConfigScreen from './components/screens/encryption-config';
const { DropboxLoginScreen } = require('./components/screens/dropbox-login.js');
const { MenuContext } = require('react-native-popup-menu');
import { MenuProvider } from 'react-native-popup-menu';
import SideMenu from './components/SideMenu';
import SideMenuContent from './components/side-menu-content';
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
@@ -1032,10 +1032,13 @@ class AppComponent extends React.Component {
let sideMenuContent: ReactNode = null;
let menuPosition: SideMenuPosition = 'left';
let disableSideMenuGestures = this.props.disableSideMenuGestures;
if (this.props.routeName === 'Note') {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
menuPosition = 'right';
} else if (this.props.routeName === 'Config') {
disableSideMenuGestures = true;
} else {
sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
}
@@ -1076,7 +1079,7 @@ class AppComponent extends React.Component {
openMenuOffset={this.state.sideMenuWidth}
menuPosition={menuPosition}
onChange={(isOpen: boolean) => this.sideMenu_change(isOpen)}
disableGestures={this.props.disableSideMenuGestures}
disableGestures={disableSideMenuGestures}
onSliding={(percent: number) => {
this.props.dispatch({
type: 'SIDE_MENU_OPEN_PERCENT',
@@ -1085,7 +1088,7 @@ class AppComponent extends React.Component {
}}
>
<StatusBar barStyle={statusBarStyle} />
<MenuContext style={{ flex: 1 }}>
<MenuProvider style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 0, backgroundColor: theme.backgroundColor2 }}/>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
@@ -1098,7 +1101,7 @@ class AppComponent extends React.Component {
sensorInfo={this.state.sensorInfo}
/> }
</SafeAreaView>
</MenuContext>
</MenuProvider>
</SideMenu>
</View>
);

View File

@@ -3,30 +3,14 @@
// 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.
import { mkdirp, readFile, writeFile } from 'fs-extra';
import { dirname, extname, basename } from 'path';
// We need this to be transpiled to `const webpack = require('webpack')`.
// As such, do a namespace import. See https://www.typescriptlang.org/tsconfig#esModuleInterop
import * as webpack from 'webpack';
import copyJs from './copyJs';
const rootDir = dirname(dirname(dirname(__dirname)));
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 {
export default class BundledFile {
private readonly bundleOutputPath: string;
private readonly bundleBaseName: string;
private readonly rootFileDirectory: string;
@@ -82,17 +66,23 @@ class BundledFile {
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 {
let failed = false;
if (error) {
console.error(`Error: ${error.name}`, error.message, error.stack);
console.error(`Error (${this.bundleName}): ${error.name}`, error.message, error.stack);
failed = true;
} else if (stats?.hasErrors() || stats?.hasWarnings()) {
const data = stats.toJson();
if (data.warnings && data.warningsCount) {
console.warn('Warnings: ', data.warningsCount);
console.warn(`Warnings (${this.bundleName}): `, data.warningsCount);
for (const warning of data.warnings) {
// Stack contains the message
if (warning.stack) {
@@ -103,7 +93,7 @@ class BundledFile {
}
}
if (data.errors && data.errorsCount) {
console.error('Errors: ', data.errorsCount);
console.error(`Errors (${this.bundleName}): `, data.errorsCount);
for (const error of data.errors) {
if (error.stack) {
console.error(error.stack);
@@ -127,19 +117,34 @@ class BundledFile {
return new Promise<void>((resolve, reject) => {
console.info(`Building bundle: ${this.bundleName}...`);
compiler.run((error, stats) => {
let failed = this.handleErrors(error, stats);
compiler.run((buildError, stats) => {
// Always output stats, even on success
console.log(`Bundle ${this.bundleName} built: `, stats?.toString());
let failed = this.handleErrors(buildError, stats);
// Clean up.
compiler.close(async (error) => {
if (error) {
console.error('Error cleaning up:', error);
compiler.close(async (closeError) => {
if (closeError) {
console.error('Error cleaning up:', closeError);
failed = true;
}
let copyError;
if (!failed) {
try {
await this.copyToImportableFile();
} catch (error) {
console.error('Error copying', error);
failed = true;
copyError = error;
}
}
if (!failed) {
resolve();
} 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

@@ -7,8 +7,6 @@
"exclude": [
//Files that don't need transpilation
"**/node_modules",
"**/*.test.ts",
"**/*.test.tsx",
"gulpfile.ts",
"tools/*.ts",
],

View File

@@ -9,6 +9,7 @@ import * as tar from 'tar-stream';
import { resolve } from 'path';
import { Buffer } from 'buffer';
import Logger from '@joplin/utils/Logger';
import JoplinError from '@joplin/lib/JoplinError';
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') {
if (!handle?.stat) {
throw new JoplinError('File does not exist (reading file chunk).', 'ENOENT');
}
const encoding = normalizeEncoding(rawEncoding);
if (handle.offset + length > handle.stat.size) {

View File

@@ -167,6 +167,18 @@ const testReadFileChunkUtf8 = async (tempDir: string) => {
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) => {

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,17 +12,6 @@ module.exports = class extends Generator {
this.option('silent');
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() {
@@ -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 {
this.fs.copyTpl(
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/)).
```bash
npm install -g yo
npm install -g yo@4.3.1
npm install -g generator-joplin
```

View File

@@ -2,7 +2,7 @@ import { ModelType } from '../../../BaseModel';
import Plugin from '../Plugin';
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.
* 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.
* * `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:
*

View File

@@ -9,7 +9,7 @@ import { ExportModule, ImportModule } from './types';
*
* 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 {
registerExportModule(module: ExportModule): Promise<void>;

View File

@@ -524,13 +524,13 @@ export interface ContentScriptModuleLoadedEvent {
}
export interface ContentScriptModule {
onLoaded?: (event:ContentScriptModuleLoadedEvent) => void;
plugin: () => any;
assets?: () => void;
onLoaded?: (event: ContentScriptModuleLoadedEvent)=> void;
plugin: ()=> any;
assets?: ()=> void;
}
export interface MarkdownItContentScriptModule extends Omit<ContentScriptModule, 'plugin'> {
plugin: (markdownIt:any, options:any) => any;
plugin: (markdownIt: any, options: any)=> any;
}
export enum ContentScriptType {
@@ -552,7 +552,7 @@ export enum ContentScriptType {
* }
* }
* ```
*
*
* See [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* 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",
"prepare": "npm run dist",
"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",
"keywords": [

View File

@@ -93,19 +93,24 @@ function validateCategories(categories) {
function validateScreenshots(screenshots) {
if (!screenshots) return null;
// eslint-disable-next-line github/array-foreach -- Old code before rule was applied
screenshots.forEach(screenshot => {
for (const screenshot of screenshots) {
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();
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
const fileMaxSize = 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`);
});
}
}
function readManifest(manifestPath) {

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ import time from './time';
import BaseSyncTarget from './BaseSyncTarget';
const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware');
const os = require('os');
import dns = require('dns');
import fs = require('fs-extra');
const EventEmitter = require('events');
const syswidecas = require('./vendor/syswide-cas');
@@ -171,6 +172,13 @@ export default class BaseApplication {
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 {
matched: flags.matched,
argv: flags.argv,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -74,6 +74,9 @@
"%s: %s": [
"%s: %s"
],
"%s: Missing password.": [
"%s: Manglende adgangskode."
],
"&Edit": [
"&Redigér"
],
@@ -108,7 +111,7 @@
"(wysiwyg: %s)"
],
"(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.": [
"- Kamera: Tilladelse til fotografering og vedhæftning af et billede til en note."
@@ -218,6 +221,12 @@
"Always": [
"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": [
"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"
],
"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?": [
"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": [
"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": [
"Udseende"
@@ -464,6 +476,9 @@
"Code View": [
"Kodevisning"
],
"Collaborate on a notebook with others": [
"Samarbejd med andre om en notesbog"
],
"Collaborate on notebooks with others": [
"Samarbejd med andre om notesbøger"
],
@@ -488,6 +503,9 @@
"Command palette...": [
"Kommando-udvalg..."
],
"Compact": [
"Kompakt"
],
"Completed": [
"Fuldført"
],
@@ -651,6 +669,12 @@
"Creates a new to-do.": [
"Opretter en ny opgave."
],
"Creating new note...": [
"Opretter ny note..."
],
"Creating new to-do...": [
"Opretter nye gøremål..."
],
"Creating report...": [
"Opretter rapport..."
],
@@ -673,7 +697,7 @@
"Brugerdefinerede TLS certifikater"
],
"Customise the note publishing banner": [
""
"Tilpas banneret til udgivelse af noter"
],
"Cut": [
"Klip"
@@ -793,7 +817,7 @@
"Destinationsformat: %s"
],
"Detailed": [
""
"Detailed"
],
"Directory": [
"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?": [
"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": [
"Kassér ændringer"
],
@@ -892,10 +919,10 @@
"Dracula"
],
"Draw picture": [
""
"Tegn billede"
],
"Drawing": [
""
"Tegner"
],
"Drop notes or files here": [
"Slip noter eller filer her"
@@ -1098,6 +1125,9 @@
"Enter notebook title": [
"Indtast notesbogstitel"
],
"Enter password": [
"Indtast adgangskode"
],
"Enum": [
"Enum"
],
@@ -1267,6 +1297,9 @@
"Opretter link...",
"Opretter links..."
],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [
"Få den nu:"
],
@@ -1334,7 +1367,7 @@
"HTML Indeks"
],
"HTML document": [
""
"HTML-dokument"
],
"HTML File": [
"HTML fil"
@@ -1553,7 +1586,7 @@
"Indstil knap-rækkefølge"
],
"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...": [
"Forlad notesbog..."
@@ -1622,8 +1655,8 @@
"Logout": [
"Log ud"
],
"Logs": [
"Logfiler"
"Logs, profiles, sync status": [
""
],
"Make a donation": [
"Giv en donation"
@@ -1685,6 +1718,9 @@
"Max Total Size": [
"Maks samlet størrelse"
],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [
"Manglende nøgler"
],
@@ -1731,7 +1767,7 @@
"N"
],
"Never resize": [
""
"Ændr aldrig størrelse"
],
"New note": [
"Ny note"
@@ -1862,6 +1898,9 @@
"Note list growth factor": [
"Noteliste-vækstfaktor"
],
"Note list style": [
"Notatliste-stil"
],
"Note properties": [
"Noteegenskaber"
],
@@ -1949,6 +1988,9 @@
"Open...": [
"Åbn..."
],
"Opening section %s": [
""
],
"Operation cancelled": [
"Udførelse annulleret"
],
@@ -2238,7 +2280,7 @@
"Nulstil hovedadgangskode"
],
"Resize large images:": [
""
"Ændr størrelsen på store billeder:"
],
"Resources: %d.": [
"Ressourcer: %d."
@@ -2312,6 +2354,9 @@
"Save changes": [
"Gem ændringer"
],
"Save changes?": [
"Gem ændringer?"
],
"Save geo-location with notes": [
"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.": [
"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": [
"Del notesbog"
],
@@ -2394,7 +2442,7 @@
"Del notesbog..."
],
"Share permissions": [
""
"Deletilladelser"
],
"Sharing notebook...": [
"Deler notesbog..."
@@ -2436,10 +2484,13 @@
"Vis/skjul sidebjælken"
],
"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": [
""
"Sidemenu åbnet"
],
"Sidebar": [
"Sidebjælke"
@@ -2672,6 +2723,9 @@
"Teams": [
"Teams"
],
"Text document": [
"Tekstdokument"
],
"Text editor command": [
"Tekstredigeringskomando"
],
@@ -2765,6 +2819,9 @@
"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."
],
"The synchronisation password is missing.": [
"Adgangskoden til synkronisering mangler."
],
"The tag \"%s\" already exists. Please choose a different name.": [
"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."
],
"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": [
"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.": [
"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.": [
"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.": [
"Tast: %s."
],
"Unable to edit resource of type %s": [
"Kan ikke redigere ressource af typen %s"
],
"Unable to export or share data. Reason: %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": [
"Ufærdige opgaver øverst"
],
@@ -3161,6 +3227,9 @@
"When creating a new to-do:": [
"Ved oprettelse af ny opgave:"
],
"Window unresponsive.": [
""
],
"Words": [
"Ord"
],

View File

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

View File

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

View File

@@ -236,6 +236,9 @@
"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?": [
""
],
@@ -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.": [
""
],
"Donate, website": [
""
],
"Done": [
""
],
@@ -999,6 +1005,9 @@
"Email to Note": [
""
],
"Email To Note, login information": [
""
],
"Emails": [
""
],
@@ -1173,6 +1182,9 @@
"Export profile": [
""
],
"Export your data": [
""
],
"Exported successfully!": [
""
],
@@ -1294,6 +1306,9 @@
"",
""
],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [
""
],
@@ -1567,6 +1582,9 @@
"Language": [
""
],
"Language, date format": [
""
],
"Last error: %s": [
""
],
@@ -1649,7 +1667,7 @@
"Logout": [
""
],
"Logs": [
"Logs, profiles, sync status": [
""
],
"Make a donation": [
@@ -1712,6 +1730,9 @@
"Max Total Size": [
""
],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [
""
],
@@ -1979,6 +2000,9 @@
"Open...": [
""
],
"Opening section %s": [
""
],
"Operation cancelled": [
""
],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [
""
],
"Restart in safe mode": [
""
],
"Restart now": [
""
],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [
""
],
"Send bug report": [
""
],
"Server is already running on port %d": [
""
],
@@ -2654,6 +2684,9 @@
"Sync your notes": [
""
],
"Sync, encryption, proxy": [
""
],
"Synchronisation": [
""
],
@@ -2831,6 +2864,9 @@
"Theme": [
""
],
"Themes, editor font": [
""
],
"There are currently no notes. Create one by clicking on the (+) button.": [
""
],
@@ -2981,6 +3017,9 @@
"Toggle external editing": [
""
],
"Toggle note history, keep notes for": [
""
],
"Toggle note list": [
""
],
@@ -3215,6 +3254,9 @@
"When creating a new to-do:": [
""
],
"Window unresponsive.": [
""
],
"Words": [
""
],

View File

@@ -236,6 +236,9 @@
"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?": [
""
],
@@ -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": [
""
],
@@ -999,6 +1005,9 @@
"Email to Note": [
""
],
"Email To Note, login information": [
""
],
"Emails": [
""
],
@@ -1173,6 +1182,9 @@
"Export profile": [
""
],
"Export your data": [
""
],
"Exported successfully!": [
""
],
@@ -1294,6 +1306,9 @@
"",
""
],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [
""
],
@@ -1567,6 +1582,9 @@
"Language": [
""
],
"Language, date format": [
""
],
"Last error: %s": [
""
],
@@ -1649,7 +1667,7 @@
"Logout": [
""
],
"Logs": [
"Logs, profiles, sync status": [
""
],
"Make a donation": [
@@ -1712,6 +1730,9 @@
"Max Total Size": [
""
],
"Media player, math, diagrams, table of contents": [
""
],
"Missing keys": [
""
],
@@ -1979,6 +2000,9 @@
"Open...": [
""
],
"Opening section %s": [
""
],
"Operation cancelled": [
""
],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [
""
],
"Restart in safe mode": [
""
],
"Restart now": [
""
],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [
""
],
"Send bug report": [
""
],
"Server is already running on port %d": [
""
],
@@ -2654,6 +2684,9 @@
"Sync your notes": [
""
],
"Sync, encryption, proxy": [
""
],
"Synchronisation": [
"Synchronization"
],
@@ -2828,6 +2861,9 @@
"Theme": [
""
],
"Themes, editor font": [
""
],
"There are currently no notes. Create one by clicking on the (+) button.": [
""
],
@@ -2978,6 +3014,9 @@
"Toggle external editing": [
""
],
"Toggle note history, keep notes for": [
""
],
"Toggle note list": [
""
],
@@ -3212,6 +3251,9 @@
"When creating a new to-do:": [
""
],
"Window unresponsive.": [
""
],
"Words": [
""
],

View File

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

View File

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

View File

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

View File

@@ -176,6 +176,9 @@
"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?": [
""
],
@@ -627,6 +630,9 @@
"Do not ask for confirmation.": [
"Ez galdetu berresteko."
],
"Donate, website": [
""
],
"Done": [
""
],
@@ -909,6 +915,9 @@
"Full name": [
""
],
"Geolocation, spellcheck, editor toolbar, image resize": [
""
],
"Get it now:": [
""
],
@@ -1174,7 +1183,7 @@
"Logout": [
""
],
"Logs": [
"Logs, profiles, sync status": [
""
],
"Manage your plugins": [
@@ -1207,6 +1216,9 @@
"Max Item Size": [
""
],
"Media player, math, diagrams, table of contents": [
""
],
"Missing required argument: %s": [
"Beharrezko argumentua faltan: %s"
],
@@ -1366,6 +1378,9 @@
"Open...": [
""
],
"Opening section %s": [
""
],
"Operation cancelled": [
" 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": [
""
],
"Toggle note history, keep notes for": [
""
],
"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`.": [
"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": [
""
],

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

View File

@@ -236,6 +236,9 @@
"An autosaved drawing was found. Attach a copy of it to the 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?": [
"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.": [
"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": [
"Terminé"
],
@@ -999,6 +1005,9 @@
"Email to Note": [
"Conversion email en note"
],
"Email To Note, login information": [
"\"Email to note\", info de connexion"
],
"Emails": [
"Emails"
],
@@ -1173,6 +1182,9 @@
"Export profile": [
"Exporter le profil"
],
"Export your data": [
"Exporter vos données"
],
"Exported successfully!": [
"Exporté avec succès !"
],
@@ -1294,6 +1306,9 @@
"Génération du lien…",
"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:": [
"L'obtenir maintenant :"
],
@@ -1567,6 +1582,9 @@
"Language": [
"Langue"
],
"Language, date format": [
"Langue, format de la date"
],
"Last error: %s": [
"Dernière erreur : %s"
],
@@ -1649,8 +1667,8 @@
"Logout": [
"Se déconnecter"
],
"Logs": [
"Journal"
"Logs, profiles, sync status": [
"Logs, profils, info synchronisation"
],
"Make a donation": [
"Faire un don"
@@ -1712,6 +1730,9 @@
"Max Total Size": [
"Taille max totale"
],
"Media player, math, diagrams, table of contents": [
"Lecteur média, math, diagrammes, sommaire"
],
"Missing keys": [
"Clefs manquantes"
],
@@ -1979,6 +2000,9 @@
"Open...": [
"Ouvrir…"
],
"Opening section %s": [
"Ouverture section %s"
],
"Operation cancelled": [
"Opération annulée"
],
@@ -2276,6 +2300,9 @@
"Restart and upgrade": [
"Redémarrer et mettre à jour"
],
"Restart in safe mode": [
"Redémarrer en mode sans échec"
],
"Restart now": [
"Redémarrer maintenant"
],
@@ -2390,6 +2417,9 @@
"Select parent notebook": [
"Sélectionner le carnet parent"
],
"Send bug report": [
"Envoyer rapport de débogage"
],
"Server is already running on port %d": [
"Le serveur tourne déjà sur le port %d"
],
@@ -2654,6 +2684,9 @@
"Sync your notes": [
"Synchroniser vos notes"
],
"Sync, encryption, proxy": [
"Synchronisation, chiffrement, proxy"
],
"Synchronisation": [
"Synchronisation"
],
@@ -2831,6 +2864,9 @@
"Theme": [
"Apparence"
],
"Themes, editor font": [
"Thèmes, police de l'éditeur"
],
"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 (+)."
],
@@ -2981,6 +3017,9 @@
"Toggle external editing": [
"Basculer l'édition externe"
],
"Toggle note history, keep notes for": [
"Activation historique des notes"
],
"Toggle note list": [
"Basculer liste de notes"
],
@@ -3215,6 +3254,9 @@
"When creating a new to-do:": [
"Lors de la création d'une tâche :"
],
"Window unresponsive.": [
"La fenêtre ne répond pas."
],
"Words": [
"Mots"
],

View File

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

View File

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

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