You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
96 Commits
cli-v2.13.
...
server-v2.
Author | SHA1 | Date | |
---|---|---|---|
|
214f9916d9 | ||
|
87aeffa160 | ||
|
14a2d2d795 | ||
|
3560bc62a2 | ||
|
0729d1db27 | ||
|
5f27d425bf | ||
|
e2956c391d | ||
|
bdc8f30705 | ||
|
2c9bf9f03a | ||
|
60c2964acd | ||
|
97248035b1 | ||
|
35c79a2cfb | ||
|
9b9762f940 | ||
|
e186fe8936 | ||
|
213cd419f0 | ||
|
b89b5fef65 | ||
|
92dccbe98d | ||
|
9b775d77f6 | ||
|
4fd6937d05 | ||
|
7230f0e698 | ||
|
ada82538ee | ||
|
e7dd981db6 | ||
|
767bf9f002 | ||
|
f698068587 | ||
|
d0955b4ca2 | ||
|
18e86a7ba3 | ||
|
f9a1ab4d40 | ||
|
062d0898a0 | ||
|
3b51b4fd72 | ||
|
1a78ff4398 | ||
|
544af8d118 | ||
|
2616c377a9 | ||
|
4a63331306 | ||
|
48621443ec | ||
|
79fd66b94c | ||
|
6a6c8c1d83 | ||
|
cf19dacbaf | ||
|
50925abc40 | ||
|
c80cbaa32f | ||
|
f7cb1aef4b | ||
|
96d5d1dfab | ||
|
98d608fec5 | ||
|
1af46b0246 | ||
|
1e530b74d4 | ||
|
e61c4acce5 | ||
|
184499711d | ||
|
2c0181d097 | ||
|
06ea12adb3 | ||
|
9923e5c821 | ||
|
9a06e59cfe | ||
|
80a2cd91f4 | ||
|
df9ed3e487 | ||
|
368d0130f6 | ||
|
824e1b44dd | ||
|
ccf1c8ee31 | ||
|
d5f6d83f6d | ||
|
5d422f85c8 | ||
|
78aeb46d56 | ||
|
091bf45149 | ||
|
8d9d24740b | ||
|
21e5f88cb2 | ||
|
5d4259d064 | ||
|
9ac03ec33a | ||
|
e760276341 | ||
|
206f35ffe5 | ||
|
ddf716479d | ||
|
ec7f94df25 | ||
|
bcbba0973f | ||
|
bd1ddb8522 | ||
|
fb47398554 | ||
|
10356f4009 | ||
|
ba83fca47a | ||
|
b01295f0fd | ||
|
b928e614cc | ||
|
973b9c354c | ||
|
c12444d6e8 | ||
|
1401d28f82 | ||
|
335269f92d | ||
|
6211606a22 | ||
|
5f7d438ac1 | ||
|
ee2df96cfb | ||
|
692e925997 | ||
|
39803f53a0 | ||
|
b3591808b7 | ||
|
2427677fd5 | ||
|
9a051effcd | ||
|
e6e9f92e01 | ||
|
ca6762c891 | ||
|
76d07beb27 | ||
|
f3daa7f0e4 | ||
|
041ad22443 | ||
|
6cd0938ee4 | ||
|
c3dc30ee5d | ||
|
37c925dcf2 | ||
|
05bd51f85c | ||
|
cfbc37df8d |
@@ -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
|
||||
|
@@ -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
62
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal 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"
|
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
52
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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/
|
||||
-->
|
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
4
.github/scripts/run_ci.sh
vendored
4
.github/scripts/run_ci.sh
vendored
@@ -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
9
.gitignore
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -6,7 +6,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
command: postgres -c work_mem=100000
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
@@ -18,7 +18,7 @@ services:
|
||||
- POSTGRES_PORT=5432
|
||||
- POSTGRES_HOST=localhost
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
|
@@ -19,7 +19,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
|
@@ -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",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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,
|
||||
|
@@ -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');
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
@@ -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;
|
||||
};
|
@@ -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 });
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
@@ -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;
|
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -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",
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -178,6 +178,7 @@ class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
onRequestClose={() => {
|
||||
closeList();
|
||||
}}
|
||||
supportedOrientations={['landscape', 'portrait']}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
accessibilityElementsHidden={true}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
]);
|
||||
}
|
||||
|
@@ -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 })}
|
||||
|
@@ -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}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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).
|
||||
|
@@ -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() {
|
||||
|
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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'}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
|
@@ -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)";
|
||||
|
@@ -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
|
||||
|
@@ -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",
|
||||
|
@@ -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
@@ -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>
|
||||
);
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
6
packages/app-mobile/tools/buildInjectedJs/constants.ts
Normal file
6
packages/app-mobile/tools/buildInjectedJs/constants.ts
Normal 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));
|
16
packages/app-mobile/tools/buildInjectedJs/copyJs.ts
Normal file
16
packages/app-mobile/tools/buildInjectedJs/copyJs.ts
Normal 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;
|
38
packages/app-mobile/tools/buildInjectedJs/gulpTasks.ts
Normal file
38
packages/app-mobile/tools/buildInjectedJs/gulpTasks.ts
Normal 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;
|
@@ -7,8 +7,6 @@
|
||||
"exclude": [
|
||||
//Files that don't need transpilation
|
||||
"**/node_modules",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"gulpfile.ts",
|
||||
"tools/*.ts",
|
||||
],
|
||||
|
@@ -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) {
|
||||
|
@@ -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) => {
|
||||
|
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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"
|
||||
|
@@ -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": {
|
||||
|
@@ -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),
|
||||
|
@@ -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
|
||||
```
|
||||
|
||||
|
@@ -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:
|
||||
*
|
||||
|
@@ -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>;
|
||||
|
@@ -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.
|
||||
|
@@ -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": [
|
||||
|
@@ -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) {
|
||||
|
@@ -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": {
|
||||
|
@@ -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",
|
||||
|
@@ -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,
|
||||
|
@@ -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: '**',
|
||||
|
@@ -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": [
|
||||
"كلمات"
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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": [
|
||||
"Λέξεις"
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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
@@ -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"
|
||||
],
|
||||
|
@@ -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"
|
||||
],
|
||||
|
@@ -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": [
|
||||
""
|
||||
],
|
||||
|
@@ -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
Reference in New Issue
Block a user