Compare commits
110 Commits
android-v3
...
v3.6.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5d9646908 | ||
|
|
876ec80911 | ||
|
|
4051f88ce7 | ||
|
|
f194c111e4 | ||
|
|
e386246bc9 | ||
|
|
292b269f1d | ||
|
|
b2fc43da2b | ||
|
|
4a23a1ed3e | ||
|
|
c8878a18bf | ||
|
|
340fba7af5 | ||
|
|
271c4f4a2a | ||
|
|
c9dba20f59 | ||
|
|
b474cc206a | ||
|
|
9d4df8cc6e | ||
|
|
a4ddfe1f58 | ||
|
|
7d15215e66 | ||
|
|
5b74e206ed | ||
|
|
9873d02b0b | ||
|
|
57b7d98d8a | ||
|
|
f075b561a2 | ||
|
|
483d051de0 | ||
|
|
106cd2778f | ||
|
|
c3aea2db80 | ||
|
|
3f067b0f77 | ||
|
|
15cf025bc2 | ||
|
|
4677586e3b | ||
|
|
b8c5b7a153 | ||
|
|
e46e634c2e | ||
|
|
b3cf4e5a35 | ||
|
|
8589e10d6e | ||
|
|
18942f0d6a | ||
|
|
3be354cdcb | ||
|
|
0575f1aa3e | ||
|
|
caa9baa460 | ||
|
|
b5284804d8 | ||
|
|
6053b4296c | ||
|
|
615fec1d2c | ||
|
|
0bbcd9a59b | ||
|
|
6931b32f17 | ||
|
|
17ac501ddb | ||
|
|
94161c5f93 | ||
|
|
196255e960 | ||
|
|
f936390ee4 | ||
|
|
5638c4b812 | ||
|
|
4222caa423 | ||
|
|
bc705acc5c | ||
|
|
f1c968c19a | ||
|
|
26c5a6181e | ||
|
|
a3bf0cfdeb | ||
|
|
606b397326 | ||
|
|
fbd157283d | ||
|
|
2e879f65fc | ||
|
|
c727156a46 | ||
|
|
4e31f1918d | ||
|
|
a1cdf67779 | ||
|
|
5cb1db197f | ||
|
|
05c3065c72 | ||
|
|
25a5be09bf | ||
|
|
f0a3f73ddb | ||
|
|
1bb5d9ade5 | ||
|
|
e75875c1b0 | ||
|
|
cce4b76e3f | ||
|
|
b310bfd0c2 | ||
|
|
e19e1ac040 | ||
|
|
3bba2f6b2a | ||
|
|
ca9addcda0 | ||
|
|
c42a49c1cf | ||
|
|
a1e056670d | ||
|
|
6d7a70c21a | ||
|
|
14fd3c66c1 | ||
|
|
376f44a0ce | ||
|
|
4d81ee4c7f | ||
|
|
d9011800b2 | ||
|
|
e64f141b28 | ||
|
|
8bba68d920 | ||
|
|
e342f2d572 | ||
|
|
5951a66fef | ||
|
|
04f9bda128 | ||
|
|
7a8a94f557 | ||
|
|
ad000fb521 | ||
|
|
435b896142 | ||
|
|
b12f31c802 | ||
|
|
ddb6d7a677 | ||
|
|
f0a1d05284 | ||
|
|
27f7cb7ca6 | ||
|
|
9e43ebcf43 | ||
|
|
05cc0fa798 | ||
|
|
ee5b631d13 | ||
|
|
e4b6b34d37 | ||
|
|
6f1280f0f5 | ||
|
|
4c9015dab4 | ||
|
|
1adcafce9d | ||
|
|
cc9f55e115 | ||
|
|
e8b3b039df | ||
|
|
d9295a69d1 | ||
|
|
b92743b068 | ||
|
|
03f65a3fb1 | ||
|
|
32a22174f7 | ||
|
|
d154ef4f5c | ||
|
|
b8dd660c28 | ||
|
|
2b20315bf5 | ||
|
|
93b9108832 | ||
|
|
0538bf0720 | ||
|
|
54018c3a94 | ||
|
|
0cb120c321 | ||
|
|
0dab436420 | ||
|
|
77331ca471 | ||
|
|
d467205b91 | ||
|
|
7a2f686228 | ||
|
|
fa37b87c98 |
@@ -115,6 +115,7 @@ packages/app-cli/app/command-export.js
|
||||
packages/app-cli/app/command-geoloc.js
|
||||
packages/app-cli/app/command-help.js
|
||||
packages/app-cli/app/command-import.js
|
||||
packages/app-cli/app/command-keymap.js
|
||||
packages/app-cli/app/command-ls.js
|
||||
packages/app-cli/app/command-mkbook.test.js
|
||||
packages/app-cli/app/command-mkbook.js
|
||||
@@ -1060,6 +1061,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1100,6 +1103,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1227,6 +1231,7 @@ packages/lib/InMemoryCache.js
|
||||
packages/lib/JoplinDatabase.js
|
||||
packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.test.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
@@ -1803,6 +1808,7 @@ packages/renderer/MdToHtml/renderMedia.js
|
||||
packages/renderer/MdToHtml/rules/abc.js
|
||||
packages/renderer/MdToHtml/rules/checkbox.js
|
||||
packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
@@ -1838,17 +1844,19 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
|
||||
40
.github/workflows/build-macos-m1.yml
vendored
@@ -48,6 +48,7 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
IS_CONTINUOUS_INTEGRATION: 1
|
||||
BUILD_SEQUENCIAL: 1
|
||||
PUBLISH_ENABLED: ${{ env.PUBLISH_ENABLED }}
|
||||
@@ -57,25 +58,38 @@ jobs:
|
||||
yarn install
|
||||
cd packages/app-desktop
|
||||
npm pkg set 'build.mac.artifactName'='${productName}-${version}-${arch}.${ext}'
|
||||
|
||||
npm pkg delete 'build.mac.target'
|
||||
npm pkg set 'build.mac.target[0].target'='dmg'
|
||||
npm pkg set 'build.mac.target[0].arch[0]'='arm64'
|
||||
npm pkg set 'build.mac.target[1].target'='zip'
|
||||
npm pkg set 'build.mac.target[1].arch[0]'='arm64'
|
||||
|
||||
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
|
||||
# Only enable pkg build in the main repository CI. As of 01/15/2026, pkg
|
||||
# build fails when running on external pull requests.
|
||||
if [[ "$GITHUB_EVENT_NAME" != "pull_request" ]]; then
|
||||
npm pkg set 'build.mac.target[2].target'='pkg'
|
||||
npm pkg set 'build.mac.target[2].arch[0]'='arm64'
|
||||
fi
|
||||
|
||||
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
build_dist() {
|
||||
if [[ "$PUBLISH_ENABLED" == "true" ]]; then
|
||||
echo "Building and publishing desktop application..."
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64
|
||||
|
||||
# We also want to disable signing the app in this case, because
|
||||
# it doesn't work and we don't need it.
|
||||
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
|
||||
yarn modifyReleaseAssets --repo="$GH_REPO" --tag="$GIT_TAG_NAME" --token="$GITHUB_TOKEN"
|
||||
else
|
||||
echo "Building but *not* publishing desktop application..."
|
||||
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npm pkg set 'build.mac.identity'=null --json
|
||||
# We also want to disable signing the app in this case, because
|
||||
# it doesn't work and we don't need it.
|
||||
# https://www.electron.build/code-signing#how-to-disable-code-signing-during-the-build-process-on-macos
|
||||
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
|
||||
fi
|
||||
export CSC_IDENTITY_AUTO_DISCOVERY=false
|
||||
npm pkg set 'build.mac.identity'=null --json
|
||||
|
||||
PYTHON_PATH=$(which python) USE_HARD_LINKS=false yarn dist --mac --arm64 --publish=never
|
||||
fi
|
||||
}
|
||||
|
||||
build_dist || build_dist
|
||||
11
.gitignore
vendored
@@ -53,6 +53,7 @@ lerna-debug.log
|
||||
docs/**/*.mustache
|
||||
.idea
|
||||
/readme/i18n
|
||||
.watchman-cookie-*
|
||||
|
||||
# Yarn stuff
|
||||
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||
@@ -88,6 +89,7 @@ packages/app-cli/app/command-export.js
|
||||
packages/app-cli/app/command-geoloc.js
|
||||
packages/app-cli/app/command-help.js
|
||||
packages/app-cli/app/command-import.js
|
||||
packages/app-cli/app/command-keymap.js
|
||||
packages/app-cli/app/command-ls.js
|
||||
packages/app-cli/app/command-mkbook.test.js
|
||||
packages/app-cli/app/command-mkbook.js
|
||||
@@ -1033,6 +1035,8 @@ packages/editor/CodeMirror/extensions/rendering/replaceBulletLists.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceCheckboxes.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceDividers.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceFormatCharacters.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.test.js
|
||||
packages/editor/CodeMirror/extensions/rendering/replaceInlineHtml.js
|
||||
packages/editor/CodeMirror/extensions/rendering/types.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeBlockReplaceExtension.js
|
||||
packages/editor/CodeMirror/extensions/rendering/utils/makeInlineReplaceExtension.js
|
||||
@@ -1073,6 +1077,7 @@ packages/editor/CodeMirror/utils/getSearchState.js
|
||||
packages/editor/CodeMirror/utils/growSelectionToNode.js
|
||||
packages/editor/CodeMirror/utils/handleLinkEditRequests.js
|
||||
packages/editor/CodeMirror/utils/handlePasteEvent.js
|
||||
packages/editor/CodeMirror/utils/htmlNodeInfo.js
|
||||
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
|
||||
packages/editor/CodeMirror/utils/isInSyntaxNode.js
|
||||
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
|
||||
@@ -1200,6 +1205,7 @@ packages/lib/InMemoryCache.js
|
||||
packages/lib/JoplinDatabase.js
|
||||
packages/lib/JoplinError.js
|
||||
packages/lib/JoplinServerApi.js
|
||||
packages/lib/ObjectUtils.test.js
|
||||
packages/lib/ObjectUtils.js
|
||||
packages/lib/PerformanceLogger.test.js
|
||||
packages/lib/PerformanceLogger.js
|
||||
@@ -1776,6 +1782,7 @@ packages/renderer/MdToHtml/renderMedia.js
|
||||
packages/renderer/MdToHtml/rules/abc.js
|
||||
packages/renderer/MdToHtml/rules/checkbox.js
|
||||
packages/renderer/MdToHtml/rules/code_inline.js
|
||||
packages/renderer/MdToHtml/rules/externalEmbed.js
|
||||
packages/renderer/MdToHtml/rules/fence.js
|
||||
packages/renderer/MdToHtml/rules/fountain.js
|
||||
packages/renderer/MdToHtml/rules/highlight_keywords.js
|
||||
@@ -1811,17 +1818,19 @@ packages/tools/checkIgnoredFiles.js
|
||||
packages/tools/checkLibPaths.test.js
|
||||
packages/tools/checkLibPaths.js
|
||||
packages/tools/convertThemesToCss.js
|
||||
packages/tools/fuzzer/ActionRunner.js
|
||||
packages/tools/fuzzer/ActionTracker.js
|
||||
packages/tools/fuzzer/Client.js
|
||||
packages/tools/fuzzer/ClientPool.js
|
||||
packages/tools/fuzzer/Server.js
|
||||
packages/tools/fuzzer/constants.js
|
||||
packages/tools/fuzzer/doRandomAction.js
|
||||
packages/tools/fuzzer/model/FolderRecord.js
|
||||
packages/tools/fuzzer/sync-fuzzer.js
|
||||
packages/tools/fuzzer/types.js
|
||||
packages/tools/fuzzer/utils/ProgressBar.js
|
||||
packages/tools/fuzzer/utils/SeededRandom.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.test.js
|
||||
packages/tools/fuzzer/utils/diffSortedStringArrays.js
|
||||
packages/tools/fuzzer/utils/getNumberProperty.js
|
||||
packages/tools/fuzzer/utils/getProperty.js
|
||||
packages/tools/fuzzer/utils/getStringProperty.js
|
||||
|
||||
BIN
Assets/WebsiteAssets/images/news/20260111-abc.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-lowercase-tags.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-mobile-tags.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-multi-select.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-profiles.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-rte1.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
Assets/WebsiteAssets/images/news/20260111-rte2.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
@@ -1,4 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Mon, 22 Sep 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Joplin]]></title><description><![CDATA[Joplin, the open source note-taking application]]></description><link>https://joplinapp.org</link><generator>RSS for Node</generator><lastBuildDate>Sun, 11 Jan 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://joplinapp.org/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><item><title><![CDATA[What's new in Joplin 3.5]]></title><description><![CDATA[<h2>Improvements across desktop and mobile<a name="improvements-across-desktop-and-mobile" href="#improvements-across-desktop-and-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>More stable and consistent Markdown editing<a name="more-stable-and-consistent-markdown-editing" href="#more-stable-and-consistent-markdown-editing" class="heading-anchor">🔗</a></h3>
|
||||
<p>The Markdown editor has been refined to feel more stable and closer to the final rendered view. Headings in the editor now more closely match how they appear when viewing a note, reducing the visual jump between editing and reading. Layout issues have also been addressed so elements like rendered checkboxes and images no longer cause the editor to shift unexpectedly while typing.</p>
|
||||
<p>The ABC music notation plugin appeared to be popular but had some limitations. With this new version, ABC is now part of the app, which means it can now work from published notes, and from the Rich Text editor!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-abc.png" alt="ABC music notation rendered directly in Joplin, showing a short musical phrase displayed from plain-text ABC syntax"></p>
|
||||
<h3>Smoother switching between notes<a name="smoother-switching-between-notes" href="#smoother-switching-between-notes" class="heading-anchor">🔗</a></h3>
|
||||
<p>Switching between notes is now less disruptive. Joplin restores cursor position and scroll location more reliably, making it easier to move back and forth between notes—especially when working with longer documents or comparing content—without losing your place.</p>
|
||||
<h3>Case insensitive tags<a name="case-insensitive-tags" href="#case-insensitive-tags" class="heading-anchor">🔗</a></h3>
|
||||
<p>Tags are now treated in a case-insensitive way, which helps prevent duplicate tags caused by differences in capitalisation, while still allowing mixed-case tag names. All this time we were hoping that @dpoulton <a href="https://discourse.joplinapp.org/t/tags-lower-case-only/4220/106">would just get used to lowercase tags</a>, but 5 years later it looks like it's not happening ;) So thank you @mrjo118 for implementing it!</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-lowercase-tags.png" alt="Joplin tag list demonstrating case-insensitive tags, with mixed-case tag names merged into a single tag."></p>
|
||||
<h3>More reliable syncing and sharing<a name="more-reliable-syncing-and-sharing" href="#more-reliable-syncing-and-sharing" class="heading-anchor">🔗</a></h3>
|
||||
<p>Syncing and sharing have been made more robust in everyday use. Joplin now handles repeated syncs more efficiently, avoids unnecessary data usage, and is better at detecting and syncing all changes, particularly when using WebDAV and S3 sync targets.</p>
|
||||
<p>Moreover filesystem synchronisation is now more reliable, in particular when used alongside tools like SyncThing on both mobile and desktop.</p>
|
||||
<h3>Accessibility and readability improvements<a name="accessibility-and-readability-improvements" href="#accessibility-and-readability-improvements" class="heading-anchor">🔗</a></h3>
|
||||
<p>Accessibility has seen further refinements in this release. Dark mode readability has been improved, common editor elements are clearer, and animations are reduced or disabled when system “reduce motion” settings are enabled, making the app more comfortable to use for a wider range of users. Keyboard navigation has also been improved on the desktop application.</p>
|
||||
<h2>Desktop-specific improvements<a name="desktop-specific-improvements" href="#desktop-specific-improvements" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Easier profile management<a name="easier-profile-management" href="#easier-profile-management" class="heading-anchor">🔗</a></h3>
|
||||
<p>Managing multiple profiles on desktop is now simpler thanks to a new, more user-friendly profile management interface. This removes the need to manually edit configuration files and makes switching between different setups easier and safer.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-profiles.png" alt="Desktop profile management screen in Joplin showing multiple profiles with options to rename or delete them."></p>
|
||||
<h3>Significantly improved OneNote import<a name="significantly-improved-onenote-import" href="#significantly-improved-onenote-import" class="heading-anchor">🔗</a></h3>
|
||||
<p>Importing content from OneNote is now more reliable and accurate. Support has been expanded to cover more OneNote file formats, and many edge cases have been addressed so imported notes more closely match their original structure and content. This makes migrating from OneNote to Joplin smoother and more trustworthy.</p>
|
||||
<h3>Better tools for organising large note collections<a name="better-tools-for-organising-large-note-collections" href="#better-tools-for-organising-large-note-collections" class="heading-anchor">🔗</a></h3>
|
||||
<p>Desktop users can now select multiple notebooks at once, making it easier to reorganise notebook structures, move groups of notes, or clean up larger collections without working notebook by notebook.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-multi-select.png" alt="Joplin desktop sidebar with several notebooks selected at the same time for bulk organisation."></p>
|
||||
<h3>Polished editing experience on desktop<a name="polished-editing-experience-on-desktop" href="#polished-editing-experience-on-desktop" class="heading-anchor">🔗</a></h3>
|
||||
<p>Both the Markdown and Rich Text editors have been further refined. Cursor behaviour is more predictable, visual consistency between editing and viewing has improved, and several layout and rendering issues have been fixed to reduce interruptions while writing.</p>
|
||||
<h3>More reliable search and navigation<a name="more-reliable-search-and-navigation" href="#more-reliable-search-and-navigation" class="heading-anchor">🔗</a></h3>
|
||||
<p>Search and navigation on desktop have been improved with fixes that ensure search results behave consistently and remain visible when moving between windows or views.</p>
|
||||
<h3>Improved math support in WebClipper<a name="improved-math-support-in-webclipper" href="#improved-math-support-in-webclipper" class="heading-anchor">🔗</a></h3>
|
||||
<p>The WebClipper is not forgotten in this release - clipping certain math formulas, in particular from Wikipedia but also other websites, has been improved. Additionally, certain scientific articles are now also better handled by the WebClipper.</p>
|
||||
<h2>Mobile-specific improvements<a name="mobile-specific-improvements" href="#mobile-specific-improvements" class="heading-anchor">🔗</a></h2>
|
||||
<h3>A more powerful Rich Text Editor on mobile<a name="a-more-powerful-rich-text-editor-on-mobile" href="#a-more-powerful-rich-text-editor-on-mobile" class="heading-anchor">🔗</a></h3>
|
||||
<p>The mobile Rich Text Editor continues to improve, with new and expanded support for tables, code blocks, and other structured content. These changes make it easier to create and edit more complex notes directly on mobile devices.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-rte1.png" alt="Joplin mobile Rich Text Editor showing table editing controls and an embedded code block inside a note."></p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-rte2.png" alt="Mobile code block editor in Joplin with a Python code snippet displayed in an editable dialog."></p>
|
||||
<h3>Easier tag management on mobile<a name="easier-tag-management-on-mobile" href="#easier-tag-management-on-mobile" class="heading-anchor">🔗</a></h3>
|
||||
<p>Managing tags on mobile is now more practical. You can rename and delete tags directly from the app, and searching through tags is easier, helping keep large tag lists organised over time.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20260111-mobile-tags.png" alt="Joplin mobile tag management screen showing a tag options menu with rename and delete actions."></p>
|
||||
<h3>Improved stability and usability on mobile devices<a name="improved-stability-and-usability-on-mobile-devices" href="#improved-stability-and-usability-on-mobile-devices" class="heading-anchor">🔗</a></h3>
|
||||
<p>Several fixes improve overall stability and usability on mobile, particularly on smaller screens. Issues causing UI elements to appear off-screen have been addressed, and the app behaves more consistently in situations that previously caused hangs or visual glitches.</p>
|
||||
<h2>Bug fixes and security fixes across platforms<a name="bug-fixes-and-security-fixes-across-platforms" href="#bug-fixes-and-security-fixes-across-platforms" class="heading-anchor">🔗</a></h2>
|
||||
<h3>A large number of stability, correctness and security fixes<a name="a-large-number-of-stability-correctness-and-security-fixes" href="#a-large-number-of-stability-correctness-and-security-fixes" class="heading-anchor">🔗</a></h3>
|
||||
<p>Joplin 3.5 includes about 114 bug fixes across desktop and mobile, addressing issues in editing, syncing, importing, rendering, and general stability. Many fixes target edge cases that could lead to crashes, inconsistent behaviour, or rare data loss scenarios. Moreover, this version includes several vulnerability fixes to make the applications more secure.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20260111-release-3-5</link><guid isPermaLink="false">20260111-release-3-5</guid><pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate><twitter-text></twitter-text></item><item><title><![CDATA[What's new in Joplin 3.4]]></title><description><![CDATA[<p>Joplin 3.4 includes many bug fixes and improvements, with a focus on the mobile app.</p>
|
||||
<h2>Mobile<a name="mobile" href="#mobile" class="heading-anchor">🔗</a></h2>
|
||||
<h3>Rich Text Editor<a name="rich-text-editor" href="#rich-text-editor" class="heading-anchor">🔗</a></h3>
|
||||
<p>The mobile app now includes a beta <a href="https://joplinapp.org/help/apps/rich_text_editor">Rich Text Editor</a>! The new editor renders formatting/math/images within the editor:</p>
|
||||
@@ -481,42 +524,4 @@ sys 0m38.013s</p>
|
||||
<p>This is a bit of an extra constraint but it is hard to avoid. Contributor License Agreements are very common for GPL or AGPL projects. For example Apache, Canonical or Python all require their contributors to sign a CLA.</p>
|
||||
<h2>Questions?<a name="questions" href="#questions" class="heading-anchor">🔗</a></h2>
|
||||
<p>If you have any questions please let us know. Overall we believe this is a positive improvements for Joplin as it means any work derives from it will also benefit the project.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item><item><title><![CDATA[What's new in Joplin 2.9]]></title><description><![CDATA[<h2>Proxy support<a name="proxy-support" href="#proxy-support" class="heading-anchor">🔗</a></h2>
|
||||
<p>Both the desktop and mobile application now support proxies thanks to the work of Jason Williams. This will allow you to use the apps in particular when you are behind a company proxy.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-proxy-support.png" alt=""></p>
|
||||
<h2>New PDF viewer<a name="new-pdf-viewer" href="#new-pdf-viewer" class="heading-anchor">🔗</a></h2>
|
||||
<p>The desktop application now features a new PDF viewer thanks to the work of Asrient during GSoC.</p>
|
||||
<p>The main advantage for now is that this viewer preserves the last PDF page that was read. In the next version, the viewer will also include a way to annotate PDF files.</p>
|
||||
<h2>Multi-language spell checking<a name="multi-language-spell-checking" href="#multi-language-spell-checking" class="heading-anchor">🔗</a></h2>
|
||||
<p>The desktop app include a multi-language spell checking features, which allows you, for example, to spell-check notes in your native language and in English.</p>
|
||||
<h2>New mobile text editor<a name="new-mobile-text-editor" href="#new-mobile-text-editor" class="heading-anchor">🔗</a></h2>
|
||||
<p>Writing formatted notes on mobile has always been cumbersome due to the need to enter special format characters like <code>*</code> or <code>[</code>, etc.</p>
|
||||
<p>Thanks to the work of Henry Heino during GSoC, writing notes on the go is now easier thanks to an improved Markdown editor.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-mobile-beta-editor.png" alt=""></p>
|
||||
<p>The most visible feature is the addition of a toolbar, which helps input those special characters, like on desktop.</p>
|
||||
<p>Moreover Henry made a lot of subtle but useful improvements to the editor, for example to improve the note appearance, to improve list continuation, etc. Search within a note is now also supported as well as spell-checking.</p>
|
||||
<p>At a more technical level, Henry also added many test units to ensure that the editor remains robust and reliable.</p>
|
||||
<p>To enable the feature, go to the configuration screen and selected "Opt-in to the editor beta". It is already very stable so we will probably promote it to be the main editor from the next version.</p>
|
||||
<h2>Improved alignment of notebook icons<a name="improved-alignment-of-notebook-icons" href="#improved-alignment-of-notebook-icons" class="heading-anchor">🔗</a></h2>
|
||||
<p>Previously, when you would assign an icon to a notebook, it would shift the title to the right, but notebook without an icon would not. It means that notebooks with and without an icon would not be vertically aligned.</p>
|
||||
<p>To tidy things up, this new version adds a default icons to notebooks without an explicitly assigned icon. This result in the notebook titles being correctly vertically aligned.</p>
|
||||
<p>Note that this feature is only enabled if you use custom icons - otherwise it will simply display the notebook titles without any default icons, as before.</p>
|
||||
<p><img src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/news/20221216-notebook-icons.png" alt=""></p>
|
||||
<h2>Improved handling of file attachments<a name="improved-handling-of-file-attachments" href="#improved-handling-of-file-attachments" class="heading-anchor">🔗</a></h2>
|
||||
<p>Self Not Found made a number of small but useful improvements to attachment handling, including increasing the maximum size to 200MB, adding support for attaching multiple files, and fixing issues with synchronising attachments via proxy.</p>
|
||||
<h2>Fixed filesystem sync on mobile<a name="fixed-filesystem-sync-on-mobile" href="#fixed-filesystem-sync-on-mobile" class="heading-anchor">🔗</a></h2>
|
||||
<p>This was a long and complex change due to the need to support new Android APIs but hopefully that should now be working again, thanks to the work of jd1378.</p>
|
||||
<p>So you can now sync again your notes with Syncthing and other file-based synchronisation systems.</p>
|
||||
<h2>And more...<a name="and-more" href="#and-more" class="heading-anchor">🔗</a></h2>
|
||||
<p>In total this new desktop version includes 36 improvements, bug fixes, and security fixes.</p>
|
||||
<p>As always, a lot of work went into the Android and iOS app too, which include 37 improvements, bug fixes, and security fixes.</p>
|
||||
<p>See here for the changelogs:</p>
|
||||
<ul>
|
||||
<li><a href="https://joplinapp.org/help/about/changelog/desktop">Desktop app changelog</a></li>
|
||||
<li><a href="https://joplinapp.org/help/about/changelog/android/">Android app changelog</a></li>
|
||||
</ul>
|
||||
<h2>About the Android version<a name="about-the-android-version" href="#about-the-android-version" class="heading-anchor">🔗</a></h2>
|
||||
<p>Unfortunately we cannot publish the Android version because it is based on a framework version that Google does not accept. To upgrade the app a lot of changes are needed and another round of pre-releases, and therefore there will not be a 2.9 version for Google Play. You may however download the official APK directly from there: <a href="https://github.com/laurent22/joplin-android/releases/tag/android-v2.9.8">Android 2.9 Official Release</a></p>
|
||||
<p>This is the reality of app stores in general - small developers being imposed never ending new requirements by all-powerful companies, and by the time a version is finally ready we can't even publish it because yet more requirements are in place.</p>
|
||||
<p>For the record the current 2.9 app works perfectly fine. It targets Android 11, which is only 2 years old and is still supported (and installed on millions of phones). Google requires us to target Android 12 which only came out last year.</p>
|
||||
]]></description><link>https://joplinapp.org/news/20221216-release-2-9</link><guid isPermaLink="false">20221216-release-2-9</guid><pubDate>Fri, 16 Dec 2022 00:00:00 GMT</pubDate><twitter-text>What's new in Joplin 2.9</twitter-text></item></channel></rss>
|
||||
]]></description><link>https://joplinapp.org/news/20221221-agpl</link><guid isPermaLink="false">20221221-agpl</guid><pubDate>Wed, 21 Dec 2022 00:00:00 GMT</pubDate><twitter-text>Joplin is switching to the GNU Affero General Public License v3 (AGPL-3.0)</twitter-text></item></channel></rss>
|
||||
@@ -11,11 +11,6 @@
|
||||
},
|
||||
"nodejs": "24.5.0",
|
||||
"pkg-config": "latest",
|
||||
"darwin.apple_sdk.frameworks.Foundation": { // satisfies missing CoreText/CoreText.h
|
||||
// https://github.com/NixOS/nixpkgs/blob/master/pkgs/os-specific/darwin/apple-sdk/default.nix
|
||||
"version": "",
|
||||
"platforms": ["aarch64-darwin", "x86_64-darwin"],
|
||||
},
|
||||
"python": "3.13.3",
|
||||
"bat": "latest",
|
||||
"electron": {
|
||||
|
||||
6
jest.config.base.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// This is the base Jest configuration - all
|
||||
// jest.config.js files should inherit from it.
|
||||
|
||||
module.exports = {
|
||||
watchman: false,
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
"./packages/app-cli/**/*.mo": true,
|
||||
"./packages/app-cli/**/build/": true,
|
||||
"./packages/app-cli/**/config.json": true,
|
||||
"**/.watchman-cookie-*": true,
|
||||
"./packages/app-cli/**/linkToLocal.sh": true,
|
||||
"./packages/app-cli/**/node_modules/": true,
|
||||
"./packages/app-cli/**/out.txt": true,
|
||||
|
||||
49
packages/app-cli/app/command-keymap.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import BaseCommand from './base-command';
|
||||
import app from './app';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
interface Args { }
|
||||
|
||||
class Command extends BaseCommand {
|
||||
public override usage() {
|
||||
return 'keymap';
|
||||
}
|
||||
|
||||
public override description() {
|
||||
return _('Displays the configured keyboard shortcuts.');
|
||||
}
|
||||
|
||||
public override compatibleUis() {
|
||||
return ['cli', 'gui'];
|
||||
}
|
||||
|
||||
public override async action(_args: Args) {
|
||||
const keymaps = await app().loadKeymaps();
|
||||
|
||||
this.stdout(_('Configured keyboard shortcuts:'));
|
||||
this.stdout('\n');
|
||||
|
||||
const rows = [];
|
||||
const padding = ' ';
|
||||
|
||||
rows.push([`${padding}${_('KEYS')}`, _('TYPE'), _('COMMAND')]);
|
||||
rows.push([`${padding}----`, '----', '-------']);
|
||||
|
||||
for (const item of keymaps) {
|
||||
const formattedKeys = item.keys
|
||||
.map((k: string) => (k === ' ' ? `(${_('SPACE')})` : k))
|
||||
.join(', ');
|
||||
rows.push([padding + formattedKeys, item.type, item.command]);
|
||||
}
|
||||
|
||||
cliUtils.printArray(this.stdout.bind(this), rows);
|
||||
|
||||
if (app().gui() && !app().gui().isDummy()) {
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Command;
|
||||
@@ -24,7 +24,10 @@
|
||||
// 4. Remove tests one by one to narrow it down to the one with the async
|
||||
// call that's causing problem.
|
||||
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
testMatch: [
|
||||
'**/tests/HtmlToHtml.js',
|
||||
'**/tests/HtmlToMd.js',
|
||||
|
||||
@@ -35,15 +35,15 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"aws-sdk": "2.1340.0",
|
||||
"chalk": "4.1.2",
|
||||
"compare-version": "0.1.2",
|
||||
@@ -70,7 +70,7 @@
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.130",
|
||||
|
||||
10
packages/app-cli/tests/md_to_html/abc.html
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
<div class="joplin-editable joplin-abc-notation">
|
||||
<pre class="joplin-source" data-abc-options="{"responsive":"resize"}" data-joplin-language="abc" data-joplin-source-open="```abc " data-joplin-source-close=" ``` ">{responsive:'resize'}
|
||||
---
|
||||
K:F
|
||||
!f!(fgag-g2c2)|</pre>
|
||||
<pre class="joplin-rendered joplin-abc-notation-rendered">K:F
|
||||
!f!(fgag-g2c2)|</pre>
|
||||
</div>
|
||||
|
||||
6
packages/app-cli/tests/md_to_html/abc.md
Normal file
@@ -0,0 +1,6 @@
|
||||
```abc
|
||||
{ responsive: 'resize' }
|
||||
---
|
||||
K:F
|
||||
!f!(fgag-g2c2)|
|
||||
```
|
||||
8
packages/app-cli/tests/md_to_html/external_embed.html
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
<div class="joplin-editable">
|
||||
<span class="joplin-source" data-joplin-source-open="" data-joplin-source-close="">https://www.youtube.com/watch?v=iJqe9pC-z-Y</span>
|
||||
<div class="joplin-youtube-player-rendered">
|
||||
<iframe src="https://www.youtube-nocookie.com/embed/iJqe9pC-z-Y" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
packages/app-cli/tests/md_to_html/external_embed.md
Normal file
@@ -0,0 +1 @@
|
||||
https://www.youtube.com/watch?v=iJqe9pC-z-Y
|
||||
BIN
packages/app-cli/tests/support/onenote/test.onepkg
Normal file
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": {
|
||||
|
||||
@@ -260,6 +260,15 @@ export default class ElectronAppWrapper {
|
||||
|
||||
require('@electron/remote/main').enable(this.win_.webContents);
|
||||
|
||||
// Add Referer header for YouTube embeds to fix Error 153
|
||||
this.win_.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
{ urls: ['*://*.youtube.com/*', '*://*.youtube-nocookie.com/*'] },
|
||||
(details, callback) => {
|
||||
details.requestHeaders['Referer'] = 'https://joplinapp.org/';
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
},
|
||||
);
|
||||
|
||||
if (!screen.getDisplayMatching(this.win_.getBounds())) {
|
||||
const { width: windowWidth, height: windowHeight } = this.win_.getBounds();
|
||||
const { width: primaryDisplayWidth, height: primaryDisplayHeight } = screen.getPrimaryDisplay().workArea;
|
||||
|
||||
@@ -95,6 +95,9 @@ export default class InteropServiceHelper {
|
||||
// Allows users to override the CSS page size.
|
||||
// See https://github.com/laurent22/joplin/issues/13096
|
||||
preferCSSPageSize: true,
|
||||
|
||||
// Include accessibility information in the output:
|
||||
generateTaggedPDF: true,
|
||||
});
|
||||
resolve(data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -742,7 +742,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
'media-src \'self\' blob: data: *', // Audio and video players
|
||||
|
||||
// Disallow certain unused features
|
||||
'child-src \'none\'', // Should not contain sub-frames
|
||||
'child-src https://*.youtube.com https://*.youtube-nocookie.com', // Allow YouTube embeds
|
||||
'object-src \'none\'', // Objects can be used for script injection
|
||||
'form-action \'none\'', // No submitting forms
|
||||
|
||||
|
||||
@@ -22,4 +22,8 @@ export const joplinCommandToTinyMceCommands: JoplinCommandToTinyMceCommands = {
|
||||
'search': { name: 'SearchReplace' },
|
||||
'attachFile': { name: 'joplinAttach' },
|
||||
'insertDateTime': true,
|
||||
'textCopy': true,
|
||||
'textCut': true,
|
||||
'textPaste': true,
|
||||
'textSelectAll': true,
|
||||
};
|
||||
|
||||
@@ -17,19 +17,19 @@ describe('editorCommandDeclarations', () => {
|
||||
test.each([
|
||||
[
|
||||
{},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
{
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, but only the viewer is visible
|
||||
@@ -37,7 +37,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
false,
|
||||
{ textBold: false },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, and the viewer is visible
|
||||
@@ -45,7 +45,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: true,
|
||||
richTextEditorVisible: false,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// In the RT editor
|
||||
@@ -53,7 +53,7 @@ describe('editorCommandDeclarations', () => {
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// In the Markdown editor, and the command palette is visible
|
||||
@@ -63,14 +63,57 @@ describe('editorCommandDeclarations', () => {
|
||||
gotoAnythingVisible: true,
|
||||
modalDialogVisible: true,
|
||||
},
|
||||
true,
|
||||
{ textBold: true },
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
])('should create the enabledCondition', (context: Record<string, any>, expected: boolean) => {
|
||||
const condition = enabledCondition('textBold');
|
||||
const wc = new WhenClause(condition);
|
||||
const actual = wc.evaluate({ ...baseContext, ...context });
|
||||
expect(actual).toBe(expected);
|
||||
[
|
||||
// In the Markdown editor, and the command palette is visible
|
||||
{
|
||||
markdownEditorPaneVisible: true,
|
||||
richTextEditorVisible: false,
|
||||
gotoAnythingVisible: true,
|
||||
modalDialogVisible: true,
|
||||
},
|
||||
{ textBold: true },
|
||||
],
|
||||
[
|
||||
// Rich Text Editor, HTML note
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
noteIsMarkdown: false,
|
||||
},
|
||||
{
|
||||
textCopy: true,
|
||||
textPaste: true,
|
||||
textSelectAll: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
// Rich Text Editor, read-only note
|
||||
{
|
||||
markdownEditorPaneVisible: false,
|
||||
richTextEditorVisible: true,
|
||||
noteIsReadOnly: true,
|
||||
},
|
||||
{
|
||||
textBold: false,
|
||||
textPaste: false,
|
||||
|
||||
// TODO: textCopy should be enabled in read-only notes:
|
||||
// textCopy: false,
|
||||
},
|
||||
],
|
||||
])('should correctly determine whether command is enabled (case %#)', (context, expectedStates) => {
|
||||
const actualStates = [];
|
||||
for (const commandName of Object.keys(expectedStates)) {
|
||||
const condition = enabledCondition(commandName);
|
||||
const wc = new WhenClause(condition);
|
||||
const actual = wc.evaluate({ ...baseContext, ...context });
|
||||
actualStates.push([commandName, actual]);
|
||||
}
|
||||
|
||||
const expectedStatesArray = Object.entries(expectedStates);
|
||||
expect(actualStates).toEqual(expectedStatesArray);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -4,6 +4,10 @@ import { joplinCommandToTinyMceCommands } from './NoteBody/TinyMCE/utils/joplinC
|
||||
|
||||
const workWithHtmlNotes = [
|
||||
'attachFile',
|
||||
'textCopy',
|
||||
'textCut',
|
||||
'textPaste',
|
||||
'textSelectAll',
|
||||
];
|
||||
|
||||
export const enabledCondition = (commandName: string) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
|
||||
import { WindowCommandDependencies, NoteBodyEditorRef, OnChangeEvent, ScrollOptionTypes } from './types';
|
||||
import editorCommandDeclarations, { enabledCondition } from '../editorCommandDeclarations';
|
||||
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext, RegisteredRuntime } from '@joplin/lib/services/CommandService';
|
||||
import time from '@joplin/lib/time';
|
||||
import { formatMsToLocal } from '@joplin/utils/time';
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
import getWindowCommandPriority from './getWindowCommandPriority';
|
||||
|
||||
@@ -50,7 +50,7 @@ function editorCommandRuntime(
|
||||
if (declaration.name === 'insertDateTime') {
|
||||
return editorRef.current.execCommand({
|
||||
name: 'insertText',
|
||||
value: time.formatMsToLocal(new Date().getTime()),
|
||||
value: formatMsToLocal(Date.now()),
|
||||
});
|
||||
} else if (declaration.name === 'scrollToHash') {
|
||||
return editorRef.current.scrollTo({
|
||||
|
||||
@@ -51,9 +51,11 @@ const getParentOffset = (childIndex: number, listItems: ListItem[]): number|null
|
||||
};
|
||||
|
||||
const findNextTypeAheadMatch = (selectedIndex: number, query: string, listItems: ListItem[]) => {
|
||||
const normalize = (text: string) => text.trim().toLowerCase();
|
||||
const matches = (item: ListItem) => {
|
||||
return item.label.startsWith(query);
|
||||
return normalize(item.label).startsWith(normalize(query));
|
||||
};
|
||||
|
||||
const indexBefore = listItems.slice(0, selectedIndex).findIndex(matches);
|
||||
// Search in all results **after** the current. This prevents the current item from
|
||||
// always being identified as the next match, if the user repeatedly presses the
|
||||
|
||||
@@ -123,8 +123,8 @@ const ToolbarBaseComponent: React.FC<Props> = props => {
|
||||
};
|
||||
|
||||
const tabIndex = indexInFocusable === (selectedIndex % focusableItems.length) ? 0 : -1;
|
||||
const setButtonRefCallback = (button: HTMLButtonElement) => {
|
||||
if (tabIndex === 0 && containerHasFocus) {
|
||||
const setButtonRefCallback = (button: HTMLButtonElement | null) => {
|
||||
if (button && tabIndex === 0 && containerHasFocus) {
|
||||
focus('ToolbarBase', button);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -124,8 +124,7 @@ export const runtime = (control: WindowControl): CommandRuntime => {
|
||||
|
||||
void CommandService.instance().execute('showModalMessage', `${modalMessage}\n\n${statusStrings.join('\n')}`);
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onError: (error: any) => {
|
||||
onError: (error: string|Error) => {
|
||||
errors.push(error);
|
||||
console.warn(error);
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
default-src 'self' joplin-content://* ;
|
||||
connect-src 'self' * http://* https://* joplin-content://* blob: ;
|
||||
style-src 'unsafe-inline' 'self' blob: joplin-content://* https://* http://* ;
|
||||
child-src 'self' joplin-content://* ;
|
||||
child-src 'self' joplin-content://* https://*.youtube.com https://*.youtube-nocookie.com ;
|
||||
script-src 'self' 'unsafe-inline' joplin-content://* ;
|
||||
media-src 'self' * blob: data: https://* http://* joplin-content://* ;
|
||||
img-src 'self' blob: data: http://* https://* joplin-content://* ;
|
||||
|
||||
@@ -64,6 +64,10 @@ test.describe('sidebar', () => {
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
|
||||
await mainWindow.keyboard.type('A');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
|
||||
// Should be case-insensitive
|
||||
await mainWindow.keyboard.type('f');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
|
||||
});
|
||||
|
||||
test('left/right arrow keys should expand/collapse notebooks', async ({ electronApp, mainWindow }) => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.5.9",
|
||||
"version": "3.6.2",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -92,6 +92,12 @@
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pkg",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
@@ -139,19 +145,19 @@
|
||||
"@electron/rebuild": "3.7.2",
|
||||
"@fortawesome/fontawesome-free": "5.15.4",
|
||||
"@joeattardi/emoji-button": "4.6.4",
|
||||
"@joplin/default-plugins": "~3.5",
|
||||
"@joplin/editor": "~3.5",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/default-plugins": "~3.6",
|
||||
"@joplin/editor": "~3.6",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@sentry/electron": "4.24.0",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react": "18.3.26",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
@@ -208,7 +214,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"@joplin/onenote-converter": "~3.5",
|
||||
"@joplin/onenote-converter": "~3.6",
|
||||
"fs-extra": "11.3.2",
|
||||
"keytar": "7.9.0",
|
||||
"node-fetch": "2.6.7",
|
||||
|
||||
@@ -140,7 +140,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
||||
// electron's autoUpdater appends automatically the platform's yml file to the link so we should remove it
|
||||
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
||||
await autoUpdater.checkForUpdates();
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
|
||||
// Wait for the installation to finish. By default, .checkForUpdates runs in the background
|
||||
await result.downloadPromise;
|
||||
} catch (error) {
|
||||
this.logger_.error(`Update download url failed: ${error.message}`);
|
||||
this.isUpdateInProgress = false;
|
||||
|
||||
@@ -130,6 +130,12 @@ const makeAccessDeniedResponse = (message: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const makeNotFoundResponse = () => {
|
||||
return new Response('not found', {
|
||||
status: 404,
|
||||
});
|
||||
};
|
||||
|
||||
// Creating a custom protocol allows us to isolate iframes by giving them
|
||||
// different domain names from the main Joplin app.
|
||||
//
|
||||
@@ -210,10 +216,24 @@ const handleCustomProtocols = (): CustomProtocolHandler => {
|
||||
|
||||
const rangeHeader = request.headers.get('Range');
|
||||
let response;
|
||||
if (!rangeHeader) {
|
||||
response = await net.fetch(asFileUrl);
|
||||
} else {
|
||||
response = await handleRangeRequest(request, pathname);
|
||||
try {
|
||||
if (!rangeHeader) {
|
||||
response = await net.fetch(asFileUrl);
|
||||
} else {
|
||||
response = await handleRangeRequest(request, pathname);
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
// Errors from NodeJS fs methods (e.g. fs.stat()
|
||||
error.code === 'ENOENT'
|
||||
// Errors from Electron's net.fetch(). Use error.message since these errors don't
|
||||
// seem to have a specific .code or .name.
|
||||
|| error.message === 'net::ERR_FILE_NOT_FOUND'
|
||||
) {
|
||||
response = makeNotFoundResponse();
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaOnly) {
|
||||
|
||||
@@ -89,8 +89,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097786
|
||||
versionName "3.5.6"
|
||||
versionCode 2097788
|
||||
versionName "3.6.0"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { CommandRuntimeProps } from '../types';
|
||||
import time from '@joplin/lib/time';
|
||||
import { formatMsToLocal } from '@joplin/utils/time';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'insertDateTime',
|
||||
@@ -12,7 +12,7 @@ export const declaration: CommandDeclaration = {
|
||||
export const runtime = (props: CommandRuntimeProps): CommandRuntime => {
|
||||
return {
|
||||
execute: async (_context: CommandContext) => {
|
||||
props.insertText(time.formatDateToLocal(new Date()));
|
||||
props.insertText(formatMsToLocal(Date.now()));
|
||||
},
|
||||
|
||||
enabledCondition: '!noteIsReadOnly',
|
||||
|
||||
@@ -345,7 +345,6 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-image-picker/RNImagePickerPrivacyInfo.bundle",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Brands.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/@react-native-vector-icons/fontawesome5/fonts/FontAwesome5_Regular.ttf",
|
||||
@@ -365,7 +364,6 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNImagePickerPrivacyInfo.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
|
||||
@@ -511,7 +509,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
@@ -520,7 +518,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.5.1;
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -546,7 +544,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
@@ -554,7 +552,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.5.1;
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -747,7 +745,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -758,7 +756,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.5.1;
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
@@ -790,7 +788,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 147;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -801,7 +799,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 13.5.1;
|
||||
MARKETING_VERSION = 13.6.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -77,21 +77,14 @@
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>AntDesign.ttf</string>
|
||||
<string>Entypo.ttf</string>
|
||||
<string>EvilIcons.ttf</string>
|
||||
<string>Feather.ttf</string>
|
||||
<string>FontAwesome.ttf</string>
|
||||
<string>FontAwesome5_Brands.ttf</string>
|
||||
<string>FontAwesome5_Regular.ttf</string>
|
||||
<string>FontAwesome5_Solid.ttf</string>
|
||||
<string>Foundation.ttf</string>
|
||||
<string>Ionicons.ttf</string>
|
||||
<string>MaterialIcons.ttf</string>
|
||||
<string>MaterialDesignIcons.ttf</string>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
<string>SimpleLineIcons.ttf</string>
|
||||
<string>Octicons.ttf</string>
|
||||
<string>Zocial.ttf</string>
|
||||
<string>Fontisto.ttf</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
||||
@@ -2306,7 +2306,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
|
||||
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
|
||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||
Expo: c8f323f74218c45c46e27eed40d8a53ba50667c3
|
||||
@@ -2319,7 +2319,7 @@ SPEC CHECKSUMS:
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2
|
||||
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
|
||||
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
||||
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
preset: 'react-native',
|
||||
|
||||
'moduleFileExtensions': [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/app-mobile",
|
||||
"description": "Joplin for Mobile",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "BROWSERSLIST_IGNORE_OLD_DATA=true react-native start --reset-cache",
|
||||
@@ -22,12 +22,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@joplin/editor": "~3.5",
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/react-native-alarm-notification": "~3.5",
|
||||
"@joplin/react-native-saf-x": "~3.5",
|
||||
"@joplin/renderer": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/editor": "~3.6",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/react-native-alarm-notification": "~3.6",
|
||||
"@joplin/react-native-saf-x": "~3.6",
|
||||
"@joplin/renderer": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@js-draw/material-icons": "1.33.0",
|
||||
"@react-native-clipboard/clipboard": "1.16.3",
|
||||
"@react-native-community/datetimepicker": "8.4.5",
|
||||
@@ -59,14 +59,14 @@
|
||||
"punycode": "2.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"react-native-device-info": "14.1.1",
|
||||
"react-native-dropdownalert": "5.2.0",
|
||||
"react-native-exit-app": "2.0.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.11.0",
|
||||
"react-native-image-picker": "8.2.1",
|
||||
"react-native-localize": "3.5.2",
|
||||
"react-native-localize": "3.5.4",
|
||||
"react-native-modal-datetime-picker": "18.0.0",
|
||||
"react-native-paper": "5.14.5",
|
||||
"react-native-popup-menu": "0.17.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"@babel/plugin-transform-export-namespace-from": "7.27.1",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/runtime": "7.25.0",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/tools": "~3.6",
|
||||
"@joplin/turndown": "~4.0.80",
|
||||
"@joplin/turndown-plugin-gfm": "~1.0.62",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.0",
|
||||
@@ -114,13 +114,13 @@
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.154",
|
||||
"@types/serviceworker": "0.0.158",
|
||||
"@types/tar-stream": "3.1.4",
|
||||
"babel-jest": "29.7.0",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-module-resolver": "4.1.0",
|
||||
"babel-plugin-react-native-web": "0.21.1",
|
||||
"esbuild": "0.25.10",
|
||||
"babel-plugin-react-native-web": "0.21.2",
|
||||
"esbuild": "0.25.11",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.3.2",
|
||||
"gulp": "4.0.2",
|
||||
@@ -131,7 +131,7 @@
|
||||
"nodemon": "3.1.10",
|
||||
"punycode": "2.3.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-native-web": "0.21.1",
|
||||
"react-native-web": "0.21.2",
|
||||
"react-refresh": "0.17.0",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"sharp": "0.34.4",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
hash:"daebd8498ff273c64cf5905d4356e66a", files: {
|
||||
hash:"88e5d809af57eac7b86c4deaf21b9345", files: {
|
||||
'abc/abc_render.js': { data: require('./abc/abc_render.js.base64.js'), mime: 'application/javascript', encoding: 'base64' },
|
||||
'abc/abcjs-basic-min.js': { data: require('./abc/abcjs-basic-min.js.base64.js'), mime: 'application/javascript', encoding: 'base64' },
|
||||
'highlight.js/atom-one-dark-reasonable.css': { data: require('./highlight.js/atom-one-dark-reasonable.css.base64.js'), mime: 'text/css', encoding: 'base64' },
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = {"hash":"daebd8498ff273c64cf5905d4356e66a","files":["abc/abc_render.js","abc/abcjs-basic-min.js","highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
||||
module.exports = {"hash":"88e5d809af57eac7b86c4deaf21b9345","files":["abc/abc_render.js","abc/abcjs-basic-min.js","highlight.js/atom-one-dark-reasonable.css","highlight.js/atom-one-light.css","katex/fonts/KaTeX_AMS-Regular.woff2","katex/fonts/KaTeX_Caligraphic-Bold.woff2","katex/fonts/KaTeX_Caligraphic-Regular.woff2","katex/fonts/KaTeX_Fraktur-Bold.woff2","katex/fonts/KaTeX_Fraktur-Regular.woff2","katex/fonts/KaTeX_Main-Bold.woff2","katex/fonts/KaTeX_Main-BoldItalic.woff2","katex/fonts/KaTeX_Main-Italic.woff2","katex/fonts/KaTeX_Main-Regular.woff2","katex/fonts/KaTeX_Math-BoldItalic.woff2","katex/fonts/KaTeX_Math-Italic.woff2","katex/fonts/KaTeX_SansSerif-Bold.woff2","katex/fonts/KaTeX_SansSerif-Italic.woff2","katex/fonts/KaTeX_SansSerif-Regular.woff2","katex/fonts/KaTeX_Script-Regular.woff2","katex/fonts/KaTeX_Size1-Regular.woff2","katex/fonts/KaTeX_Size2-Regular.woff2","katex/fonts/KaTeX_Size3-Regular.woff2","katex/fonts/KaTeX_Size4-Regular.woff2","katex/fonts/KaTeX_Typewriter-Regular.woff2","katex/katex.css","mermaid/mermaid.min.js","mermaid/mermaid_render.js"]}
|
||||
@@ -12,7 +12,7 @@
|
||||
default-src 'self' ;
|
||||
connect-src 'self' * http://* https://* blob: ;
|
||||
style-src 'unsafe-inline' 'self' blob: ;
|
||||
child-src 'self' ;
|
||||
child-src 'self' https://*.youtube.com https://*.youtube-nocookie.com ;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' ;
|
||||
media-src 'self' blob: data: https://* http://* ;
|
||||
img-src 'self' blob: data: http://* https://* ;
|
||||
|
||||
@@ -7,115 +7,129 @@ import { tmpdir } from 'os';
|
||||
import { chdir, cwd } from 'process';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import { glob } from 'glob';
|
||||
import readRepositoryJson from './utils/readRepositoryJson';
|
||||
import readRepositoryJson, { BuiltInPluginType, RepositoryData } from './utils/readRepositoryJson';
|
||||
import getPathToPatchFileFor from './utils/getPathToPatchFileFor';
|
||||
import getCurrentCommitHash from './utils/getCurrentCommitHash';
|
||||
import { waitForCliInput } from '@joplin/utils/cli';
|
||||
|
||||
interface Options {
|
||||
outputParentDir: string|null;
|
||||
beforeInstall: (buildDir: string, pluginName: string)=> Promise<void>;
|
||||
beforePatch: ()=> Promise<void>;
|
||||
}
|
||||
|
||||
const buildDefaultPlugins = async (outputParentDir: string|null, options: Options) => {
|
||||
const pluginSourcesDir = resolve(join(__dirname, 'plugin-sources'));
|
||||
const buildDefaultPlugins = async (options: Options) => {
|
||||
const pluginRepositoryData = await readRepositoryJson(join(__dirname, 'pluginRepositories.json'));
|
||||
|
||||
const originalDirectory = cwd();
|
||||
|
||||
const logStatus = (...message: string[]) => {
|
||||
const blue = '\x1b[96m';
|
||||
const reset = '\x1b[0m';
|
||||
console.log(blue, ...message, reset);
|
||||
};
|
||||
|
||||
for (const pluginId in pluginRepositoryData) {
|
||||
const repositoryData = pluginRepositoryData[pluginId];
|
||||
const outputPath = options.outputParentDir && join(options.outputParentDir, `${pluginId}.jpl`);
|
||||
|
||||
const buildDir = await mkdtemp(join(tmpdir(), 'default-plugin-build'));
|
||||
try {
|
||||
logStatus('Building plugin', pluginId, 'at', buildDir);
|
||||
const pluginDir = resolve(join(pluginSourcesDir, pluginId));
|
||||
|
||||
// Clone the repository if not done yet
|
||||
if (!(await exists(pluginDir)) || (await readdir(pluginDir)).length === 0) {
|
||||
logStatus(`Cloning from repository ${repositoryData.cloneUrl}`);
|
||||
await execCommand(['git', 'clone', '--', repositoryData.cloneUrl, pluginDir]);
|
||||
chdir(pluginDir);
|
||||
if (repositoryData.type === BuiltInPluginType.Built) {
|
||||
await buildPlugin(pluginId, repositoryData, outputPath, options);
|
||||
} else {
|
||||
if (!outputPath) {
|
||||
console.warn('Skipping NPM plugin,', pluginId, ': missing output path.');
|
||||
continue;
|
||||
}
|
||||
|
||||
chdir(pluginDir);
|
||||
const expectedCommitHash = repositoryData.commit;
|
||||
|
||||
logStatus(`Switching to commit ${expectedCommitHash}`);
|
||||
await execCommand(['git', 'switch', repositoryData.branch]);
|
||||
|
||||
try {
|
||||
await execCommand(['git', 'checkout', expectedCommitHash]);
|
||||
} catch (error) {
|
||||
logStatus(`git checkout failed with error ${error}. Fetching...`);
|
||||
await execCommand(['git', 'fetch']);
|
||||
await execCommand(['git', 'checkout', expectedCommitHash]);
|
||||
}
|
||||
|
||||
if (await getCurrentCommitHash() !== expectedCommitHash) {
|
||||
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
|
||||
}
|
||||
|
||||
logStatus('Copying repository files...');
|
||||
await copy(pluginDir, buildDir, {
|
||||
filter: fileName => {
|
||||
return basename(fileName) !== '.git';
|
||||
},
|
||||
});
|
||||
|
||||
chdir(buildDir);
|
||||
|
||||
logStatus('Initializing repository.');
|
||||
await execCommand('git init . -b main');
|
||||
|
||||
logStatus('Running before-patch hook.');
|
||||
await options.beforePatch();
|
||||
|
||||
const patchFile = getPathToPatchFileFor(pluginId);
|
||||
if (await exists(patchFile)) {
|
||||
logStatus('Applying patch.');
|
||||
await execCommand(['git', 'apply', patchFile]);
|
||||
}
|
||||
|
||||
await options.beforeInstall(buildDir, pluginId);
|
||||
|
||||
logStatus('Installing dependencies.');
|
||||
await execCommand('npm install');
|
||||
|
||||
const jplFiles = await glob('publish/*.jpl');
|
||||
logStatus(`Found built .jpl files: ${JSON.stringify(jplFiles)}`);
|
||||
|
||||
if (jplFiles.length === 0) {
|
||||
throw new Error(`No published files found in ${buildDir}/publish`);
|
||||
}
|
||||
|
||||
if (outputParentDir !== null) {
|
||||
logStatus(`Checking output directory in ${outputParentDir}`);
|
||||
const outputPath = join(outputParentDir, `${pluginId}.jpl`);
|
||||
|
||||
const sourceFile = jplFiles[0];
|
||||
logStatus(`Copying built file from ${sourceFile} to ${outputPath}`);
|
||||
await copy(sourceFile, outputPath);
|
||||
} else {
|
||||
console.warn('No output directory specified. Not copying built .jpl files.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.log('Build directory', buildDir);
|
||||
await waitForCliInput();
|
||||
throw error;
|
||||
} finally {
|
||||
chdir(originalDirectory);
|
||||
await remove(buildDir);
|
||||
logStatus('Removed build directory');
|
||||
logStatus('Copying plugin', pluginId, 'JPL file to', outputPath);
|
||||
await copy(join(__dirname, 'node_modules', repositoryData.package, 'publish', `${pluginId}.jpl`), outputPath);
|
||||
logStatus('Copied.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const logStatus = (...message: string[]) => {
|
||||
const blue = '\x1b[96m';
|
||||
const reset = '\x1b[0m';
|
||||
console.log(blue, ...message, reset);
|
||||
};
|
||||
|
||||
const buildPlugin = async (pluginId: string, repositoryData: RepositoryData, outputPath: string|null, options: Options) => {
|
||||
const pluginSourcesDir = resolve(join(__dirname, 'plugin-sources'));
|
||||
const originalDirectory = cwd();
|
||||
|
||||
const buildDir = await mkdtemp(join(tmpdir(), 'default-plugin-build'));
|
||||
try {
|
||||
logStatus('Building plugin', pluginId, 'at', buildDir);
|
||||
const pluginDir = resolve(join(pluginSourcesDir, pluginId));
|
||||
|
||||
// Clone the repository if not done yet
|
||||
if (!(await exists(pluginDir)) || (await readdir(pluginDir)).length === 0) {
|
||||
logStatus(`Cloning from repository ${repositoryData.cloneUrl}`);
|
||||
await execCommand(['git', 'clone', '--', repositoryData.cloneUrl, pluginDir]);
|
||||
chdir(pluginDir);
|
||||
}
|
||||
|
||||
chdir(pluginDir);
|
||||
const expectedCommitHash = repositoryData.commit;
|
||||
|
||||
logStatus(`Switching to commit ${expectedCommitHash}`);
|
||||
await execCommand(['git', 'switch', repositoryData.branch]);
|
||||
|
||||
try {
|
||||
await execCommand(['git', 'checkout', expectedCommitHash]);
|
||||
} catch (error) {
|
||||
logStatus(`git checkout failed with error ${error}. Fetching...`);
|
||||
await execCommand(['git', 'fetch']);
|
||||
await execCommand(['git', 'checkout', expectedCommitHash]);
|
||||
}
|
||||
|
||||
if (await getCurrentCommitHash() !== expectedCommitHash) {
|
||||
throw new Error(`Unable to checkout commit ${expectedCommitHash}`);
|
||||
}
|
||||
|
||||
logStatus('Copying repository files...');
|
||||
await copy(pluginDir, buildDir, {
|
||||
filter: fileName => {
|
||||
return basename(fileName) !== '.git';
|
||||
},
|
||||
});
|
||||
|
||||
chdir(buildDir);
|
||||
|
||||
logStatus('Initializing repository.');
|
||||
await execCommand('git init . -b main');
|
||||
|
||||
logStatus('Running before-patch hook.');
|
||||
await options.beforePatch();
|
||||
|
||||
const patchFile = getPathToPatchFileFor(pluginId);
|
||||
if (await exists(patchFile)) {
|
||||
logStatus('Applying patch.');
|
||||
await execCommand(['git', 'apply', patchFile]);
|
||||
}
|
||||
|
||||
await options.beforeInstall(buildDir, pluginId);
|
||||
|
||||
logStatus('Installing dependencies.');
|
||||
await execCommand('npm install');
|
||||
|
||||
const jplFiles = await glob('publish/*.jpl');
|
||||
logStatus(`Found built .jpl files: ${JSON.stringify(jplFiles)}`);
|
||||
|
||||
if (jplFiles.length === 0) {
|
||||
throw new Error(`No published files found in ${buildDir}/publish`);
|
||||
}
|
||||
|
||||
if (outputPath !== null) {
|
||||
const sourceFile = jplFiles[0];
|
||||
logStatus(`Copying built file from ${sourceFile} to ${outputPath}`);
|
||||
await copy(sourceFile, outputPath);
|
||||
} else {
|
||||
console.warn('No output directory specified. Not copying built .jpl files.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.log('Build directory', buildDir);
|
||||
await waitForCliInput();
|
||||
throw error;
|
||||
} finally {
|
||||
chdir(originalDirectory);
|
||||
await remove(buildDir);
|
||||
logStatus('Removed build directory');
|
||||
}
|
||||
};
|
||||
|
||||
export default buildDefaultPlugins;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import buildDefaultPlugins from '../buildDefaultPlugins';
|
||||
|
||||
const buildAll = (outputDirectory: string) => {
|
||||
return buildDefaultPlugins(outputDirectory, {
|
||||
return buildDefaultPlugins({
|
||||
outputParentDir: outputDirectory,
|
||||
beforeInstall: async () => { },
|
||||
beforePatch: async () => { },
|
||||
});
|
||||
|
||||
@@ -8,7 +8,8 @@ import getPathToPatchFileFor from '../utils/getPathToPatchFileFor';
|
||||
const editPatch = async (targetPluginId: string, outputParentDir: string|null) => {
|
||||
let patchedPlugin = false;
|
||||
|
||||
await buildDefaultPlugins(outputParentDir, {
|
||||
await buildDefaultPlugins({
|
||||
outputParentDir: outputParentDir,
|
||||
beforePatch: async () => {
|
||||
// To make updating just the patch possible, a commit is created just before applying
|
||||
// the patch.
|
||||
@@ -34,7 +35,7 @@ const editPatch = async (targetPluginId: string, outputParentDir: string|null) =
|
||||
});
|
||||
|
||||
if (!patchedPlugin) {
|
||||
throw new Error(`No default plugin with ID ${targetPluginId} found!`);
|
||||
throw new Error(`No patchable default plugin with ID ${targetPluginId} found! Make sure that the plugin has a "cloneUrl" and "branch" in pluginRepositories.json.`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/default-plugins",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"description": "Default plugins bundler",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -14,11 +14,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "17.0.33",
|
||||
"joplin-plugin-freehand-drawing": "4.3.0",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/utils": "~3.6",
|
||||
"fs-extra": "11.3.2",
|
||||
"yargs": "17.7.2"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
"io.github.jackgruber.backup": {
|
||||
"cloneUrl": "https://github.com/JackGruber/joplin-plugin-backup.git",
|
||||
"branch": "master",
|
||||
"commit": "abb58175e2d2bf34899f1b32cb74137e5c788bf9"
|
||||
"commit": "2c3da7056e7ac39c86c2051a4fdb99d9534dd0a1"
|
||||
},
|
||||
"io.github.personalizedrefrigerator.js-draw": {
|
||||
"cloneUrl": "https://github.com/personalizedrefrigerator/joplin-plugin-freehand-drawing.git",
|
||||
"branch": "main",
|
||||
"commit": "63b6d3f185b5b3664632e498df7c7ad7824038d0"
|
||||
"package": "joplin-plugin-freehand-drawing"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import { readFile } from 'fs-extra';
|
||||
|
||||
export enum BuiltInPluginType {
|
||||
// Plugins that need to be built when building Joplin (e.g. if the plugin
|
||||
// needs to be patched)
|
||||
Built,
|
||||
// Plugins that can be fetched directly from NPM. Must also be marked as a
|
||||
// dev dependency.
|
||||
FromNpm,
|
||||
}
|
||||
|
||||
export interface RepositoryData {
|
||||
type: BuiltInPluginType.Built;
|
||||
cloneUrl: string;
|
||||
branch: string;
|
||||
commit: string;
|
||||
}
|
||||
|
||||
export interface NpmReference {
|
||||
type: BuiltInPluginType.FromNpm;
|
||||
package: string;
|
||||
}
|
||||
|
||||
export interface AllRepositoryData {
|
||||
[pluginId: string]: RepositoryData;
|
||||
[pluginId: string]: RepositoryData|NpmReference;
|
||||
}
|
||||
|
||||
const readRepositoryJson = async (repositoryDataFilepath: string): Promise<AllRepositoryData> => {
|
||||
@@ -26,9 +41,17 @@ const readRepositoryJson = async (repositoryDataFilepath: string): Promise<AllRe
|
||||
}
|
||||
};
|
||||
|
||||
assertPropertyIsString('cloneUrl');
|
||||
assertPropertyIsString('branch');
|
||||
assertPropertyIsString('commit');
|
||||
let type;
|
||||
if ('branch' in parsedJson[pluginId]) {
|
||||
assertPropertyIsString('cloneUrl');
|
||||
assertPropertyIsString('branch');
|
||||
assertPropertyIsString('commit');
|
||||
type = BuiltInPluginType.Built;
|
||||
} else {
|
||||
assertPropertyIsString('package');
|
||||
type = BuiltInPluginType.FromNpm;
|
||||
}
|
||||
parsedJson[pluginId] = { ...parsedJson[pluginId], type };
|
||||
}
|
||||
|
||||
return parsedJson;
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('createEditor', () => {
|
||||
|
||||
const headerLine = document.body.querySelector('.cm-headerLine')!;
|
||||
expect(headerLine.textContent).toBe(headerLineText);
|
||||
expect(getComputedStyle(headerLine).fontSize).toBe('1.6em');
|
||||
expect(getComputedStyle(headerLine).fontSize).toBe('1.5em');
|
||||
|
||||
// CodeMirror nests the tag that styles the header within .cm-headerLine:
|
||||
// <div class='cm-headerLine'><span class='someclass'>Testing...</span></div>
|
||||
|
||||
@@ -3,39 +3,17 @@ import { EditorSelection } from '@codemirror/state';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import uslug from '@joplin/fork-uslug/lib/uslug';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
import htmlNodeInfo from '../utils/htmlNodeInfo';
|
||||
|
||||
const jumpToHash = (view: EditorView, hash: string) => {
|
||||
const state = view.state;
|
||||
const timeout = 1_000; // Maximum time to spend parsing the syntax tree
|
||||
let targetLocation: number|undefined = undefined;
|
||||
|
||||
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
|
||||
|
||||
const makeEnterNode = (offset: number) => (node: SyntaxNodeRef) => {
|
||||
const nodeToText = (node: SyntaxNodeRef) => {
|
||||
return state.sliceDoc(node.from + offset, node.to + offset);
|
||||
};
|
||||
// Returns the attribute with the given name for [node]
|
||||
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string) => {
|
||||
if (node.from === node.to) return null; // Empty
|
||||
const content = node.node.resolveInner(node.from + 1);
|
||||
|
||||
// Search for the "id" attribute
|
||||
const attributes = content.getChildren('Attribute');
|
||||
for (const attribute of attributes) {
|
||||
const nameNode = attribute.getChild('AttributeName');
|
||||
const valueNode = attribute.getChild('AttributeValue');
|
||||
|
||||
if (nameNode && valueNode) {
|
||||
const name = nodeToText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
|
||||
if (name === attrName) {
|
||||
return removeQuotes(nodeToText(valueNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const found = targetLocation !== undefined;
|
||||
if (found) return false; // Skip this node
|
||||
@@ -46,13 +24,14 @@ const jumpToHash = (view: EditorView, hash: string) => {
|
||||
.replace(/^#+\s/, '') // Leading #s in headers
|
||||
.replace(/\n-+$/, ''); // Trailing --s in headers
|
||||
matches = hash === uslug(nodeText);
|
||||
} else if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
|
||||
} else if (node.name === 'HTMLBlock') {
|
||||
// CodeMirror adds HTML information to Markdown documents using overlays attached
|
||||
// to HTMLTag and HTMLBlock nodes.
|
||||
// Use .enter to enter the overlay and visit the HTML nodes:
|
||||
node.node.enter(node.from, 1).toTree().iterate({ enter: makeEnterNode(node.from) });
|
||||
} else if (node.name === 'OpenTag') {
|
||||
matches = getHtmlNodeAttr(node, 'id') === hash || getHtmlNodeAttr(node, 'name') === hash;
|
||||
} else if (node.name === 'OpenTag' || node.name === 'HTMLTag') {
|
||||
const htmlNodeDetails = htmlNodeInfo(node, state);
|
||||
matches = htmlNodeDetails.getAttr('id') === hash || htmlNodeDetails.getAttr('name') === hash;
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import replaceBulletLists from './replaceBulletLists';
|
||||
import replaceCheckboxes from './replaceCheckboxes';
|
||||
import replaceDividers from './replaceDividers';
|
||||
import replaceFormatCharacters from './replaceFormatCharacters';
|
||||
import replaceInlineHtml from './replaceInlineHtml';
|
||||
|
||||
export default () => {
|
||||
return [
|
||||
@@ -13,5 +14,6 @@ export default () => {
|
||||
replaceBackslashEscapes,
|
||||
replaceDividers,
|
||||
addFormattingClasses,
|
||||
replaceInlineHtml,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -3,20 +3,31 @@ import { SyntaxNodeRef } from '@lezer/common';
|
||||
import makeReplaceExtension from './utils/makeInlineReplaceExtension';
|
||||
import toggleCheckboxAt from '../../utils/markdown/toggleCheckboxAt';
|
||||
|
||||
const checkboxClassName = 'cm-ext-checkbox-toggle';
|
||||
const checkboxContainerClassName = 'cm-ext-checkbox-toggle';
|
||||
const checkboxClassName = 'cm-ext-checkbox';
|
||||
|
||||
|
||||
class CheckboxWidget extends WidgetType {
|
||||
public constructor(private checked: boolean, private depth: number, private label: string) {
|
||||
public constructor(
|
||||
private checked: boolean,
|
||||
private depth: number,
|
||||
private label: string,
|
||||
private markup: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public eq(other: CheckboxWidget) {
|
||||
return other.checked === this.checked && other.depth === this.depth && other.label === this.label;
|
||||
return other.checked === this.checked
|
||||
&& other.depth === this.depth
|
||||
&& other.label === this.label
|
||||
&& other.markup === this.markup;
|
||||
}
|
||||
|
||||
private applyContainerClasses(container: HTMLElement) {
|
||||
container.classList.add(checkboxClassName);
|
||||
container.classList.add(checkboxContainerClassName);
|
||||
// For sizing: Should have the same font/styles as non-rendered checkboxes:
|
||||
container.classList.add('cm-taskMarker');
|
||||
|
||||
for (const className of [...container.classList]) {
|
||||
if (className.startsWith('-depth-')) {
|
||||
@@ -30,12 +41,22 @@ class CheckboxWidget extends WidgetType {
|
||||
public toDOM(view: EditorView) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
const sizingNode = document.createElement('span');
|
||||
sizingNode.classList.add('sizing');
|
||||
sizingNode.textContent = this.markup;
|
||||
container.appendChild(sizingNode);
|
||||
|
||||
const checkboxWrapper = document.createElement('span');
|
||||
checkboxWrapper.classList.add('content');
|
||||
container.appendChild(checkboxWrapper);
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = this.checked;
|
||||
checkbox.ariaLabel = this.label;
|
||||
checkbox.title = this.label;
|
||||
container.appendChild(checkbox);
|
||||
checkbox.classList.add(checkboxClassName);
|
||||
checkboxWrapper.appendChild(checkbox);
|
||||
|
||||
checkbox.oninput = () => {
|
||||
toggleCheckboxAt(view.posAtDOM(container))(view);
|
||||
@@ -66,16 +87,32 @@ const completedListItemDecoration = Decoration.line({ class: completedTaskClassN
|
||||
|
||||
const replaceCheckboxes = [
|
||||
EditorView.theme({
|
||||
[`& .${checkboxContainerClassName}`]: {
|
||||
position: 'relative',
|
||||
|
||||
'& > .sizing': {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
|
||||
'& > .content': {
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
right: '0',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
[`& .${checkboxClassName}`]: {
|
||||
'& > input': {
|
||||
width: '1.1em',
|
||||
height: '1.1em',
|
||||
margin: '4px',
|
||||
verticalAlign: 'middle',
|
||||
},
|
||||
'&:not(.-depth-1) > input': {
|
||||
marginInlineStart: 0,
|
||||
},
|
||||
verticalAlign: 'middle',
|
||||
|
||||
// Ensure that the checkbox grows as the font size increases:
|
||||
width: '100%',
|
||||
minHeight: '70%',
|
||||
|
||||
// Shift the checkbox slightly so that it's aligned with the list item bullet point
|
||||
margin: '0',
|
||||
marginBottom: '3px',
|
||||
},
|
||||
[`& .${completedTaskClassName}`]: {
|
||||
opacity: 0.69,
|
||||
@@ -84,7 +121,7 @@ const replaceCheckboxes = [
|
||||
EditorView.domEventHandlers({
|
||||
mousedown: (event) => {
|
||||
const target = event.target as Element;
|
||||
if (target.nodeName === 'INPUT' && target.parentElement?.classList?.contains(checkboxClassName)) {
|
||||
if (target.nodeName === 'INPUT' && target.classList?.contains(checkboxClassName)) {
|
||||
// Let the checkbox handle the event
|
||||
return true;
|
||||
}
|
||||
@@ -101,8 +138,14 @@ const replaceCheckboxes = [
|
||||
if (node.name === 'TaskMarker') {
|
||||
const containerLine = state.doc.lineAt(node.from);
|
||||
const labelText = state.doc.sliceString(node.to, containerLine.to);
|
||||
const markerText = state.doc.sliceString(node.from, node.to);
|
||||
|
||||
return new CheckboxWidget(markerIsChecked(node), parentTags.get('ListItem') ?? 0, labelText);
|
||||
return new CheckboxWidget(
|
||||
markerIsChecked(node),
|
||||
parentTags.get('ListItem') ?? 0,
|
||||
labelText,
|
||||
markerText,
|
||||
);
|
||||
} else if (node.name === 'Task') {
|
||||
const marker = node.node.getChild('TaskMarker');
|
||||
if (marker && markerIsChecked(marker)) {
|
||||
@@ -119,7 +162,7 @@ const replaceCheckboxes = [
|
||||
return null;
|
||||
}
|
||||
|
||||
return [listMarker.from, node.to];
|
||||
return [node.from, node.to];
|
||||
} else if (node.name === 'Task') {
|
||||
const taskLine = state.doc.lineAt(node.from);
|
||||
return [taskLine.from];
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import createTestEditor from '../../testing/createTestEditor';
|
||||
import replaceInlineHtml from './replaceInlineHtml';
|
||||
|
||||
const createEditor = async (initialMarkdown: string, expectedTags: string[] = ['HTMLTag']) => {
|
||||
const editor = await createTestEditor(
|
||||
initialMarkdown,
|
||||
EditorSelection.cursor(0),
|
||||
expectedTags,
|
||||
[replaceInlineHtml],
|
||||
);
|
||||
return editor;
|
||||
};
|
||||
|
||||
describe('replaceInlineHtml', () => {
|
||||
test.each([
|
||||
{ markdown: '<sup>Test</sup>', expectedTagsQuery: 'sup' },
|
||||
{ markdown: '<strike>Test</strike>', expectedTagsQuery: 'strike' },
|
||||
{ markdown: 'Test: <span style="color: red;">Test</span>', expectedTagsQuery: 'span[style]' },
|
||||
{ markdown: 'Test: <span style="color: rgb(123, 0, 0);">Test</span>', expectedTagsQuery: 'span[style]' },
|
||||
])('should render inline HTML (case %#)', async ({ markdown, expectedTagsQuery }) => {
|
||||
// Add additional newlines: Ensure that the cursor isn't initially on the same line as the content to be rendered:
|
||||
const editor = await createEditor(`\n\n${markdown}\n\n`);
|
||||
|
||||
expect(editor.contentDOM.querySelector(expectedTagsQuery)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import makeInlineReplaceExtension from './utils/makeInlineReplaceExtension';
|
||||
import { Decoration } from '@codemirror/view';
|
||||
import htmlNodeInfo, { HtmlNodeInfo } from '../../utils/htmlNodeInfo';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
|
||||
const hideDecoration = Decoration.replace({});
|
||||
|
||||
type OnRenderTagContent = (openingTag: HtmlNodeInfo)=> Decoration;
|
||||
const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRenderTagContent) => {
|
||||
const isMatchingTag = (info: HtmlNodeInfo) => {
|
||||
return info.tagName().toLowerCase() === tagName;
|
||||
};
|
||||
const isMatchingOpeningTag = (info: HtmlNodeInfo) => {
|
||||
return isMatchingTag(info) && info.opening;
|
||||
};
|
||||
const isMatchingClosingTag = (info: HtmlNodeInfo) => {
|
||||
return isMatchingTag(info) && info.closing;
|
||||
};
|
||||
|
||||
const findClosingTag = (openingTag: SyntaxNodeRef, state: EditorState) => {
|
||||
const openingTagInfo = htmlNodeInfo(openingTag, state);
|
||||
// Self-closing?
|
||||
if (openingTagInfo.closing) {
|
||||
return openingTag;
|
||||
}
|
||||
|
||||
let cursor = openingTag.node.nextSibling;
|
||||
let nestedTagCounter = 1;
|
||||
|
||||
// Find the matching closing tag
|
||||
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
|
||||
const info = htmlNodeInfo(cursor, state);
|
||||
if (isMatchingOpeningTag(info)) {
|
||||
nestedTagCounter ++;
|
||||
} else if (isMatchingClosingTag(info)) {
|
||||
nestedTagCounter --;
|
||||
}
|
||||
|
||||
if (nestedTagCounter === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return cursor;
|
||||
};
|
||||
|
||||
const hideTags = makeInlineReplaceExtension({
|
||||
createDecoration: (node, state) => {
|
||||
const info = htmlNodeInfo(node, state);
|
||||
return info && isMatchingTag(info) ? hideDecoration : null;
|
||||
},
|
||||
});
|
||||
|
||||
const styleContent = makeInlineReplaceExtension({
|
||||
createDecoration: (node, state) => {
|
||||
const info = htmlNodeInfo(node, state);
|
||||
if (!info || !isMatchingOpeningTag(info)) return null;
|
||||
return onRenderContent(info);
|
||||
},
|
||||
getDecorationRange(node, state) {
|
||||
const closingTag = findClosingTag(node, state);
|
||||
|
||||
if (closingTag) {
|
||||
return [node.to, closingTag.from];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return [hideTags, styleContent];
|
||||
};
|
||||
|
||||
|
||||
export default [
|
||||
createHtmlReplacementExtension('sub', () => Decoration.mark({ tagName: 'sub' })),
|
||||
createHtmlReplacementExtension('sup', () => Decoration.mark({ tagName: 'sup' })),
|
||||
createHtmlReplacementExtension('strike', () => Decoration.mark({ tagName: 'strike' })),
|
||||
createHtmlReplacementExtension('span', (info) => {
|
||||
const styles = info.getAttr('style') ?? '';
|
||||
const colorMatch = styles.match(/color:\s*(#?[a-z0-9A-Z]+|rgba?\([0-9, ]+\))(;|$)/);
|
||||
|
||||
return Decoration.mark({
|
||||
attributes: {
|
||||
style: colorMatch ? `color: ${colorMatch[1]};` : '',
|
||||
},
|
||||
});
|
||||
}),
|
||||
].flat();
|
||||
@@ -77,7 +77,9 @@ const makeBlockReplaceExtension = (extensionSpec: ReplacementExtension) => {
|
||||
return extensionSpec.shouldFullReRender(transaction);
|
||||
};
|
||||
|
||||
if (transaction.docChanged || selectionChanged || wasRerenderRequested()) {
|
||||
const treeChanged = syntaxTree(transaction.state) !== syntaxTree(transaction.startState);
|
||||
|
||||
if (transaction.docChanged || selectionChanged || wasRerenderRequested() || treeChanged) {
|
||||
decorations = updateDecorations(transaction.state, extensionSpec);
|
||||
}
|
||||
|
||||
|
||||
8
packages/editor/CodeMirror/theme.ts
vendored
@@ -86,6 +86,7 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
const baseHeadingStyle = {
|
||||
fontWeight: 'bold',
|
||||
fontFamily: theme.fontFamily,
|
||||
paddingBottom: '0.2em',
|
||||
};
|
||||
|
||||
const codeMirrorTheme = EditorView.theme({
|
||||
@@ -210,7 +211,12 @@ const createTheme = (theme: EditorTheme): Extension[] => {
|
||||
// small.
|
||||
'& .cm-h1': {
|
||||
...baseHeadingStyle,
|
||||
fontSize: '1.6em',
|
||||
fontSize: '1.5em',
|
||||
},
|
||||
// Only underline level 1 headings not in block quotes. The underline overlaps with the blockquote border.
|
||||
'& .cm-h1:not(.cm-blockQuote)': {
|
||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||
marginBottom: '0.1em',
|
||||
},
|
||||
'& .cm-h2': {
|
||||
...baseHeadingStyle,
|
||||
|
||||
88
packages/editor/CodeMirror/utils/htmlNodeInfo.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { SyntaxNodeRef } from '@lezer/common';
|
||||
|
||||
export interface HtmlNodeInfo {
|
||||
node: SyntaxNodeRef;
|
||||
opening: boolean;
|
||||
closing: boolean;
|
||||
from: number;
|
||||
to: number;
|
||||
tagName: ()=> string;
|
||||
getAttr: (attributeName: string)=> string;
|
||||
}
|
||||
|
||||
type OnGetNodeContent = (node: SyntaxNodeRef)=> string;
|
||||
|
||||
const removeQuotes = (quoted: string) => quoted.replace(/^["'](.*)["']$/, '$1');
|
||||
|
||||
const getHtmlNodeAttr = (node: SyntaxNodeRef, attrName: string, getText: OnGetNodeContent) => {
|
||||
if (node.from === node.to) return null; // Empty
|
||||
const content = node.node.resolveInner(node.from + 1);
|
||||
|
||||
// Search for the "id" attribute
|
||||
const attributes = content.getChildren('Attribute');
|
||||
for (const attribute of attributes) {
|
||||
const nameNode = attribute.getChild('AttributeName');
|
||||
const valueNode = attribute.getChild('AttributeValue');
|
||||
|
||||
if (nameNode && valueNode) {
|
||||
const name = getText(nameNode).toLowerCase().replace(/^"(.*)"$/, '$1');
|
||||
if (name === attrName) {
|
||||
return removeQuotes(getText(valueNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Utility function to access CodeMirror HTML node information, based on
|
||||
// the corresponding Markdown node.
|
||||
const htmlNodeInfo = (node: SyntaxNodeRef, state: EditorState, offset = 0): HtmlNodeInfo|null => {
|
||||
// Already an HTML node?
|
||||
if (node.name === 'OpenTag' || node.name === 'CloseTag' || node.name === 'SelfClosingTag') {
|
||||
const getNodeText = (childNode: SyntaxNodeRef) => state.sliceDoc(childNode.from + offset, childNode.to + offset);
|
||||
const selfClosing = node.name === 'SelfClosingTag';
|
||||
|
||||
return {
|
||||
node,
|
||||
opening: node.name === 'OpenTag' || selfClosing,
|
||||
closing: node.name === 'CloseTag' || selfClosing,
|
||||
from: node.from + offset,
|
||||
to: node.to + offset,
|
||||
tagName: () => {
|
||||
const nodeText = getNodeText(node).trim();
|
||||
const tagNameMatch = nodeText.match(/^<\/?([^>\s]+)/);
|
||||
if (tagNameMatch) {
|
||||
return tagNameMatch[1];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getAttr: (name: string) => {
|
||||
return getHtmlNodeAttr(node, name, getNodeText);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Convert Markdown HTML nodes to HTML nodes
|
||||
if (node.name === 'HTMLTag' || node.name === 'HTMLBlock') {
|
||||
const globalOffset = node.from + offset;
|
||||
let resolved: HtmlNodeInfo|null = null;
|
||||
|
||||
// CodeMirror adds HTML information to Markdown documents using overlays attached
|
||||
// to HTMLTag and HTMLBlock nodes.
|
||||
// Use .enter to enter the overlay and visit the HTML nodes:
|
||||
node.node.enter(node.from, 1).toTree().iterate({
|
||||
enter: (subNode) => {
|
||||
resolved ??= htmlNodeInfo(subNode, state, globalOffset);
|
||||
return !resolved;
|
||||
},
|
||||
});
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default htmlNodeInfo;
|
||||
@@ -135,9 +135,12 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
if (view) {
|
||||
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
|
||||
const content = selectedText || '...';
|
||||
return showCreateEditablePrompt(
|
||||
block ? `$$\n\t${content}\n$$` : `$${content}$`, !block,
|
||||
)(state, dispatch, view);
|
||||
const blockStart = block ? '$$\n\t' : '$';
|
||||
return showCreateEditablePrompt({
|
||||
source: block ? `${blockStart}${content}\n$$` : `${blockStart}${content}$`,
|
||||
inline: !block,
|
||||
cursor: blockStart.length,
|
||||
})(state, dispatch, view);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -180,10 +183,13 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
return true;
|
||||
},
|
||||
[EditorCommandType.InsertCodeBlock]: (state, dispatch, view) => {
|
||||
const sourceBlockStart = '```\n';
|
||||
const selectedText = getTextBetween(state.doc, state.selection.from, state.selection.to);
|
||||
return showCreateEditablePrompt(
|
||||
`\`\`\`\n${selectedText}\n\`\`\``, false,
|
||||
)(state, dispatch, view);
|
||||
return showCreateEditablePrompt({
|
||||
source: `${sourceBlockStart}${selectedText}\n\`\`\``,
|
||||
inline: false,
|
||||
cursor: sourceBlockStart.length,
|
||||
})(state, dispatch, view);
|
||||
},
|
||||
[EditorCommandType.ToggleSearch]: (state, dispatch, view) => {
|
||||
const command = setSearchVisible(!getSearchVisible(state));
|
||||
|
||||
@@ -4,19 +4,20 @@ import { MarkType, ResolvedPos } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { closeHistory } from 'prosemirror-history';
|
||||
import showCreateEditablePrompt from './joplinEditablePlugin/showCreateEditablePrompt';
|
||||
|
||||
|
||||
interface InlineInputRule {
|
||||
interface InputRuleData {
|
||||
match: RegExp;
|
||||
matchEndCharacter: string;
|
||||
handler: (state: EditorState, match: RegExpMatchArray, start: number, end: number, commitCharacter: string)=> Transaction|null;
|
||||
commitCharacter: string|null;
|
||||
handler: (view: EditorView, match: RegExpMatchArray, start: number, end: number, commitCharacter: string)=> Transaction|null;
|
||||
}
|
||||
|
||||
// A custom input rule extension for inline input replacements.
|
||||
//
|
||||
// Ref: https://github.com/ProseMirror/prosemirror-inputrules/blob/43ef04ce9c1512ef8f2289578309c40b431ed3c5/src/inputrules.ts#L82
|
||||
// See https://discuss.prosemirror.net/t/trigger-inputrule-on-enter/1118 for why this approach is needed.
|
||||
const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: RegExp) => {
|
||||
const inlineInputRules = (rules: InputRuleData[], commitCharacterExpression: RegExp) => {
|
||||
const getContentBeforeCursor = (cursorInformation: ResolvedPos) => {
|
||||
const parent = cursorInformation.parent;
|
||||
const offsetInParent = cursorInformation.parentOffset;
|
||||
@@ -25,9 +26,11 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
|
||||
};
|
||||
|
||||
const getApplicableRule = (state: EditorState, cursor: number, justTypedText: string) => {
|
||||
if (!rules.some(rule => justTypedText.endsWith(rule.matchEndCharacter))) {
|
||||
const candidateRules = rules.filter(rule => justTypedText.endsWith(rule.commitCharacter ?? ''));
|
||||
if (!candidateRules.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursorInformation = state.doc.resolve(cursor);
|
||||
const inCode = cursorInformation.parent.type.spec.code;
|
||||
if (inCode) {
|
||||
@@ -35,7 +38,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
|
||||
}
|
||||
const beforeCursor = getContentBeforeCursor(cursorInformation) + justTypedText;
|
||||
|
||||
for (const rule of rules) {
|
||||
for (const rule of candidateRules) {
|
||||
const match = beforeCursor.match(rule.match);
|
||||
if (!match) continue;
|
||||
|
||||
@@ -44,7 +47,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
|
||||
return null;
|
||||
};
|
||||
|
||||
type PluginState = { pendingRule: InlineInputRule };
|
||||
type PluginState = { pendingRule: InputRuleData };
|
||||
const run = (view: EditorView, cursor: number, commitData: string) => {
|
||||
const commitCharacter = commitCharacterExpression.exec(commitData) ? commitData : '';
|
||||
|
||||
@@ -57,7 +60,7 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
|
||||
const beforeCursor = getContentBeforeCursor(view.state.doc.resolve(cursor));
|
||||
const match = beforeCursor.match(availableRule.match);
|
||||
if (match) {
|
||||
const transaction = availableRule.handler(view.state, match, cursor - match[0].length, cursor, commitCharacter);
|
||||
const transaction = availableRule.handler(view, match, cursor - match[0].length, cursor, commitCharacter);
|
||||
if (transaction) {
|
||||
// closeHistory: Move the markup completion to a separate history event so that it
|
||||
// can be undone separately.
|
||||
@@ -109,17 +112,25 @@ const inlineInputRules = (rules: InlineInputRule[], commitCharacterExpression: R
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const makeMarkInputRule = (
|
||||
regExpString: string, matchEndCharacter: string, replacement: (matches: RegExpMatchArray)=> string, mark: MarkType,
|
||||
): InlineInputRule => {
|
||||
type OnReplace = (matches: RegExpMatchArray, view: EditorView)=> string;
|
||||
interface InputRuleOptions {
|
||||
contentRegex: string|RegExp;
|
||||
commitCharacter: string|null; // null => only "Enter"
|
||||
onReplace: OnReplace;
|
||||
marks: MarkType[];
|
||||
}
|
||||
|
||||
const makeInputRule = ({
|
||||
contentRegex, commitCharacter, onReplace, marks,
|
||||
}: InputRuleOptions): InputRuleData => {
|
||||
const commitCharacterExp = '[.?!,:;¡¿() \\n]';
|
||||
const regex = new RegExp(`(^|${commitCharacterExp})${regExpString}$`);
|
||||
const regex = typeof contentRegex === 'string' ? new RegExp(`(^|${commitCharacterExp})${contentRegex}$`) : contentRegex;
|
||||
return {
|
||||
match: regex,
|
||||
matchEndCharacter,
|
||||
handler: (state, match, start, end, endCommitCharacter) => {
|
||||
commitCharacter,
|
||||
handler: (view, match, start, end, endCommitCharacter) => {
|
||||
const state = view.state;
|
||||
let transaction = state.tr.delete(start, end);
|
||||
const marks = [schema.mark(mark)];
|
||||
|
||||
const startCommitCharacter = match[1];
|
||||
|
||||
@@ -131,13 +142,12 @@ const makeMarkInputRule = (
|
||||
];
|
||||
matchesWithoutCommitCharacters.groups = match.groups;
|
||||
|
||||
const replacementText = replacement(matchesWithoutCommitCharacters);
|
||||
|
||||
const replacement = onReplace(matchesWithoutCommitCharacters, view);
|
||||
transaction = transaction.insert(
|
||||
transaction.mapping.map(start, -1),
|
||||
[
|
||||
!!startCommitCharacter && schema.text(startCommitCharacter),
|
||||
!!replacementText && schema.text(replacementText, marks),
|
||||
!!replacement && schema.text(replacement, marks.map(type => schema.mark(type))),
|
||||
!!endCommitCharacter && schema.text(endCommitCharacter),
|
||||
].filter(node => !!node),
|
||||
);
|
||||
@@ -149,33 +159,51 @@ const makeMarkInputRule = (
|
||||
|
||||
const baseInputRules = buildInputRules(schema);
|
||||
const inlineContentExp = '\\S[^\\n]*\\S|\\S';
|
||||
const noMatchRegex = /$^/;
|
||||
const inputRulesExtension = [
|
||||
baseInputRules,
|
||||
inlineInputRules([
|
||||
makeMarkInputRule(
|
||||
`\\*\\*(${inlineContentExp})\\*\\*`,
|
||||
'*',
|
||||
(match) => match[1],
|
||||
schema.marks.strong,
|
||||
),
|
||||
makeMarkInputRule(
|
||||
`\\*(${inlineContentExp})\\*`,
|
||||
'*',
|
||||
(match) => match[1],
|
||||
schema.marks.emphasis,
|
||||
),
|
||||
makeMarkInputRule(
|
||||
`_(${inlineContentExp})_`,
|
||||
'_',
|
||||
(match) => match[1],
|
||||
schema.marks.emphasis,
|
||||
),
|
||||
makeMarkInputRule(
|
||||
`[\`](${inlineContentExp})[\`]`,
|
||||
'`',
|
||||
(match) => match[1],
|
||||
schema.marks.code,
|
||||
),
|
||||
makeInputRule({
|
||||
contentRegex: /(^|[\n])(```+)(\w*)$/,
|
||||
commitCharacter: '',
|
||||
onReplace: (match, view) => {
|
||||
const blockStart = `${match[1]}${match[2]}\n`;
|
||||
const block = `${blockStart}\n${match[1]}`;
|
||||
showCreateEditablePrompt({
|
||||
source: block,
|
||||
inline: false,
|
||||
cursor: blockStart.length,
|
||||
})(view.state, view.dispatch, view);
|
||||
return '';
|
||||
},
|
||||
marks: [],
|
||||
}),
|
||||
], noMatchRegex),
|
||||
inlineInputRules([
|
||||
makeInputRule({
|
||||
contentRegex: `\\*\\*(${inlineContentExp})\\*\\*`,
|
||||
commitCharacter: '*',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.strong],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `\\*(${inlineContentExp})\\*`,
|
||||
commitCharacter: '*',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.emphasis],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `_(${inlineContentExp})_`,
|
||||
commitCharacter: '_',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.emphasis],
|
||||
}),
|
||||
makeInputRule({
|
||||
contentRegex: `\`(${inlineContentExp})\``,
|
||||
commitCharacter: '`',
|
||||
onReplace: (match) => match[1],
|
||||
marks: [schema.marks.code],
|
||||
}),
|
||||
], /[ .,?)!;]/),
|
||||
];
|
||||
export default inputRulesExtension;
|
||||
|
||||
@@ -23,13 +23,15 @@ const createEditorDialogForNode = (nodePosition: number, view: EditorView, onHid
|
||||
view.state.doc.nodeAt(nodePosition)
|
||||
);
|
||||
|
||||
const openCharacters = getNode().attrs.openCharacters ?? '';
|
||||
const { dismiss } = createEditorDialog({
|
||||
editorApi: getEditorApi(view.state),
|
||||
source: [
|
||||
getNode().attrs.openCharacters,
|
||||
openCharacters,
|
||||
getNode().attrs.source,
|
||||
getNode().attrs.closeCharacters,
|
||||
getNode().attrs.closeCharacters ?? '',
|
||||
].join(''),
|
||||
cursor: openCharacters.length,
|
||||
onSave: async (source) => {
|
||||
view.dispatch(
|
||||
view.state.tr.setNodeAttribute(
|
||||
|
||||
@@ -38,7 +38,11 @@ describe('showCreateEditorPrompt', () => {
|
||||
|
||||
test('should allow creating a new code block', async () => {
|
||||
const editor = createEditor('');
|
||||
showCreateEditablePrompt('```\ntest\n```', false)(editor.state, editor.dispatch, editor);
|
||||
showCreateEditablePrompt({
|
||||
source: '```\ntest\n```',
|
||||
inline: false,
|
||||
cursor: 8,
|
||||
})(editor.state, editor.dispatch, editor);
|
||||
|
||||
const dialog = findEditorDialog();
|
||||
dialog.submitButton.click();
|
||||
@@ -54,4 +58,16 @@ describe('showCreateEditorPrompt', () => {
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test('should position the cursor at the provided location', () => {
|
||||
const editor = createEditor('');
|
||||
showCreateEditablePrompt({
|
||||
source: '```\n\n```',
|
||||
inline: false,
|
||||
cursor: 4,
|
||||
})(editor.state, editor.dispatch, editor);
|
||||
|
||||
const dialog = findEditorDialog();
|
||||
expect(dialog.editor.selectionStart).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,20 @@ import postProcessRenderedHtml from './utils/postProcessRenderedHtml';
|
||||
import schema from '../../schema';
|
||||
import { JoplinEditableAttributes } from './joplinEditablePlugin';
|
||||
|
||||
const showCreateEditablePrompt = (source: string, inline: boolean): Command => (_state, dispatch, view) => {
|
||||
interface EditablePromptOptions {
|
||||
source: string;
|
||||
inline: boolean;
|
||||
cursor: number;
|
||||
}
|
||||
|
||||
const showCreateEditablePrompt = ({ source, inline, cursor }: EditablePromptOptions): Command => (_state, dispatch, view) => {
|
||||
if (!dispatch) return true;
|
||||
if (!view) throw new Error('Missing required argument: view');
|
||||
|
||||
createEditorDialog({
|
||||
editorApi: getEditorApi(view.state),
|
||||
source,
|
||||
cursor,
|
||||
onSave: async (newSource) => {
|
||||
source = newSource;
|
||||
},
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { EditorApi } from '../../joplinEditorApiPlugin';
|
||||
import { EditorLanguageType } from '../../../../types';
|
||||
import showModal from '../../../utils/dom/showModal';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
interface Options {
|
||||
source: string;
|
||||
cursor: number;
|
||||
editorApi: EditorApi;
|
||||
onSave: (newContent: string)=> void;
|
||||
onDismiss: ()=> void;
|
||||
}
|
||||
|
||||
const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) => {
|
||||
const createEditorDialog = ({ editorApi, source, cursor, onSave, onDismiss }: Options) => {
|
||||
const content = document.createElement('div');
|
||||
content.classList.add('editor-dialog-content');
|
||||
document.body.appendChild(content);
|
||||
@@ -22,9 +24,10 @@ const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) =
|
||||
},
|
||||
);
|
||||
editor.updateBody(source);
|
||||
editor.select(cursor, cursor);
|
||||
|
||||
const _ = editorApi.localize;
|
||||
return showModal({
|
||||
const modal = showModal({
|
||||
content,
|
||||
doneLabel: _('Done'),
|
||||
onDismiss: () => {
|
||||
@@ -32,6 +35,10 @@ const createEditorDialog = ({ editorApi, source, onSave, onDismiss }: Options) =
|
||||
editor.remove();
|
||||
},
|
||||
});
|
||||
|
||||
focus('createEditorDialog', editor);
|
||||
|
||||
return modal;
|
||||
};
|
||||
|
||||
export default createEditorDialog;
|
||||
|
||||
@@ -48,6 +48,9 @@ const joplinEditorApiPlugin = new Plugin<EditorApi>({
|
||||
updateBody: (newValue) => {
|
||||
editor.textArea.value = newValue;
|
||||
},
|
||||
select: (anchor, head) => {
|
||||
editor.textArea.setSelectionRange(Math.min(anchor, head), Math.max(anchor, head));
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface CodeEditorControl {
|
||||
focus: ()=> void;
|
||||
remove: ()=> void;
|
||||
updateBody: (newValue: string)=> void;
|
||||
select: (from: number, to: number)=> void;
|
||||
}
|
||||
export type OnCodeEditorChange = (newValue: string)=> void;
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
'moduleFileExtensions': [
|
||||
'ts',
|
||||
'tsx',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/editor",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.0",
|
||||
"description": "Web-based markdown editor",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -14,12 +14,12 @@
|
||||
"url": "git+https://github.com/laurent22/joplin.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/utils": "~3.5",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/utils": "~3.6",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react": "18.3.26",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
testMatch: [
|
||||
'**/*.test.js',
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"id": "<%= pluginId %>",
|
||||
"app_min_version": "3.5",
|
||||
"app_min_version": "3.6",
|
||||
"version": "1.0.0",
|
||||
"name": "<%= pluginName %>",
|
||||
"description": "<%= pluginDescription %>",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
testMatch: [
|
||||
'**/*.test.js',
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
@@ -30,8 +30,8 @@
|
||||
"yosay": "2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/lib": "~3.5",
|
||||
"@joplin/tools": "~3.5",
|
||||
"@joplin/lib": "~3.6",
|
||||
"@joplin/tools": "~3.6",
|
||||
"jest": "29.7.0",
|
||||
"ts-node": "10.9.2"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
|
||||
testMatch: ['**/*.test.js'],
|
||||
testPathIgnorePatterns: ['<rootDir>/node_modules/'],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/htmlpack",
|
||||
"version": "3.5.1",
|
||||
"version": "3.6.0",
|
||||
"description": "Pack an HTML file and all its linked resources into a single HTML file",
|
||||
"main": "dist/index.js",
|
||||
"types": "index.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Resource from './models/Resource';
|
||||
import shim from './shim';
|
||||
import Database from './database';
|
||||
import Database, { Row } from './database';
|
||||
import { SqlQuery } from './services/database/types';
|
||||
import addMigrationFile from './services/database/addMigrationFile';
|
||||
import sqlStringToLines from './services/database/sqlStringToLines';
|
||||
@@ -314,34 +314,44 @@ export default class JoplinDatabase extends Database {
|
||||
throw new Error(`\`notes_fts\` (${countFieldsNotesFts} fields) must have the same number of fields as \`items_fts\` (${countFieldsItemsFts} fields) for the search engine BM25 algorithm to work`);
|
||||
}
|
||||
|
||||
const tableRows = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
|
||||
interface TableRow {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const tableRows: TableRow[] = await this.selectAll('SELECT name FROM sqlite_master WHERE type=\'table\'');
|
||||
|
||||
for (let i = 0; i < tableRows.length; i++) {
|
||||
let pragmas: Row[] = [];
|
||||
const tableName = tableRows[i].name;
|
||||
if (tableName === 'android_metadata') continue;
|
||||
if (tableName === 'table_fields') continue;
|
||||
if (tableName === 'sqlite_sequence') continue;
|
||||
if (tableName.indexOf('notes_fts') === 0) continue;
|
||||
if (tableName.indexOf('items_fts') === 0) continue;
|
||||
if (tableName === 'notes_spellfix') continue;
|
||||
if (tableName === 'search_aux') continue;
|
||||
try {
|
||||
if (tableName === 'android_metadata') continue;
|
||||
if (tableName === 'table_fields') continue;
|
||||
if (tableName.startsWith('sqlite_')) continue;
|
||||
if (tableName.indexOf('notes_fts') === 0) continue;
|
||||
if (tableName.indexOf('items_fts') === 0) continue;
|
||||
if (tableName === 'notes_spellfix') continue;
|
||||
if (tableName === 'search_aux') continue;
|
||||
|
||||
const pragmas = await this.selectAll(`PRAGMA table_info("${tableName}")`);
|
||||
pragmas = await this.selectAll(`PRAGMA table_info("${tableName}")`);
|
||||
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
const item = pragmas[i];
|
||||
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
|
||||
let defaultValue = item.dflt_value;
|
||||
if (typeof defaultValue === 'string' && defaultValue.length >= 2 && defaultValue[0] === '"' && defaultValue[defaultValue.length - 1] === '"') {
|
||||
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
const item = pragmas[i];
|
||||
// In SQLite, if the default value is a string it has double quotes around it, so remove them here
|
||||
let defaultValue = item.dflt_value;
|
||||
if (typeof defaultValue === 'string' && defaultValue.length >= 2 && defaultValue[0] === '"' && defaultValue[defaultValue.length - 1] === '"') {
|
||||
defaultValue = defaultValue.substr(1, defaultValue.length - 2);
|
||||
}
|
||||
const q = Database.insertQuery('table_fields', {
|
||||
table_name: tableName,
|
||||
field_name: item.name,
|
||||
field_type: Database.enumId('fieldType', item.type),
|
||||
field_default: defaultValue,
|
||||
});
|
||||
queries.push(q);
|
||||
}
|
||||
const q = Database.insertQuery('table_fields', {
|
||||
table_name: tableName,
|
||||
field_name: item.name,
|
||||
field_type: Database.enumId('fieldType', item.type),
|
||||
field_default: defaultValue,
|
||||
});
|
||||
queries.push(q);
|
||||
} catch (error) {
|
||||
error.message = `On table: ${tableName}: Pragma: ${JSON.stringify(pragmas)}: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
packages/lib/ObjectUtils.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { convertValuesToFunctions, sortByValue } from './ObjectUtils';
|
||||
|
||||
describe('ObjectUtils', () => {
|
||||
test('should convert object values to functions', () => {
|
||||
const v = convertValuesToFunctions({ a: 6, b: ()=>7, c: 'test' });
|
||||
expect(v.a()).toBe(6);
|
||||
expect(v.b()).toBe(7);
|
||||
expect(v.c()).toBe('test');
|
||||
});
|
||||
|
||||
test('should sort an object\'s entries by value', () => {
|
||||
expect(sortByValue({
|
||||
a: 1,
|
||||
b: 'test1',
|
||||
c: 'test3',
|
||||
d: 'test2',
|
||||
})).toEqual({
|
||||
b: 'test1',
|
||||
d: 'test2',
|
||||
c: 'test3',
|
||||
a: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function sortByValue(object: any) {
|
||||
type AnyObject = Record<string|symbol, unknown>;
|
||||
|
||||
type SortableValue = string|number;
|
||||
type SortableObject = Record<string, SortableValue>;
|
||||
|
||||
export function sortByValue<T extends SortableObject>(object: T): T {
|
||||
const temp = [];
|
||||
for (const k in object) {
|
||||
if (!object.hasOwnProperty(k)) continue;
|
||||
@@ -10,8 +14,8 @@ export function sortByValue(object: any) {
|
||||
}
|
||||
|
||||
temp.sort((a, b) => {
|
||||
let v1 = a.value;
|
||||
let v2 = b.value;
|
||||
let v1: SortableValue = a.value;
|
||||
let v2: SortableValue = b.value;
|
||||
if (typeof v1 === 'string') v1 = v1.toLowerCase();
|
||||
if (typeof v2 === 'string') v2 = v2.toLowerCase();
|
||||
if (v1 === v2) return 0;
|
||||
@@ -28,8 +32,7 @@ export function sortByValue(object: any) {
|
||||
return output;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function fieldsEqual(o1: any, o2: any) {
|
||||
export function fieldsEqual(o1: AnyObject, o2: AnyObject) {
|
||||
if ((!o1 || !o2) && o1 !== o2) return false;
|
||||
|
||||
for (const k in o1) {
|
||||
@@ -45,8 +48,11 @@ export function fieldsEqual(o1: any, o2: any) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function convertValuesToFunctions(o: any) {
|
||||
type ValuesToFunctions<T extends AnyObject> = {
|
||||
[k in keyof T]: T[k] extends ()=> unknown ? T[k] : ()=> T[k]
|
||||
};
|
||||
|
||||
export function convertValuesToFunctions<T extends AnyObject>(o: T): ValuesToFunctions<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const output: any = {};
|
||||
for (const n in o) {
|
||||
@@ -58,8 +64,7 @@ export function convertValuesToFunctions(o: any) {
|
||||
return output;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
export function isEmpty(o: any) {
|
||||
export function isEmpty(o: AnyObject|null) {
|
||||
if (!o) return true;
|
||||
return Object.keys(o).length === 0 && o.constructor === Object;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Setting, { AppType, SettingMetadataSection, SettingSectionSource } from '../../../models/Setting';
|
||||
import SyncTargetRegistry from '../../../SyncTargetRegistry';
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
const { _ } = require('../../../locale');
|
||||
import { createSelector } from 'reselect';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
@@ -8,6 +7,7 @@ import Logger from '@joplin/utils/Logger';
|
||||
import { type ReactNode } from 'react';
|
||||
import { type Registry } from '../../../registry';
|
||||
import settingValidations from '../../../models/settings/settingValidations';
|
||||
import { convertValuesToFunctions } from '../../../ObjectUtils';
|
||||
|
||||
const logger = Logger.create('config-shared');
|
||||
|
||||
@@ -75,11 +75,11 @@ export const checkSyncConfig = async (comp: ConfigScreenComponent, settings: any
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
|
||||
|
||||
const options = {
|
||||
...Setting.subValues(`sync.${syncTargetId}`, settings),
|
||||
...Setting.subValues('net', settings) };
|
||||
...Setting.subValues(`sync.${syncTargetId}`, settings, { includeConstants: true }),
|
||||
...Setting.subValues('net', settings, { includeConstants: true }) };
|
||||
|
||||
comp.setState({ checkSyncConfigResult: 'checking' });
|
||||
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
|
||||
const result = await SyncTargetClass.checkConfig(convertValuesToFunctions(options));
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
|
||||
if (result.ok) {
|
||||
|
||||
@@ -151,7 +151,7 @@ shared.saveNoteButton_press = async function(comp: BaseNoteScreenComponent, stat
|
||||
|
||||
const savedNote = 'fields' in saveOptions && !saveOptions.fields.length ? { ...note } : await Note.save(note, saveOptions);
|
||||
|
||||
const stateNote = state.note;
|
||||
const stateNote = comp.state.note;
|
||||
|
||||
// Note was reloaded while being saved.
|
||||
if (!recreatedNote && (!stateNote || stateNote.id !== savedNote.id)) return releaseMutex();
|
||||
|
||||
@@ -26,11 +26,6 @@ export interface ArchiveExtractOptions {
|
||||
extractTo: string;
|
||||
}
|
||||
|
||||
export interface CabExtractOptions extends ArchiveExtractOptions {
|
||||
// Only files matching the pattern will be extracted
|
||||
fileNamePattern: string;
|
||||
}
|
||||
|
||||
export interface ZipEntry {
|
||||
entryName: string;
|
||||
name: string;
|
||||
@@ -276,8 +271,4 @@ export default class FsDriverBase {
|
||||
public async zipExtract(_options: ArchiveExtractOptions): Promise<ZipEntry[]> {
|
||||
throw new Error('Not implemented: zipExtract');
|
||||
}
|
||||
|
||||
public async cabExtract(_options: CabExtractOptions) {
|
||||
throw new Error('Not implemented: cabExtract.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import AdmZip = require('adm-zip');
|
||||
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions, CabExtractOptions } from './fs-driver-base';
|
||||
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions } from './fs-driver-base';
|
||||
import time from './time';
|
||||
import { execCommand } from '@joplin/utils';
|
||||
import { extname } from 'path';
|
||||
const md5File = require('md5-file');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -218,25 +216,4 @@ export default class FsDriverNode extends FsDriverBase {
|
||||
zip.extractAllTo(options.extractTo, false);
|
||||
return zip.getEntries();
|
||||
}
|
||||
|
||||
public async cabExtract(options: CabExtractOptions) {
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('Extracting CAB archives is only supported on Windows.');
|
||||
}
|
||||
|
||||
const source = this.resolve(options.source);
|
||||
const extractTo = this.resolve(options.extractTo);
|
||||
|
||||
if (extname(source).toLowerCase() !== '.cab') {
|
||||
throw new Error(`Invalid file extension. Expected .CAB. Was ${extname(source)}`);
|
||||
}
|
||||
|
||||
// See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/expand
|
||||
await execCommand([
|
||||
'expand.exe',
|
||||
source,
|
||||
`-f:${options.fileNamePattern}`,
|
||||
extractTo,
|
||||
], { quiet: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const baseConfig = require('../../jest.config.base.js');
|
||||
|
||||
const testPathIgnorePatterns = [
|
||||
'<rootDir>/node_modules/',
|
||||
@@ -11,6 +12,7 @@ if (!process.env.IS_CONTINUOUS_INTEGRATION) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
...baseConfig,
|
||||
testMatch: [
|
||||
'**/*.test.js',
|
||||
],
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -371,6 +374,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"غامق"
|
||||
],
|
||||
@@ -497,6 +503,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"التحقق جارٍ... فضلاً انتظر."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"إختر خيار"
|
||||
],
|
||||
@@ -569,6 +578,9 @@
|
||||
"Command": [
|
||||
"أمر"
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
"لوحة الأوامر"
|
||||
],
|
||||
@@ -859,6 +871,9 @@
|
||||
"Delete selected notes": [
|
||||
"حدف الملاحظات المحددة"
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -946,6 +961,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"يعرض المعلومات الكاملة عن الملاحظة."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"يعرض الملاحظة المعطاة."
|
||||
],
|
||||
@@ -1695,6 +1713,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
"سلسلة المفاتيح المدعومة: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
"المفاتيح التي تحتاج إلى ترقية"
|
||||
],
|
||||
@@ -1808,6 +1829,9 @@
|
||||
"Manage your plugins": [
|
||||
"إدارة الإضافات الخاصة بك"
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manual": [
|
||||
"يدوي"
|
||||
],
|
||||
@@ -1985,6 +2009,9 @@
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
"لم يعرَّف محرر نصوص. فضلاً اضبطه باستخدام `config editor <editor-path>`"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"Nord": [
|
||||
"Nord"
|
||||
],
|
||||
@@ -2141,6 +2168,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
"فتح معالج المزامنة..."
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"فتح..."
|
||||
],
|
||||
@@ -2153,6 +2183,9 @@
|
||||
"Options": [
|
||||
"خيارات"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
@@ -2573,6 +2606,9 @@
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2711,6 +2747,9 @@
|
||||
"Source: ": [
|
||||
"المصدر: "
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -3188,6 +3227,9 @@
|
||||
"Try again": [
|
||||
"حاول ثانية"
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"أكتب `help [command]` للمزيد من المعلومات عن أمر ما ، أو `help all` لمعلومات الاستخدام الكاملة."
|
||||
],
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"A5": [
|
||||
""
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -332,6 +335,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Удебелен"
|
||||
],
|
||||
@@ -437,6 +443,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Проверявам... Моля изчакайте."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Chrome Web Store": [
|
||||
""
|
||||
],
|
||||
@@ -497,6 +506,9 @@
|
||||
"Command": [
|
||||
""
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
""
|
||||
],
|
||||
@@ -521,6 +533,9 @@
|
||||
"Configuration": [
|
||||
"Настройки"
|
||||
],
|
||||
"Configured keyboard shortcuts:": [
|
||||
""
|
||||
],
|
||||
"Configures the size of scrollbars used in the app.": [
|
||||
""
|
||||
],
|
||||
@@ -683,6 +698,9 @@
|
||||
"Delete local data and re-download from sync target": [
|
||||
""
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -764,6 +782,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Показва пълната информация за бележката."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Показва зададената бележка."
|
||||
],
|
||||
@@ -1379,6 +1400,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
""
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
""
|
||||
],
|
||||
@@ -1466,6 +1490,9 @@
|
||||
"Manage your plugins": [
|
||||
""
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manual": [
|
||||
"Ръчен"
|
||||
],
|
||||
@@ -1619,6 +1646,9 @@
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
"Няма зададен текстови редактор. Моля задайте го с командата: `config editor <път-до-редактора>`"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
""
|
||||
],
|
||||
@@ -1742,6 +1772,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
""
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Отвори..."
|
||||
],
|
||||
@@ -1754,6 +1787,9 @@
|
||||
"Options": [
|
||||
"Конфигурация"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
@@ -2162,6 +2198,9 @@
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2282,6 +2321,9 @@
|
||||
"Source format: %s": [
|
||||
"Формат на източника: %s"
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -2739,6 +2781,9 @@
|
||||
"Try it now": [
|
||||
""
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Въведете `help [команда]` за повече информация относно командата, или въведете `help all` за пълната информация за ползването на командния ред."
|
||||
],
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -335,6 +338,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Masna slova"
|
||||
],
|
||||
@@ -440,6 +446,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Provjeravam... Pričekajte."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Odaberi opciju"
|
||||
],
|
||||
@@ -500,6 +509,9 @@
|
||||
"Command": [
|
||||
""
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
""
|
||||
],
|
||||
@@ -671,6 +683,9 @@
|
||||
"Delete local data and re-download from sync target": [
|
||||
""
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -752,6 +767,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Prikazuje potpune informacije o bilješci."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Prikazuje datu bilješku."
|
||||
],
|
||||
@@ -1370,6 +1388,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Podržani privjesak: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
@@ -1451,6 +1472,9 @@
|
||||
"Manage your plugins": [
|
||||
""
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manual": [
|
||||
"Ručno"
|
||||
],
|
||||
@@ -1604,6 +1628,9 @@
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
"Nije određen program za uređivanje teksta. Precizirajte ga pomoću `config editor <editor-path>`"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
""
|
||||
],
|
||||
@@ -1715,6 +1742,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
""
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Otvori..."
|
||||
],
|
||||
@@ -1727,6 +1757,9 @@
|
||||
"Options": [
|
||||
"Opcije"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
@@ -2126,6 +2159,9 @@
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2249,6 +2285,9 @@
|
||||
"Source format: %s": [
|
||||
"Izvorni format: %s"
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -2701,6 +2740,9 @@
|
||||
"Try again": [
|
||||
""
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Kucajte `help [command]` za više informacija o komandi; ili kucajte `help all` za detaljne informacije o načinu korištenja."
|
||||
],
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
"Quant a"
|
||||
],
|
||||
@@ -470,6 +473,9 @@
|
||||
"Beta": [
|
||||
"Beta"
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Negreta"
|
||||
],
|
||||
@@ -608,6 +614,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Verificant... Espereu."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Escolliu una opció"
|
||||
],
|
||||
@@ -674,9 +683,6 @@
|
||||
"Code View": [
|
||||
"Vista de codi"
|
||||
],
|
||||
"Code:": [
|
||||
"Codi:"
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
"Col·laborar en una llibreta amb altres"
|
||||
],
|
||||
@@ -704,6 +710,9 @@
|
||||
"Command": [
|
||||
"Ordre"
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
"Paleta d'ordres"
|
||||
],
|
||||
@@ -792,9 +801,6 @@
|
||||
"Convert it": [
|
||||
"Converteix-lo"
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Converteix la nota a Markdown"
|
||||
],
|
||||
"Convert to note": [
|
||||
"Converteix a nota"
|
||||
],
|
||||
@@ -850,9 +856,6 @@
|
||||
"Could not connect to plugin repository.": [
|
||||
"No s'ha pogut connectar al repositori d'extensions."
|
||||
],
|
||||
"Could not convert note to Markdown: %s": [
|
||||
"No s'ha pogut convertir la nota a Markdown: %s"
|
||||
],
|
||||
"Could not export notes: %s": [
|
||||
"No s'han pogut exportar les notes: %s"
|
||||
],
|
||||
@@ -1063,6 +1066,9 @@
|
||||
"Delete selected notes": [
|
||||
"Suprimeix les notes seleccionades"
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
"Suprimir la llibreta de la safata d'entrada?\n\nSi suprimiu la llibreta de la safata d'entrada, és possible que es perdin els correus electrònics que s'hagin enviat recentment."
|
||||
],
|
||||
@@ -1168,6 +1174,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Mostra la informació completa sobre la nota."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Mostra la nota indicada."
|
||||
],
|
||||
@@ -2084,6 +2093,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Clauer admès: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
"Claus que requereixen actualització"
|
||||
],
|
||||
@@ -2230,6 +2242,9 @@
|
||||
"Manage your plugins": [
|
||||
"Gestioneu les extensions"
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
"Gestiona la configuració E2EE. Les ordres són `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, i `target-status`."
|
||||
],
|
||||
@@ -2486,6 +2501,9 @@
|
||||
"No updates available": [
|
||||
"No hi ha actualitzacions disponibles"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
"(cap)"
|
||||
],
|
||||
@@ -2696,6 +2714,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
"Obre l'assistent de sincronització..."
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Obre..."
|
||||
],
|
||||
@@ -2717,6 +2738,9 @@
|
||||
"Options": [
|
||||
"Opcions"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
"Llista ordenada"
|
||||
],
|
||||
@@ -3348,6 +3372,9 @@
|
||||
"Select parent notebook": [
|
||||
"Selecciona la llibreta principal"
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Selected: %s": [
|
||||
"Seleccionat: %s"
|
||||
],
|
||||
@@ -3552,6 +3579,9 @@
|
||||
"Source: ": [
|
||||
"Font: "
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
"Espaiador"
|
||||
],
|
||||
@@ -3859,9 +3889,6 @@
|
||||
"The note \"%s\" has been successfully restored to the notebook \"%s\".": [
|
||||
"La nota \"%s\" s'ha restaurat correctament a la llibreta \"%s\"."
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"La nota s'ha convertit a Markdown i la nota original s'ha traslladat a la paperera"
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
"La nota s'ha traslladat amb èxit a la paperera.",
|
||||
"Les notes s'han traslladat amb èxit a la paperera."
|
||||
@@ -4163,6 +4190,9 @@
|
||||
"Try it now": [
|
||||
"Proveu-ho ara"
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Escriviu «help [command]» per a més informació sobre l'ordre; o escriviu «help all» per a la informació d'ús completa."
|
||||
],
|
||||
|
||||
@@ -143,6 +143,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -374,6 +377,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Tučně"
|
||||
],
|
||||
@@ -500,6 +506,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Probíhá kontrola... Počkejte prosím."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Zvolit možnost"
|
||||
],
|
||||
@@ -572,6 +581,9 @@
|
||||
"Command": [
|
||||
"Příkaz"
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
"Nabídka příkazů"
|
||||
],
|
||||
@@ -844,6 +856,9 @@
|
||||
"Delete selected notes": [
|
||||
"Odstranění vybraných poznámek"
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -931,6 +946,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Zobrazí veškeré informace o poznámce."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Zobrazí vybranou poznámku."
|
||||
],
|
||||
@@ -1689,6 +1707,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Podpora klíčenky: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
"Klíče, které je třeba aktualizovat"
|
||||
],
|
||||
@@ -1802,6 +1823,9 @@
|
||||
"Manage your plugins": [
|
||||
"Správa vašich rozšíření"
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
"Spravuje konfiguraci koncového šifrování. Příkazy jsou `zapnout`, `vypnout`, `dešifrovat`, `stav`, `dešifrovat soubor` a `cílový stav`."
|
||||
],
|
||||
@@ -1997,6 +2021,9 @@
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
"Nebyl vybrán textový editor. Prosím nastavte jej pomocí `config editor <editor-path>`"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"Nord": [
|
||||
"Nord"
|
||||
],
|
||||
@@ -2156,6 +2183,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
"Otevřete Průvodce synchronizací..."
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Otevřít..."
|
||||
],
|
||||
@@ -2168,6 +2198,9 @@
|
||||
"Options": [
|
||||
"Nastavení"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
"Seznam objednávek"
|
||||
],
|
||||
@@ -2639,6 +2672,9 @@
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2795,6 +2831,9 @@
|
||||
"Source: ": [
|
||||
"Zdroj: "
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -3308,6 +3347,9 @@
|
||||
"Try it now": [
|
||||
"Vyzkoušejte to nyní"
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Zadejte `help [příkaz]` pro více informací o příkazu; nebo `help all` pro kompletní nápovědu."
|
||||
],
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
"[Ingen]"
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
"En kort beskrivelse af billedet:"
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
"En kommasepareret liste af ord. Kan bruges til ualmindelige ord for at hjælpe stemmeindtastning med at stave dem korrekt."
|
||||
@@ -185,6 +185,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
"Om"
|
||||
],
|
||||
@@ -243,7 +246,7 @@
|
||||
"Tilføj brødtekst"
|
||||
],
|
||||
"Add column": [
|
||||
""
|
||||
"Tilføj kolonne"
|
||||
],
|
||||
"Add new": [
|
||||
"Tilføj ny"
|
||||
@@ -254,6 +257,9 @@
|
||||
"Add recipient:": [
|
||||
"Tilføj modtager:"
|
||||
],
|
||||
"Add row": [
|
||||
"Tilføj række"
|
||||
],
|
||||
"Add tags:": [
|
||||
"Tilføj etiketter:"
|
||||
],
|
||||
@@ -342,7 +348,7 @@
|
||||
"Der blev fundet en automatisk gemt tegning. Vedhæfter en kopi af den til noten?"
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
"Der opstod en fejl under afsendelse af svaret. Dette kan ske, hvis appen er offline eller ikke kan oprette forbindelse til serveren.\nFejl: %s"
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
"Der opstod en fejl: %s"
|
||||
@@ -467,6 +473,9 @@
|
||||
"Beta": [
|
||||
"Beta"
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Fed"
|
||||
],
|
||||
@@ -605,6 +614,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Tjekker... Vent venligst."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Vælg en mulighed"
|
||||
],
|
||||
@@ -671,9 +683,6 @@
|
||||
"Code View": [
|
||||
"Kodevisning"
|
||||
],
|
||||
"Code:": [
|
||||
"Kode:"
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
"Samarbejd med andre om en notesbog"
|
||||
],
|
||||
@@ -683,6 +692,9 @@
|
||||
"Collapse all notebooks": [
|
||||
"Luk alle notesbøger"
|
||||
],
|
||||
"Collapse title": [
|
||||
"Fold titel sammen"
|
||||
],
|
||||
"Collapsed": [
|
||||
"Foldet sammen"
|
||||
],
|
||||
@@ -698,6 +710,9 @@
|
||||
"Command": [
|
||||
"Kommando"
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
"Kommando-udvalg"
|
||||
],
|
||||
@@ -786,9 +801,6 @@
|
||||
"Convert it": [
|
||||
"Konverter den"
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Konverter note til Markdown"
|
||||
],
|
||||
"Convert to note": [
|
||||
"Konverter til note"
|
||||
],
|
||||
@@ -844,9 +856,6 @@
|
||||
"Could not connect to plugin repository.": [
|
||||
"Kunne ikke forbinde til plugin-lager."
|
||||
],
|
||||
"Could not convert note to Markdown: %s": [
|
||||
"Kunne ikke konvertere note til Markdown: %s"
|
||||
],
|
||||
"Could not export notes: %s": [
|
||||
"Kunne ikke eksportere noterne: %s"
|
||||
],
|
||||
@@ -1021,6 +1030,9 @@
|
||||
"Delete attachment \"%s\"?": [
|
||||
"Slet venhæftning \"%s\"?"
|
||||
],
|
||||
"Delete column": [
|
||||
"Slet kolonne"
|
||||
],
|
||||
"Delete expired sessions": [
|
||||
"Slet udløbne sessioner"
|
||||
],
|
||||
@@ -1048,9 +1060,15 @@
|
||||
"Delete profile \"%s\"": [
|
||||
"Slet profilen \"%s\""
|
||||
],
|
||||
"Delete row": [
|
||||
"Slet række"
|
||||
],
|
||||
"Delete selected notes": [
|
||||
"Slet valgte noter"
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
"Slet indbakke notesbogen?\n\nHvis du sletter indbakke-notesbogen, kan e-mails, der for nylig er blevet sendt til den, gå tabt."
|
||||
],
|
||||
@@ -1156,6 +1174,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Viser komplet information om note."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Viser valgt note."
|
||||
],
|
||||
@@ -1181,7 +1202,7 @@
|
||||
"Glem ikke adgangkoden, da den af sikkerhedshensyn er den *eneste* mulighed for dekryptering af data! For at aktivere kryptering til, skal du indtaste din adgangskode herunder."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
"Synes du, at Joplin-webappen er nyttig?"
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Dokumentscanner: Skabelon til titel"
|
||||
@@ -1540,6 +1561,9 @@
|
||||
"Expand all notebooks": [
|
||||
"Udvid alle notesbøger"
|
||||
],
|
||||
"Expand title": [
|
||||
"Udvid titel"
|
||||
],
|
||||
"Expanded": [
|
||||
"Udvidet"
|
||||
],
|
||||
@@ -1604,7 +1628,7 @@
|
||||
"Feature-markeringer"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
"Feedback"
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Hentede emner: %d/%d."
|
||||
@@ -2018,6 +2042,9 @@
|
||||
"Joplin Server": [
|
||||
"Joplin server"
|
||||
],
|
||||
"Joplin Server (SAML)": [
|
||||
"Joplin Server (SAML)"
|
||||
],
|
||||
"Joplin Server Business": [
|
||||
"Joplin Server Business"
|
||||
],
|
||||
@@ -2037,7 +2064,7 @@
|
||||
"Joplin SSO-godkendelse"
|
||||
],
|
||||
"Joplin supports saving the location at which notes are saved or created. Do you want to enable it? This can be changed at any time in settings.": [
|
||||
""
|
||||
"Joplin understøtter muligheden for at gemme den placering, hvor noter gemmes eller oprettes. Vil du aktivere denne funktion? Dette kan ændres når som helst i indstillingerne."
|
||||
],
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"Joplin Web-Clipper gør at du kan gemme hjemmesider og screenshots fra din browser i Joplin."
|
||||
@@ -2066,11 +2093,14 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Keychain-understøttet: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
"Nøgler der har brug for opgradering"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
"Etikette"
|
||||
],
|
||||
"Landscape": [
|
||||
"Landskab"
|
||||
@@ -2212,6 +2242,9 @@
|
||||
"Manage your plugins": [
|
||||
"Administrer dine udvidelser"
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
"Administrer E2EE-konfiguration. Kommandoer er `enable`(aktiver), `disable`(sluk), `decrypt`(dekrypter), `status`, `decrypt-file`(dekrypter fil) og `target-status` (modtager-status)."
|
||||
],
|
||||
@@ -2227,6 +2260,9 @@
|
||||
"Markdown editor": [
|
||||
"Markdown-editor"
|
||||
],
|
||||
"Markdown editor: Highlight active line": [
|
||||
"Markdown-editor: Fremhæv aktiv linje"
|
||||
],
|
||||
"Markdown editor: Render images": [
|
||||
"Markdown-editor: Gengiv billeder"
|
||||
],
|
||||
@@ -2465,6 +2501,9 @@
|
||||
"No updates available": [
|
||||
"Ingen opdatering tilgængelig"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
"Ingen"
|
||||
],
|
||||
@@ -2487,7 +2526,7 @@
|
||||
"Ikke nu"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
"Ikke nyttig"
|
||||
],
|
||||
"note": [
|
||||
"note"
|
||||
@@ -2537,6 +2576,9 @@
|
||||
"Note list style": [
|
||||
"Notatliste-stil"
|
||||
],
|
||||
"Note not published: %s": [
|
||||
"Note ikke offentliggjort: %s"
|
||||
],
|
||||
"Note preview": [
|
||||
"Note-forhåndsvisning"
|
||||
],
|
||||
@@ -2672,6 +2714,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
"Open Synk-hjælper..."
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Åbn..."
|
||||
],
|
||||
@@ -2693,11 +2738,14 @@
|
||||
"Options": [
|
||||
"Indstillinger"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
"Sorteret liste"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
"Andre"
|
||||
],
|
||||
"Other applications...": [
|
||||
"Andre applikationer..."
|
||||
@@ -2949,6 +2997,9 @@
|
||||
"Publish Note": [
|
||||
"Publicer note"
|
||||
],
|
||||
"Publish note \"%s\" (in notebook \"%s\")?": [
|
||||
"Udgiv note \"%s\" (i notesbogen \"%s\")?"
|
||||
],
|
||||
"Publish note...": [
|
||||
"Publicer note..."
|
||||
],
|
||||
@@ -2961,8 +3012,11 @@
|
||||
"Publish/unpublish": [
|
||||
"Publicer/afpublicer"
|
||||
],
|
||||
"Published at URL: %s": [
|
||||
"Offentliggjort på URL: %s"
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
"Offentliggør en note til Joplin Server eller Joplin Cloud"
|
||||
],
|
||||
"QR Code": [
|
||||
"QR-kode"
|
||||
@@ -3009,6 +3063,9 @@
|
||||
"Recipients:": [
|
||||
"Modtagere:"
|
||||
],
|
||||
"Recognise text:": [
|
||||
"Genkend tekst:"
|
||||
],
|
||||
"Recognize handwritten image": [
|
||||
"Genkend håndskrevet billede"
|
||||
],
|
||||
@@ -3237,6 +3294,9 @@
|
||||
"Save geo-location with notes": [
|
||||
"Gem geo-lokation i noter"
|
||||
],
|
||||
"Save geolocation?": [
|
||||
"Gem geo-lokation?"
|
||||
],
|
||||
"Scan notebook": [
|
||||
"Scan notesbog"
|
||||
],
|
||||
@@ -3307,11 +3367,14 @@
|
||||
"Vælg notesbog"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
"Vælg et af de andre understøttede synkroniseringsmål."
|
||||
],
|
||||
"Select parent notebook": [
|
||||
"Vælg overordnet notesbog"
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Selected: %s": [
|
||||
"Valgte: %s"
|
||||
],
|
||||
@@ -3516,6 +3579,9 @@
|
||||
"Source: ": [
|
||||
"Kilde: "
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
"Mellemrum"
|
||||
],
|
||||
@@ -3646,7 +3712,7 @@
|
||||
"Skifter til [notebook] - alle fremtidige handlinger sker i denne notesbog."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
"Synk"
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Synkroniser så mange enheder, du vil"
|
||||
@@ -3726,6 +3792,9 @@
|
||||
"Take photo": [
|
||||
"Tag et foto"
|
||||
],
|
||||
"Take survey": [
|
||||
"Deltag i undersøgelse"
|
||||
],
|
||||
"Task \"%s\" failed with error: %s": [
|
||||
"Afstandsstykke"
|
||||
],
|
||||
@@ -3745,7 +3814,7 @@
|
||||
"Tekstredigeringskomando"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
"Tak for din feedback!\nHar du tid til at udfylde en kort undersøgelse?"
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
"Den aktive profil kan ikke slettes. Skift til en anden profil, og prøv igen."
|
||||
@@ -3820,9 +3889,6 @@
|
||||
"The note \"%s\" has been successfully restored to the notebook \"%s\".": [
|
||||
"Noten \"%s\" er blevet gendannet til notesbogen \"%s\"."
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"Noten er blevet konverteret til Markdown, og den oprindelige note er blevet flyttet til papirkurven."
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
"Noten blev flyttet til papirkurven.",
|
||||
"Noter blev flyttet til papirkurven."
|
||||
@@ -4124,6 +4190,9 @@
|
||||
"Try it now": [
|
||||
"Prøv det nu"
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Tast `help [kommando]` for mere info om en kommando; eller tast `help all` for fuld brugsinformation."
|
||||
],
|
||||
@@ -4145,6 +4214,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
"Kan ikke dele logdata. Årsag: %s"
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
"Kan ikke dele notedata. Årsag: %s"
|
||||
],
|
||||
"Unchecked": [
|
||||
"Ikke markeret"
|
||||
],
|
||||
@@ -4290,7 +4362,7 @@
|
||||
"Bruges hvor en font med fast bredde er nødvendig for at vise læsbar tekst (f.eks. tabeller, afkrydsningsfelter, kode). Hvis ikke fundet, bruges en generisk monospatieret (fast bredde) font."
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
"Nyttig"
|
||||
],
|
||||
"User deletions": [
|
||||
"Brugersletninger"
|
||||
@@ -4398,7 +4470,7 @@
|
||||
"Ved oprettelse af ny opgave:"
|
||||
],
|
||||
"When enabled, requests that the images in the note be transcribed with a higher-quality on-server transcription service. Requires sync with a copy of the desktop app.": [
|
||||
""
|
||||
"Når denne funktion er aktiveret, anmodes der om, at billederne i noten transskriberes med en transskriptionstjeneste på serveren, der leverer højere kvalitet. Kræver synkronisering med en kopi af desktop-appen."
|
||||
],
|
||||
"When enabled, the application will scan your attachments and extract the text from it. This will allow you to search for text in these attachments.": [
|
||||
"Når det er aktiveret, vil applikationen scanne dine vedhæftede filer og udtrække teksten fra dem. Dette giver dig mulighed for at søge efter tekst i disse vedhæftede filer."
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"%d Minuten"
|
||||
],
|
||||
"%d note matches this pattern. Delete it?": [
|
||||
"%d Notize stimmt mit diesem Muster überein. Löschen?",
|
||||
"%d Notiz stimmt mit diesem Muster überein. Löschen?",
|
||||
"%d Notizen stimmen mit diesem Muster überein. Löschen?"
|
||||
],
|
||||
"%d note will be permanently deleted. Continue?": [
|
||||
@@ -168,7 +168,7 @@
|
||||
"[Nichts]"
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
"Eine kurze Beschreibung des Bildes:"
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
"Eine kommagetrennte Liste von Wörtern. Kann für ungewöhnliche Wörter verwendet werden, um die korrekte Schreibweise bei der Sprachsteuerung zu unterstützen."
|
||||
@@ -185,6 +185,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
"Über"
|
||||
],
|
||||
@@ -243,7 +246,7 @@
|
||||
"Text hinzufügen"
|
||||
],
|
||||
"Add column": [
|
||||
""
|
||||
"Spalte hinzufügen"
|
||||
],
|
||||
"Add new": [
|
||||
"Neu hinzufügen"
|
||||
@@ -254,6 +257,9 @@
|
||||
"Add recipient:": [
|
||||
"Füge Empfänger hinzu:"
|
||||
],
|
||||
"Add row": [
|
||||
"Zeile hinzufügen"
|
||||
],
|
||||
"Add tags:": [
|
||||
"Schlagwörter hinzufügen:"
|
||||
],
|
||||
@@ -342,7 +348,7 @@
|
||||
"Es wurde eine automatisch gespeicherte Zeichnung gefunden. Eine Kopie davon an die Notiz anhängen?"
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
"Beim Senden der Antwort ist ein Fehler aufgetreten. Dies kann passieren, wenn die App offline ist oder keine Verbindung zum Server herstellen kann.\nFehler: %s"
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
"Ein Fehler trat auf: %s"
|
||||
@@ -467,6 +473,9 @@
|
||||
"Beta": [
|
||||
"Beta"
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Fett"
|
||||
],
|
||||
@@ -605,6 +614,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Überprüfen… Bitte warten."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Wähle eine Option"
|
||||
],
|
||||
@@ -671,9 +683,6 @@
|
||||
"Code View": [
|
||||
"Code-Ansicht"
|
||||
],
|
||||
"Code:": [
|
||||
"Code:"
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
"Gemeinsam mit anderen an einem Notizbuch arbeiten"
|
||||
],
|
||||
@@ -683,6 +692,9 @@
|
||||
"Collapse all notebooks": [
|
||||
"Alle Notizbücher einklappen"
|
||||
],
|
||||
"Collapse title": [
|
||||
"Titel einklappen"
|
||||
],
|
||||
"Collapsed": [
|
||||
"Eingeklappt"
|
||||
],
|
||||
@@ -698,6 +710,9 @@
|
||||
"Command": [
|
||||
"Befehl"
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
"Befehlspalette"
|
||||
],
|
||||
@@ -786,9 +801,6 @@
|
||||
"Convert it": [
|
||||
"Umwandeln"
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Notiz in Markdown umwandeln"
|
||||
],
|
||||
"Convert to note": [
|
||||
"In eine Notiz umwandeln"
|
||||
],
|
||||
@@ -839,14 +851,11 @@
|
||||
"Konnte die Anwendung nicht autorisieren:\n\n%s\n\nBitte versuche es erneut."
|
||||
],
|
||||
"Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s": [
|
||||
"Es konnte keine Verbindung zum Joplin-Server hergestellt werden. Bitte überprüfe die Synchronisationsoptionen im Konfigurationsmenü. Vollständiger Fehler:\n\n%s"
|
||||
"Es konnte keine Verbindung zu Joplin Server hergestellt werden. Bitte überprüfe die Synchronisationsoptionen im Konfigurationsmenü. Vollständiger Fehler:\n\n%s"
|
||||
],
|
||||
"Could not connect to plugin repository.": [
|
||||
"Konnte keine Verbindung zum Plugin-Repository herstellen."
|
||||
],
|
||||
"Could not convert note to Markdown: %s": [
|
||||
"Konnte Notiz nicht in Markdown umwandeln: %s"
|
||||
],
|
||||
"Could not export notes: %s": [
|
||||
"Konnte Notizen nicht exportieren: %s"
|
||||
],
|
||||
@@ -1021,6 +1030,9 @@
|
||||
"Delete attachment \"%s\"?": [
|
||||
"Anhang „%s“ löschen?"
|
||||
],
|
||||
"Delete column": [
|
||||
"Spalte löschen"
|
||||
],
|
||||
"Delete expired sessions": [
|
||||
"Lösche abgelaufene Sitzungen"
|
||||
],
|
||||
@@ -1048,9 +1060,15 @@
|
||||
"Delete profile \"%s\"": [
|
||||
"Lösche Profil „%s“"
|
||||
],
|
||||
"Delete row": [
|
||||
"Zeile löschen"
|
||||
],
|
||||
"Delete selected notes": [
|
||||
"Ausgewählte Notizen löschen"
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
"Das Inbox-Notizbuch löschen?\n\nWenn du das Inbox-Notizbuch löschst, gehen kürzlich dort eingegangene Emails möglicherweise verloren."
|
||||
],
|
||||
@@ -1156,6 +1174,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
"Zeigt alle Informationen über die Notiz an."
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
"Zeigt die angegebene Notiz an."
|
||||
],
|
||||
@@ -1181,7 +1202,7 @@
|
||||
"Achte darauf, dass du das Passwort nicht verlierst, da dies aus Sicherheitsgründen die *einzige* Möglichkeit ist, deine Daten zu entschlüsseln! Um die Verschlüsselung zu aktivieren, gib bitte unten dein Passwort ein."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
"Findest du die Joplin-Webanwendung nützlich?"
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Dokumentenscanner: Titelvorlage"
|
||||
@@ -1253,7 +1274,7 @@
|
||||
"Duplizieren"
|
||||
],
|
||||
"Duplicate line": [
|
||||
"Dupliziere Zeile"
|
||||
"Zeile duplizieren"
|
||||
],
|
||||
"Duplicate selected notes": [
|
||||
"Ausgewählte Notizen duplizieren"
|
||||
@@ -1367,7 +1388,7 @@
|
||||
"Verschlüsselung aktivieren"
|
||||
],
|
||||
"Enable file:// URLs for images and videos": [
|
||||
"Aktiviere file://-URLs für Bilder und Videos"
|
||||
"URLs mit file:// für Bilder und Videos aktivieren"
|
||||
],
|
||||
"Enable footnotes": [
|
||||
"Fußnoten aktivieren"
|
||||
@@ -1376,10 +1397,10 @@
|
||||
"Fountain-Syntaxunterstützung aktivieren"
|
||||
],
|
||||
"Enable handwritten transcription": [
|
||||
"Handschriftumwandlung aktivieren"
|
||||
"Handschrifttranskription aktivieren"
|
||||
],
|
||||
"Enable HTML-to-Markdown conversion banner": [
|
||||
"Umwandlungsbanner für HTML zu Markdown aktivieren"
|
||||
"Banner für Umwandlung von HTML zu Markdown aktivieren"
|
||||
],
|
||||
"Enable Linkify": [
|
||||
"Linkify aktivieren"
|
||||
@@ -1540,6 +1561,9 @@
|
||||
"Expand all notebooks": [
|
||||
"Alle Notizbücher ausklappen"
|
||||
],
|
||||
"Expand title": [
|
||||
"Titel ausklappen"
|
||||
],
|
||||
"Expanded": [
|
||||
"Ausgeklappt"
|
||||
],
|
||||
@@ -1604,7 +1628,7 @@
|
||||
"Funktionsargumente"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
"Feedback"
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Geladene Elemente: %d/%d."
|
||||
@@ -2018,6 +2042,9 @@
|
||||
"Joplin Server": [
|
||||
"Joplin Server"
|
||||
],
|
||||
"Joplin Server (SAML)": [
|
||||
"Joplin Server (SAML)"
|
||||
],
|
||||
"Joplin Server Business": [
|
||||
"Joplin Server Business"
|
||||
],
|
||||
@@ -2037,7 +2064,7 @@
|
||||
"Joplin SSO-Authentifizierung"
|
||||
],
|
||||
"Joplin supports saving the location at which notes are saved or created. Do you want to enable it? This can be changed at any time in settings.": [
|
||||
""
|
||||
"Joplin unterstützt das Speichern des Standortes, an dem Notizen gespeichert oder erstellt werden. Möchtest du das aktivieren? Dies kann jederzeit in den Einstellungen geändert werden."
|
||||
],
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"Joplin Web Clipper ermöglicht das Speichern von Webseiten und Screenshots aus deinem Browser in Joplin."
|
||||
@@ -2046,7 +2073,7 @@
|
||||
"Joplin Webseite"
|
||||
],
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": [
|
||||
"Joplins eigener Synchronisationsdienst. Ermöglicht auch den Zugriff auf Joplin-spezifische Funktionen wie das Veröffentlichen von Notizen oder die Zusammenarbeit an Notizbüchern mit anderen."
|
||||
"Joplins eigener Synchronisationsdienst. Ermöglicht auch den Zugriff auf joplinspezifische Funktionen wie das Veröffentlichen von Notizen oder die Zusammenarbeit an Notizbüchern mit anderen."
|
||||
],
|
||||
"Keep note history for": [
|
||||
"Notizenverlauf speichern für"
|
||||
@@ -2066,11 +2093,14 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Unterstützter Schlüsselbund: %s"
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
"Hauptschlüssel, die aktualisiert werden müssen"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
"Beschriftung"
|
||||
],
|
||||
"Landscape": [
|
||||
"Querformat"
|
||||
@@ -2212,6 +2242,9 @@
|
||||
"Manage your plugins": [
|
||||
"Erweiterungen verwalten"
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
"Verwaltet die E2EE-Konfiguration. Die Befehle lauten `enable`, `disable`, `decrypt`, `status`, `decrypt-file` und `target-status`."
|
||||
],
|
||||
@@ -2227,11 +2260,14 @@
|
||||
"Markdown editor": [
|
||||
"Markdown-Editor"
|
||||
],
|
||||
"Markdown editor: Highlight active line": [
|
||||
"Markdown-Editor: Aktive Zeile hervorheben"
|
||||
],
|
||||
"Markdown editor: Render images": [
|
||||
"Markdown-Editor: Bilder darstellen"
|
||||
],
|
||||
"Markdown editor: Render markup in editor": [
|
||||
"Markdown-Editor: Auszeichnungen im Editor darstellen"
|
||||
"Markdown-Editor: Markup im Editor darstellen"
|
||||
],
|
||||
"Marks a to-do as done.": [
|
||||
"Markiert eine Aufgabe als erledigt."
|
||||
@@ -2465,6 +2501,9 @@
|
||||
"No updates available": [
|
||||
"Keine Updates verfügbar"
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
"Nichts"
|
||||
],
|
||||
@@ -2487,7 +2526,7 @@
|
||||
"Nicht jetzt"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
"Nicht nützlich"
|
||||
],
|
||||
"note": [
|
||||
"Notiz"
|
||||
@@ -2537,6 +2576,9 @@
|
||||
"Note list style": [
|
||||
"Stil der Notiz-Liste"
|
||||
],
|
||||
"Note not published: %s": [
|
||||
"Notiz nicht veröffentlicht: %s"
|
||||
],
|
||||
"Note preview": [
|
||||
"Notizvorschau"
|
||||
],
|
||||
@@ -2672,6 +2714,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
"Sync-Assistent öffnen..."
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Öffnen..."
|
||||
],
|
||||
@@ -2693,11 +2738,14 @@
|
||||
"Options": [
|
||||
"Optionen"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
"Geordnete Liste"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
"Andere"
|
||||
],
|
||||
"Other applications...": [
|
||||
"Andere Anwendungen..."
|
||||
@@ -2949,6 +2997,9 @@
|
||||
"Publish Note": [
|
||||
"Notiz veröffentlichen"
|
||||
],
|
||||
"Publish note \"%s\" (in notebook \"%s\")?": [
|
||||
"Notiz \"%s\" (im Notizbuch \"%s\") veröffentlichen?"
|
||||
],
|
||||
"Publish note...": [
|
||||
"Notiz veröffentlichen..."
|
||||
],
|
||||
@@ -2961,8 +3012,11 @@
|
||||
"Publish/unpublish": [
|
||||
"Veröffentlichen / nicht veröffentlichen"
|
||||
],
|
||||
"Published at URL: %s": [
|
||||
"Veröffentlicht unter URL: %s"
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
"Veröffentlicht eine Notiz auf Joplin Server oder Joplin Cloud"
|
||||
],
|
||||
"QR Code": [
|
||||
"QR-Code"
|
||||
@@ -3009,6 +3063,9 @@
|
||||
"Recipients:": [
|
||||
"Empfänger:"
|
||||
],
|
||||
"Recognise text:": [
|
||||
"Text erkennen:"
|
||||
],
|
||||
"Recognize handwritten image": [
|
||||
"Handschrift-Bild erkennen"
|
||||
],
|
||||
@@ -3076,7 +3133,7 @@
|
||||
"Benennt das angegebene <item> (Notiz oder Notizbuch) zu <name> um."
|
||||
],
|
||||
"Renders markup on all lines that don't include the cursor.": [
|
||||
"Stellt Auszeichnungen in allen Zeilen dar, die den Cursor nicht enthalten."
|
||||
"Rendert Markup in allen Zeilen, die den Cursor nicht enthalten."
|
||||
],
|
||||
"Renew token": [
|
||||
"Token erneuern"
|
||||
@@ -3237,6 +3294,9 @@
|
||||
"Save geo-location with notes": [
|
||||
"Geografischen Standort zusammen mit Notizen speichern"
|
||||
],
|
||||
"Save geolocation?": [
|
||||
"Geografischen Standort speichern?"
|
||||
],
|
||||
"Scan notebook": [
|
||||
"Notizbuch scannen"
|
||||
],
|
||||
@@ -3307,11 +3367,14 @@
|
||||
"Notizbuch auswählen"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
"Wähle eines der anderen unterstützten Synchronisationsziele."
|
||||
],
|
||||
"Select parent notebook": [
|
||||
"Eltern-Notizbuch auswählen"
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Selected: %s": [
|
||||
"Ausgewählt: %s"
|
||||
],
|
||||
@@ -3346,7 +3409,7 @@
|
||||
"Setze auf 0 um den gesamten verfügbaren Platz zu verwenden. Die empfohlene Breite ist 600."
|
||||
],
|
||||
"Set the password": [
|
||||
"Gib das Passwort ein"
|
||||
"Passwort festlegen"
|
||||
],
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s": [
|
||||
"Setzt die Eigenschaft <name> der gegebenen <note> auf den gegebenen [Wert]. Mögliche Werte sind:\n\n%s"
|
||||
@@ -3358,10 +3421,10 @@
|
||||
"Freigeben"
|
||||
],
|
||||
"Share a copy of all notes in a file format that can be imported by Joplin on a computer.": [
|
||||
"Teile eine Kopie aller Notizen in einem Dateiformat, welches von Joplin auf einem anderen Computer importiert werden kann."
|
||||
"Gib eine Kopie aller Notizen in einem Dateiformat frei, welches von Joplin auf einem Computer importiert werden kann."
|
||||
],
|
||||
"Share a notebook with others": [
|
||||
"Teile ein Notizbuch mit anderen"
|
||||
"Gib ein Notizbuch für andere frei"
|
||||
],
|
||||
"Share from %s (%s)": [
|
||||
"Freigeben von %s (%s)"
|
||||
@@ -3385,7 +3448,7 @@
|
||||
"Gibt das angegebene [Notizbuch] für [Benutzer] frei oder hebt diese Freigabe auf. Erfordert Joplin Cloud oder Joplin Server."
|
||||
],
|
||||
"Sharing notebook...": [
|
||||
"Notizbuch teilen..."
|
||||
"Notizbuch freigeben..."
|
||||
],
|
||||
"Shortcuts are not available in CLI mode.": [
|
||||
"Tastenkürzel sind im CLI-Modus nicht verfügbar."
|
||||
@@ -3496,7 +3559,7 @@
|
||||
"Notizbücher sortieren nach"
|
||||
],
|
||||
"Sort notes by": [
|
||||
"Sortiere Notizen nach"
|
||||
"Notizen sortieren nach"
|
||||
],
|
||||
"Sort selected lines": [
|
||||
"Ausgewählte Zeilen sortieren"
|
||||
@@ -3516,6 +3579,9 @@
|
||||
"Source: ": [
|
||||
"Quelle: "
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
"Abstandhalter"
|
||||
],
|
||||
@@ -3646,7 +3712,7 @@
|
||||
"Wechselt zu [notebook] - alle weiteren Aktionen werden in diesem Notizbuch ausgeführt."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
"Synchronisieren"
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Synchronisiere mit beliebig vielen Geräten"
|
||||
@@ -3726,6 +3792,9 @@
|
||||
"Take photo": [
|
||||
"Foto aufnehmen"
|
||||
],
|
||||
"Take survey": [
|
||||
"An Umfrage teilnehmen"
|
||||
],
|
||||
"Task \"%s\" failed with error: %s": [
|
||||
"Aufgabe \"%s\" fehlgeschlagen mit Fehler: %s"
|
||||
],
|
||||
@@ -3745,7 +3814,7 @@
|
||||
"Texteditor-Befehl"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
"Danke für das Feedback!\nHast du Zeit, eine kurze Umfrage auszufüllen?"
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
"Das aktive Profil kann nicht gelöscht werden. Bitte wechsle in ein anderes Profil und versuche es erneut."
|
||||
@@ -3820,9 +3889,6 @@
|
||||
"The note \"%s\" has been successfully restored to the notebook \"%s\".": [
|
||||
"Die Notiz „%s“ wurde erfolgreich im Notizbuch „%s“ wiederhergestellt."
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"Die Notiz wurde in Markdown umgewandelt und die Originalnotiz in den Papierkorb verschoben"
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
"Die Notiz wurde erfolgreich in den Papierkorb verschoben.",
|
||||
"Die Notizen wurden erfolgreich in den Papierkorb verschoben."
|
||||
@@ -3927,7 +3993,7 @@
|
||||
"Diese Zeichnung kann ungespeicherte Änderungen enthalten."
|
||||
],
|
||||
"This feature is disabled by default, you need to manually enable it by turning on the option to 'Enable handwritten transcription'.": [
|
||||
"Diese Funktion ist per Voreinstellung deaktiviert. Du musst sie manuell aktivieren, indem du die Option ‚Handschriftumwandlung aktivieren‘ einschaltest."
|
||||
"Diese Funktion ist per Voreinstellung deaktiviert. Du musst sie manuell aktivieren, indem du die Option ‚Handschrifttranskription aktivieren‘ einschaltest."
|
||||
],
|
||||
"This feature is only available on Joplin Cloud and Joplin Server.": [
|
||||
"Diese Funktion ist nur verfügbar in Joplin Cloud und Joplin Server."
|
||||
@@ -4124,6 +4190,9 @@
|
||||
"Try it now": [
|
||||
"Jetzt testen"
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
"Tippe `help [Befehl]`, um weitere Informationen über einen Befehl zu erhalten oder tippe `help all` für die vollständigen Hinweise zur Nutzung des Befehls."
|
||||
],
|
||||
@@ -4145,6 +4214,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
"Protokolldaten können nicht geteilt werden. Grund: %s"
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
"Notizdaten können nicht geteilt werden. Grund: %s"
|
||||
],
|
||||
"Unchecked": [
|
||||
"Nicht angehakt"
|
||||
],
|
||||
@@ -4155,7 +4227,7 @@
|
||||
"Rückgängig"
|
||||
],
|
||||
"Uninstall and reinstall the application. Make sure you create a backup first by exporting all your notes as JEX from the desktop application.": [
|
||||
"Deinstalliere die Anwendung und installieren sie dann neu. Stelle sicher, dass du zuerst ein Backup erstellst, indem du alle Notizen als JEX aus der Desktop-Anwendung exportierst."
|
||||
"Deinstalliere die Anwendung und installiere sie erneut. Erstelle zuvor unbedingt ein Backup, indem du alle deine Notizen aus der Desktop-Anwendung als JEX exportierst."
|
||||
],
|
||||
"Unknown": [
|
||||
"Unbekannt"
|
||||
@@ -4176,13 +4248,13 @@
|
||||
"Ungeordnete Liste"
|
||||
],
|
||||
"Unpublish": [
|
||||
"Nicht veröffentlichen"
|
||||
"Nicht mehr veröffentlichen"
|
||||
],
|
||||
"Unpublish \"%s\"": [
|
||||
"„%s“ nicht veröffentlichen"
|
||||
"„%s“ nicht mehr veröffentlichen"
|
||||
],
|
||||
"Unpublish note": [
|
||||
"Notiz nicht veröffentlichen"
|
||||
"Notiz nicht mehr veröffentlichen"
|
||||
],
|
||||
"Unshare": [
|
||||
"Nicht mehr freigeben"
|
||||
@@ -4290,7 +4362,7 @@
|
||||
"Verwendete Schriftart mit fester Breite, um Text lesbar zu machen (z.B. in Tabellen, Kontrollkästchen, Code). Falls nicht vorhanden wird eine Monotype-Scrhiftart (mit fester Breite) verwendet."
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
"Nützlich"
|
||||
],
|
||||
"User deletions": [
|
||||
"Benutzer-Löschungen"
|
||||
@@ -4398,7 +4470,7 @@
|
||||
"Beim Erstellen einer neuen Aufgabe:"
|
||||
],
|
||||
"When enabled, requests that the images in the note be transcribed with a higher-quality on-server transcription service. Requires sync with a copy of the desktop app.": [
|
||||
""
|
||||
"Wenn aktiviert, werden die Bilder in der Notiz mit einem qualitativ höherwertigen serverseitigen Transkriptionsdienst umgewandelt. Erfordert die Synchronisation mit einer Kopie der Desktop-App."
|
||||
],
|
||||
"When enabled, the application will scan your attachments and extract the text from it. This will allow you to search for text in these attachments.": [
|
||||
"Wenn aktiviert, wird die Anwendung deine Anhänge einlesen und Text aus ihnen extrahieren. Damit kannst du innerhalb dieser Anhänge nach Text suchen."
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
"A5": [
|
||||
""
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -470,6 +473,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
""
|
||||
],
|
||||
@@ -524,6 +530,9 @@
|
||||
"Cannot change encrypted item": [
|
||||
""
|
||||
],
|
||||
"Cannot convert read-only item: \"%s\"": [
|
||||
""
|
||||
],
|
||||
"Cannot copy note to \"%s\" notebook": [
|
||||
""
|
||||
],
|
||||
@@ -608,6 +617,9 @@
|
||||
"Checking... Please wait.": [
|
||||
""
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
""
|
||||
],
|
||||
@@ -674,9 +686,6 @@
|
||||
"Code View": [
|
||||
""
|
||||
],
|
||||
"Code:": [
|
||||
""
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
""
|
||||
],
|
||||
@@ -704,6 +713,9 @@
|
||||
"Command": [
|
||||
""
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
""
|
||||
],
|
||||
@@ -743,6 +755,9 @@
|
||||
"Configuration": [
|
||||
""
|
||||
],
|
||||
"Configured keyboard shortcuts:": [
|
||||
""
|
||||
],
|
||||
"Configures the size of scrollbars used in the app.": [
|
||||
""
|
||||
],
|
||||
@@ -792,7 +807,7 @@
|
||||
"Convert it": [
|
||||
""
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Convert to Markdown": [
|
||||
""
|
||||
],
|
||||
"Convert to note": [
|
||||
@@ -850,7 +865,7 @@
|
||||
"Could not connect to plugin repository.": [
|
||||
""
|
||||
],
|
||||
"Could not convert note to Markdown: %s": [
|
||||
"Could not convert notes to Markdown: %s": [
|
||||
""
|
||||
],
|
||||
"Could not export notes: %s": [
|
||||
@@ -1057,12 +1072,18 @@
|
||||
"Delete profile \"%s\"": [
|
||||
""
|
||||
],
|
||||
"Delete profile \"%s\"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.": [
|
||||
""
|
||||
],
|
||||
"Delete row": [
|
||||
""
|
||||
],
|
||||
"Delete selected notes": [
|
||||
""
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -1168,6 +1189,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
""
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
""
|
||||
],
|
||||
@@ -1300,6 +1324,9 @@
|
||||
"Edit profile configuration...": [
|
||||
""
|
||||
],
|
||||
"Edit tag": [
|
||||
""
|
||||
],
|
||||
"Editor": [
|
||||
""
|
||||
],
|
||||
@@ -1366,6 +1393,9 @@
|
||||
"Enable abbreviation syntax": [
|
||||
""
|
||||
],
|
||||
"Enable ABC musical notation support": [
|
||||
""
|
||||
],
|
||||
"Enable audio player": [
|
||||
""
|
||||
],
|
||||
@@ -1859,6 +1889,9 @@
|
||||
"Import or export your data": [
|
||||
""
|
||||
],
|
||||
"Import...": [
|
||||
""
|
||||
],
|
||||
"Imported successfully!": [
|
||||
""
|
||||
],
|
||||
@@ -2084,6 +2117,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
""
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
""
|
||||
],
|
||||
@@ -2230,6 +2266,9 @@
|
||||
"Manage your plugins": [
|
||||
""
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
""
|
||||
],
|
||||
@@ -2336,6 +2375,9 @@
|
||||
"",
|
||||
""
|
||||
],
|
||||
"Move %d notebooks to the trash?\n\nAll notes and sub-notebooks within these notebooks will also be moved to the trash.": [
|
||||
""
|
||||
],
|
||||
"Move down": [
|
||||
""
|
||||
],
|
||||
@@ -2486,6 +2528,9 @@
|
||||
"No updates available": [
|
||||
""
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
""
|
||||
],
|
||||
@@ -2696,6 +2741,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
""
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
""
|
||||
],
|
||||
@@ -2717,6 +2765,9 @@
|
||||
"Options": [
|
||||
""
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
""
|
||||
],
|
||||
@@ -2946,6 +2997,9 @@
|
||||
"Profile name": [
|
||||
""
|
||||
],
|
||||
"Profile name cannot be empty": [
|
||||
""
|
||||
],
|
||||
"Profile name:": [
|
||||
""
|
||||
],
|
||||
@@ -3078,6 +3132,9 @@
|
||||
"Remove": [
|
||||
""
|
||||
],
|
||||
"Remove %d tags from all notes? This cannot be undone.": [
|
||||
""
|
||||
],
|
||||
"Remove %s": [
|
||||
""
|
||||
],
|
||||
@@ -3348,6 +3405,9 @@
|
||||
"Select parent notebook": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Selected: %s": [
|
||||
""
|
||||
],
|
||||
@@ -3552,6 +3612,9 @@
|
||||
"Source: ": [
|
||||
""
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -3750,9 +3813,15 @@
|
||||
"Tab moves focus": [
|
||||
""
|
||||
],
|
||||
"Table": [
|
||||
""
|
||||
],
|
||||
"Tabloid": [
|
||||
""
|
||||
],
|
||||
"Tag: %s": [
|
||||
""
|
||||
],
|
||||
"Tagged: %d.": [
|
||||
""
|
||||
],
|
||||
@@ -3860,6 +3929,7 @@
|
||||
""
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"",
|
||||
""
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
@@ -4163,6 +4233,9 @@
|
||||
"Try it now": [
|
||||
""
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
""
|
||||
],
|
||||
|
||||
@@ -185,6 +185,9 @@
|
||||
"A5": [
|
||||
""
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -458,6 +461,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
""
|
||||
],
|
||||
@@ -512,6 +518,9 @@
|
||||
"Cannot change encrypted item": [
|
||||
""
|
||||
],
|
||||
"Cannot convert read-only item: \"%s\"": [
|
||||
""
|
||||
],
|
||||
"Cannot copy note to \"%s\" notebook": [
|
||||
""
|
||||
],
|
||||
@@ -596,6 +605,9 @@
|
||||
"Checking... Please wait.": [
|
||||
""
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
""
|
||||
],
|
||||
@@ -662,9 +674,6 @@
|
||||
"Code View": [
|
||||
""
|
||||
],
|
||||
"Code:": [
|
||||
""
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
""
|
||||
],
|
||||
@@ -692,6 +701,9 @@
|
||||
"Command": [
|
||||
""
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
""
|
||||
],
|
||||
@@ -731,6 +743,9 @@
|
||||
"Configuration": [
|
||||
""
|
||||
],
|
||||
"Configured keyboard shortcuts:": [
|
||||
""
|
||||
],
|
||||
"Configures the size of scrollbars used in the app.": [
|
||||
""
|
||||
],
|
||||
@@ -780,7 +795,7 @@
|
||||
"Convert it": [
|
||||
""
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Convert to Markdown": [
|
||||
""
|
||||
],
|
||||
"Convert to note": [
|
||||
@@ -838,7 +853,7 @@
|
||||
"Could not connect to plugin repository.": [
|
||||
""
|
||||
],
|
||||
"Could not convert note to Markdown: %s": [
|
||||
"Could not convert notes to Markdown: %s": [
|
||||
""
|
||||
],
|
||||
"Could not export notes: %s": [
|
||||
@@ -1045,12 +1060,18 @@
|
||||
"Delete profile \"%s\"": [
|
||||
""
|
||||
],
|
||||
"Delete profile \"%s\"?\n\nAll data, including notes, notebooks and tags will be permanently deleted.": [
|
||||
""
|
||||
],
|
||||
"Delete row": [
|
||||
""
|
||||
],
|
||||
"Delete selected notes": [
|
||||
""
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -1153,6 +1174,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
""
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
""
|
||||
],
|
||||
@@ -1285,6 +1309,9 @@
|
||||
"Edit profile configuration...": [
|
||||
""
|
||||
],
|
||||
"Edit tag": [
|
||||
""
|
||||
],
|
||||
"Editor": [
|
||||
""
|
||||
],
|
||||
@@ -1351,6 +1378,9 @@
|
||||
"Enable abbreviation syntax": [
|
||||
""
|
||||
],
|
||||
"Enable ABC musical notation support": [
|
||||
""
|
||||
],
|
||||
"Enable audio player": [
|
||||
""
|
||||
],
|
||||
@@ -1841,6 +1871,9 @@
|
||||
"Import or export your data": [
|
||||
""
|
||||
],
|
||||
"Import...": [
|
||||
""
|
||||
],
|
||||
"Imported successfully!": [
|
||||
""
|
||||
],
|
||||
@@ -2063,6 +2096,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
""
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
""
|
||||
],
|
||||
@@ -2209,6 +2245,9 @@
|
||||
"Manage your plugins": [
|
||||
""
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
""
|
||||
],
|
||||
@@ -2315,6 +2354,9 @@
|
||||
"",
|
||||
""
|
||||
],
|
||||
"Move %d notebooks to the trash?\n\nAll notes and sub-notebooks within these notebooks will also be moved to the trash.": [
|
||||
""
|
||||
],
|
||||
"Move down": [
|
||||
""
|
||||
],
|
||||
@@ -2465,6 +2507,9 @@
|
||||
"No updates available": [
|
||||
""
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
""
|
||||
],
|
||||
@@ -2672,6 +2717,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
""
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
""
|
||||
],
|
||||
@@ -2693,6 +2741,9 @@
|
||||
"Options": [
|
||||
""
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Ordered list": [
|
||||
""
|
||||
],
|
||||
@@ -3318,6 +3369,9 @@
|
||||
"Select parent notebook": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Selected: %s": [
|
||||
""
|
||||
],
|
||||
@@ -3519,6 +3573,9 @@
|
||||
"Source: ": [
|
||||
""
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -3717,9 +3774,15 @@
|
||||
"Tab moves focus": [
|
||||
""
|
||||
],
|
||||
"Table": [
|
||||
""
|
||||
],
|
||||
"Tabloid": [
|
||||
""
|
||||
],
|
||||
"Tag: %s": [
|
||||
""
|
||||
],
|
||||
"Tagged: %d.": [
|
||||
""
|
||||
],
|
||||
@@ -3827,6 +3890,7 @@
|
||||
""
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"",
|
||||
""
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
@@ -4121,6 +4185,9 @@
|
||||
"Try it now": [
|
||||
""
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
""
|
||||
],
|
||||
|
||||
@@ -132,6 +132,9 @@
|
||||
"A5": [
|
||||
"A5"
|
||||
],
|
||||
"ABC musical notation: Options": [
|
||||
""
|
||||
],
|
||||
"About": [
|
||||
""
|
||||
],
|
||||
@@ -354,6 +357,9 @@
|
||||
"Beta": [
|
||||
""
|
||||
],
|
||||
"Block code": [
|
||||
""
|
||||
],
|
||||
"Bold": [
|
||||
"Grasa"
|
||||
],
|
||||
@@ -462,6 +468,9 @@
|
||||
"Checking... Please wait.": [
|
||||
"Kontrolante... Atendu."
|
||||
],
|
||||
"Chinese/Japanese/Korean characters": [
|
||||
""
|
||||
],
|
||||
"Choose an option": [
|
||||
"Elekti opcion"
|
||||
],
|
||||
@@ -525,6 +534,9 @@
|
||||
"Command": [
|
||||
""
|
||||
],
|
||||
"COMMAND": [
|
||||
""
|
||||
],
|
||||
"Command palette": [
|
||||
""
|
||||
],
|
||||
@@ -549,6 +561,9 @@
|
||||
"Configuration": [
|
||||
"Agordoj"
|
||||
],
|
||||
"Configured keyboard shortcuts:": [
|
||||
""
|
||||
],
|
||||
"Configures the size of scrollbars used in the app.": [
|
||||
""
|
||||
],
|
||||
@@ -735,6 +750,9 @@
|
||||
"Delete local data and re-download from sync target": [
|
||||
""
|
||||
],
|
||||
"Delete tag \"%s\"?\n\nAll notes associated with this tag will remain, but the tag will be removed from all notes.": [
|
||||
""
|
||||
],
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": [
|
||||
""
|
||||
],
|
||||
@@ -816,6 +834,9 @@
|
||||
"Displays the complete information about note.": [
|
||||
""
|
||||
],
|
||||
"Displays the configured keyboard shortcuts.": [
|
||||
""
|
||||
],
|
||||
"Displays the given note.": [
|
||||
""
|
||||
],
|
||||
@@ -1512,6 +1533,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
""
|
||||
],
|
||||
"KEYS": [
|
||||
""
|
||||
],
|
||||
"Keys that need upgrading": [
|
||||
""
|
||||
],
|
||||
@@ -1603,6 +1627,9 @@
|
||||
"Manage your plugins": [
|
||||
""
|
||||
],
|
||||
"Manage your profiles. You can rename or delete profiles. The active profile cannot be deleted.": [
|
||||
""
|
||||
],
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": [
|
||||
""
|
||||
],
|
||||
@@ -1777,6 +1804,9 @@
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
""
|
||||
],
|
||||
"No URL for SAML authentication set.": [
|
||||
""
|
||||
],
|
||||
"None": [
|
||||
""
|
||||
],
|
||||
@@ -1897,6 +1927,9 @@
|
||||
"Open Sync Wizard...": [
|
||||
""
|
||||
],
|
||||
"Open-source licences": [
|
||||
""
|
||||
],
|
||||
"Open...": [
|
||||
"Malfermi..."
|
||||
],
|
||||
@@ -1909,6 +1942,9 @@
|
||||
"Options": [
|
||||
"Opcioj"
|
||||
],
|
||||
"Options that should be used whenever rendering ABC code. It must be a JSON5 object. The full list of options is available at: %s": [
|
||||
""
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
@@ -2338,6 +2374,9 @@
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select the type of file to be imported:": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2482,6 +2521,9 @@
|
||||
"Source format: %s": [
|
||||
"Fonta aranĝo: %s"
|
||||
],
|
||||
"SPACE": [
|
||||
""
|
||||
],
|
||||
"Spacer": [
|
||||
""
|
||||
],
|
||||
@@ -2748,6 +2790,7 @@
|
||||
""
|
||||
],
|
||||
"The note has been converted to Markdown and the original note has been moved to the trash": [
|
||||
"",
|
||||
""
|
||||
],
|
||||
"The note was successfully moved to the trash.": [
|
||||
@@ -2984,6 +3027,9 @@
|
||||
"Try again": [
|
||||
""
|
||||
],
|
||||
"TYPE": [
|
||||
""
|
||||
],
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": [
|
||||
""
|
||||
],
|
||||
|
||||