You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-08 23:07:32 +02:00
Compare commits
50 Commits
v3.5.4
...
cli-v3.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8713cd2fd8 | ||
|
|
d0fc4ea21b | ||
|
|
8bd62800ef | ||
|
|
00f9e932e6 | ||
|
|
b8b55e4a55 | ||
|
|
ef5be2ded3 | ||
|
|
00702dde00 | ||
|
|
2a6af9bed9 | ||
|
|
c26fe0960b | ||
|
|
ab9d36fc08 | ||
|
|
28eb53bd9f | ||
|
|
3097c3e589 | ||
|
|
08371ef718 | ||
|
|
561716efea | ||
|
|
0d457d1bde | ||
|
|
8c11f17c93 | ||
|
|
f7a90ee1d2 | ||
|
|
8822409f7c | ||
|
|
cd3e7f485a | ||
|
|
8d42b01d4f | ||
|
|
2c37197641 | ||
|
|
c2c37b3741 | ||
|
|
3e770300dc | ||
|
|
683291d5df | ||
|
|
d239035417 | ||
|
|
5ef37d9de0 | ||
|
|
1111bde017 | ||
|
|
468cf00d77 | ||
|
|
3c5b41b992 | ||
|
|
5f66c51dba | ||
|
|
bfeaa67ec4 | ||
|
|
348fd0333f | ||
|
|
51c4d6d6ef | ||
|
|
09d77a65e8 | ||
|
|
d1aec4a9f7 | ||
|
|
cab1525589 | ||
|
|
a52f3fea9e | ||
|
|
dfbd5eb8ed | ||
|
|
3131f36033 | ||
|
|
dc5b2cfa21 | ||
|
|
cad0f35fcc | ||
|
|
38ea92ff57 | ||
|
|
830deada22 | ||
|
|
38cd4033ea | ||
|
|
02900752d9 | ||
|
|
091e9813b5 | ||
|
|
e61e5ac32a | ||
|
|
414970c9a1 | ||
|
|
d4ed49ff23 | ||
|
|
8751d5d152 |
@@ -90,7 +90,7 @@ plugin_types/
|
||||
readme/
|
||||
packages/react-native-vosk/lib/
|
||||
packages/lib/countable/Countable.js
|
||||
packages/onenote-converter/pkg/onenote_converter.js
|
||||
packages/onenote-converter/renderer/pkg/*
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
packages/app-cli/app/LinkSelector.js
|
||||
@@ -270,6 +270,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -280,6 +281,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -321,6 +323,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
@@ -604,6 +607,7 @@ packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/resolveSourceMap.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -844,6 +848,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -1097,8 +1102,9 @@ packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/commands/commands.test.js
|
||||
packages/editor/ProseMirror/commands/commands.js
|
||||
packages/editor/ProseMirror/commands/focusEditor.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
@@ -1117,6 +1123,7 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/tablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
@@ -1146,6 +1153,12 @@ packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/icons/addColumnRight.js
|
||||
packages/editor/ProseMirror/vendor/icons/addRowBelow.js
|
||||
packages/editor/ProseMirror/vendor/icons/icon.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeColumn.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeRow.js
|
||||
packages/editor/ProseMirror/vendor/icons/types.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -1414,6 +1427,7 @@ packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/49.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1742,6 +1756,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
|
||||
19
.gitignore
vendored
19
.gitignore
vendored
@@ -243,6 +243,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useSyncEditorValue.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js
|
||||
@@ -253,6 +254,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/shouldPasteResources.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useCursorPositioning.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialogEventListeners.js
|
||||
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHandler.js
|
||||
@@ -294,6 +296,7 @@ packages/app-desktop/gui/NoteEditor/utils/useEffectiveNoteId.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFolder.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.test.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useFormNote.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useInitialCursorLocation.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useNoteSearchBar.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/usePluginEditorView.test.js
|
||||
@@ -577,6 +580,7 @@ packages/app-desktop/tools/generateLatestArm64Yml.js
|
||||
packages/app-desktop/tools/githubReleasesUtils.js
|
||||
packages/app-desktop/tools/modifyReleaseAssets.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/tools/resolveSourceMap.js
|
||||
packages/app-desktop/utils/7zip/getPathToExecutable7Zip.js
|
||||
packages/app-desktop/utils/7zip/pathToBundled7Zip.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
@@ -817,6 +821,7 @@ packages/app-mobile/components/screens/NoteTagsDialog.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.test.js
|
||||
packages/app-mobile/components/screens/Notes/NewNoteButton.js
|
||||
packages/app-mobile/components/screens/Notes/Notes.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.test.js
|
||||
packages/app-mobile/components/screens/SearchScreen/SearchResults.js
|
||||
packages/app-mobile/components/screens/SearchScreen/index.js
|
||||
packages/app-mobile/components/screens/ShareManager/AcceptedShareItem.js
|
||||
@@ -1070,8 +1075,9 @@ packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
|
||||
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
|
||||
packages/editor/CodeMirror/utils/setupVim.js
|
||||
packages/editor/CodeMirror/vendor/announceSearchMatch.js
|
||||
packages/editor/ProseMirror/commands.test.js
|
||||
packages/editor/ProseMirror/commands.js
|
||||
packages/editor/ProseMirror/commands/commands.test.js
|
||||
packages/editor/ProseMirror/commands/commands.js
|
||||
packages/editor/ProseMirror/commands/focusEditor.js
|
||||
packages/editor/ProseMirror/createEditor.js
|
||||
packages/editor/ProseMirror/index.js
|
||||
packages/editor/ProseMirror/plugins/detailsPlugin.test.js
|
||||
@@ -1090,6 +1096,7 @@ packages/editor/ProseMirror/plugins/linkTooltipPlugin.js
|
||||
packages/editor/ProseMirror/plugins/listPlugin.js
|
||||
packages/editor/ProseMirror/plugins/originalMarkupPlugin.js
|
||||
packages/editor/ProseMirror/plugins/searchPlugin.js
|
||||
packages/editor/ProseMirror/plugins/tablePlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createExternalEditorPlugin.js
|
||||
packages/editor/ProseMirror/plugins/utils/createFloatingButtonPlugin.js
|
||||
packages/editor/ProseMirror/schema.js
|
||||
@@ -1119,6 +1126,12 @@ packages/editor/ProseMirror/utils/sanitizeHtml.js
|
||||
packages/editor/ProseMirror/utils/selectFirstInstanceOfNode.js
|
||||
packages/editor/ProseMirror/utils/trimEmptyParagraphs.js
|
||||
packages/editor/ProseMirror/vendor/changedDescendants.js
|
||||
packages/editor/ProseMirror/vendor/icons/addColumnRight.js
|
||||
packages/editor/ProseMirror/vendor/icons/addRowBelow.js
|
||||
packages/editor/ProseMirror/vendor/icons/icon.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeColumn.js
|
||||
packages/editor/ProseMirror/vendor/icons/removeRow.js
|
||||
packages/editor/ProseMirror/vendor/icons/types.js
|
||||
packages/editor/ProseMirror/vendor/splitBlockAs.js
|
||||
packages/editor/SelectionFormatting.js
|
||||
packages/editor/events.js
|
||||
@@ -1387,6 +1400,7 @@ packages/lib/services/database/migrations/45.js
|
||||
packages/lib/services/database/migrations/46.js
|
||||
packages/lib/services/database/migrations/47.js
|
||||
packages/lib/services/database/migrations/48.js
|
||||
packages/lib/services/database/migrations/49.js
|
||||
packages/lib/services/database/migrations/index.js
|
||||
packages/lib/services/database/sqlStringToLines.js
|
||||
packages/lib/services/database/types.js
|
||||
@@ -1715,6 +1729,7 @@ packages/plugin-repo-cli/lib/gitCompareUrl.test.js
|
||||
packages/plugin-repo-cli/lib/gitCompareUrl.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.test.js
|
||||
packages/plugin-repo-cli/lib/overrideUtils.js
|
||||
packages/plugin-repo-cli/lib/searchPlugins.js
|
||||
packages/plugin-repo-cli/lib/types.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.test.js
|
||||
packages/plugin-repo-cli/lib/updateReadme.js
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"bin": "./main.js",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -73,7 +73,7 @@
|
||||
"@joplin/tools": "~3.5",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"gulp": "4.0.2",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<en-media style="--en-viewerProps:{};" type="image/jpeg" hash="e2d4887c5a32ab1686276c7c5ae733ef" width="1.125in" />
|
||||
</div>
|
||||
<div>
|
||||
<br />
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -0,0 +1,8 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<img src=":/e2d4887c5a32ab1686276c7c5ae733ef" style="--en-viewerProps:{};" type="image/jpeg" hash="e2d4887c5a32ab1686276c7c5ae733ef" width="108" alt="attachment-image" />
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,6 +1,8 @@
|
||||
<en-note>
|
||||
<div><a href=":/21ca2b948f222a38802940ec7e2e5de3" hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1">attachment-1</a></div>
|
||||
<div>
|
||||
<br>
|
||||
<a href=':/21ca2b948f222a38802940ec7e2e5de3' hash="21ca2b948f222a38802940ec7e2e5de3" type="application/pdf" style="cursor:pointer;" alt="attachment-1"> attachment-1</a>
|
||||
</div>
|
||||
<div>
|
||||
<br/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,16 +1,11 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>For example, consider an exported Evernote list with todo checkboxes like this:</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<div><input checked="checked" type="checkbox" onclick="return false;">Foo</div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><b>Bar</b></div>
|
||||
</li>
|
||||
<li>
|
||||
<div><input type="checkbox" onclick="return false;"><i>Baz</i></div>
|
||||
</li>
|
||||
<li><div><input checked="checked" type="checkbox" onclick="return false;" />Foo</div></li>
|
||||
<li><div><input type="checkbox" onclick="return false;" /><b>Bar</b></div></li>
|
||||
<li><div><input type="checkbox" onclick="return false;" /><i>Baz</i></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,19 +1,11 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<p>In Evernote a checklist is not the same as a list with checkboxes.</p>
|
||||
<ul style="--en-todo:true;">
|
||||
<li style="--en-checked:false;">
|
||||
<input type="checkbox" onclick="return false;">
|
||||
<div>One</div>
|
||||
</li>
|
||||
<li style="--en-checked:true;">
|
||||
<input checked="checked" type="checkbox" onclick="return false;">
|
||||
<div>Two</div>
|
||||
</li>
|
||||
<li style="--en-checked:false;">
|
||||
<input type="checkbox" onclick="return false;">
|
||||
<div>Three</div>
|
||||
</li>
|
||||
|
||||
<ul STYLE="--en-todo:true;">
|
||||
<li STYLE="--en-checked:false;"> <input type="checkbox" onclick="return false;" /><div>One</div></li>
|
||||
<li STYLE="--en-checked:true;"> <input checked="checked" type="checkbox" onclick="return false;" /><div>Two</div>
|
||||
</li><li STYLE="--en-checked:false;"> <input type="checkbox" onclick="return false;" /><div>Three</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -1,12 +1 @@
|
||||
<en-note>
|
||||
<div>
|
||||
<audio controls="" preload="none" style="width:480px;">
|
||||
<source src=":/9168ee833d03c5ea7c730ac6673978c1" type="audio/mp4">
|
||||
<p>Your browser does not support HTML5 audio.</p>
|
||||
</audio>
|
||||
<p><a href=":/9168ee833d03c5ea7c730ac6673978c1">audio test</a></p>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
</en-note>
|
||||
<en-note><div><audio controls preload="none" style="width:480px;"><source src=":/9168ee833d03c5ea7c730ac6673978c1" type="audio/mp4" /><p>Your browser does not support HTML5 audio.</p></audio><p><a href=":/9168ee833d03c5ea7c730ac6673978c1">audio test</a></p></div><div><br/></div></en-note>
|
||||
@@ -1,12 +1 @@
|
||||
<en-note>
|
||||
<div><input type="checkbox" onclick="return false;">This is a test</div>
|
||||
<div><input type="checkbox" onclick="return false;">A test for <span style="font-weight: bold;">bold</span></div>
|
||||
<div>
|
||||
<input type="checkbox" onclick="return false;">A test for <i>italic</i>
|
||||
<br>
|
||||
</div>
|
||||
<div>
|
||||
<br>
|
||||
</div>
|
||||
<div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt=""></i></div>
|
||||
</en-note>
|
||||
<en-note><div><input type="checkbox" onclick="return false;" />This is a test</div><div><input type="checkbox" onclick="return false;" />A test for <span STYLE="font-weight: bold;">bold</span></div><div><input type="checkbox" onclick="return false;" />A test for <i>italic</i><br/></div><div><br/></div><div><i><img src=":/89ce7da62c6b2832929a6964237e98e9" hash="89ce7da62c6b2832929a6964237e98e9" type="image/jpeg" alt="" /></i></div></en-note>
|
||||
@@ -1,3 +1,3 @@
|
||||
<en-note>
|
||||
<h1 style="box-sizing:inherit;font-family:"Guardian TextSans Web", "Helvetica Neue", Helvetica, Arial, sans-serif;margin-top:0.2em;margin-bottom:0.35em;font-size:2.125em;font-weight:600;line-height:1.3;">Association Between mRNA Vaccination and COVID-19 Hospitalization and Disease Severity</h1>
|
||||
<h1 STYLE="box-sizing:inherit;font-family:"Guardian TextSans Web", "Helvetica Neue", Helvetica, Arial, sans-serif;margin-top:0.2em;margin-bottom:0.35em;font-size:2.125em;font-weight:600;line-height:1.3;">Association Between mRNA Vaccination and COVID-19 Hospitalization and Disease Severity</h1>
|
||||
</en-note>
|
||||
@@ -1,3 +1,5 @@
|
||||
<en-note>
|
||||
<div><img style="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" src="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"></div>
|
||||
<div>
|
||||
<img STYLE="margin:0px;padding:0px;outline:0px;width:74px;height:36px;position:absolute;bottom:-5px;left:0px;transform:translate(0px, 100%);stroke-dasharray:90;transition:stroke-dashoffset 0.5s cubic-bezier(0.97, 0.16, 0.62, 0.76) 0s;stroke-dashoffset:0;" SRC="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' data-evernote-id='97' class='js-evernote-checked'%3e%3cuse xlink:href='https://wordminds.com/wp-content/themes/wordminds/assets/img/hint_left.svg%23hint_left' data-evernote-id='98' class='js-evernote-checked'%3e%3c/use%3e%3c/svg%3e"/>
|
||||
</div>
|
||||
</en-note>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE en-export SYSTEM "http://xml.evernote.com/pub/evernote-export4.dtd">
|
||||
<en-export export-date="20230724T173816Z" application="Evernote" version="10.58.8">
|
||||
<note>
|
||||
<title>test.json</title>
|
||||
<content><![CDATA[<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
|
||||
<en-note><en-media hash="ac91cc691d21261b222681dd38c1e4ad" type="application/json"/></en-note>
|
||||
]]>
|
||||
</content>
|
||||
<created>20191002T075850Z</created>
|
||||
<updated>20191002T075850Z</updated>
|
||||
<note-attributes><latitude>48.79547119140625</latitude><longitude>9.809423921920198</longitude><altitude>398.0</altitude><author>Laurent</author><source>desktop.mac</source></note-attributes>
|
||||
<resource><data>eyAidGVzdCI6IDEyMyB9</data><mime>application/json</mime><width>0</width><height>0</height><resource-attributes><file-name>test.json</file-name><attachment>false</attachment></resource-attributes></resource></note></en-export>
|
||||
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
Binary file not shown.
@@ -52,7 +52,7 @@ describe('app.reducer', () => {
|
||||
...createAppDefaultState({}),
|
||||
backgroundWindows: {
|
||||
testWindow: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
windowId: 'testWindow',
|
||||
|
||||
visibleDialogs: {
|
||||
|
||||
@@ -26,10 +26,21 @@ export interface AppStateDialog {
|
||||
props: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface EditorScrollPercents {
|
||||
export interface NoteIdToScrollPercent {
|
||||
[noteId: string]: number;
|
||||
}
|
||||
|
||||
type RichTextEditorSelectionBookmark = unknown;
|
||||
|
||||
export interface EditorCursorLocations {
|
||||
readonly richText?: RichTextEditorSelectionBookmark;
|
||||
readonly markdown?: number;
|
||||
}
|
||||
|
||||
export interface NoteIdToEditorCursorLocations {
|
||||
[noteId: string]: EditorCursorLocations;
|
||||
}
|
||||
|
||||
export interface VisibleDialogs {
|
||||
[dialogKey: string]: boolean;
|
||||
}
|
||||
@@ -42,6 +53,9 @@ export interface AppWindowState extends WindowState {
|
||||
devToolsVisible: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
watchedResources: any;
|
||||
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
}
|
||||
|
||||
interface BackgroundWindowStates {
|
||||
@@ -55,7 +69,6 @@ export interface AppState extends State, AppWindowState {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
navHistory: any[];
|
||||
watchedNoteFiles: string[];
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
focusedField: string;
|
||||
layoutMoveMode: boolean;
|
||||
startupPluginsLoaded: boolean;
|
||||
@@ -66,7 +79,7 @@ export interface AppState extends State, AppWindowState {
|
||||
isResettingLayout: boolean;
|
||||
}
|
||||
|
||||
export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export const createAppDefaultWindowState = (globalState: AppState|null): AppWindowState => {
|
||||
return {
|
||||
...defaultWindowState,
|
||||
visibleDialogs: {},
|
||||
@@ -75,6 +88,12 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
editorCodeView: true,
|
||||
devToolsVisible: false,
|
||||
watchedResources: {},
|
||||
|
||||
// Maintain the scroll and cursor location for secondary windows separate from the
|
||||
// main window. This prevents scrolling in a secondary window from changing/resetting
|
||||
// the default scroll position in the main window:
|
||||
lastEditorCursorLocations: globalState?.lastEditorCursorLocations ?? {},
|
||||
lastEditorScrollPercents: globalState?.lastEditorScrollPercents ?? {},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -82,7 +101,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
|
||||
export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
|
||||
return {
|
||||
...defaultState,
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
route: {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Main',
|
||||
@@ -90,7 +109,6 @@ export function createAppDefaultState(resourceEditWatcherDefaultState: any): App
|
||||
},
|
||||
navHistory: [],
|
||||
watchedNoteFiles: [],
|
||||
lastEditorScrollPercents: {},
|
||||
visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
|
||||
focusedField: null,
|
||||
layoutMoveMode: false,
|
||||
@@ -299,6 +317,18 @@ export default function(state: AppState, action: any) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EDITOR_CURSOR_POSITION_SET':
|
||||
{
|
||||
newState = { ...state };
|
||||
const newCursorLocations = { ...newState.lastEditorCursorLocations };
|
||||
newCursorLocations[action.noteId] = {
|
||||
...(newCursorLocations[action.noteId] ?? {}),
|
||||
...action.location,
|
||||
};
|
||||
newState.lastEditorCursorLocations = newCursorLocations;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'NOTE_DEVTOOLS_TOGGLE':
|
||||
newState = { ...state };
|
||||
newState.devToolsVisible = !newState.devToolsVisible;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { createAppDefaultWindowState } from '../app.reducer';
|
||||
import { AppState, createAppDefaultWindowState } from '../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
@@ -25,7 +25,7 @@ export const runtime = (): CommandRuntime => {
|
||||
folderId: note.parent_id,
|
||||
windowId: `window-${noteId}-${idCounter++}`,
|
||||
defaultAppWindowState: {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(context.state as AppState),
|
||||
noteVisiblePanes: Setting.value('noteVisiblePanes'),
|
||||
editorCodeView: Setting.value('editor.codeView'),
|
||||
},
|
||||
|
||||
@@ -31,6 +31,7 @@ import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useRefocusOnVisiblePaneChange from './utils/useRefocusOnVisiblePaneChange';
|
||||
import { WindowIdContext } from '../../../../NewWindowOrIFrame';
|
||||
import eventManager, { EventName, ResourceChangeEvent } from '@joplin/lib/eventManager';
|
||||
import useSyncEditorValue from './utils/useSyncEditorValue';
|
||||
|
||||
const logger = Logger.create('CodeMirror6');
|
||||
const logDebug = (message: string) => logger.debug(message);
|
||||
@@ -167,9 +168,8 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
},
|
||||
scrollTo: (options: ScrollOptions) => {
|
||||
if (options.type === ScrollOptionTypes.Hash) {
|
||||
if (!webviewRef.current) return;
|
||||
const hash: string = options.value;
|
||||
webviewRef.current.send('scrollToHash', hash);
|
||||
webviewRef.current?.send('scrollToHash', hash);
|
||||
editorRef.current.jumpToHash(hash);
|
||||
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||
const percent = options.value as number;
|
||||
@@ -342,6 +342,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.Change) {
|
||||
codeMirror_change(event.value);
|
||||
} else if (event.kind === EditorEventType.SelectionRangeChange) {
|
||||
props.onCursorMotion({ markdown: event.from });
|
||||
setSelectionRange({ from: event.from, to: event.to });
|
||||
} else if (event.kind === EditorEventType.UpdateSearchDialog) {
|
||||
if (lastSearchState.current?.searchText !== event.searchState.searchText) {
|
||||
@@ -355,7 +356,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
} else if (event.kind === EditorEventType.FollowLink) {
|
||||
void CommandService.instance().execute('openItem', event.link);
|
||||
}
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch]);
|
||||
}, [editor_scroll, codeMirror_change, props.setLocalSearch, props.setShowLocalSearch, props.onCursorMotion]);
|
||||
|
||||
const onSelectPastBeginning = useCallback(() => {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
@@ -400,15 +401,17 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
props.tabMovesFocus,
|
||||
]);
|
||||
|
||||
// Update the editor's value
|
||||
useEffect(() => {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
const updateProps = { noteId: props.noteId };
|
||||
if (editorRef.current?.updateBody(props.content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
}
|
||||
}, [props.content, props.noteId]);
|
||||
const initialCursorLocationRef = useRef(0);
|
||||
initialCursorLocationRef.current = props.initialCursorLocation.markdown ?? 0;
|
||||
|
||||
useSyncEditorValue({
|
||||
content: props.content,
|
||||
visiblePanes: props.visiblePanes,
|
||||
onMessage: props.onMessage,
|
||||
editorRef,
|
||||
noteId: props.noteId,
|
||||
initialCursorLocationRef,
|
||||
});
|
||||
|
||||
const renderEditor = () => {
|
||||
return (
|
||||
@@ -416,6 +419,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
|
||||
<Editor
|
||||
style={styles.editor}
|
||||
initialText={props.content}
|
||||
initialSelectionRef={initialCursorLocationRef}
|
||||
initialNoteId={props.noteId}
|
||||
ref={editorRef}
|
||||
settings={editorSettings}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { ForwardedRef } from 'react';
|
||||
import { ForwardedRef, RefObject } from 'react';
|
||||
import { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { EditorProps, LogMessageCallback, OnEventCallback, ContentScriptData } from '@joplin/editor/types';
|
||||
import createEditor from '@joplin/editor/CodeMirror/createEditor';
|
||||
@@ -23,6 +23,7 @@ import getResourceBaseUrl from '../../../utils/getResourceBaseUrl';
|
||||
interface Props extends EditorProps {
|
||||
style: React.CSSProperties;
|
||||
pluginStates: PluginStates;
|
||||
initialSelectionRef: RefObject<number>;
|
||||
|
||||
onEditorPaste: (event: Event)=> void;
|
||||
externalSearch: SearchMarkers;
|
||||
@@ -127,6 +128,9 @@ const Editor = (props: Props, ref: ForwardedRef<CodeMirrorControl>) => {
|
||||
direction: 'unset',
|
||||
},
|
||||
});
|
||||
const cursor = props.initialSelectionRef.current;
|
||||
editor.select(cursor, cursor);
|
||||
|
||||
setEditor(editor);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useEffect, useRef, RefObject } from 'react';
|
||||
import { OnMessage } from '../../../../utils/types';
|
||||
import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
|
||||
visiblePanes: string[];
|
||||
onMessage: OnMessage;
|
||||
editorRef: RefObject<CodeMirrorControl>;
|
||||
noteId: string;
|
||||
initialCursorLocationRef: RefObject<number>;
|
||||
}
|
||||
|
||||
// Updates the editor's value as necessary
|
||||
const useSyncEditorValue = ({ content, visiblePanes, onMessage, editorRef, noteId, initialCursorLocationRef }: Props) => {
|
||||
const visiblePanesRef = useRef(visiblePanes);
|
||||
visiblePanesRef.current = visiblePanes;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
const lastNoteIdRef = useRef(noteId);
|
||||
|
||||
useEffect(() => {
|
||||
// Include the noteId in the update props to give plugins access
|
||||
// to the current note ID.
|
||||
const updateProps = { noteId: noteId };
|
||||
if (editorRef.current?.updateBody(content, updateProps)) {
|
||||
editorRef.current?.clearHistory();
|
||||
|
||||
// Only reset the cursor location when switching notes. If, for example,
|
||||
// the note is updated from a secondary window, the cursor location shouldn't
|
||||
// reset.
|
||||
const noteChanged = lastNoteIdRef.current !== noteId;
|
||||
if (noteChanged) {
|
||||
const cursorLocation = initialCursorLocationRef.current;
|
||||
editorRef.current?.select(cursorLocation, cursorLocation);
|
||||
}
|
||||
lastNoteIdRef.current = noteId;
|
||||
|
||||
// If the viewer isn't visible, the content should be considered rendered
|
||||
// after the editor has finished updating:
|
||||
if (!visiblePanesRef.current.includes('viewer')) {
|
||||
onMessageRef.current({ channel: 'noteRenderComplete' });
|
||||
}
|
||||
}
|
||||
}, [content, noteId, editorRef, initialCursorLocationRef]);
|
||||
};
|
||||
|
||||
export default useSyncEditorValue;
|
||||
@@ -23,7 +23,7 @@ import { themeStyle } from '@joplin/lib/theme';
|
||||
import { loadScript } from '../../../utils/loadScript';
|
||||
import bridge from '../../../../services/bridge';
|
||||
import { TinyMceEditorEvents } from './utils/types';
|
||||
import type { Editor, EditorEvent } from 'tinymce';
|
||||
import type { Bookmark, Editor, EditorEvent } from 'tinymce';
|
||||
import { joplinCommandToTinyMceCommands, TinyMceCommand } from './utils/joplinCommandToTinyMceCommands';
|
||||
import shouldPasteResources from './utils/shouldPasteResources';
|
||||
import lightTheme from '@joplin/lib/themes/light';
|
||||
@@ -47,6 +47,7 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import useTextPatternsLookup, { TextPatternContext } from './utils/useTextPatternsLookup';
|
||||
import { toFileProtocolPath } from '@joplin/utils/path';
|
||||
import { RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import useCursorPositioning from './utils/useCursorPositioning';
|
||||
|
||||
const logger = Logger.create('TinyMCE');
|
||||
|
||||
@@ -1046,6 +1047,12 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { onInitialContentSet } = useCursorPositioning({
|
||||
initialCursorLocation: props.initialCursorLocation.richText as Bookmark,
|
||||
onCursorUpdate: props.onCursorMotion,
|
||||
editor,
|
||||
});
|
||||
|
||||
const lastNoteIdRef = useRef(props.noteId);
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
@@ -1136,6 +1143,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: Ref<NoteBodyEditorRef>) => {
|
||||
await loadDocumentAssets(props.themeId, editor, allAssets);
|
||||
|
||||
dispatchDidUpdate(editor);
|
||||
onInitialContentSet();
|
||||
};
|
||||
|
||||
void loadContent();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Bookmark, Editor } from 'tinymce';
|
||||
import { OnCursorMotion } from '../../../utils/types';
|
||||
|
||||
interface Props {
|
||||
initialCursorLocation: Bookmark;
|
||||
editor: Editor;
|
||||
onCursorUpdate: OnCursorMotion;
|
||||
}
|
||||
|
||||
const useCursorPositioning = ({ initialCursorLocation, editor, onCursorUpdate }: Props) => {
|
||||
const initialCursorLocationRef = useRef(initialCursorLocation);
|
||||
initialCursorLocationRef.current = initialCursorLocation;
|
||||
|
||||
const appliedInitialCursorLocationRef = useRef(false);
|
||||
const onInitialContentSet = useCallback(() => {
|
||||
if (editor) {
|
||||
if (initialCursorLocationRef.current) {
|
||||
editor.selection.moveToBookmark(initialCursorLocationRef.current);
|
||||
}
|
||||
|
||||
appliedInitialCursorLocationRef.current = true;
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
editor.on('ContentSet', onInitialContentSet);
|
||||
|
||||
const onSelectionChange = () => {
|
||||
// Wait until the initial cursor position has been set. This avoids resetting
|
||||
// the initial cursor position to zero when the editor first loads.
|
||||
if (!appliedInitialCursorLocationRef.current) return;
|
||||
|
||||
// Use an offset bookmark -- the default bookmark type is not preserved after unloading
|
||||
// and reloading the editor.
|
||||
const offsetBookmarkId = 2;
|
||||
onCursorUpdate({
|
||||
richText: editor.selection.getBookmark(offsetBookmarkId, true),
|
||||
});
|
||||
};
|
||||
|
||||
editor.on('SelectionChange', onSelectionChange);
|
||||
|
||||
return () => {
|
||||
editor.off('ContentSet', onInitialContentSet);
|
||||
editor.off('SelectionChange', onSelectionChange);
|
||||
};
|
||||
}, [editor, onCursorUpdate, onInitialContentSet]);
|
||||
|
||||
return { onInitialContentSet };
|
||||
};
|
||||
|
||||
export default useCursorPositioning;
|
||||
@@ -18,7 +18,7 @@ import { NoteEditorProps, FormNote, OnChangeEvent, AllAssetsOptions, NoteBodyEdi
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { AppState, EditorCursorLocations } from '../../app.reducer';
|
||||
import ToolbarButtonUtils, { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { _, _n } from '@joplin/lib/locale';
|
||||
import NoteTitleBar from './NoteTitle/NoteTitleBar';
|
||||
@@ -57,6 +57,7 @@ import StatusBar from './StatusBar';
|
||||
import useVisiblePluginEditorViewIds from '@joplin/lib/hooks/plugins/useVisiblePluginEditorViewIds';
|
||||
import useConnectToEditorPlugin from './utils/useConnectToEditorPlugin';
|
||||
import getResourceBaseUrl from './utils/getResourceBaseUrl';
|
||||
import useInitialCursorLocation from './utils/useInitialCursorLocation';
|
||||
|
||||
const debounce = require('debounce');
|
||||
|
||||
@@ -329,13 +330,13 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
});
|
||||
}, [formNote, setFormNote, handleProvisionalFlag, props.dispatch]);
|
||||
|
||||
const { scrollWhenReady, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||
const { scrollWhenReadyRef, clearScrollWhenReady } = useScrollWhenReadyOptions({
|
||||
noteId: formNote.id,
|
||||
selectedNoteHash: props.selectedNoteHash,
|
||||
lastEditorScrollPercents: props.lastEditorScrollPercents,
|
||||
editorRef,
|
||||
});
|
||||
const onMessage = useMessageHandler(scrollWhenReady, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
const onMessage = useMessageHandler(scrollWhenReadyRef, clearScrollWhenReady, windowId, editorRef, setLocalSearchResultCount, props.dispatch, formNote, htmlToMarkdown, markupToHtml);
|
||||
|
||||
useResourceUnwatcher({ noteId: formNote.id, windowId });
|
||||
|
||||
@@ -409,6 +410,14 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const onCursorMotion = useCallback((location: EditorCursorLocations) => {
|
||||
props.dispatch({
|
||||
type: 'EDITOR_CURSOR_POSITION_SET',
|
||||
noteId: formNoteRef.current.id,
|
||||
location,
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
function renderNoNotes(rootStyle: React.CSSProperties) {
|
||||
const emptyDivStyle = {
|
||||
backgroundColor: 'black',
|
||||
@@ -419,6 +428,9 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
}
|
||||
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
const initialCursorLocation = useInitialCursorLocation({
|
||||
lastEditorCursorLocations: props.lastEditorCursorLocations, noteId: props.noteId,
|
||||
});
|
||||
|
||||
const markupLanguage = formNote.markup_language;
|
||||
const editorProps: NoteBodyEditorPropsAndRef = {
|
||||
@@ -432,6 +444,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
content: formNote.body,
|
||||
contentMarkupLanguage: markupLanguage,
|
||||
contentOriginalCss: formNote.originalCss,
|
||||
initialCursorLocation,
|
||||
resourceInfos: resourceInfos,
|
||||
resourceDirectory: Setting.value('resourceDir'),
|
||||
htmlToMarkdown: htmlToMarkdown,
|
||||
@@ -442,6 +455,7 @@ function NoteEditorContent(props: NoteEditorProps) {
|
||||
dispatch: props.dispatch,
|
||||
noteToolbar: null,
|
||||
onScroll: onScroll,
|
||||
onCursorMotion,
|
||||
setLocalSearchResultCount: setLocalSearchResultCount,
|
||||
setLocalSearch: localSearch_change,
|
||||
setShowLocalSearch,
|
||||
@@ -729,6 +743,7 @@ const mapStateToProps = (state: AppState, ownProps: ConnectProps) => {
|
||||
notesParentType: windowState.notesParentType,
|
||||
selectedNoteTags: windowState.selectedNoteTags,
|
||||
lastEditorScrollPercents: state.lastEditorScrollPercents,
|
||||
lastEditorCursorLocations: state.lastEditorCursorLocations,
|
||||
selectedNoteHash: windowState.selectedNoteHash,
|
||||
searches: state.searches,
|
||||
selectedSearchId: windowState.selectedSearchId,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ScrollbarSize } from '@joplin/lib/models/settings/builtInMetadata';
|
||||
import { RefObject, SetStateAction } from 'react';
|
||||
import * as React from 'react';
|
||||
import { ResourceEntity, ResourceLocalStateEntity } from '@joplin/lib/services/database/types';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations, NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -40,8 +41,8 @@ export interface NoteEditorProps {
|
||||
notesParentType: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
selectedNoteTags: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
lastEditorScrollPercents: any;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
selectedNoteHash: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
searches: any[];
|
||||
@@ -83,6 +84,14 @@ export interface NoteBodyEditorRef {
|
||||
export { MarkupToHtmlOptions };
|
||||
export type MarkupToHtmlHandler = (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
|
||||
export type HtmlToMarkdownHandler = (markupLanguage: number, html: string, originalCss: string, parseOptions?: ParseOptions)=> Promise<string>;
|
||||
export type OnCursorMotion = (event: EditorCursorLocations)=> void;
|
||||
|
||||
export interface MessageEvent {
|
||||
channel: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partially refactored old code before rule was applied
|
||||
args?: any[];
|
||||
}
|
||||
export type OnMessage = (event: MessageEvent)=> void;
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -102,12 +111,13 @@ export interface NoteBodyEditorProps {
|
||||
contentKey: string;
|
||||
contentMarkupLanguage: number;
|
||||
contentOriginalCss: string;
|
||||
initialCursorLocation: EditorCursorLocations;
|
||||
onChange(event: OnChangeEvent): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onWillChange(event: any): void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
onMessage(event: any): void;
|
||||
onMessage: OnMessage;
|
||||
onScroll(event: { percent: number }): void;
|
||||
onCursorMotion: OnCursorMotion;
|
||||
markupToHtml: MarkupToHtmlHandler;
|
||||
htmlToMarkdown: HtmlToMarkdownHandler;
|
||||
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react';
|
||||
import { EditorCursorLocations, NoteIdToEditorCursorLocations } from '../../../app.reducer';
|
||||
|
||||
interface Props {
|
||||
lastEditorCursorLocations: NoteIdToEditorCursorLocations;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
const useInitialCursorLocation = ({ noteId, lastEditorCursorLocations }: Props) => {
|
||||
const lastCursorLocation = lastEditorCursorLocations[noteId];
|
||||
|
||||
return useMemo((): EditorCursorLocations => {
|
||||
return lastCursorLocation ?? { };
|
||||
}, [lastCursorLocation]);
|
||||
};
|
||||
|
||||
export default useInitialCursorLocation;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler, ScrollOptions } from './types';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { FormNote, HtmlToMarkdownHandler, MarkupToHtmlHandler, ScrollOptions, MessageEvent } from './types';
|
||||
import contextMenu from './contextMenu';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PostMessageService from '@joplin/lib/services/PostMessageService';
|
||||
@@ -8,7 +8,7 @@ import { reg } from '@joplin/lib/registry';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
export default function useMessageHandler(
|
||||
scrollWhenReady: ScrollOptions|null,
|
||||
scrollWhenReadyRef: RefObject<ScrollOptions|null>,
|
||||
clearScrollWhenReady: ()=> void,
|
||||
windowId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
@@ -21,8 +21,7 @@ export default function useMessageHandler(
|
||||
htmlToMd: HtmlToMarkdownHandler,
|
||||
mdToHtml: MarkupToHtmlHandler,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
return useCallback(async (event: any) => {
|
||||
return useCallback(async (event: MessageEvent) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
@@ -35,8 +34,8 @@ export default function useMessageHandler(
|
||||
s.splice(0, 1);
|
||||
reg.logger().error(s.join(':'));
|
||||
} else if (msg === 'noteRenderComplete') {
|
||||
if (scrollWhenReady) {
|
||||
const options = { ...scrollWhenReady };
|
||||
if (scrollWhenReadyRef.current) {
|
||||
const options = { ...scrollWhenReadyRef.current };
|
||||
clearScrollWhenReady();
|
||||
editorRef.current.scrollTo(options);
|
||||
}
|
||||
@@ -78,5 +77,5 @@ export default function useMessageHandler(
|
||||
// bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [dispatch, setLocalSearchResultCount, scrollWhenReady, formNote]);
|
||||
}, [dispatch, setLocalSearchResultCount, scrollWhenReadyRef, formNote]);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { RefObject, useCallback, useRef } from 'react';
|
||||
import { NoteBodyEditorRef, ScrollOptions, ScrollOptionTypes } from './types';
|
||||
import usePrevious from '@joplin/lib/hooks/usePrevious';
|
||||
import type { EditorScrollPercents } from '../../../app.reducer';
|
||||
import type { NoteIdToScrollPercent } from '../../../app.reducer';
|
||||
import useNowEffect from '@joplin/lib/hooks/useNowEffect';
|
||||
|
||||
interface Props {
|
||||
noteId: string;
|
||||
selectedNoteHash: string;
|
||||
lastEditorScrollPercents: EditorScrollPercents;
|
||||
lastEditorScrollPercents: NoteIdToScrollPercent;
|
||||
editorRef: RefObject<NoteBodyEditorRef>;
|
||||
}
|
||||
|
||||
const useScrollWhenReadyOptions = ({ noteId, selectedNoteHash, lastEditorScrollPercents, editorRef }: Props) => {
|
||||
const [scrollWhenReady, setScrollWhenReady] = useState<ScrollOptions|null>(null);
|
||||
const scrollWhenReadyRef = useRef<ScrollOptions|null>(null);
|
||||
|
||||
const previousNoteId = usePrevious(noteId);
|
||||
const lastScrollPercentsRef = useRef<EditorScrollPercents>(null);
|
||||
const lastScrollPercentsRef = useRef<NoteIdToScrollPercent>(null);
|
||||
lastScrollPercentsRef.current = lastEditorScrollPercents;
|
||||
|
||||
useEffect(() => {
|
||||
if (noteId === previousNoteId) return;
|
||||
// This needs to be a nowEffect to prevent race conditions
|
||||
useNowEffect(() => {
|
||||
if (noteId === previousNoteId) return () => {};
|
||||
|
||||
if (editorRef.current) {
|
||||
editorRef.current.resetScroll();
|
||||
}
|
||||
|
||||
const lastScrollPercent = lastScrollPercentsRef.current[noteId] || 0;
|
||||
setScrollWhenReady({
|
||||
scrollWhenReadyRef.current = {
|
||||
type: selectedNoteHash ? ScrollOptionTypes.Hash : ScrollOptionTypes.Percent,
|
||||
value: selectedNoteHash ? selectedNoteHash : lastScrollPercent,
|
||||
});
|
||||
};
|
||||
return () => {};
|
||||
}, [noteId, previousNoteId, selectedNoteHash, editorRef]);
|
||||
|
||||
const clearScrollWhenReady = useCallback(() => {
|
||||
setScrollWhenReady(null);
|
||||
scrollWhenReadyRef.current = null;
|
||||
}, []);
|
||||
|
||||
return { scrollWhenReady, clearScrollWhenReady };
|
||||
return { scrollWhenReadyRef, clearScrollWhenReady };
|
||||
};
|
||||
|
||||
export default useScrollWhenReadyOptions;
|
||||
|
||||
@@ -356,6 +356,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
onClick={tagItem_click}
|
||||
onTagDrop={onTagDrop_}
|
||||
onContextMenu={onItemContextMenu}
|
||||
label={item.label}
|
||||
tag={tag}
|
||||
itemCount={itemCount}
|
||||
index={index}
|
||||
@@ -384,7 +385,7 @@ const useOnRenderItem = (props: Props) => {
|
||||
anchorRef={anchorRef}
|
||||
selected={selected}
|
||||
folderId={folder.id}
|
||||
folderTitle={Folder.displayTitle(folder)}
|
||||
folderTitle={item.label}
|
||||
folderIcon={Folder.unserializeIcon(folder.icon)}
|
||||
depth={item.depth}
|
||||
isExpanded={isExpanded}
|
||||
|
||||
@@ -49,6 +49,24 @@ const getParentOffset = (childIndex: number, listItems: ListItem[]): number|null
|
||||
return null;
|
||||
};
|
||||
|
||||
const findNextTypeAheadMatch = (selectedIndex: number, query: string, listItems: ListItem[]) => {
|
||||
const matches = (item: ListItem) => {
|
||||
return item.label.startsWith(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
|
||||
// same key.
|
||||
const startAfter = selectedIndex + 1;
|
||||
let indexAfter = listItems.slice(startAfter).findIndex(matches);
|
||||
if (indexAfter !== -1) {
|
||||
indexAfter += startAfter;
|
||||
}
|
||||
// Prefer jumping to the next match, rather than the previous
|
||||
const matchingIndex = indexAfter !== -1 ? indexAfter : indexBefore;
|
||||
return matchingIndex;
|
||||
};
|
||||
|
||||
const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
const { updateSelectedIndex, listItems, selectedIndex, collapsedFolderIds, dispatch } = props;
|
||||
|
||||
@@ -82,9 +100,22 @@ const useOnSidebarKeyDownHandler = (props: Props) => {
|
||||
indexChange = 1;
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.code === 'KeyA') { // ctrl+a or cmd+a
|
||||
event.preventDefault();
|
||||
} else if (event.code === 'Home') {
|
||||
event.preventDefault();
|
||||
updateSelectedIndex(0);
|
||||
indexChange = 0;
|
||||
} else if (event.code === 'End') {
|
||||
event.preventDefault();
|
||||
updateSelectedIndex(listItems.length - 1);
|
||||
indexChange = 0;
|
||||
} else if (event.code === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
void CommandService.instance().execute('focusElement', 'noteList');
|
||||
} else if (selectedIndex && selectedIndex >= 0 && event.key.length === 1) {
|
||||
const nextMatch = findNextTypeAheadMatch(selectedIndex, event.key, listItems);
|
||||
if (nextMatch !== -1) {
|
||||
indexChange = nextMatch - selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (indexChange !== 0) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { FolderEntity, TagsWithNoteCountEntity } from '@joplin/lib/services/data
|
||||
import { buildFolderTree, renderFolders, renderTags } from '@joplin/lib/components/shared/side-menu-shared';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import toggleHeader from './utils/toggleHeader';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
|
||||
interface Props {
|
||||
tags: TagsWithNoteCountEntity[];
|
||||
@@ -18,6 +20,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
return renderTags<ListItem>(props.tags, (tag): TagListItem => {
|
||||
return {
|
||||
kind: ListItemType.Tag,
|
||||
label: Tag.displayTitle(tag),
|
||||
tag,
|
||||
key: tag.id,
|
||||
depth: 1,
|
||||
@@ -38,6 +41,7 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
return renderFolders<ListItem>(renderProps, (folder, hasChildren, depth): FolderListItem => {
|
||||
return {
|
||||
kind: ListItemType.Folder,
|
||||
label: Folder.displayTitle(folder),
|
||||
folder,
|
||||
hasChildren,
|
||||
// The toplevel headers have depth 1, so the toplevel notebook needs
|
||||
@@ -65,9 +69,9 @@ const useSidebarListData = (props: Props): ListItem[] => {
|
||||
hasChildren: folderItems.items.length > 0,
|
||||
};
|
||||
const foldersSectionContent: ListItem[] = props.folderHeaderIsExpanded ? [
|
||||
{ kind: ListItemType.AllNotes, key: 'all-notes', depth: 2, hasChildren: false },
|
||||
{ kind: ListItemType.AllNotes, label: _('All notes'), key: 'all-notes', depth: 2, hasChildren: false },
|
||||
...folderItems.items,
|
||||
{ kind: ListItemType.Spacer, key: 'after-folders-spacer', depth: 1, hasChildren: false },
|
||||
{ kind: ListItemType.Spacer, label: '', key: 'after-folders-spacer', depth: 1, hasChildren: false },
|
||||
] : [];
|
||||
|
||||
const tagsHeader: HeaderListItem = {
|
||||
|
||||
@@ -7,7 +7,6 @@ import Setting from '@joplin/lib/models/Setting';
|
||||
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import PerFolderSortOrderService from '../../../services/sortOrder/PerFolderSortOrderService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { connect } from 'react-redux';
|
||||
import EmptyExpandLink from './EmptyExpandLink';
|
||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
||||
@@ -70,7 +69,7 @@ const AllNotesItem: React.FC<Props> = props => {
|
||||
onClick={onAllNotesClick_}
|
||||
onContextMenu={toggleAllNotesContextMenu}
|
||||
>
|
||||
{_('All notes')}
|
||||
{props.item.label}
|
||||
</StyledListItemAnchor>
|
||||
</ListItemWrapper>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { StyledListItemAnchor, StyledSpanFix } from '../styles';
|
||||
import { TagsWithNoteCountEntity } from '@joplin/lib/services/database/types';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import NoteCount from './NoteCount';
|
||||
import Tag from '@joplin/lib/models/Tag';
|
||||
import EmptyExpandLink from './EmptyExpandLink';
|
||||
import ListItemWrapper, { ListItemRef } from './ListItemWrapper';
|
||||
|
||||
@@ -15,6 +14,7 @@ interface Props {
|
||||
anchorRef: ListItemRef;
|
||||
selected: boolean;
|
||||
tag: TagsWithNoteCountEntity;
|
||||
label: string;
|
||||
onTagDrop: React.DragEventHandler<HTMLElement>;
|
||||
onContextMenu: React.MouseEventHandler<HTMLElement>;
|
||||
onClick: (event: TagLinkClickEvent)=> void;
|
||||
@@ -58,7 +58,7 @@ const TagItem = (props: Props) => {
|
||||
onContextMenu={props.onContextMenu}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
<StyledSpanFix className="tag-label">{Tag.displayTitle(tag)}</StyledSpanFix>
|
||||
<StyledSpanFix className="tag-label">{props.label}</StyledSpanFix>
|
||||
{noteCount}
|
||||
</StyledListItemAnchor>
|
||||
</ListItemWrapper>
|
||||
|
||||
@@ -16,6 +16,8 @@ export enum ListItemType {
|
||||
|
||||
interface BaseListItem {
|
||||
key: string;
|
||||
// Used for typeahead
|
||||
label: string;
|
||||
depth: number;
|
||||
hasChildren: boolean;
|
||||
}
|
||||
@@ -26,7 +28,6 @@ interface ToplevelListItem extends BaseListItem {
|
||||
|
||||
export interface HeaderListItem extends ToplevelListItem {
|
||||
kind: ListItemType.Header;
|
||||
label: string;
|
||||
expanded: boolean;
|
||||
iconName: string;
|
||||
id: HeaderId;
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('NoteListUtils', () => {
|
||||
const mockStore = {
|
||||
getState: () => {
|
||||
return {
|
||||
...createAppDefaultWindowState(),
|
||||
...createAppDefaultWindowState(null),
|
||||
settings: {},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -180,8 +180,8 @@ test.describe('markdownEditor', () => {
|
||||
await expect(matches).toHaveCount(1);
|
||||
|
||||
// Should continue searching after switching to view-only mode
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await expect(noteEditor.codeMirrorEditor).not.toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
await expect(noteEditor.viewerSearchInput).toBeVisible();
|
||||
@@ -194,7 +194,7 @@ test.describe('markdownEditor', () => {
|
||||
await expect(matches).toHaveCount(0);
|
||||
|
||||
// After showing the viewer again, search should still be hidden
|
||||
await noteEditor.toggleEditorLayoutButton.click();
|
||||
await noteEditor.toggleEditorLayout();
|
||||
await expect(noteEditor.codeMirrorEditor).toBeVisible();
|
||||
await expect(noteEditor.editorSearchInput).not.toBeVisible();
|
||||
});
|
||||
@@ -274,5 +274,57 @@ test.describe('markdownEditor', () => {
|
||||
expect(imageSize[0]).toBeGreaterThan(0);
|
||||
expect(imageSize[1]).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('ctrl-clicking on note links should open the linked note (when the viewer is hidden)', async ({ mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
await mainScreen.createNewNote('Original');
|
||||
const noteEditor = mainScreen.noteEditor;
|
||||
await noteEditor.hideViewer();
|
||||
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.type('# Test');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('## Test 2');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('### Test 3');
|
||||
|
||||
const editorContent = await noteEditor.contentLocator();
|
||||
|
||||
// Extract the note ID
|
||||
const note1Locator = mainScreen.noteList.getNoteItemByTitle('Original');
|
||||
await note1Locator.dragTo(editorContent);
|
||||
const linkExpression = /\[[^\]]*\]\(:\/([a-z0-9]{32})\)/;
|
||||
await noteEditor.expectToHaveText(linkExpression);
|
||||
const targetNoteId = (await editorContent.textContent()).match(linkExpression)[1];
|
||||
|
||||
await mainScreen.createNewNote('Test note links');
|
||||
|
||||
// Create a new link to a header
|
||||
await noteEditor.focusCodeMirrorEditor();
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
await mainWindow.keyboard.type('[link](:/');
|
||||
await mainWindow.keyboard.type(targetNoteId);
|
||||
await mainWindow.keyboard.type('#test-2');
|
||||
await mainWindow.keyboard.type(')');
|
||||
await mainWindow.keyboard.press('Enter');
|
||||
|
||||
// Clicking the link should navigate to note1
|
||||
const link = editorContent.getByText(/\[?link\]?/);
|
||||
await link.click({ modifiers: ['ControlOrMeta'] });
|
||||
await expect(noteEditor.noteTitleInput).toHaveValue('Original');
|
||||
await noteEditor.expectToHaveText(/^# Test/);
|
||||
await expect.poll(() => editorContent.evaluate(async editor => {
|
||||
const selection = getSelection();
|
||||
return editor.contains(selection.anchorNode);
|
||||
})).toBe(true);
|
||||
|
||||
// The cursor should be positioned on the linked-to header
|
||||
await expect.poll(async () => {
|
||||
await mainWindow.keyboard.type('[[cursor]]');
|
||||
await noteEditor.expectToHaveText(/## Test 2\[\[cursor\]\]/);
|
||||
return true;
|
||||
}).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -65,12 +65,20 @@ export default class NoteEditorPage {
|
||||
}
|
||||
}
|
||||
|
||||
public async expectToHaveText(content: string) {
|
||||
public async expectToHaveText(expected: string|RegExp) {
|
||||
// expect(...).toHaveText can fail in the Rich Text Editor (perhaps due to frame locators).
|
||||
// Using expect.poll refreshes the locator on each attempt, which seems to prevent flakiness.
|
||||
await expect.poll(
|
||||
async () => (await this.contentLocator()).textContent(),
|
||||
).toBe(content);
|
||||
const expectResult = expect.poll(
|
||||
// Use .innerText: textContent doesn't handle line breaks correctly in the CodeMirror
|
||||
// editor.
|
||||
async () => (await this.contentLocator()).innerText(),
|
||||
);
|
||||
// Allow `expected` to be either an exact match (a string) or a pattern
|
||||
if (typeof expected === 'string') {
|
||||
await expectResult.toBe(expected);
|
||||
} else {
|
||||
await expectResult.toMatch(expected);
|
||||
}
|
||||
}
|
||||
|
||||
public getNoteViewerFrameLocator() {
|
||||
@@ -117,4 +125,14 @@ export default class NoteEditorPage {
|
||||
await expect(backButton).not.toBeDisabled();
|
||||
await backButton.click();
|
||||
}
|
||||
|
||||
public async toggleEditorLayout() {
|
||||
await this.toggleEditorLayoutButton.click();
|
||||
}
|
||||
|
||||
public async hideViewer() {
|
||||
await expect(this.noteViewerContainer).toBeVisible();
|
||||
await this.toggleEditorLayout();
|
||||
await expect(this.noteViewerContainer).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('pluginApi', () => {
|
||||
await mainScreen.createNewNote('First note');
|
||||
|
||||
const editor = mainScreen.noteEditor;
|
||||
await editor.expectToHaveText('');
|
||||
await editor.expectToHaveText('\n');
|
||||
|
||||
await mainScreen.goToAnything.runCommand(app, 'showTestDialog');
|
||||
// Wait for the iframe to load
|
||||
|
||||
@@ -44,6 +44,28 @@ test.describe('sidebar', () => {
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
});
|
||||
|
||||
test('should allow changing the focused folder by pressing the first character of the title', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
const folderAHeader = await sidebar.createNewFolder('1-Test A');
|
||||
await expect(folderAHeader).toBeVisible();
|
||||
|
||||
const folderBHeader = await sidebar.createNewFolder('Folder b');
|
||||
await expect(folderBHeader).toBeVisible();
|
||||
await folderBHeader.click();
|
||||
|
||||
await sidebar.forceUpdateSorting(electronApp);
|
||||
|
||||
await folderBHeader.click();
|
||||
await mainWindow.keyboard.type('1');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('1-Test A');
|
||||
await mainWindow.keyboard.type('F');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('Folder b');
|
||||
await mainWindow.keyboard.type('A');
|
||||
await expect(mainWindow.locator(':focus')).toHaveText('All notes');
|
||||
});
|
||||
|
||||
test('left/right arrow keys should expand/collapse notebooks', async ({ electronApp, mainWindow }) => {
|
||||
const mainScreen = await new MainScreen(mainWindow).setup();
|
||||
const sidebar = mainScreen.sidebar;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "3.5.4",
|
||||
"version": "3.5.5",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.bundle.js",
|
||||
"private": true,
|
||||
@@ -16,6 +16,7 @@
|
||||
"test": "jest",
|
||||
"test-ui": "gulp before-start && playwright test",
|
||||
"test-ci": "yarn test",
|
||||
"resolve-sourcemap": "node tools/resolveSourceMap.js",
|
||||
"modifyReleaseAssets": "node tools/modifyReleaseAssets.js"
|
||||
},
|
||||
"repository": {
|
||||
@@ -148,7 +149,7 @@
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/mustache": "4.2.6",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-dom": "18.3.7",
|
||||
"@types/react-redux": "7.1.33",
|
||||
@@ -160,7 +161,7 @@
|
||||
"compare-versions": "6.1.1",
|
||||
"countable": "3.0.1",
|
||||
"debounce": "1.2.1",
|
||||
"electron": "37.4.0",
|
||||
"electron": "37.7.0",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-updater": "6.6.2",
|
||||
"electron-window-state": "5.0.3",
|
||||
|
||||
@@ -9,7 +9,7 @@ const baseNodeModules = join(baseDir, 'node_modules');
|
||||
|
||||
// Note: Roughly based on js-draw's use of esbuild:
|
||||
// https://github.com/personalizedrefrigerator/js-draw/blob/6fe6d6821402a08a8d17f15a8f48d95e5d7b084f/packages/build-tool/src/BundledFile.ts#L64
|
||||
const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSizeStats: boolean) => {
|
||||
const makeBuildContext = (entryPoint: string, renderer: boolean, addDebugStats: boolean) => {
|
||||
return esbuild.context({
|
||||
entryPoints: [entryPoint],
|
||||
outfile: `${filename(entryPoint)}.bundle.js`,
|
||||
@@ -19,7 +19,7 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
format: 'iife', // Immediately invoked function expression
|
||||
sourcemap: true,
|
||||
sourcesContent: false, // Do not embed full source file content in the .map file
|
||||
metafile: computeFileSizeStats,
|
||||
metafile: addDebugStats,
|
||||
platform: 'node',
|
||||
target: ['node20.0'],
|
||||
mainFields: renderer ? ['browser', 'main'] : ['main'],
|
||||
@@ -92,26 +92,29 @@ const makeBuildContext = (entryPoint: string, renderer: boolean, computeFileSize
|
||||
{
|
||||
name: 'joplin--smaller-source-map-size',
|
||||
setup: build => {
|
||||
// Exclude dependencies from node_modules. This significantly reduces the size of the
|
||||
// Unless bundling with additional debug information, exclude 3rd-party
|
||||
// dependencies from source maps. This significantly reduces the size of the
|
||||
// source map, improving startup performance.
|
||||
//
|
||||
// See https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409
|
||||
// and https://github.com/evanw/esbuild/issues/4130
|
||||
const emptyMapData = Buffer.from(
|
||||
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
|
||||
if (!addDebugStats) {
|
||||
const emptyMapData = Buffer.from(
|
||||
JSON.stringify({ version: 3, sources: [null], mappings: 'AAAA' }),
|
||||
'utf-8',
|
||||
).toString('base64');
|
||||
const emptyMapUrl = `data:application/json;base64,${emptyMapData}`;
|
||||
|
||||
build.onLoad({ filter: /node_modules.*js$/ }, args => {
|
||||
return {
|
||||
contents: [
|
||||
readFileSync(args.path, 'utf8'),
|
||||
`//# sourceMappingURL=${emptyMapUrl}`,
|
||||
].join('\n'),
|
||||
loader: 'default',
|
||||
};
|
||||
});
|
||||
build.onLoad({ filter: /node_modules.*js$/ }, args => {
|
||||
return {
|
||||
contents: [
|
||||
readFileSync(args.path, 'utf8'),
|
||||
`//# sourceMappingURL=${emptyMapUrl}`,
|
||||
].join('\n'),
|
||||
loader: 'default',
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
55
packages/app-desktop/tools/resolveSourceMap.ts
Normal file
55
packages/app-desktop/tools/resolveSourceMap.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { dirname, relative } from 'path';
|
||||
import * as yargs from 'yargs';
|
||||
const { wrapCallSite } = require('source-map-support');
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const resolveLine = (lineNumber: number, columnNumber: number, filePath: string) => {
|
||||
// Note: This is an undocumented function provided by source-map-support. It
|
||||
// may change in the future:
|
||||
const frame = wrapCallSite({
|
||||
getFileName: () => filePath,
|
||||
isEval: ()=>false,
|
||||
isNative: ()=>false,
|
||||
getLineNumber: ()=>lineNumber,
|
||||
getColumnNumber: ()=>columnNumber,
|
||||
});
|
||||
|
||||
const baseDir = dirname(dirname(dirname(__dirname)));
|
||||
const relativeFilePath = relative(baseDir, frame.getFileName());
|
||||
return `${relativeFilePath}:${frame.getLineNumber()}`;
|
||||
};
|
||||
|
||||
const resolvePosition = (position: string, sourceMap: string) => {
|
||||
const match = /^(\d{1,10}):(\d{1,10})$/.exec(position.trim());
|
||||
if (!match) {
|
||||
throw new Error('Invalid format. Expected line:col');
|
||||
}
|
||||
|
||||
const lineNumber = Number(match[1]);
|
||||
const columnNumber = Number(match[2]);
|
||||
return resolveLine(lineNumber, columnNumber, sourceMap);
|
||||
};
|
||||
|
||||
void yargs
|
||||
.usage('$0 [args]')
|
||||
.command(
|
||||
'$0 <position>',
|
||||
'Resolves a position based on a source map. If resolving a position in a specific error message, be sure to use the source map generated by "yarn bundle" from that specific commit.',
|
||||
(yargs) => {
|
||||
return yargs.options({
|
||||
'position': { type: 'string', help: 'A line:col position (e.g. 123:4567)' },
|
||||
'sourcemap': {
|
||||
type: 'string',
|
||||
default: './main-html.bundle.js',
|
||||
help: 'The path to the source map. This source map should be a source map compiled from the commit/release that created the error.',
|
||||
},
|
||||
});
|
||||
},
|
||||
async (args) => {
|
||||
console.log(await resolvePosition(args.position, args.sourcemap));
|
||||
process.exit(0);
|
||||
},
|
||||
)
|
||||
.help()
|
||||
.argv;
|
||||
@@ -10,6 +10,7 @@ import JoplinCloudIcon from './JoplinCloudIcon';
|
||||
import NavService from '@joplin/lib/services/NavService';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import CardButton from '../buttons/CardButton';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
interface Props {
|
||||
dispatch: Dispatch;
|
||||
@@ -86,6 +87,11 @@ const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
const onManualDismiss = useCallback(() => {
|
||||
Setting.setValue('sync.wizard.autoShowOnStartup', false);
|
||||
onDismiss();
|
||||
}, [onDismiss]);
|
||||
|
||||
const onSelectJoplinCloud = useCallback(async () => {
|
||||
onDismiss();
|
||||
await NavService.go('JoplinCloudLogin');
|
||||
@@ -99,7 +105,7 @@ const SyncWizard: React.FC<Props> = ({ themeId, visible, dispatch }) => {
|
||||
return <DismissibleDialog
|
||||
themeId={themeId}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
onDismiss={onManualDismiss}
|
||||
size={DialogVariant.SmallResize}
|
||||
scrollOverflow={true}
|
||||
heading={_('Sync')}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import createMockReduxStore from '../../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../../utils/testing/setupGlobalStore';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { render, screen } from '../../../utils/testing/testingLibrary';
|
||||
import SearchResults from './SearchResults';
|
||||
import SearchEngine from '@joplin/lib/services/search/SearchEngine';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import TestProviderStack from '../../testing/TestProviderStack';
|
||||
|
||||
const createNotes = async (count: number) => {
|
||||
const folder = await Folder.save({ title: 'Test Note' });
|
||||
for (let i = 0; i < count; i++) {
|
||||
await Note.save({ title: `abcd ${i}`, body: 'body', parent_id: folder.id });
|
||||
}
|
||||
await SearchEngine.instance().syncTables();
|
||||
};
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
interface WrapperProps {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
}
|
||||
const WrappedSearchResults: React.FC<WrapperProps> = props => (
|
||||
<TestProviderStack store={store}>
|
||||
<SearchResults paused={props.paused} query={props.query} onHighlightedWordsChange={() => { }} ftsEnabled={1} />
|
||||
</TestProviderStack>
|
||||
);
|
||||
|
||||
describe('SearchResult', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
});
|
||||
|
||||
test('should show results when unpaused', async () => {
|
||||
const noteCount = 8;
|
||||
await createNotes(noteCount);
|
||||
|
||||
render(<WrappedSearchResults query='abcd' paused={false}/>);
|
||||
const items = await screen.findAllByText(/abcd \d\d?\d?/);
|
||||
expect(items.length).toBe(noteCount);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import shim from '@joplin/lib/shim';
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
paused: boolean;
|
||||
onHighlightedWordsChange: (highlightedWords: (ComplexTerm | string)[])=> void;
|
||||
|
||||
ftsEnabled: number;
|
||||
@@ -28,7 +29,7 @@ const useResults = (props: Props) => {
|
||||
let notes: NoteEntity[] = [];
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
if (query) {
|
||||
if (query && !props.paused) {
|
||||
if (ftsEnabled) {
|
||||
const r = await SearchEngineUtils.notesForQuery(query, true, { appendWildCards: true });
|
||||
notes = r.notes;
|
||||
@@ -57,7 +58,7 @@ const useResults = (props: Props) => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}, [query, ftsEnabled], { interval: 200 });
|
||||
}, [query, props.paused, ftsEnabled], { interval: 200 });
|
||||
|
||||
return {
|
||||
notes,
|
||||
|
||||
@@ -53,11 +53,36 @@ const useStyles = (theme: ThemeStyle, visible: boolean) => {
|
||||
}, [theme, visible]);
|
||||
};
|
||||
|
||||
// Workaround for https://github.com/laurent22/joplin/issues/12823:
|
||||
// Disable search-as-you-type for short 0-2 character searches that
|
||||
// are likely to match the start of a large number of words.
|
||||
const useSearchPaused = (query: string) => {
|
||||
const [pauseDisabled, setPauseDisabled] = useState(false);
|
||||
// Only disable search-as-you-type for a subset of all characters.
|
||||
// This is, for example, to ensure that search-as-you-type remains
|
||||
// enabled for CJK characters (e.g. U+6570 has length 1).
|
||||
const paused = query.match(/^[a-z0-9]{0,2}$/i);
|
||||
|
||||
const onOverridePause = useCallback(() => {
|
||||
setPauseDisabled(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setPauseDisabled(false);
|
||||
}, [query]);
|
||||
|
||||
return {
|
||||
paused: paused && !pauseDisabled,
|
||||
onOverridePause,
|
||||
};
|
||||
};
|
||||
|
||||
const SearchScreenComponent: React.FC<Props> = props => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
const styles = useStyles(theme, props.visible);
|
||||
|
||||
const [query, setQuery] = useState(props.query);
|
||||
const { paused, onOverridePause } = useSearchPaused(query);
|
||||
|
||||
const globalQueryRef = useRef(props.query);
|
||||
globalQueryRef.current = props.query;
|
||||
@@ -99,6 +124,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
autoFocus={props.visible}
|
||||
underlineColorAndroid="#ffffff00"
|
||||
onChangeText={setQuery}
|
||||
onSubmitEditing={onOverridePause}
|
||||
value={query}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
keyboardAppearance={theme.keyboardAppearance}
|
||||
@@ -114,6 +140,7 @@ const SearchScreenComponent: React.FC<Props> = props => {
|
||||
|
||||
<SearchResults
|
||||
query={query}
|
||||
paused={paused}
|
||||
ftsEnabled={props.ftsEnabled}
|
||||
onHighlightedWordsChange={onHighlightedWordsChange}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXConstants (17.1.7):
|
||||
- ExpoModulesCore
|
||||
- Expo (53.0.19):
|
||||
- Expo (53.0.20):
|
||||
- DoubleConversion
|
||||
- ExpoModulesCore
|
||||
- glog
|
||||
@@ -35,7 +35,7 @@ PODS:
|
||||
- Yoga
|
||||
- ExpoAsset (11.1.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoCamera (16.1.10):
|
||||
- ExpoCamera (16.1.11):
|
||||
- ExpoModulesCore
|
||||
- ZXingObjC/OneD
|
||||
- ZXingObjC/PDF417
|
||||
@@ -45,7 +45,7 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoLocalAuthentication (16.0.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (2.4.2):
|
||||
- ExpoModulesCore (2.5.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1408,7 +1408,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-alarm-notification (3.5.0):
|
||||
- React
|
||||
- react-native-document-picker (10.1.3):
|
||||
- react-native-document-picker (10.1.5):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1522,7 +1522,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-version-info (1.1.1):
|
||||
- React-Core
|
||||
- react-native-webview (13.14.2):
|
||||
- react-native-webview (13.15.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -1890,7 +1890,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.1):
|
||||
- React
|
||||
- RNShare (12.0.11):
|
||||
- RNShare (12.1.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
@@ -2293,13 +2293,13 @@ SPEC CHECKSUMS:
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXAV: ae28256069c4cdde93d185c007d8f68d92902c2e
|
||||
EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8
|
||||
Expo: 4b1c6de7c441e1caa1918671ae0aa34d51f019a5
|
||||
Expo: b527631da3b11e085809e877b845f9e6cdd68f9c
|
||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||
ExpoCamera: 7edf99216d92e40b991d4e7ed69eba9527c94cda
|
||||
ExpoCamera: e1879906d41184e84b57d7643119f8509414e318
|
||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
||||
ExpoLocalAuthentication: c35f18692dcb35775a1be0f37b2131096951a6bd
|
||||
ExpoModulesCore: e2c98670a94932b744f5bc4e394520e1c63b5462
|
||||
ExpoModulesCore: f55e7872391bae03ee5547c83152c81750d89508
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: 84b955f7b4da8b895faf5946f73748267347c975
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
@@ -2340,7 +2340,7 @@ SPEC CHECKSUMS:
|
||||
React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468
|
||||
React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6
|
||||
react-native-alarm-notification: a4326a743df72a94d361a4c3a21515556f650341
|
||||
react-native-document-picker: da39c5e4f279d39c0356dca157b98f9dc349e5bb
|
||||
react-native-document-picker: d7580f6e287bbf2c31c071d6b3f252ae1c6586f1
|
||||
react-native-geolocation: ec15ffebc53790314885eb9e5f2132132fbc2600
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-image-picker: 7babe45e727db306b3f00d08c72eda3586d6e9c1
|
||||
@@ -2352,7 +2352,7 @@ SPEC CHECKSUMS:
|
||||
react-native-safe-area-context: dde2052b903c11d677c320b599c3244021c34ce8
|
||||
react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed
|
||||
react-native-version-info: f0b04e16111c4016749235ff6d9a757039189141
|
||||
react-native-webview: 2d9ffd72b87cf905cdf8821d7d27d551188bac70
|
||||
react-native-webview: 0dceb35a9d050f5fa55f7fe2d8c4d1903651eb7d
|
||||
React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e
|
||||
React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c
|
||||
React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d
|
||||
@@ -2395,7 +2395,7 @@ SPEC CHECKSUMS:
|
||||
RNLocalize: 6a87f0490f1793d7a70042e4c55eb9a1ba6dd5b4
|
||||
RNQuickAction: c2c8f379e614428be0babe4d53a575739667744d
|
||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||
RNShare: 675e8e4a84f0137baf33057cac8f7334b0bb4b98
|
||||
RNShare: 9528acd4e374d3cb76b994b9e167d4a75cd8f452
|
||||
RNSVG: 295a96bc43f2baa5958d64aeec9847a1d8ca7a3d
|
||||
RNVectorIcons: d53917643fddb261b22bd6d889776f336893622b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
"crypto-browserify": "3.12.1",
|
||||
"deprecated-react-native-prop-types": "5.0.0",
|
||||
"events": "3.3.0",
|
||||
"expo": "53.0.19",
|
||||
"expo": "53.0.20",
|
||||
"expo-av": "15.1.7",
|
||||
"expo-camera": "16.1.10",
|
||||
"expo-camera": "16.1.11",
|
||||
"expo-local-authentication": "16.0.5",
|
||||
"lodash": "4.17.21",
|
||||
"md5": "2.3.0",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "5.4.1",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "12.0.11",
|
||||
"react-native-share": "12.1.0",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
"react-native-svg": "15.13.0",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
@@ -99,23 +99,23 @@
|
||||
"@react-native-community/cli": "16.0.3",
|
||||
"@react-native-community/cli-platform-android": "16.0.3",
|
||||
"@react-native-community/cli-platform-ios": "16.0.3",
|
||||
"@react-native/babel-preset": "0.79.5",
|
||||
"@react-native/babel-preset": "0.80.1",
|
||||
"@react-native/metro-config": "0.79.5",
|
||||
"@react-native/typescript-config": "0.79.5",
|
||||
"@sqlite.org/sqlite-wasm": "3.46.0-build2",
|
||||
"@testing-library/react-native": "13.2.0",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "19.0.14",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/serviceworker": "0.0.141",
|
||||
"@types/serviceworker": "0.0.144",
|
||||
"@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.20.0",
|
||||
"esbuild": "0.25.6",
|
||||
"esbuild": "0.25.7",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fs-extra": "11.2.0",
|
||||
"gulp": "4.0.2",
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// files: First here we convert the JS file to a plain string, and that string
|
||||
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
|
||||
|
||||
import { dirname, extname, basename } from 'path';
|
||||
import { dirname, extname, basename, resolve } from 'path';
|
||||
|
||||
import * as esbuild from 'esbuild';
|
||||
import copyAssets from './copyAssets';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { writeFile, readFile } from 'fs-extra';
|
||||
|
||||
export default class BundledFile {
|
||||
private readonly bundleOutputPathBase_: string;
|
||||
@@ -54,6 +54,32 @@ export default class BundledFile {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
// Supports require(...)ing SVG images
|
||||
name: 'joplin--require-svg',
|
||||
setup: build => {
|
||||
// A relative path to an SVG:
|
||||
build.onResolve({ filter: /^\.{1,2}\/.*\.svg$/ }, args => ({
|
||||
path: resolve(args.resolveDir, args.path),
|
||||
namespace: 'joplin-require-svg',
|
||||
}));
|
||||
|
||||
build.onLoad({ filter: /^.*$/, namespace: 'joplin-require-svg' }, async args => {
|
||||
const fileContent = await readFile(args.path, 'utf-8');
|
||||
return { contents: `
|
||||
let svg = null;
|
||||
export default () => {
|
||||
svg ??= (() => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(${JSON.stringify(fileContent)}, 'image/svg+xml');
|
||||
return doc.querySelector('svg');
|
||||
})();
|
||||
return svg.cloneNode(true);
|
||||
};
|
||||
` };
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'joplin--copy-final',
|
||||
setup: build => {
|
||||
|
||||
@@ -488,6 +488,14 @@ const buildStartupTasks = (
|
||||
|
||||
// await printTestData();
|
||||
});
|
||||
addTask('buildStartupTasks/optionally show sync wizard', async () => {
|
||||
if (Setting.value('sync.wizard.autoShowOnStartup') && Setting.value('sync.target') === 0) {
|
||||
dispatch({
|
||||
type: 'SYNC_WIZARD_VISIBLE_CHANGE',
|
||||
visible: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return startupTasks;
|
||||
};
|
||||
|
||||
@@ -89,8 +89,14 @@ export default class CodeMirrorControl extends CodeMirror5Emulation implements E
|
||||
}
|
||||
|
||||
public select(anchor: number, head: number) {
|
||||
const maximumPosition = this.editor.state.doc.length;
|
||||
this.editor.dispatch(this.editor.state.update({
|
||||
selection: { anchor, head },
|
||||
selection: {
|
||||
// Ensure that (anchor, head) are in range.
|
||||
// (CodeMirror throws when (anchor, head) are out-of-range.)
|
||||
anchor: Math.min(anchor, maximumPosition),
|
||||
head: Math.min(head, maximumPosition),
|
||||
},
|
||||
scrollIntoView: true,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { EditorCommandType } from '../types';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import commands from './commands';
|
||||
import createTestEditor from './testing/createTestEditor';
|
||||
import createTestEditor from '../testing/createTestEditor';
|
||||
|
||||
const selectAll = (editor: EditorView) => {
|
||||
commands[EditorCommandType.SelectAll](editor.state, editor.dispatch, editor);
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Command, EditorState, Transaction } from 'prosemirror-state';
|
||||
import { EditorCommandType } from '../types';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import { redo, undo } from 'prosemirror-history';
|
||||
import { autoJoin, selectAll, setBlockType, toggleMark } from 'prosemirror-commands';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
import schema from './schema';
|
||||
import schema from '../schema';
|
||||
import { liftListItem, sinkListItem, wrapRangeInList } from 'prosemirror-schema-list';
|
||||
import { NodeType } from 'prosemirror-model';
|
||||
import { getSearchVisible, setSearchVisible } from './plugins/searchPlugin';
|
||||
import { getSearchVisible, setSearchVisible } from '../plugins/searchPlugin';
|
||||
import { findNext, findPrev, replaceAll, replaceNext } from 'prosemirror-search';
|
||||
import { getEditorApi } from './plugins/joplinEditorApiPlugin';
|
||||
import { EditorEventType } from '../events';
|
||||
import extractSelectedLinesTo from './utils/extractSelectedLinesTo';
|
||||
import { getEditorApi } from '../plugins/joplinEditorApiPlugin';
|
||||
import { EditorEventType } from '../../events';
|
||||
import extractSelectedLinesTo from '../utils/extractSelectedLinesTo';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import jumpToHash from './utils/jumpToHash';
|
||||
import canReplaceSelectionWith from './utils/canReplaceSelectionWith';
|
||||
import jumpToHash from '../utils/jumpToHash';
|
||||
import canReplaceSelectionWith from '../utils/canReplaceSelectionWith';
|
||||
import focusEditor from './focusEditor';
|
||||
|
||||
type Dispatch = (tr: Transaction)=> void;
|
||||
type ExtendedCommand = (state: EditorState, dispatch: Dispatch, view?: EditorView, options?: string[])=> boolean;
|
||||
@@ -81,12 +81,7 @@ const commands: Record<EditorCommandType, ExtendedCommand|null> = {
|
||||
[EditorCommandType.Undo]: undo,
|
||||
[EditorCommandType.Redo]: redo,
|
||||
[EditorCommandType.SelectAll]: selectAll,
|
||||
[EditorCommandType.Focus]: (_state, _dispatch?, view?) => {
|
||||
if (view) {
|
||||
focus('commands::focus', view);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[EditorCommandType.Focus]: focusEditor,
|
||||
[EditorCommandType.ToggleBolded]: toggleMark(schema.marks.strong),
|
||||
[EditorCommandType.ToggleItalicized]: toggleMark(schema.marks.emphasis),
|
||||
[EditorCommandType.ToggleCode]: toggleCode,
|
||||
11
packages/editor/ProseMirror/commands/focusEditor.ts
Normal file
11
packages/editor/ProseMirror/commands/focusEditor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Command } from 'prosemirror-state';
|
||||
import { focus } from '@joplin/lib/utils/focusHandler';
|
||||
|
||||
const focusEditor: Command = (_state, _dispatch?, view?) => {
|
||||
if (view) {
|
||||
focus('commands::focus', view);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default focusEditor;
|
||||
@@ -4,7 +4,7 @@ import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { DOMParser as ProseMirrorDomParser } from 'prosemirror-model';
|
||||
import { history } from 'prosemirror-history';
|
||||
import commands from './commands';
|
||||
import commands from './commands/commands';
|
||||
import schema from './schema';
|
||||
import { gapCursor } from 'prosemirror-gapcursor';
|
||||
import { dropCursor } from 'prosemirror-dropcursor';
|
||||
@@ -16,7 +16,6 @@ import joplinEditablePlugin from './plugins/joplinEditablePlugin/joplinEditableP
|
||||
import keymapExtension from './plugins/keymapPlugin';
|
||||
import inputRulesExtension from './plugins/inputRulesPlugin';
|
||||
import originalMarkupPlugin from './plugins/originalMarkupPlugin';
|
||||
import { tableEditing } from 'prosemirror-tables';
|
||||
import preprocessEditorInput from './utils/preprocessEditorInput';
|
||||
import listPlugin from './plugins/listPlugin';
|
||||
import searchExtension from './plugins/searchPlugin';
|
||||
@@ -28,6 +27,7 @@ import getFileFromPasteEvent from '../utils/getFileFromPasteEvent';
|
||||
import { RenderResult } from '../../renderer/types';
|
||||
import postprocessEditorOutput from './utils/postprocessEditorOutput';
|
||||
import detailsPlugin from './plugins/detailsPlugin';
|
||||
import tablePlugin from './plugins/tablePlugin';
|
||||
|
||||
interface ProseMirrorControl extends EditorControl {
|
||||
getSettings(): EditorSettings;
|
||||
@@ -90,7 +90,7 @@ const createEditor = async (
|
||||
markupTracker,
|
||||
listPlugin,
|
||||
linkTooltipPlugin,
|
||||
tableEditing({ allowTableNodeSelection: true }),
|
||||
tablePlugin,
|
||||
joplinEditorApiPlugin,
|
||||
imagePlugin,
|
||||
].flat(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getEditorApi } from './joplinEditorApiPlugin';
|
||||
import showModal from '../utils/dom/showModal';
|
||||
import createTextArea from '../utils/dom/createTextArea';
|
||||
import createExternalEditorPlugin, { OnHide } from './utils/createExternalEditorPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarPosition } from './utils/createFloatingButtonPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
|
||||
|
||||
// See the fold example for more information about
|
||||
// writing similar ProseMirror plugins:
|
||||
@@ -263,7 +263,7 @@ const imagePlugin = [
|
||||
}),
|
||||
createFloatingButtonPlugin('image', [
|
||||
{ label: _ => _('Label'), command: (_node, offset) => editAltTextAt(offset) },
|
||||
], ToolbarPosition.TopRightInside),
|
||||
], ToolbarType.AnchorTopRight),
|
||||
];
|
||||
|
||||
export default imagePlugin;
|
||||
|
||||
@@ -9,7 +9,7 @@ import postProcessRenderedHtml from './postProcessRenderedHtml';
|
||||
import makeLinksClickableInElement from '../../utils/makeLinksClickableInElement';
|
||||
import SelectableNodeView from '../../utils/SelectableNodeView';
|
||||
import createExternalEditorPlugin, { OnHide } from '../utils/createExternalEditorPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarPosition } from '../utils/createFloatingButtonPlugin';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from '../utils/createFloatingButtonPlugin';
|
||||
|
||||
// See the fold example for more information about
|
||||
// writing similar ProseMirror plugins:
|
||||
@@ -245,6 +245,6 @@ export default [
|
||||
className: 'edit-button',
|
||||
command: (_node, offset) => editAt(offset),
|
||||
},
|
||||
], ToolbarPosition.TopRightInside)
|
||||
], ToolbarType.AnchorTopRight)
|
||||
)),
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import schema from '../schema';
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { baseKeymap, chainCommands, exitCode, liftEmptyBlock, newlineInCode } from 'prosemirror-commands';
|
||||
import { liftListItem, sinkListItem, splitListItem } from 'prosemirror-schema-list';
|
||||
import commands from '../commands';
|
||||
import commands from '../commands/commands';
|
||||
import { EditorCommandType } from '../../types';
|
||||
import { Command, EditorState, TextSelection, Plugin } from 'prosemirror-state';
|
||||
import splitBlockAs from '../vendor/splitBlockAs';
|
||||
|
||||
40
packages/editor/ProseMirror/plugins/tablePlugin.ts
Normal file
40
packages/editor/ProseMirror/plugins/tablePlugin.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { addColumnAfter, addRowAfter, deleteColumn, deleteRow, tableEditing } from 'prosemirror-tables';
|
||||
import createFloatingButtonPlugin, { ToolbarType } from './utils/createFloatingButtonPlugin';
|
||||
import addColumnRightIcon from '../vendor/icons/addColumnRight';
|
||||
import addRowBelowIcon from '../vendor/icons/addRowBelow';
|
||||
import removeRowIcon from '../vendor/icons/removeRow';
|
||||
import removeColumnIcon from '../vendor/icons/removeColumn';
|
||||
import focusEditor from '../commands/focusEditor';
|
||||
import { Command } from 'prosemirror-state';
|
||||
|
||||
const tableCommand = (command: Command): Command => (state, dispatch, view) => {
|
||||
return command(state, dispatch, view) && focusEditor(state, dispatch, view);
|
||||
};
|
||||
|
||||
const tablePlugin = [
|
||||
tableEditing({ allowTableNodeSelection: true }),
|
||||
createFloatingButtonPlugin('table', [
|
||||
{
|
||||
icon: addRowBelowIcon,
|
||||
label: (_) => _('Add row'),
|
||||
command: () => tableCommand(addRowAfter),
|
||||
},
|
||||
{
|
||||
icon: addColumnRightIcon,
|
||||
label: (_) => _('Add column'),
|
||||
command: () => tableCommand(addColumnAfter),
|
||||
},
|
||||
{
|
||||
icon: removeRowIcon,
|
||||
label: (_) => _('Delete row'),
|
||||
command: () => tableCommand(deleteRow),
|
||||
},
|
||||
{
|
||||
icon: removeColumnIcon,
|
||||
label: (_) => _('Delete column'),
|
||||
command: () => tableCommand(deleteColumn),
|
||||
},
|
||||
], ToolbarType.FloatAboveBelow),
|
||||
];
|
||||
|
||||
export default tablePlugin;
|
||||
@@ -1,42 +1,158 @@
|
||||
import { Command, EditorState, Plugin } from 'prosemirror-state';
|
||||
import { Command, EditorState, Plugin, PluginView } from 'prosemirror-state';
|
||||
import { LocalizationResult, OnLocalize } from '../../../types';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import createButton from '../../utils/dom/createButton';
|
||||
import { getEditorApi } from '../joplinEditorApiPlugin';
|
||||
import { Node } from 'prosemirror-model';
|
||||
import { Icon } from '../../vendor/icons/types';
|
||||
|
||||
type LocalizeFunction = (_: OnLocalize)=> LocalizationResult;
|
||||
|
||||
interface ButtonSpec {
|
||||
icon?: Icon;
|
||||
label: LocalizeFunction;
|
||||
command: (node: Node, offset: number)=> Command;
|
||||
showForNode?: (node: Node)=> boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export enum ToolbarPosition {
|
||||
TopLeftOutside,
|
||||
TopRightInside,
|
||||
export enum ToolbarType {
|
||||
// Attempts to keep the toolbar visible when the node
|
||||
// is visible. While showing the toolbar outside the node
|
||||
// is preferred, the toolbar will be shown inside the node
|
||||
// if insufficient outside space is available.
|
||||
FloatAboveBelow,
|
||||
// Anchors the toolbar to the top right corner of the
|
||||
// associated element.
|
||||
AnchorTopRight,
|
||||
}
|
||||
|
||||
class FloatingButtonBar {
|
||||
interface TargetNode {
|
||||
offset: number;
|
||||
node: Node;
|
||||
element: Element|null;
|
||||
}
|
||||
|
||||
class FloatingButtonBar implements PluginView {
|
||||
private container_: HTMLElement;
|
||||
private buttonRow_: ButtonRow;
|
||||
|
||||
private currentTarget_: TargetNode|null = null;
|
||||
private observer_: ElementObserver;
|
||||
|
||||
public constructor(
|
||||
view: EditorView, private targetNode_: string, private buttons_: ButtonSpec[], private position_: ToolbarPosition,
|
||||
private view_: EditorView,
|
||||
private targetNodeName_: string,
|
||||
buttons: ButtonSpec[],
|
||||
private type_: ToolbarType,
|
||||
) {
|
||||
this.container_ = document.createElement('div');
|
||||
this.container_.classList.add('floating-button-bar');
|
||||
|
||||
this.buttonRow_ = new ButtonRow(this.container_, buttons);
|
||||
|
||||
this.observer_ = new ElementObserver(
|
||||
() => this.repositionOverlay_(),
|
||||
);
|
||||
|
||||
// Prevent other elements (e.g. checkboxes, links) from being between the toolbar button and the
|
||||
// target element. If the toolbar is instead included **after** the Rich Text Editor's main content,
|
||||
// then all items included directly within the Rich Text Editor come before the toolbar in the focus
|
||||
// order.
|
||||
view.dom.parentElement.prepend(this.container_);
|
||||
this.update(view, null);
|
||||
view_.dom.parentElement.prepend(this.container_);
|
||||
this.update(view_, null);
|
||||
|
||||
if (this.type_ === ToolbarType.AnchorTopRight) {
|
||||
this.container_.classList.add('-anchored');
|
||||
} else if (this.type_ === ToolbarType.FloatAboveBelow) {
|
||||
this.container_.classList.add('-floating');
|
||||
} else {
|
||||
const unreachable_: never = this.type_;
|
||||
throw new Error(`Unknown toolbar type: ${unreachable_}`);
|
||||
}
|
||||
}
|
||||
|
||||
private repositionOverlay_() {
|
||||
if (!this.currentTarget_) return;
|
||||
|
||||
const overlay = this.container_;
|
||||
const view = this.view_;
|
||||
const target = this.currentTarget_;
|
||||
const position = this.view_.coordsAtPos(target.offset);
|
||||
const targetElement = view.nodeDOM(target.offset);
|
||||
|
||||
// Fall back to document.body to support testing environments:
|
||||
const parentBox = (this.container_.offsetParent ?? document.body).getBoundingClientRect();
|
||||
const tooltipBox = this.container_.getBoundingClientRect();
|
||||
const targetBox = targetElement instanceof HTMLElement ? targetElement.getBoundingClientRect() : {
|
||||
...position,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
this.container_.style.left = '';
|
||||
this.container_.style.right = '';
|
||||
|
||||
if (this.type_ === ToolbarType.FloatAboveBelow) {
|
||||
const padding = 10;
|
||||
const above = targetBox.top - tooltipBox.height - parentBox.top - padding;
|
||||
const below = targetBox.top + targetBox.height - parentBox.top + padding;
|
||||
const viewportTop = window.visualViewport?.pageTop;
|
||||
const viewportBottom = viewportTop + window.visualViewport?.height;
|
||||
const cursorTop = viewportTop + view.coordsAtPos(view.state.selection.head).top;
|
||||
|
||||
const getOffsetTop = () => {
|
||||
// If the toolbar must be displayed within the element to be visible, prefer
|
||||
// less movement:
|
||||
const previousTop = tooltipBox.top + viewportTop;
|
||||
const insideCandidates = [
|
||||
Math.max(viewportTop + padding, above),
|
||||
Math.min(viewportBottom - padding - tooltipBox.height, below),
|
||||
].sort((a, b) => {
|
||||
const distanceA = Math.abs(a - previousTop);
|
||||
const distanceB = Math.abs(b - previousTop);
|
||||
return distanceA - distanceB;
|
||||
}).filter(position => {
|
||||
return position >= above && position <= below;
|
||||
});
|
||||
|
||||
const positionCandidates = [
|
||||
// Always prefer showing the toolbar outside the element
|
||||
above, below,
|
||||
// Fall back to showing the toolbar inside
|
||||
...insideCandidates,
|
||||
];
|
||||
|
||||
const validCandidates = positionCandidates.filter((position) => {
|
||||
const candidateTop = position;
|
||||
const candidateBottom = position + tooltipBox.height;
|
||||
const candidateCenter = position + tooltipBox.height / 2;
|
||||
const distanceFromCursor = Math.abs(candidateCenter - cursorTop);
|
||||
|
||||
return candidateTop >= viewportTop
|
||||
// Avoid showing the toolbar off the bottom edge of the screen
|
||||
&& candidateBottom <= viewportBottom
|
||||
// Avoid showing the toolbar on the same line as the cursor
|
||||
&& distanceFromCursor > tooltipBox.height / 2 + padding;
|
||||
});
|
||||
return validCandidates[0] ?? positionCandidates[0];
|
||||
};
|
||||
|
||||
const targetCenter = targetBox.left + targetBox.width / 2;
|
||||
const currentCenter = parentBox.left + tooltipBox.width / 2;
|
||||
// Subtract (parentBox.left, parentBox.top): style.left and style.top
|
||||
// are relative to the parent, but the computed position is not.
|
||||
overlay.style.left = `${Math.max(targetCenter - currentCenter, 0)}px`;
|
||||
overlay.style.top = `${getOffsetTop()}px`;
|
||||
} else if (this.type_ === ToolbarType.AnchorTopRight) {
|
||||
overlay.style.right = `${parentBox.width - targetBox.width - (targetBox.left - parentBox.left)}px`;
|
||||
overlay.style.top = `${targetBox.top - parentBox.top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public update(view: EditorView, lastState: EditorState|null) {
|
||||
this.view_ = view;
|
||||
|
||||
const state = view.state;
|
||||
const sameSelection = lastState && state.selection.eq(lastState.selection);
|
||||
const sameDoc = lastState && state.doc.eq(lastState.doc);
|
||||
@@ -45,11 +161,12 @@ class FloatingButtonBar {
|
||||
}
|
||||
|
||||
const findTargetNode = () => {
|
||||
type TargetNode = { offset: number; node: Node };
|
||||
let target: TargetNode = null;
|
||||
let target: TargetNode|null = null;
|
||||
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, offset) => {
|
||||
if (node.type.name === this.targetNode_) {
|
||||
target = { node, offset };
|
||||
if (node.type.name === this.targetNodeName_) {
|
||||
const dom = view.nodeDOM(offset);
|
||||
const domElement = dom instanceof HTMLElement ? dom : dom.parentElement;
|
||||
target = { node, offset, element: domElement };
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -59,69 +176,109 @@ class FloatingButtonBar {
|
||||
};
|
||||
|
||||
const target = findTargetNode();
|
||||
this.observer_.setElement(target?.element);
|
||||
this.currentTarget_ = target;
|
||||
|
||||
if (!target) {
|
||||
this.container_.classList.add('-hidden');
|
||||
} else {
|
||||
this.container_.classList.remove('-hidden');
|
||||
|
||||
const hasCreatedButtons = this.container_.children.length === this.buttons_.length;
|
||||
if (!hasCreatedButtons) {
|
||||
const { localize } = getEditorApi(view.state);
|
||||
this.container_.replaceChildren(...this.buttons_.map(buttonSpec => {
|
||||
const button = createButton(
|
||||
buttonSpec.label(localize),
|
||||
() => { },
|
||||
);
|
||||
this.buttonRow_.updateButtons(view, target);
|
||||
this.repositionOverlay_();
|
||||
}
|
||||
}
|
||||
|
||||
button.classList.add('action');
|
||||
if (buttonSpec.className) {
|
||||
button.classList.add(buttonSpec.className);
|
||||
}
|
||||
public destroy() {
|
||||
this.observer_.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
return button;
|
||||
}));
|
||||
}
|
||||
// Emits changes when the element's position changes.
|
||||
class ElementObserver {
|
||||
private intersectionObserver_: IntersectionObserver|null;
|
||||
private lastElement_: Element|null = null;
|
||||
|
||||
for (let i = 0; i < this.buttons_.length; i++) {
|
||||
const button = this.container_.children[i] as HTMLButtonElement;
|
||||
const buttonSpec = this.buttons_[i];
|
||||
public constructor(private onNodeUpdate_: ()=> void) {
|
||||
if (typeof IntersectionObserver !== 'undefined') {
|
||||
this.intersectionObserver_ = new IntersectionObserver(() => {
|
||||
this.onNodeUpdate_();
|
||||
});
|
||||
}
|
||||
document.addEventListener('scroll', this.onNodeUpdate_);
|
||||
window.addEventListener('resize', this.onNodeUpdate_);
|
||||
}
|
||||
|
||||
const command = buttonSpec.command(target.node, target.offset);
|
||||
button.onclick = () => {
|
||||
command(view.state, view.dispatch, view);
|
||||
};
|
||||
public setElement(element: Element|null) {
|
||||
if (element === this.lastElement_) return;
|
||||
|
||||
button.disabled = !command(view.state);
|
||||
}
|
||||
if (this.lastElement_) {
|
||||
this.intersectionObserver_?.unobserve(this.lastElement_);
|
||||
}
|
||||
|
||||
const position = view.coordsAtPos(target.offset);
|
||||
// Fall back to document.body to support testing environments:
|
||||
const parentBox = (this.container_.offsetParent ?? document.body).getBoundingClientRect();
|
||||
const tooltipBox = this.container_.getBoundingClientRect();
|
||||
if (element) {
|
||||
this.intersectionObserver_?.observe(element);
|
||||
}
|
||||
|
||||
this.container_.style.left = '';
|
||||
this.container_.style.right = '';
|
||||
this.lastElement_ = element;
|
||||
}
|
||||
|
||||
const nodeElement = view.nodeDOM(target.offset);
|
||||
const nodeBbox = nodeElement instanceof HTMLElement ? nodeElement.getBoundingClientRect() : {
|
||||
...position,
|
||||
width: 0,
|
||||
height: 0,
|
||||
public destroy() {
|
||||
this.intersectionObserver_?.disconnect();
|
||||
this.intersectionObserver_ = null;
|
||||
|
||||
document.removeEventListener('scroll', this.onNodeUpdate_);
|
||||
window.removeEventListener('resize', this.onNodeUpdate_);
|
||||
}
|
||||
}
|
||||
|
||||
class ButtonRow {
|
||||
private created_ = false;
|
||||
public constructor(private container_: HTMLElement, private buttons_: ButtonSpec[]) { }
|
||||
|
||||
public updateButtons(view: EditorView, targetNode: TargetNode) {
|
||||
// Late-init the buttons to allow accessing `view`:
|
||||
if (!this.created_) {
|
||||
this.created_ = true;
|
||||
|
||||
const { localize } = getEditorApi(view.state);
|
||||
this.container_.replaceChildren(...this.buttons_.map(buttonSpec => {
|
||||
const label = buttonSpec.label(localize);
|
||||
const button = createButton(
|
||||
buttonSpec.icon ? { label, icon: buttonSpec.icon() } : label,
|
||||
() => { },
|
||||
);
|
||||
|
||||
button.classList.add('action', 'action-button');
|
||||
if (buttonSpec.icon) {
|
||||
button.classList.add('-icon');
|
||||
}
|
||||
|
||||
if (buttonSpec.className) {
|
||||
button.classList.add(buttonSpec.className);
|
||||
}
|
||||
|
||||
return button;
|
||||
}));
|
||||
}
|
||||
|
||||
// Update the button listeners and states based on the current view and
|
||||
// target node
|
||||
for (let i = 0; i < this.buttons_.length; i++) {
|
||||
const button = this.container_.children[i] as HTMLButtonElement;
|
||||
const buttonSpec = this.buttons_[i];
|
||||
|
||||
const command = buttonSpec.command(targetNode.node, targetNode.offset);
|
||||
button.onclick = () => {
|
||||
command(view.state, view.dispatch, view);
|
||||
};
|
||||
|
||||
let top = nodeBbox.top - parentBox.top;
|
||||
if (this.position_ === ToolbarPosition.TopLeftOutside) {
|
||||
top -= tooltipBox.height;
|
||||
this.container_.style.left = `${Math.max(nodeBbox.left - parentBox.left, 0)}px`;
|
||||
} else if (this.position_ === ToolbarPosition.TopRightInside) {
|
||||
this.container_.style.right = `${parentBox.width - nodeBbox.width - (nodeBbox.left - parentBox.left)}px`;
|
||||
}
|
||||
this.container_.style.top = `${top}px`;
|
||||
button.disabled = !command(view.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createFloatingButtonPlugin = (nodeName: string, actions: ButtonSpec[], position: ToolbarPosition) => {
|
||||
const createFloatingButtonPlugin = (nodeName: string, actions: ButtonSpec[], position: ToolbarType) => {
|
||||
return new Plugin({
|
||||
view: (view) => new FloatingButtonBar(view, nodeName, actions, position),
|
||||
});
|
||||
|
||||
@@ -11,4 +11,5 @@ import './styles/link-tooltip.css';
|
||||
import './styles/joplin-image-view.css';
|
||||
import './styles/alt-text-editor.css';
|
||||
import './styles/floating-button-bar.css';
|
||||
import './styles/action-button.css';
|
||||
|
||||
|
||||
20
packages/editor/ProseMirror/styles/action-button.css
Normal file
20
packages/editor/ProseMirror/styles/action-button.css
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
.action-button {
|
||||
background-color: transparent;
|
||||
color: currentColor;
|
||||
border-radius: 48px;
|
||||
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
.action-button.-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.action-button > .icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -2,6 +2,15 @@
|
||||
.floating-button-bar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
|
||||
background-color: var(--joplin-background-color3);
|
||||
color: var(--joplin-color);
|
||||
|
||||
box-shadow: 0px 0px 2px var(--joplin-color);
|
||||
}
|
||||
|
||||
.floating-button-bar.-hidden {
|
||||
@@ -9,5 +18,32 @@
|
||||
}
|
||||
|
||||
.floating-button-bar > .action {
|
||||
opacity: 0.9;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.floating-button-bar.-floating {
|
||||
border-radius: 64px;
|
||||
height: 64px;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored {
|
||||
border-radius: 48px;
|
||||
padding: 0px;
|
||||
gap: 4px;
|
||||
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored > .action {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.floating-button-bar.-anchored:focus-within, .floating-button-bar.-anchored:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
background-color: var(--joplin-background-color);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 0px 2px var(--joplin-color);
|
||||
box-shadow: 0px 0px 3px var(--joplin-color);
|
||||
color: var(--joplin-color);
|
||||
|
||||
width: min(80vw, 600px);
|
||||
|
||||
@@ -2,3 +2,10 @@
|
||||
table .selectedCell {
|
||||
outline: 2px solid var(--joplin-text-selection-color);
|
||||
}
|
||||
|
||||
/* Prevent table entries from having more spacing in the Rich Text Editor
|
||||
than in the note viewer. Unlike the note viewer, the Rich Text Editor
|
||||
always adds a <p> wrapper element around the table cells. */
|
||||
th > p:only-child, td > p:only-child {
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,28 @@ import createTextNode from './createTextNode';
|
||||
|
||||
type OnClick = ()=> void;
|
||||
|
||||
const createButton = (label: LocalizationResult, onClick: OnClick) => {
|
||||
type Content = LocalizationResult|{
|
||||
icon: Element;
|
||||
label: LocalizationResult;
|
||||
};
|
||||
|
||||
const isLocalizationResult = (content: Content): content is LocalizationResult => {
|
||||
return typeof content === 'string' || !('icon' in content);
|
||||
};
|
||||
|
||||
const createButton = (content: Content, onClick: OnClick) => {
|
||||
const button = document.createElement('button');
|
||||
button.appendChild(createTextNode(label));
|
||||
if (isLocalizationResult(content)) {
|
||||
button.appendChild(createTextNode(content));
|
||||
} else {
|
||||
button.appendChild(content.icon);
|
||||
|
||||
void (async () => {
|
||||
const label = await content.label;
|
||||
button.ariaLabel = label;
|
||||
button.title = label;
|
||||
})();
|
||||
}
|
||||
|
||||
button.onclick = onClick;
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ const removeListItemWrapperParagraphs = (container: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const restoreOriginalLinks = (container: HTMLElement) => {
|
||||
// Restore HREFs
|
||||
const links = container.querySelectorAll<HTMLAnchorElement>('a[href="#"][data-original-href]');
|
||||
@@ -32,6 +31,18 @@ const restoreOriginalLinks = (container: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
const removeTableItemExtraPadding = (container: HTMLElement) => {
|
||||
const cells = container.querySelectorAll<HTMLTableCellElement>('th, td');
|
||||
for (const cell of cells) {
|
||||
// Table cells can exist in Markdown without the need for invisible
|
||||
// content.
|
||||
// Remove single nonbreaking space padding:
|
||||
if (cell.textContent === '\u00A0') {
|
||||
cell.textContent = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const postprocessEditorOutput = (node: Node|DocumentFragment) => {
|
||||
// By default, if `src` is specified on an image, the browser will try to load the image, even if it isn't added
|
||||
// to the DOM. (A similar problem is described here: https://stackoverflow.com/q/62019538).
|
||||
@@ -52,6 +63,7 @@ const postprocessEditorOutput = (node: Node|DocumentFragment) => {
|
||||
fixResourceUrls(html);
|
||||
restoreOriginalLinks(html);
|
||||
removeListItemWrapperParagraphs(html);
|
||||
removeTableItemExtraPadding(html);
|
||||
|
||||
return html;
|
||||
};
|
||||
|
||||
208
packages/editor/ProseMirror/vendor/icons/LICENSE
vendored
Normal file
208
packages/editor/ProseMirror/vendor/icons/LICENSE
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
The icons included in this folder are vendored from https://fonts.google.com/icons.
|
||||
Changes made:
|
||||
- File names have been changed.
|
||||
|
||||
|
||||
License:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
packages/editor/ProseMirror/vendor/icons/addColumnRight.svg
vendored
Normal file
1
packages/editor/ProseMirror/vendor/icons/addColumnRight.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-760v560h240v-560H160ZM80-120v-720h720v160h-80v-80H480v560h240v-80h80v160H80Zm400-360Zm-80 0h80-80Zm0 0Zm320 120v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>
|
||||
|
After Width: | Height: | Size: 284 B |
3
packages/editor/ProseMirror/vendor/icons/addColumnRight.ts
vendored
Normal file
3
packages/editor/ProseMirror/vendor/icons/addColumnRight.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import icon from "./icon";
|
||||
|
||||
export default icon(require('./addColumnRight.svg'));
|
||||
1
packages/editor/ProseMirror/vendor/icons/addRowBelow.svg
vendored
Normal file
1
packages/editor/ProseMirror/vendor/icons/addRowBelow.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-560h560v-240H200v240Zm-80 400v-720h720v720H680v-80h80v-240H200v240h80v80H120Zm360-320Zm0-80v80-80Zm0 0ZM440-80v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>
|
||||
|
After Width: | Height: | Size: 283 B |
3
packages/editor/ProseMirror/vendor/icons/addRowBelow.ts
vendored
Normal file
3
packages/editor/ProseMirror/vendor/icons/addRowBelow.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import icon from "./icon";
|
||||
|
||||
export default icon(require('./addRowBelow.svg'));
|
||||
15
packages/editor/ProseMirror/vendor/icons/icon.ts
vendored
Normal file
15
packages/editor/ProseMirror/vendor/icons/icon.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Icon } from "./types";
|
||||
|
||||
type ImportedIcon = { default: Icon };
|
||||
|
||||
const icon = (importedIcon: ImportedIcon): Icon => {
|
||||
return () => {
|
||||
const icon = importedIcon.default();
|
||||
icon.removeAttribute('fill');
|
||||
icon.style.fill = 'currentColor';
|
||||
icon.classList.add('icon');
|
||||
return icon;
|
||||
};
|
||||
};
|
||||
|
||||
export default icon;
|
||||
7
packages/editor/ProseMirror/vendor/icons/removeColumn.ts
vendored
Normal file
7
packages/editor/ProseMirror/vendor/icons/removeColumn.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import icon from "./icon";
|
||||
|
||||
export default () => {
|
||||
const element = icon(require('./variableRemove.svg'))();
|
||||
element.style.transform = 'rotate(90deg)';
|
||||
return element;
|
||||
};
|
||||
5
packages/editor/ProseMirror/vendor/icons/removeRow.ts
vendored
Normal file
5
packages/editor/ProseMirror/vendor/icons/removeRow.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import icon from "./icon";
|
||||
|
||||
// As of October 2025, Material Symbols seems to lack a "remove row".
|
||||
// However, the "variable remove" icon has a similar appearance.
|
||||
export default icon(require('./variableRemove.svg'));
|
||||
2
packages/editor/ProseMirror/vendor/icons/types.ts
vendored
Normal file
2
packages/editor/ProseMirror/vendor/icons/types.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export type Icon = ()=>SVGSVGElement;
|
||||
1
packages/editor/ProseMirror/vendor/icons/variableRemove.svg
vendored
Normal file
1
packages/editor/ProseMirror/vendor/icons/variableRemove.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M560-280H120v-400h720v120h-80v-40H200v240h360v80Zm-360-80v-240 240Zm440 104 84-84-84-84 56-56 84 84 84-84 56 56-83 84 83 84-56 56-84-83-84 83-56-56Z"/></svg>
|
||||
|
After Width: | Height: | Size: 273 B |
@@ -18,7 +18,7 @@
|
||||
"@joplin/utils": "~3.5",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@types/react": "18.3.23",
|
||||
"@types/react-redux": "7.1.33",
|
||||
"@types/styled-components": "5.1.32",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@joplin/fork-htmlparser2",
|
||||
"description": "Fast & forgiving HTML/XML/RSS parser",
|
||||
"version": "4.1.59",
|
||||
"version": "4.1.60",
|
||||
"author": "Felix Boehm <me@feedic.com>",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"coveralls": "3.1.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@joplin/fork-sax",
|
||||
"description": "An evented streaming XML parser in JavaScript",
|
||||
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
|
||||
"version": "1.2.63",
|
||||
"version": "1.2.64",
|
||||
"main": "lib/sax.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/fork-uslug",
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.3",
|
||||
"description": "A permissive slug generator that works with unicode.",
|
||||
"author": "Jeremy Selier <jerem.selier@gmail.com>",
|
||||
"publishConfig": {
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "18.19.118",
|
||||
"@types/node": "18.19.130",
|
||||
"jest": "29.7.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ClipboardContent } from './types';
|
||||
export default class JoplinClipboard {
|
||||
private electronClipboard_;
|
||||
private electronNativeImage_;
|
||||
@@ -26,4 +27,19 @@ export default class JoplinClipboard {
|
||||
* For example [ 'text/plain', 'text/html' ]
|
||||
*/
|
||||
availableFormats(): Promise<string[]>;
|
||||
/**
|
||||
* Writes multiple formats to the clipboard simultaneously.
|
||||
* This allows setting both text/plain and text/html at the same time.
|
||||
*
|
||||
* <span class="platform-desktop">desktop</span>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await joplin.clipboard.write({
|
||||
* text: 'Plain text version',
|
||||
* html: '<strong>HTML version</strong>'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
write(content: ClipboardContent): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -588,6 +588,30 @@ export interface SettingSection {
|
||||
*/
|
||||
export type Path = string[];
|
||||
|
||||
// =================================================================
|
||||
// Clipboard API types
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Represents content that can be written to the clipboard in multiple formats.
|
||||
*/
|
||||
export interface ClipboardContent {
|
||||
/**
|
||||
* Plain text representation of the content
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* HTML representation of the content
|
||||
*/
|
||||
html?: string;
|
||||
|
||||
/**
|
||||
* Image in [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format
|
||||
*/
|
||||
image?: string;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// Content Script types
|
||||
// =================================================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "generator-joplin",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"description": "Scaffolds out a new Joplin plugin",
|
||||
"homepage": "https://github.com/laurent22/joplin/tree/dev/packages/generator-joplin",
|
||||
"author": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/htmlpack",
|
||||
"version": "3.5.0",
|
||||
"version": "3.5.1",
|
||||
"description": "Pack an HTML file and all its linked resources into a single HTML file",
|
||||
"main": "dist/index.js",
|
||||
"types": "index.ts",
|
||||
@@ -26,7 +26,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "4.4.3",
|
||||
"@joplin/fork-htmlparser2": "^4.1.59",
|
||||
"@joplin/fork-htmlparser2": "^4.1.60",
|
||||
"datauri": "4.1.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"html-entities": "1.4.0"
|
||||
|
||||
@@ -20,7 +20,7 @@ import JoplinError from './JoplinError';
|
||||
import ShareService from './services/share/ShareService';
|
||||
import TaskQueue from './TaskQueue';
|
||||
import ItemUploader from './services/synchronizer/ItemUploader';
|
||||
import { FileApi, getSupportsDeltaWithItems, isLocalServer, PaginatedList, RemoteItem } from './file-api';
|
||||
import { FileApi, getSupportsDeltaWithItems, isLocalServer, PaginatedList, RemoteItem, enableEnhancedBasicDeltaAlgorithm } from './file-api';
|
||||
import JoplinDatabase from './JoplinDatabase';
|
||||
import { checkIfCanSync, fetchSyncInfo, checkSyncTargetIsValid, getActiveMasterKey, localSyncInfo, mergeSyncInfos, saveLocalSyncInfo, setMasterKeyHasBeenUsed, SyncInfo, syncInfoEquals, uploadSyncInfo } from './services/synchronizer/syncInfoUtils';
|
||||
import { getMasterPassword, setupAndDisableEncryption, setupAndEnableEncryption } from './services/e2ee/utils';
|
||||
@@ -820,6 +820,15 @@ export default class Synchronizer {
|
||||
// on it (instead it uses a more reliable `context` object) and the itemsThatNeedSync loop
|
||||
// above also doesn't use it because it fetches the whole remote object and read the
|
||||
// more reliable 'updated_time' property. Basically remote.updated_time is deprecated.
|
||||
// 2025-08-27: remote.updated_time can now be utilised by the basic delta when using a sync target
|
||||
// where the 'server' is actually the same device that is running the client eg. file system sync.
|
||||
// This is required to correctly detect updated objects where an external sync service is being
|
||||
// used in combination with Joplin, as there are essentially multiple sources of truth, rather
|
||||
// than just one. So we can't rely on the server always containing the latest remote changes
|
||||
// during synchronization, as new changes can be later added which have a timestamp in the past.
|
||||
// In this scenario, we don't know the exact timestamp to specify for remoteItemUpdatedTime upon
|
||||
// uploading. So we can leave it unspecified and then on the next run of the delta step, it will
|
||||
// get set there
|
||||
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
|
||||
}
|
||||
@@ -880,6 +889,11 @@ export default class Synchronizer {
|
||||
return BaseItem.syncedItemIds(syncTargetId);
|
||||
},
|
||||
|
||||
// This is only used by the basic delta
|
||||
allItemMetadataHandler: async () => {
|
||||
return BaseItem.remoteItemMetadata(syncTargetId);
|
||||
},
|
||||
|
||||
wipeOutFailSafe: Setting.value('sync.wipeOutFailSafe'),
|
||||
|
||||
logger: logger,
|
||||
@@ -973,6 +987,11 @@ export default class Synchronizer {
|
||||
if (content && content.updated_time > local.updated_time) {
|
||||
action = SyncAction.UpdateLocal;
|
||||
reason = 'remote is more recent than local';
|
||||
} else if (enableEnhancedBasicDeltaAlgorithm()) {
|
||||
// When the enhanced basic delta algorithm is first used, all items are rescanned and we need to persist the remoteItemUpdatedTime
|
||||
// to set up the initial synced state. This also catches the case if content.updated_time < local.updated_time due to manual manipulation
|
||||
// of the md files, to prevent these items being continually fetched on every sync
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time, remote.updated_time);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1012,7 +1031,7 @@ export default class Synchronizer {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const options: any = {
|
||||
autoTimestamp: false,
|
||||
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
|
||||
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs(), remote.updated_time),
|
||||
changeSource: ItemChange.SOURCE_SYNC,
|
||||
};
|
||||
if (action === SyncAction.CreateLocal) options.isNew = true;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PaginatedList, RemoteItem, getSupportsDeltaWithItems, isLocalServer } from './file-api';
|
||||
import { PaginatedList, RemoteItem, getSupportsDeltaWithItems, enableEnhancedBasicDeltaAlgorithm, basicDelta, ItemStat, isLocalServer } from './file-api';
|
||||
import { RemoteItemMetadata } from './models/BaseItem';
|
||||
import Setting from './models/Setting';
|
||||
import SyncTargetRegistry from './SyncTargetRegistry';
|
||||
|
||||
const defaultPaginatedList = (): PaginatedList => {
|
||||
return {
|
||||
@@ -14,6 +17,86 @@ const defaultItem = (): RemoteItem => {
|
||||
};
|
||||
};
|
||||
|
||||
const validNoteId = '1b175bb38bba47baac22b0b47f778113';
|
||||
const basePath = '/';
|
||||
const baseTimestamp = new Date().getTime();
|
||||
|
||||
const setupWebDavSync = (isLocal: boolean) => {
|
||||
let url = 'http://www.example.com';
|
||||
if (isLocal) url = 'http://localhost';
|
||||
Setting.setValue('sync.target', SyncTargetRegistry.nameToId('webdav'));
|
||||
Setting.setValue('sync.6.path', url);
|
||||
};
|
||||
|
||||
const remotePath = (noteId: string) => {
|
||||
return `${noteId}.md`;
|
||||
};
|
||||
|
||||
const statItem = (noteId: string, remoteUpdatedTime: number) => {
|
||||
const stat: ItemStat = {
|
||||
path: remotePath(noteId),
|
||||
updated_time: remoteUpdatedTime,
|
||||
isDir: false,
|
||||
};
|
||||
|
||||
return stat;
|
||||
};
|
||||
|
||||
const dirStatFunc = (statItem: ItemStat) => {
|
||||
return (): ItemStat[] => {
|
||||
if (statItem) {
|
||||
return [statItem];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const syncOptions = (noteId: string, localUpdatedTime: number, contextTimestamp: number = undefined, includeFilesAtTimestamp = true) => {
|
||||
const syncContextTimestamp = contextTimestamp ? contextTimestamp : localUpdatedTime;
|
||||
const metadataMap = new Map<string, RemoteItemMetadata>();
|
||||
let itemIds: string[] = [];
|
||||
let filesAtTimestamp: string[] = [];
|
||||
|
||||
const metadata = {
|
||||
item_id: noteId,
|
||||
updated_time: localUpdatedTime,
|
||||
};
|
||||
|
||||
if (noteId) {
|
||||
metadataMap.set(noteId, metadata);
|
||||
itemIds = [noteId];
|
||||
}
|
||||
|
||||
if (includeFilesAtTimestamp) {
|
||||
filesAtTimestamp = [remotePath(noteId)];
|
||||
}
|
||||
|
||||
const allItemIdsHandler = async () => {
|
||||
return itemIds;
|
||||
};
|
||||
|
||||
const allItemMetadataHandler = async () => {
|
||||
return metadataMap;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
const syncContext: any = {
|
||||
timestamp: syncContextTimestamp,
|
||||
filesAtTimestamp: filesAtTimestamp,
|
||||
statsCache: null,
|
||||
statIdsCache: null,
|
||||
deletedItemsProcessed: false,
|
||||
};
|
||||
|
||||
return {
|
||||
allItemIdsHandler: allItemIdsHandler,
|
||||
allItemMetadataHandler: allItemMetadataHandler,
|
||||
wipeOutFailSafe: false,
|
||||
context: syncContext,
|
||||
};
|
||||
};
|
||||
|
||||
describe('file-api', () => {
|
||||
|
||||
test.each([
|
||||
@@ -94,4 +177,139 @@ describe('file-api', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should use enhanced basic delta algorithm when using file system sync', () => {
|
||||
Setting.setValue('sync.target', SyncTargetRegistry.nameToId('filesystem'));
|
||||
const result = enableEnhancedBasicDeltaAlgorithm();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'http://localhost',
|
||||
'http://localhost/',
|
||||
'https://localhost:8080',
|
||||
'http://127.0.0.1',
|
||||
'https://127.100.50.25:3000/test',
|
||||
'http://[::1]',
|
||||
'http://localhost/api/v1',
|
||||
])('should use enhanced basic delta algorithm when using WebDAV for a local server url', (url: string) => {
|
||||
Setting.setValue('sync.target', SyncTargetRegistry.nameToId('webdav'));
|
||||
Setting.setValue('sync.6.path', url);
|
||||
const result = enableEnhancedBasicDeltaAlgorithm();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'http://localhostXYZ',
|
||||
'http://127.0.0.1foobar',
|
||||
'http://192.168.1.1',
|
||||
'http://example.com',
|
||||
'https://my-localhost.com',
|
||||
])('should not use enhanced basic delta algorithm when using WebDAV for a non local server url', (url: string) => {
|
||||
Setting.setValue('sync.target', SyncTargetRegistry.nameToId('webdav'));
|
||||
Setting.setValue('sync.6.path', url);
|
||||
const result = enableEnhancedBasicDeltaAlgorithm();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not use enhanced basic delta algorithm when not using file system sync or WebDAV', () => {
|
||||
Setting.setValue('sync.target', SyncTargetRegistry.nameToId('joplinServer'));
|
||||
const result = enableEnhancedBasicDeltaAlgorithm();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should not return item, where remote item is a directory', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const stat = {
|
||||
path: remotePath(validNoteId),
|
||||
updated_time: baseTimestamp + 1,
|
||||
isDir: true,
|
||||
};
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(undefined, baseTimestamp));
|
||||
expect(context.items.length).toBe(0);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should not return item, where remote item is not a system path', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const noteId = '1b175bb38bba47baac22b0b47f77811'; // 1 char too short
|
||||
const stat = statItem(noteId, baseTimestamp + 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(undefined, baseTimestamp));
|
||||
expect(context.items.length).toBe(0);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should return item with isDeleted true, where remote item not longer exists', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const context = await basicDelta(basePath, dirStatFunc(undefined), syncOptions(validNoteId, baseTimestamp));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0].isDeleted).toBe(true);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should return item, where local item does not exist', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const stat = statItem(validNoteId, baseTimestamp);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(undefined, baseTimestamp));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should return item, where local item exists and remote item has a newer timestamp', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const stat = statItem(validNoteId, baseTimestamp + 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
test.each([false, true])('basicDelta (enhancedAlgorithm: %s) should not return item, where local item exists and remote item has an equal timestamp', async (enhancedAlgorithm) => {
|
||||
setupWebDavSync(enhancedAlgorithm);
|
||||
const stat = statItem(validNoteId, baseTimestamp);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp));
|
||||
expect(context.items.length).toBe(0);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: false) should return item, where local item exists and remote item has an equal timestamp, but it is not present in fileAtTimestamp', async () => {
|
||||
setupWebDavSync(false);
|
||||
const stat = statItem(validNoteId, baseTimestamp);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp, baseTimestamp, false));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: false) should not return item, where local item exists and remote item has an older timestamp', async () => {
|
||||
setupWebDavSync(false);
|
||||
const stat = statItem(validNoteId, baseTimestamp - 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp));
|
||||
expect(context.items.length).toBe(0);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: false) should use context timestamp for timestamp comparisons, ignoring items with earlier timestamps', async () => {
|
||||
setupWebDavSync(false);
|
||||
const stat = statItem(validNoteId, baseTimestamp + 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp, baseTimestamp + 2));
|
||||
expect(context.items.length).toBe(0);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: true) should return item, where local item exists and remote item has an older timestamp', async () => {
|
||||
setupWebDavSync(true);
|
||||
const stat = statItem(validNoteId, baseTimestamp - 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: true) should ignore context timestamp for timestamp comparisons, and return item based on metadata timestamp', async () => {
|
||||
setupWebDavSync(true);
|
||||
const stat = statItem(validNoteId, baseTimestamp + 1);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, baseTimestamp, baseTimestamp + 2));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
test('basicDelta (enhancedAlgorithm: true) should always return item if there is no metadata timestamp set', async () => {
|
||||
setupWebDavSync(true);
|
||||
const stat = statItem(validNoteId, baseTimestamp);
|
||||
const context = await basicDelta(basePath, dirStatFunc(stat), syncOptions(validNoteId, undefined, baseTimestamp + 1));
|
||||
expect(context.items.length).toBe(1);
|
||||
expect(context.items[0]).toBe(stat);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Logger, { LoggerWrapper } from '@joplin/utils/Logger';
|
||||
import shim from './shim';
|
||||
import BaseItem from './models/BaseItem';
|
||||
import BaseItem, { RemoteItemMetadata } from './models/BaseItem';
|
||||
import time from './time';
|
||||
|
||||
const { isHidden } = require('./path-utils');
|
||||
import JoplinError from './JoplinError';
|
||||
import { Lock, LockClientType, LockType } from './services/synchronizer/LockHandler';
|
||||
import * as ArrayUtils from './ArrayUtils';
|
||||
import Setting from './models/Setting';
|
||||
import SyncTargetRegistry from './SyncTargetRegistry';
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
@@ -58,6 +60,21 @@ export const isLocalServer = (url: string) => {
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
// The enhanced basic delta algorithm detects incoming changes based on both timestamp increases and decreases, which resolves issues where an external
|
||||
// service is syncing to the sync target directory at the same time as Joplin. Change detection is still limited by the precision of the modified timestamp
|
||||
// of the filesystem in use, but at worst this would mean that if 2 Joplin clients synced a conflicting change to the same note within 2 seconds, the incoming
|
||||
// change may get ignored (but this is a limitation of the normal basic algorithm as well). However, with the enhanced algorithm, the timing of syncs made by
|
||||
// an external sync service are irrelevant, providing the service is set to sync the modified time of files it syncs
|
||||
export const enableEnhancedBasicDeltaAlgorithm = () => {
|
||||
if (Setting.value('sync.target') === SyncTargetRegistry.nameToId('filesystem')) {
|
||||
return true;
|
||||
} else if (Setting.value('sync.target') === SyncTargetRegistry.nameToId('webdav')) {
|
||||
return isLocalServer(Setting.value('sync.6.path'));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||
function requestCanBeRepeated(error: any) {
|
||||
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
||||
@@ -107,6 +124,7 @@ async function tryAndRepeat(fn: Function, count: number) {
|
||||
|
||||
export interface DeltaOptions {
|
||||
allItemIdsHandler(): Promise<string[]>;
|
||||
allItemMetadataHandler(): Promise<Map<string, RemoteItemMetadata>>;
|
||||
logger?: LoggerWrapper;
|
||||
wipeOutFailSafe: boolean;
|
||||
}
|
||||
@@ -458,7 +476,8 @@ function basicDeltaContextFromOptions_(options: any) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied
|
||||
async function basicDelta(path: string, getDirStatFn: Function, options: DeltaOptions) {
|
||||
const outputLimit = 50;
|
||||
const itemIds = await options.allItemIdsHandler();
|
||||
const itemIds: string[] = await options.allItemIdsHandler();
|
||||
|
||||
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
||||
|
||||
const logger = options && options.logger ? options.logger : new Logger();
|
||||
@@ -499,6 +518,12 @@ async function basicDelta(path: string, getDirStatFn: Function, options: DeltaOp
|
||||
equal: 0,
|
||||
};
|
||||
|
||||
let remoteItemMetadata: Map<string, RemoteItemMetadata>;
|
||||
|
||||
if (enableEnhancedBasicDeltaAlgorithm()) {
|
||||
remoteItemMetadata = await options.allItemMetadataHandler();
|
||||
}
|
||||
|
||||
// Find out which files have been changed since the last time. Note that we keep
|
||||
// both the timestamp of the most recent change, *and* the items that exactly match
|
||||
// this timestamp. This to handle cases where an item is modified while this delta
|
||||
@@ -512,33 +537,72 @@ async function basicDelta(path: string, getDirStatFn: Function, options: DeltaOp
|
||||
const stat = newContext.statsCache[i];
|
||||
|
||||
if (stat.isDir) continue;
|
||||
if (!BaseItem.isSystemPath(stat.path)) continue;
|
||||
|
||||
if (stat.updated_time < context.timestamp) {
|
||||
updateReport.older++;
|
||||
continue;
|
||||
}
|
||||
let lastRemoteItemUpdatedTime = 0;
|
||||
const itemId = BaseItem.pathToId(stat.path);
|
||||
|
||||
// Special case for items that exactly match the timestamp
|
||||
if (stat.updated_time === context.timestamp) {
|
||||
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) {
|
||||
updateReport.equal++;
|
||||
if (enableEnhancedBasicDeltaAlgorithm()) {
|
||||
const metadata = remoteItemMetadata.get(itemId);
|
||||
|
||||
if (metadata) {
|
||||
// Check if update is needed
|
||||
lastRemoteItemUpdatedTime = metadata.updated_time;
|
||||
|
||||
if (stat.updated_time === lastRemoteItemUpdatedTime) {
|
||||
// Item has already been synced and is up to date
|
||||
updateReport.equal++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stat.updated_time < lastRemoteItemUpdatedTime) {
|
||||
updateReport.older++;
|
||||
}
|
||||
|
||||
if (stat.updated_time > lastRemoteItemUpdatedTime) {
|
||||
updateReport.newer++;
|
||||
}
|
||||
} else {
|
||||
// Item needs to be created locally
|
||||
updateReport.newer++;
|
||||
}
|
||||
|
||||
output.push(stat);
|
||||
} else {
|
||||
if (stat.updated_time < context.timestamp) {
|
||||
updateReport.older++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (stat.updated_time > newContext.timestamp) {
|
||||
newContext.timestamp = stat.updated_time;
|
||||
newContext.filesAtTimestamp = [];
|
||||
updateReport.newer++;
|
||||
}
|
||||
// Special case for items that exactly match the timestamp
|
||||
if (stat.updated_time === context.timestamp) {
|
||||
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) {
|
||||
updateReport.equal++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newContext.filesAtTimestamp.push(stat.path);
|
||||
output.push(stat);
|
||||
if (stat.updated_time > newContext.timestamp) {
|
||||
newContext.timestamp = stat.updated_time;
|
||||
newContext.filesAtTimestamp = [];
|
||||
updateReport.newer++;
|
||||
}
|
||||
|
||||
newContext.filesAtTimestamp.push(stat.path);
|
||||
output.push(stat);
|
||||
}
|
||||
|
||||
if (output.length >= outputLimit) break;
|
||||
}
|
||||
|
||||
logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`);
|
||||
if (enableEnhancedBasicDeltaAlgorithm()) {
|
||||
// context.timestamp and filesAtTimestamp are not required when syncing based on any timestamp changes, but should be updated for backwards compatibility
|
||||
newContext.timestamp = time.unixMs();
|
||||
newContext.filesAtTimestamp = [];
|
||||
logger.info(`BasicDelta (enhanced): Report: ${JSON.stringify(updateReport)}`);
|
||||
} else {
|
||||
logger.info(`BasicDelta: Report: ${JSON.stringify(updateReport)}`);
|
||||
}
|
||||
|
||||
if (!newContext.deletedItemsProcessed) {
|
||||
// Find out which items have been deleted on the sync target by comparing the items
|
||||
|
||||
@@ -21,11 +21,16 @@ export interface RemoveOptions {
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface ZipExtractOptions {
|
||||
export interface ArchiveExtractOptions {
|
||||
source: string;
|
||||
extractTo: string;
|
||||
}
|
||||
|
||||
export interface CabExtractOptions extends ArchiveExtractOptions {
|
||||
// Only files matching the pattern will be extracted
|
||||
fileNamePattern: string;
|
||||
}
|
||||
|
||||
export interface ZipEntry {
|
||||
entryName: string;
|
||||
name: string;
|
||||
@@ -268,8 +273,11 @@ export default class FsDriverBase {
|
||||
throw new Error('Not implemented: tarCreate');
|
||||
}
|
||||
|
||||
public async zipExtract(_options: ZipExtractOptions): Promise<ZipEntry[]> {
|
||||
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,6 +1,8 @@
|
||||
import AdmZip = require('adm-zip');
|
||||
import FsDriverBase, { Stat, ZipEntry, ZipExtractOptions } from './fs-driver-base';
|
||||
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions, CabExtractOptions } 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');
|
||||
|
||||
@@ -211,9 +213,30 @@ export default class FsDriverNode extends FsDriverBase {
|
||||
await require('tar').create(options, filePaths);
|
||||
}
|
||||
|
||||
public async zipExtract(options: ZipExtractOptions): Promise<ZipEntry[]> {
|
||||
public async zipExtract(options: ArchiveExtractOptions): Promise<ZipEntry[]> {
|
||||
const zip = new AdmZip(options.source);
|
||||
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,12 +1,14 @@
|
||||
const stringToStream = require('string-to-stream');
|
||||
// const cleanHtml = require('clean-html');
|
||||
const resourceUtils = require('./resourceUtils.js');
|
||||
const { cssValue } = require('./import-enex-md-gen');
|
||||
const htmlUtils = require('./htmlUtils').default;
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const { fixAttributes } = require('@joplin/utils/html');
|
||||
const htmlentities = new Entities().encode;
|
||||
|
||||
function addResourceTag(lines, resource, attributes) {
|
||||
attributes = fixAttributes(attributes);
|
||||
|
||||
// Note: refactor to use Resource.markdownTag
|
||||
if (!attributes.alt) attributes.alt = resource.title;
|
||||
if (!attributes.alt) attributes.alt = resource.filename;
|
||||
@@ -137,7 +139,7 @@ function enexXmlToHtml_(stream, resources) {
|
||||
|
||||
saxStream.on('closetag', (node) => {
|
||||
const tagName = node ? node.toLowerCase() : node;
|
||||
if (!htmlUtils.isSelfClosingTag(tagName)) section.lines.push(`</${tagName}>`);
|
||||
if (!htmlUtils.isSelfClosingTag(tagName) && tagName !== 'en-media' && tagName !== 'en-todo') section.lines.push(`</${tagName}>`);
|
||||
});
|
||||
|
||||
saxStream.on('attribute', () => {});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const { setupDatabaseAndSynchronizer, switchClient, supportDir } = require('./testing/test-utils.js');
|
||||
const shim = require('./shim').default;
|
||||
const { enexXmlToHtml } = require('./import-enex-html-gen.js');
|
||||
const cleanHtml = require('clean-html');
|
||||
|
||||
const fileWithPath = (filename) =>
|
||||
`${supportDir}/../enex_to_html/${filename}`;
|
||||
@@ -14,20 +13,6 @@ const audioResource = {
|
||||
title: 'audio test',
|
||||
};
|
||||
|
||||
// All the test HTML files are beautified ones, so we need to run
|
||||
// this before the comparison. Before, beautifying was done by `enexXmlToHtml`
|
||||
// but that was removed due to problems with the clean-html package.
|
||||
const beautifyHtml = (html) => {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
cleanHtml.clean(html, { wrap: 0 }, (...cleanedHtml) => resolve(cleanedHtml.join('')));
|
||||
} catch (error) {
|
||||
console.warn(`Could not clean HTML - the "unclean" version will be used: ${error.message}: ${html.trim().substr(0, 512).replace(/[\n\r]/g, ' ')}...`);
|
||||
resolve([html].join(''));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Tests the importer for a single note, checking that the result of
|
||||
// processing the given `.enex` input file matches the contents of the given
|
||||
// `.html` file.
|
||||
@@ -51,7 +36,7 @@ const compareOutputToExpected = (options) => {
|
||||
it(testTitle, (async () => {
|
||||
const enexInput = await shim.fsDriver().readFile(inputFile);
|
||||
const expectedOutput = await shim.fsDriver().readFile(outputFile);
|
||||
const actualOutput = await beautifyHtml(await enexXmlToHtml(enexInput, options.resources));
|
||||
const actualOutput = (await enexXmlToHtml(enexInput, options.resources)).trim();
|
||||
expect(actualOutput).toEqual(expectedOutput);
|
||||
}));
|
||||
};
|
||||
@@ -100,6 +85,16 @@ describe('EnexToHtml', () => {
|
||||
}],
|
||||
});
|
||||
|
||||
compareOutputToExpected({
|
||||
testName: 'attachment-image',
|
||||
resources: [{
|
||||
filename: 'attachment-image',
|
||||
id: 'e2d4887c5a32ab1686276c7c5ae733ef',
|
||||
mime: 'image/jpeg', // Any non-image/non-audio mime type will do
|
||||
width: '1.125in',
|
||||
}],
|
||||
});
|
||||
|
||||
compareOutputToExpected({
|
||||
testName: 'quoted-attributes',
|
||||
});
|
||||
|
||||
@@ -202,6 +202,18 @@ describe('import-enex-md-gen', () => {
|
||||
expect(Resource.fullPath(resource).endsWith('.mscz')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle resources without encoding', async () => {
|
||||
// Handle case where the resource has a certain extension, eg. "mscz"
|
||||
// and a mime type that doesn't really match (application/zip). In that
|
||||
// case we want to make sure that the file is not converted to a .zip
|
||||
// file. Fixes https://discourse.joplinapp.org/t/import-issue-evernote-enex-containing-musescore-file-mscz/31394/1
|
||||
await importEnexFile('resource_with_missing_encoding.enex');
|
||||
const resource = (await Resource.all())[0];
|
||||
const resourcePath = Resource.fullPath(resource);
|
||||
const content = await readFile(resourcePath, 'utf-8');
|
||||
expect(content).toBe('{ "test": 123 }');
|
||||
});
|
||||
|
||||
it('should sanitize resource filenames with slashes', async () => {
|
||||
await importEnexFile('resource_filename_with_slashes.enex');
|
||||
const resource: ResourceEntity = (await Resource.all())[0];
|
||||
|
||||
@@ -150,12 +150,16 @@ async function processNoteResource(resource: ExtractedResource) {
|
||||
// Some resources have no data, go figure, so we need a special case for this.
|
||||
await handleNoDataResource(resource, true);
|
||||
} else {
|
||||
if (resource.dataEncoding === 'base64') {
|
||||
// If encoding is not specified, it defaults to base64.
|
||||
// Source: enex.dtd: <!ATTLIST data encoding (base64) "base64">
|
||||
const dataEncoding = resource.dataEncoding ? resource.dataEncoding : 'base64';
|
||||
|
||||
if (dataEncoding === 'base64') {
|
||||
const decodedFilePath = `${resource.dataFilePath}.decoded`;
|
||||
await decodeBase64File(resource.dataFilePath, decodedFilePath);
|
||||
resource.dataFilePath = decodedFilePath;
|
||||
} else if (resource.dataEncoding) {
|
||||
throw new Error(`Cannot decode resource with encoding: ${resource.dataEncoding}`);
|
||||
} else if (dataEncoding) {
|
||||
throw new Error(`Cannot decode resource with encoding: ${dataEncoding}`);
|
||||
}
|
||||
|
||||
const stats = await shim.fsDriver().stat(resource.dataFilePath);
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.": [
|
||||
"يمكن أن يكون <todo-command> (أمر قائمة المهام) \"toggle\" (تبديل) أو \"clear\" (مسح) فقط. استخدم \"toggle\" لتبديل حال قائمة المهام المعطاة بين مكتملة و غير مكتملة (إذا كانت الملاحظة المقصودة عادية فسيتم تحويلها إلى قائمة مهام). استخدم \"clear\" لتحويل قائمة المهام ثانيةً إلى ملاحظة عادية."
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
""
|
||||
],
|
||||
@@ -251,6 +254,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
""
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
""
|
||||
],
|
||||
@@ -955,6 +961,9 @@
|
||||
"Do not ask for confirmation.": [
|
||||
"لا تطلب التأكيد."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
""
|
||||
],
|
||||
@@ -1303,6 +1312,9 @@
|
||||
"Feature flags": [
|
||||
"علامات الميزة"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"العناصر المجلوبة: %d/%d."
|
||||
],
|
||||
@@ -1521,9 +1533,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"في أي أمر ، يمكن الإشارة إلى ملاحظة أو دفتر ملاحظات بعنوانه أو بمعرّفه ، أو باستخدام الاختصارات `$n` أو `$b` التي تشير ، على الترتيب ، إلى الملاحظة أو دفتر الملاحظات المختار حالياً. يمكن استخدام `$c` للإشارة إلى العنصر المختار حالياً."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"لربط الموقع الجغرافي بالملاحظة ، يحتاج التطبيق إلى إذنك للوصول إلى موقعك.\n\nيمكنك إيقاف تشغيل هذا الخيار في أي وقت في شاشة الإعدادات."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"للقيام بذلك، يجب تشفير مجموعة بياناتك بالكامل ومزامنتها، لذا من الأفضل تشغيلها خلال الليل.\n\nللبدء، يرجى اتباع هذه التعليمات:\n\n1. قم بمزامنة جميع أجهزتك.\n2. انقر فوق \"%s\" \n3. دعها تعمل حتى الاكتمال. أثناء تشغيله، تجنب تغيير أي ملاحظة على أجهزتك الأخرى، لتجنب التعارضات.\n4. بمجرد إتمام المزامنة على هذا الجهاز، قم بمزامنة جميع أجهزتك الأخرى واتركها تعمل حتى الاكتمال.\n\nهام: ما عليك سوى تشغيل هذا مرة واحدة على جهاز واحد."
|
||||
],
|
||||
@@ -1656,6 +1665,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
""
|
||||
],
|
||||
"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 Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"تتيح Joplin Web clipper حفظ صفحات الويب ولقطات الشاشة من مستعرضك إلى Joplin."
|
||||
],
|
||||
@@ -1683,6 +1695,9 @@
|
||||
"Keys that need upgrading": [
|
||||
"المفاتيح التي تحتاج إلى ترقية"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"عرضي"
|
||||
],
|
||||
@@ -1985,6 +2000,9 @@
|
||||
"Not now": [
|
||||
"ليس الآن"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"ملاحظة"
|
||||
],
|
||||
@@ -2132,6 +2150,9 @@
|
||||
"Options": [
|
||||
"خيارات"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Other applications...": [
|
||||
"تطبيقات أخزى..."
|
||||
],
|
||||
@@ -2177,9 +2198,6 @@
|
||||
"Per user. Minimum of 2 users.": [
|
||||
""
|
||||
],
|
||||
"Permission needed": [
|
||||
"مطلوب الإذن"
|
||||
],
|
||||
"Photo %d": [
|
||||
""
|
||||
],
|
||||
@@ -2267,9 +2285,6 @@
|
||||
"Preferred light theme": [
|
||||
"التنسيق الغامق المفضّل"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
""
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
""
|
||||
],
|
||||
@@ -2348,6 +2363,9 @@
|
||||
"Publish/unpublish": [
|
||||
""
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"Quit": [
|
||||
"إغلاق"
|
||||
],
|
||||
@@ -2549,6 +2567,9 @@
|
||||
"Select all": [
|
||||
"اختيار الكل"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2792,6 +2813,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"التبديل إلى [notebook] - جميع العمليات التالية ستجري في هذا الدفتر."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
""
|
||||
],
|
||||
@@ -2879,6 +2903,9 @@
|
||||
"Text editor command": [
|
||||
"أمر محرر النصوص"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
""
|
||||
],
|
||||
@@ -3176,6 +3203,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unchecked": [
|
||||
""
|
||||
],
|
||||
@@ -3284,6 +3314,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
"الخط المستخدم عند الحاجة إلى خط ثابت العرض لوضع النص بصورة مقروءة (في الجداول وخانات الاختيار والشفرة البرمجية مثلاً). عندما لا يُعثر عليه سيتم استخدام خط عامّ أحادي المسافة (ثابت العرض)."
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
""
|
||||
],
|
||||
@@ -3326,9 +3359,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
""
|
||||
],
|
||||
"Vosk": [
|
||||
""
|
||||
],
|
||||
"Warning": [
|
||||
"تحذير"
|
||||
],
|
||||
@@ -3371,10 +3401,10 @@
|
||||
"When creating a new to-do:": [
|
||||
"عند إنشاء قائمة جديدة للمهام:"
|
||||
],
|
||||
"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.": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Whisper": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
|
||||
@@ -113,6 +113,9 @@
|
||||
"[None]": [
|
||||
""
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
""
|
||||
],
|
||||
@@ -227,6 +230,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
""
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
""
|
||||
],
|
||||
@@ -773,6 +779,9 @@
|
||||
"Do not ask for confirmation.": [
|
||||
"Не питай за потвърждение."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
""
|
||||
],
|
||||
@@ -1034,6 +1043,9 @@
|
||||
"Feature flags": [
|
||||
""
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Взети обекти: %d/%d."
|
||||
],
|
||||
@@ -1232,9 +1244,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"Във всяка команда може да реферирате бележка или тетрадка по заглавие или по идентификатор, или използвайки някоя от кратките команди `$n` и `$b`, представляващи съответно настоящата бележка и настоящата тетрадка. `$c` може да се ползва за рефериране на избрания обект."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
""
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
""
|
||||
],
|
||||
@@ -1340,6 +1349,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
""
|
||||
],
|
||||
"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 Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"Уеб-клиперът на Joplin позволява за да запазвате уеб-страници и снимки на екрана от браузъра директно в Joplin."
|
||||
],
|
||||
@@ -1367,6 +1379,9 @@
|
||||
"Keys that need upgrading": [
|
||||
""
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
""
|
||||
],
|
||||
@@ -1622,6 +1637,9 @@
|
||||
"Not now": [
|
||||
""
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"бележка"
|
||||
],
|
||||
@@ -1733,6 +1751,9 @@
|
||||
"Options": [
|
||||
"Конфигурация"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Page orientation for PDF export": [
|
||||
""
|
||||
],
|
||||
@@ -1850,9 +1871,6 @@
|
||||
"Preferred light theme": [
|
||||
""
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
""
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
""
|
||||
],
|
||||
@@ -1922,6 +1940,12 @@
|
||||
"Publish/unpublish": [
|
||||
""
|
||||
],
|
||||
"Published at URL: %s": [
|
||||
""
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"Quit": [
|
||||
"Изход"
|
||||
],
|
||||
@@ -2132,6 +2156,9 @@
|
||||
"Select all": [
|
||||
"Избери всичко"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2357,6 +2384,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Превключва на [тетрадка] - всички нататъчни команди ще се извършват в тази тетрадка."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
""
|
||||
],
|
||||
@@ -2429,6 +2459,9 @@
|
||||
"Text editor command": [
|
||||
"Команда за стартиране на текстов редактор"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
""
|
||||
],
|
||||
@@ -2718,6 +2751,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unchecked": [
|
||||
""
|
||||
],
|
||||
@@ -2805,6 +2841,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
""
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
""
|
||||
],
|
||||
@@ -2838,9 +2877,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
""
|
||||
],
|
||||
"Vosk": [
|
||||
""
|
||||
],
|
||||
"Warning": [
|
||||
""
|
||||
],
|
||||
@@ -2880,10 +2916,10 @@
|
||||
"When creating a new to-do:": [
|
||||
"Когато се създава нова задача:"
|
||||
],
|
||||
"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.": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Whisper": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
|
||||
@@ -110,6 +110,9 @@
|
||||
"[None]": [
|
||||
""
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
""
|
||||
],
|
||||
@@ -227,6 +230,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
""
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
""
|
||||
],
|
||||
@@ -758,6 +764,9 @@
|
||||
"Do not ask for confirmation.": [
|
||||
"Ne pitaj za potvrdu."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
""
|
||||
],
|
||||
@@ -1031,6 +1040,9 @@
|
||||
"Feature flags": [
|
||||
""
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Preuzeto stavki: %d/%d."
|
||||
],
|
||||
@@ -1229,9 +1241,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"U svakoj komandi, bilješka ili bilježnica mogu se precizirati pomoću naziva ili identifikacijskog broja, odnosno pomoću kratica za naziv `$n` i broj `$b` trenutno označene bilješke ili bilježnice. `$c` se može koristiti za trenutno označenu stavku."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"Da bi dodali geo-lokaciju vasoj biljesci, aplikaciji je potrebna dozvola pristupu vasoj lokacij.\n\nOvu opciju je moguce promijeniti bilo kada u podesavanjima."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"Da biste to učinili, svi Vaši podaci moraju biti šifrirani i sinhronizovani, pa je najbolje da to učinite preko noći.\n\nDa započnete, slijedite sljedeće upute:\n\n1. Sinhronizujte sve svoje uređaje.\n2. Pritisnite \"%s\".\n3. Sačekajte dok se proces ne okonča. Tokom rada, nemojte uređivati bilo koju bilješku na nekom od Vaših uređaja kako biste izbjegli konflikte.\n4. Kad je sinhronizacija završena na ovom uređaju, sinhronizujte i ostale uređaje.\n\nVažno: ovaj proces trebate pokrenuti samo JEDNOM i na samo jednom uređaju."
|
||||
],
|
||||
@@ -1337,6 +1346,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
""
|
||||
],
|
||||
"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 Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"Joplin Web clipper omogućava pohranjivanje internet stranica i slika ekrana iz Vašeg internet preglednika u Joplin."
|
||||
],
|
||||
@@ -1355,6 +1367,9 @@
|
||||
"Keychain Supported: %s": [
|
||||
"Podržani privjesak: %s"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"Vodoravno"
|
||||
],
|
||||
@@ -1604,6 +1619,9 @@
|
||||
"Not generated": [
|
||||
""
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"bilješka"
|
||||
],
|
||||
@@ -1706,6 +1724,9 @@
|
||||
"Options": [
|
||||
"Opcije"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Page orientation for PDF export": [
|
||||
"Položaj stranice za pohranjivanje u PDF"
|
||||
],
|
||||
@@ -1820,9 +1841,6 @@
|
||||
"Preferred light theme": [
|
||||
"Preferirana svijetla tema"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
""
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
""
|
||||
],
|
||||
@@ -1895,6 +1913,12 @@
|
||||
"Publish/unpublish": [
|
||||
""
|
||||
],
|
||||
"Published at URL: %s": [
|
||||
""
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"Quit": [
|
||||
"Zatvori"
|
||||
],
|
||||
@@ -2096,6 +2120,9 @@
|
||||
"Select all": [
|
||||
"Označi sve"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2324,6 +2351,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Prelazi u [notebook] – sve dalje radnje vršit će se u ovoj bilježnici."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
""
|
||||
],
|
||||
@@ -2399,6 +2429,9 @@
|
||||
"Text editor command": [
|
||||
"Komanda za uređivača teksta"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
""
|
||||
],
|
||||
@@ -2680,6 +2713,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unchecked": [
|
||||
""
|
||||
],
|
||||
@@ -2770,6 +2806,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
""
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
""
|
||||
],
|
||||
@@ -2803,9 +2842,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
""
|
||||
],
|
||||
"Vosk": [
|
||||
""
|
||||
],
|
||||
"Warning": [
|
||||
"Upozorenje"
|
||||
],
|
||||
@@ -2851,10 +2887,10 @@
|
||||
"When creating a new to-do:": [
|
||||
"Prilikom kreiranja novog zadatka:"
|
||||
],
|
||||
"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.": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Whisper": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
"[None]": [
|
||||
"[None]"
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
"Una llista de paraules separades per comes. Es pot utilitzar per a paraules poc comunes, per ajudar a escriure la veu a escriure-les correctament."
|
||||
],
|
||||
@@ -248,6 +251,9 @@
|
||||
"Add recipient:": [
|
||||
"Afegeix un destinatari:"
|
||||
],
|
||||
"Add tags:": [
|
||||
"Afegir etiquetes:"
|
||||
],
|
||||
"Add title": [
|
||||
"Afegeix títol"
|
||||
],
|
||||
@@ -257,8 +263,14 @@
|
||||
"Add to note": [
|
||||
"Afegeix a la nota"
|
||||
],
|
||||
"Added new: %s": [
|
||||
"S'ha afegit una nova: %s"
|
||||
],
|
||||
"Added tag: %s": [
|
||||
"Etiqueta afegida: %s"
|
||||
],
|
||||
"Adds tag": [
|
||||
""
|
||||
"Afegeix una etiqueta"
|
||||
],
|
||||
"Admin": [
|
||||
"Administració"
|
||||
@@ -326,6 +338,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
"S'ha trobat un dibuix desat automàticament. Voleu adjuntar-ne una còpia a la nota?"
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
"S'ha produït un error: %s"
|
||||
],
|
||||
@@ -366,7 +381,7 @@
|
||||
"Aritim fosc"
|
||||
],
|
||||
"Associated tags:": [
|
||||
""
|
||||
"Etiquetes associades:"
|
||||
],
|
||||
"At present, Joplin Web can only be open in one tab at a time. Please close the other instance of Joplin.": [
|
||||
"Actualment, Joplin Web només es pot obrir en una pestanya alhora. Si us plau, tanqueu l'altra instància de Joplin."
|
||||
@@ -653,6 +668,9 @@
|
||||
"Code View": [
|
||||
"Vista de codi"
|
||||
],
|
||||
"Code:": [
|
||||
"Codi:"
|
||||
],
|
||||
"Collaborate on a notebook with others": [
|
||||
"Col·laborar en una llibreta amb altres"
|
||||
],
|
||||
@@ -762,6 +780,12 @@
|
||||
"Control character": [
|
||||
"Caràcter de control"
|
||||
],
|
||||
"Convert it": [
|
||||
"Converteix-lo"
|
||||
],
|
||||
"Convert note to Markdown": [
|
||||
"Converteix la nota a Markdown"
|
||||
],
|
||||
"Convert to note": [
|
||||
"Converteix a nota"
|
||||
],
|
||||
@@ -817,6 +841,9 @@
|
||||
"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"
|
||||
],
|
||||
@@ -847,9 +874,15 @@
|
||||
"Create a notebook": [
|
||||
"Crear una llibreta"
|
||||
],
|
||||
"Create new notebook": [
|
||||
"Crea una llibreta nova"
|
||||
],
|
||||
"Create new profile...": [
|
||||
"Crea un perfil nou..."
|
||||
],
|
||||
"Create note": [
|
||||
"Crea una nota"
|
||||
],
|
||||
"Create notebook": [
|
||||
"Crea una llibreta"
|
||||
],
|
||||
@@ -898,6 +931,12 @@
|
||||
"Creating new to-do...": [
|
||||
"Creant una tasca nova..."
|
||||
],
|
||||
"Creating note \"%s\"...": [
|
||||
"S'està creant la nota \"%s\"..."
|
||||
],
|
||||
"Creating note.": [
|
||||
"S'està creant una nota."
|
||||
],
|
||||
"Creating report...": [
|
||||
"Creant informe..."
|
||||
],
|
||||
@@ -968,7 +1007,7 @@
|
||||
"Per defecte"
|
||||
],
|
||||
"Default title to use for documents created by the scanner.": [
|
||||
""
|
||||
"Títol per defecte que s'utilitzarà per als documents creats per l'escàner."
|
||||
],
|
||||
"Default: %s": [
|
||||
"Per defecte: %s"
|
||||
@@ -1138,14 +1177,17 @@
|
||||
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
|
||||
"No perdeu les contrasenyes perquè, per motius de seguretat, és l'única forma de desxifrar les dades. Per habilitar el xifrat, introduïu la vostra contrasenya."
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Escàner de documents: plantilla de títol"
|
||||
],
|
||||
"Don't allow the share recipient to write to the shared notebook. Valid only for the `add` subcommand.": [
|
||||
"No permetis que el destinatari compartit escrigui a la llibreta compartida. Vàlid només per a la subordre 'add'."
|
||||
],
|
||||
"Don't show this message again": [
|
||||
""
|
||||
"No tornis a mostrar aquest missatge"
|
||||
],
|
||||
"Donate, website": [
|
||||
"Dona, lloc web"
|
||||
@@ -1222,6 +1264,12 @@
|
||||
"Edit": [
|
||||
"Edita"
|
||||
],
|
||||
"Edit as Markdown": [
|
||||
"Edita com a Markdown"
|
||||
],
|
||||
"Edit as Rich Text": [
|
||||
"Edita com a text enriquit"
|
||||
],
|
||||
"Edit link": [
|
||||
"Edita l'enllaç"
|
||||
],
|
||||
@@ -1324,6 +1372,12 @@
|
||||
"Enable Fountain syntax support": [
|
||||
"Activa el suport de sintaxi de Fountain"
|
||||
],
|
||||
"Enable handwritten transcription": [
|
||||
"Habilita la transcripció manuscrita"
|
||||
],
|
||||
"Enable HTML-to-Markdown conversion banner": [
|
||||
"Activar el bàner de conversió d'HTML a Markdown"
|
||||
],
|
||||
"Enable Linkify": [
|
||||
"Activa Linkify"
|
||||
],
|
||||
@@ -1546,6 +1600,9 @@
|
||||
"Feature flags": [
|
||||
"Interruptors de funcions"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Elements obtinguts: %d/%d."
|
||||
],
|
||||
@@ -1715,6 +1772,9 @@
|
||||
"Hide password": [
|
||||
"Amaga la contrasenya"
|
||||
],
|
||||
"Hides warning": [
|
||||
"Amaga l'avís"
|
||||
],
|
||||
"Highlight": [
|
||||
"Ressalta"
|
||||
],
|
||||
@@ -1746,7 +1806,7 @@
|
||||
"Ociós"
|
||||
],
|
||||
"If an image attachment is on its own line and followed by a blank line, it will be rendered just below its Markdown source.": [
|
||||
""
|
||||
"Si un adjunt d'imatge està en la seva pròpia línia i seguit d'una línia en blanc, es mostrarà just a sota de la seva font Markdown."
|
||||
],
|
||||
"If you have already authorised, please wait for the application to sync to Joplin Cloud.": [
|
||||
"Si ja heu autoritzat, espereu que l'aplicació es sincronitzi amb Joplin Cloud."
|
||||
@@ -1802,9 +1862,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"En qualsevol ordre, es pot fer referència a una nota o llibreta per títol o ID, o utilitzant les dreceres \"$n\" o \"$b\" per, respectivament, la nota o llibreta seleccionada actualment. '$c' es pot utilitzar per referir-se a l'element seleccionat actualment."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"Per tal d'associar una geolocalització a la nota, l'aplicació necessita permís per a accedir a la vostra ubicació.\n\nPodeu desactivar aquesta opció en qualsevol moment a la pantalla de configuració."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"Per a fer-ho, totes les vostres dades hauran de ser encriptades i sincronitzades, per tant és millor executar-ho durant la nit.\n\nPer a començar, seguiu aquestes instruccions:\n\n1. Sincronitzeu tots els vostres dispositius.\n2. Feu click en \"%s\".\n3. Deixeu-lo executant-se fins que acabi. Mentre s'executa, eviteu fer canvis en cap nota des dels altres dispositius per a evitar els conflictes.\n4. Un cop la sincronització està acabada en aquest dispositiu, sincronitzeu tots els altres dispositius, i deixeu-los executant-se fins que acabin.\n\nImportant: només cal executar això UNA SOLA VEGADA en un dispositiu."
|
||||
],
|
||||
@@ -1976,6 +2033,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
"Autenticació SSO de Joplin"
|
||||
],
|
||||
"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 Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"El porta-retalls de webs del Joplin us permet desar pàgines web i captures de pantalla del navegador web al Joplin."
|
||||
],
|
||||
@@ -2006,6 +2066,9 @@
|
||||
"Keys that need upgrading": [
|
||||
"Claus que requereixen actualització"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"Paisatge"
|
||||
],
|
||||
@@ -2161,8 +2224,11 @@
|
||||
"Markdown editor": [
|
||||
"Editor Markdown"
|
||||
],
|
||||
"Markdown editor: Render images": [
|
||||
"Editor Markdown: Renderització d'imatges"
|
||||
],
|
||||
"Markdown editor: Render markup in editor": [
|
||||
""
|
||||
"Editor de Markdown: Renderitza el marcatge a l'editor"
|
||||
],
|
||||
"Marks a to-do as done.": [
|
||||
"Marca un llistat de tasques pendents com a fet."
|
||||
@@ -2306,6 +2372,9 @@
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it": [
|
||||
"Es crearà una nova llibreta \"%s\" i s'hi importarà el fitxer \"%s\""
|
||||
],
|
||||
"New notebook title": [
|
||||
"Nou títol de llibreta"
|
||||
],
|
||||
"New photo": [
|
||||
"Nova foto"
|
||||
],
|
||||
@@ -2384,6 +2453,9 @@
|
||||
"No tab selected": [
|
||||
"No hi ha cap pestanya seleccionada"
|
||||
],
|
||||
"No tags": [
|
||||
"Sense etiquetes"
|
||||
],
|
||||
"No text editor is defined. Please set it using `config editor <editor-path>`": [
|
||||
"No hi ha definit cap editor de text. Establiu-ne un usant \"config editor <editor-path>\""
|
||||
],
|
||||
@@ -2411,6 +2483,9 @@
|
||||
"Not now": [
|
||||
"No ara"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"nota"
|
||||
],
|
||||
@@ -2459,6 +2534,9 @@
|
||||
"Note list style": [
|
||||
"Estil de llista de notes"
|
||||
],
|
||||
"Note preview": [
|
||||
"Vista prèvia de la nota"
|
||||
],
|
||||
"Note properties": [
|
||||
"Propietats de la nota"
|
||||
],
|
||||
@@ -2516,6 +2594,9 @@
|
||||
"OCR: Language data URL or path": [
|
||||
"OCR: URL o camí de dades de l'idioma"
|
||||
],
|
||||
"OCR: Search in extracted content": [
|
||||
"OCR: Cerca en el contingut extret"
|
||||
],
|
||||
"OK": [
|
||||
"D'acord"
|
||||
],
|
||||
@@ -2612,6 +2693,9 @@
|
||||
"Ordered list": [
|
||||
"Llista ordenada"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Other applications...": [
|
||||
"Altres aplicacions."
|
||||
],
|
||||
@@ -2673,11 +2757,8 @@
|
||||
"Permanently deletes the notebook, skipping the trash.": [
|
||||
"Elimina permanentment la llibreta, ometent la paperera."
|
||||
],
|
||||
"Permission needed": [
|
||||
"Cal permís"
|
||||
],
|
||||
"Photo %d": [
|
||||
""
|
||||
"Foto %d"
|
||||
],
|
||||
"Please click on \"%s\" to proceed, or set the passwords in the \"%s\" list below.": [
|
||||
"Feu clic a \"%s\" per a continuar, o definiu les contrasenyes a la llista \"%s\" inferior."
|
||||
@@ -2778,9 +2859,6 @@
|
||||
"Preferred light theme": [
|
||||
"Tema clar preferit"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
"Proveïdor preferit d'escriptura per veu"
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
"Conserva els colors en enganxar text a l'editor de text enriquit"
|
||||
],
|
||||
@@ -2880,6 +2958,9 @@
|
||||
"Publish/unpublish": [
|
||||
"Publica/despublica"
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"QR Code": [
|
||||
"Codi QR"
|
||||
],
|
||||
@@ -2901,12 +2982,18 @@
|
||||
"Re-upload local data to sync target": [
|
||||
"Torna a pujar les dades locals per a sincronitzar la destinació"
|
||||
],
|
||||
"Read more": [
|
||||
"Llegeix més"
|
||||
],
|
||||
"Read more about it": [
|
||||
"Llegiu-ne més sobre ell"
|
||||
],
|
||||
"Read time: %s min": [
|
||||
"Temps de lectura: %s min"
|
||||
],
|
||||
"Reading commands from standard input is only available in CLI mode.": [
|
||||
"La lectura d'ordres des de l'entrada estàndard només està disponible en mode CLI."
|
||||
],
|
||||
"Recipient has accepted the invitation": [
|
||||
"El destinatari ha acceptat la invitació"
|
||||
],
|
||||
@@ -2919,6 +3006,9 @@
|
||||
"Recipients:": [
|
||||
"Destinataris:"
|
||||
],
|
||||
"Recognize handwritten image": [
|
||||
"Reconeix la imatge manuscrita"
|
||||
],
|
||||
"Recommended": [
|
||||
"Recomanat"
|
||||
],
|
||||
@@ -2952,6 +3042,9 @@
|
||||
"Remove": [
|
||||
"Elimina"
|
||||
],
|
||||
"Remove %s": [
|
||||
"Elimina %s"
|
||||
],
|
||||
"Remove %s from share": [
|
||||
"Elimina %s de la compartició"
|
||||
],
|
||||
@@ -2964,6 +3057,9 @@
|
||||
"Removed %s from share.": [
|
||||
"S'ha eliminat %s de la compartició."
|
||||
],
|
||||
"Removed tag: %s": [
|
||||
"Etiqueta eliminada: %s"
|
||||
],
|
||||
"Rename": [
|
||||
"Canvia el nom"
|
||||
],
|
||||
@@ -2977,7 +3073,7 @@
|
||||
"Canvia el nom de la nota o de la llibreta indicat de <item> a <name>."
|
||||
],
|
||||
"Renders markup on all lines that don't include the cursor.": [
|
||||
""
|
||||
"Representa el marcatge en totes les línies que no inclouen el cursor."
|
||||
],
|
||||
"Renew token": [
|
||||
"Renova el testimoni"
|
||||
@@ -3138,6 +3234,9 @@
|
||||
"Save geo-location with notes": [
|
||||
"Desa la geolocalització a les notes"
|
||||
],
|
||||
"Scan notebook": [
|
||||
"Escaneja la llibreta"
|
||||
],
|
||||
"Scanned code": [
|
||||
"Codi escanejat"
|
||||
],
|
||||
@@ -3168,6 +3267,9 @@
|
||||
"Search shown": [
|
||||
"Cerca mostrada"
|
||||
],
|
||||
"Search tags": [
|
||||
"Cercar etiquetes"
|
||||
],
|
||||
"Search...": [
|
||||
"Cerca..."
|
||||
],
|
||||
@@ -3198,9 +3300,18 @@
|
||||
"Select file...": [
|
||||
"Selecciona un fitxer..."
|
||||
],
|
||||
"Select notebook": [
|
||||
"Selecciona la llibreta"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select parent notebook": [
|
||||
"Selecciona la llibreta principal"
|
||||
],
|
||||
"Selected: %s": [
|
||||
"Seleccionat: %s"
|
||||
],
|
||||
"Selection deleted": [
|
||||
"Selecció eliminada"
|
||||
],
|
||||
@@ -3531,6 +3642,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Canvia a [notebook] - totes les operacions posteriors s'aplicaran en aquesta llibreta."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Sincronitza tants dispositius com vulguis"
|
||||
],
|
||||
@@ -3627,6 +3741,9 @@
|
||||
"Text editor command": [
|
||||
"Ordre de l'editor de text"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
"El perfil actiu no es pot esborrar. Canvia a un perfil diferent i torna-ho a provar."
|
||||
],
|
||||
@@ -3700,6 +3817,9 @@
|
||||
"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."
|
||||
@@ -3804,13 +3924,13 @@
|
||||
"Aquest dibuix pot tenir canvis sense desar."
|
||||
],
|
||||
"This feature is disabled by default, you need to manually enable it by turning on the option to 'Enable handwritten transcription'.": [
|
||||
""
|
||||
"Aquesta funció està desactivada per defecte, cal activar-la manualment activant l'opció \"Activa la transcripció manuscrita\"."
|
||||
],
|
||||
"This feature is only available on Joplin Cloud and Joplin Server.": [
|
||||
""
|
||||
"Aquesta funció només està disponible a Joplin Cloud i Joplin Server."
|
||||
],
|
||||
"This image type is not supported by the recognition system.": [
|
||||
""
|
||||
"Aquest tipus d'imatge no és compatible amb el sistema de reconeixement."
|
||||
],
|
||||
"This is an advanced tool to show the attachments that are linked to your notes. Please be careful when deleting one of them as they cannot be restored afterwards.": [
|
||||
"Aquesta és una eina avançada per a mostrar els adjunts que estan enllaçats a les vostres notes. Tingueu precaució en suprimir-ne un, ja que després no es poden restaurar."
|
||||
@@ -3840,7 +3960,7 @@
|
||||
"Aquesta nota no té historial"
|
||||
],
|
||||
"This note is in HTML format. Convert it to Markdown to edit it more easily.": [
|
||||
""
|
||||
"Aquesta nota està en format HTML. Converteix-la a Markdown per editar-la més fàcilment."
|
||||
],
|
||||
"This plugin doesn't support %s.": [
|
||||
"Aquest connector no admet %s."
|
||||
@@ -3888,7 +4008,7 @@
|
||||
"Per a continuar, introduïu la contrasenya mestra."
|
||||
],
|
||||
"To create a new tag, type the name and press enter.": [
|
||||
""
|
||||
"Per crear una etiqueta nova, escriviu el nom i premeu Retorn."
|
||||
],
|
||||
"To delete a tag, untag the associated notes.": [
|
||||
"Per a suprimir una etiqueta, traieu l'etiqueta en les notes associades."
|
||||
@@ -4127,6 +4247,9 @@
|
||||
"Upgrade the sync target to the latest version.": [
|
||||
"Actualitza la destinació de sincronització a la versió més recent."
|
||||
],
|
||||
"Upload photo": [
|
||||
"Pujar la foto"
|
||||
],
|
||||
"URL": [
|
||||
"URL"
|
||||
],
|
||||
@@ -4163,6 +4286,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
"S'utilitza quan es necessita un tipus de lletra d'amplada fixa per a mostrar text de manera llegible (p. ex. taules, caselles de selecció, codi). Si no es troba, s'utilitza un tipus de lletra genèric monoespai (amplada fixa)."
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
"Supressions d'usuari"
|
||||
],
|
||||
@@ -4211,9 +4337,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
"Escriptura per veu: Glossari"
|
||||
],
|
||||
"Vosk": [
|
||||
"Vosk"
|
||||
],
|
||||
"Waiting for authorisation...": [
|
||||
"S'està esperant autorització..."
|
||||
],
|
||||
@@ -4271,12 +4394,12 @@
|
||||
"When creating a new to-do:": [
|
||||
"En crear una tasca nova:"
|
||||
],
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"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.": [
|
||||
"Quan estigui activada, l'aplicació escanejarà els adjunts i n'extraurà el text. Això us permetrà cercar text en aquests adjunts."
|
||||
],
|
||||
"Whisper": [
|
||||
"Whisper"
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
"La finestra no respon."
|
||||
],
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to toggle the given to-do between completed and uncompleted state (If the target is a regular note it will be converted to a to-do). Use \"clear\" to convert the to-do back to a regular note.": [
|
||||
"<todo-command> může být buď \"toggle\" (přepnout) nebo \"clear\" (odstranit). Použijte \"toggle\" pro přepnutí daného úkolu na dokončený resp. nedokončený (pokud je cílem normální poznámka, bude konvertována na úkol). Použijte \"clear\" pro konverzi úkolu na normální poznámku."
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"A comma-separated list of words. May be used for uncommon words, to help voice typing spell them correctly.": [
|
||||
""
|
||||
],
|
||||
@@ -254,6 +257,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
""
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
""
|
||||
],
|
||||
@@ -943,6 +949,9 @@
|
||||
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
|
||||
"Heslo neztrácejte, protože z bezpečnostních důvodů je to *jediný* způsob, jak data dešifrovat! Chcete-li povolit šifrování, zadejte níže heslo."
|
||||
],
|
||||
"Do you find the Joplin web app useful?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
""
|
||||
],
|
||||
@@ -1285,6 +1294,9 @@
|
||||
"Feature flags": [
|
||||
"Příznaky funkcí"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Získané položky: %d/%d."
|
||||
],
|
||||
@@ -1515,9 +1527,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"Ve všech příkazech může být poznámka či zápisník referována svým názvem či ID, nebo zkratkami `$n` a `$b` pro nyní vybranou poznámku či zápisník. `$c` odkazuje na současně vybranou položku."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"Pro přidání informací o zeměpisné poloze k poznámce potřebujte aplikace oprávnění pro přístupu k vaší poloze.\n\nTuto možnost můžete kdykoliv vypnout v Nastavení."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"Aby to bylo možné, bude nutné zašifrovat a synchronizovat všechna vaše data, takže je nejlepší celou akci spustit přes noc.\n\nPostupujte podle těchto pokynů:\n\n1. Proveďte synchronizaci všech svých zařízení.\n2. Klepněte na \"%s\".\n3. Nechte vše doběhnout do konce. Dokud operace poběži, vyhněte se změnám poznámek na ostatních zařízeních, aby nedocházelo ke konfliktům.\n4. Až bude synchronizace na tomto zařízení dokončena, synchronizujte všechna ostatní zařízení a nechejte na nich synchronizaci doběhnout do konce.\n\nDůležité: toto stačí spustit pouze JEDNOU a na jednom zařízení."
|
||||
],
|
||||
@@ -1650,6 +1659,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
""
|
||||
],
|
||||
"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 Web Clipper allows saving web pages and screenshots from your browser to Joplin.": [
|
||||
"Joplin Web Clipper umožňuje ukládat webové stránky a screenshoty z vašeho prohlížeče do Joplin."
|
||||
],
|
||||
@@ -1677,6 +1689,9 @@
|
||||
"Keys that need upgrading": [
|
||||
"Klíče, které je třeba aktualizovat"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"Na šířku"
|
||||
],
|
||||
@@ -1997,6 +2012,9 @@
|
||||
"Not now": [
|
||||
"Vyzkoušejte to hned"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"Nová poznámka"
|
||||
],
|
||||
@@ -2150,6 +2168,9 @@
|
||||
"Ordered list": [
|
||||
"Seznam objednávek"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Other applications...": [
|
||||
"Další aplikace..."
|
||||
],
|
||||
@@ -2192,9 +2213,6 @@
|
||||
"PDF File": [
|
||||
"PDF soubor"
|
||||
],
|
||||
"Permission needed": [
|
||||
"Vyžadováno oprávnění"
|
||||
],
|
||||
"Photo %d": [
|
||||
""
|
||||
],
|
||||
@@ -2282,9 +2300,6 @@
|
||||
"Preferred light theme": [
|
||||
"Preferovaný světlý vzhled"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
""
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
""
|
||||
],
|
||||
@@ -2372,6 +2387,9 @@
|
||||
"Publish/unpublish": [
|
||||
""
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"Quit": [
|
||||
"Ukončit"
|
||||
],
|
||||
@@ -2615,6 +2633,9 @@
|
||||
"Select file...": [
|
||||
"Vybrat soubor..."
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Self-hosted": [
|
||||
""
|
||||
],
|
||||
@@ -2882,6 +2903,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Přepne do zápisníku [zápisník]. Všechny další operace budou prováděny na tomto zápisníku."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Synchronizace libovolného počtu zařízení"
|
||||
],
|
||||
@@ -2972,6 +2996,9 @@
|
||||
"Text editor command": [
|
||||
"Textový editor"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": [
|
||||
"Aktivní profil nelze odstranit. Přepněte na jiný profil a zkuste to znovu."
|
||||
],
|
||||
@@ -3296,6 +3323,9 @@
|
||||
"Unable to share log data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unable to share note data. Reason: %s": [
|
||||
""
|
||||
],
|
||||
"Unchecked": [
|
||||
""
|
||||
],
|
||||
@@ -3413,6 +3443,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
"Používá se, kdekoliv je potřeba znaky s jednotnou šířkou pro čitelné rozložení textu (např. tabulky, zaškrtávací tlačítka, ukázky kódu). Pokud nebude nalezeno, bude použito obecné neproporcionální písmo s jednotnou šířkou znaků."
|
||||
],
|
||||
"Useful": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
"Odstranění uživatele"
|
||||
],
|
||||
@@ -3455,9 +3488,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
""
|
||||
],
|
||||
"Vosk": [
|
||||
""
|
||||
],
|
||||
"Warning": [
|
||||
"Upozornění"
|
||||
],
|
||||
@@ -3500,10 +3530,10 @@
|
||||
"When creating a new to-do:": [
|
||||
"Při vytváření nového úkolu:"
|
||||
],
|
||||
"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.": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Whisper": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
"[None]": [
|
||||
"[Ingen]"
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
@@ -335,6 +338,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
"Der blev fundet en automatisk gemt tegning. Vedhæfter en kopi af den til noten?"
|
||||
],
|
||||
"An error occurred while sending the response. This can happen if the app is offline or cannot connect to the server.\nError: %s": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
"Der opstod en fejl: %s"
|
||||
],
|
||||
@@ -1171,6 +1177,9 @@
|
||||
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
|
||||
"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?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Dokumentscanner: Skabelon til titel"
|
||||
],
|
||||
@@ -1591,6 +1600,9 @@
|
||||
"Feature flags": [
|
||||
"Feature-markeringer"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Hentede emner: %d/%d."
|
||||
],
|
||||
@@ -1850,9 +1862,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"I enhver kommando kan en note eller notesbog refereres med titel eller ID, eller ved at bruge links `$n` eller `$b` for valgte noter eller notesbøger. `$c` kan bruges som reference til aktuel/valgt emne."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"For at knytte en geo-lokation til noten har app'en brug for din tilladelse til at tilgå din placering.\n\nDu kan slå denne mulighed fra når som helst i Indstillinger."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"For at gøre dette skal hele dit datasæt krypteres og synkroniseres, så det er bedst at lade det køre hen over natten.\n\nFølg disse instruktioner for at starte:\n\n1. Synkroniser alle dine enheder.\n2. Klik \"%s\".\n3. Lad det køre færdig. Mens synkroniseringen kører, bør du undgå at ændre nogen noter på dine andre enheder for at undgå konflikter.\n4. Når synk er færdig på denne enhed, synk'er du alle dine andre enheder og lader det køre helt færdig.\n\nVigtigt: Du behøver kun at gøre dette EN gang på en enhed."
|
||||
],
|
||||
@@ -2024,6 +2033,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
"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 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."
|
||||
],
|
||||
@@ -2054,6 +2066,9 @@
|
||||
"Keys that need upgrading": [
|
||||
"Nøgler der har brug for opgradering"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"Landskab"
|
||||
],
|
||||
@@ -2468,6 +2483,9 @@
|
||||
"Not now": [
|
||||
"Ikke nu"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"note"
|
||||
],
|
||||
@@ -2675,6 +2693,9 @@
|
||||
"Ordered list": [
|
||||
"Sorteret liste"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Other applications...": [
|
||||
"Andre applikationer..."
|
||||
],
|
||||
@@ -2736,9 +2757,6 @@
|
||||
"Permanently deletes the notebook, skipping the trash.": [
|
||||
"Sletter notesbogen permanent og springer papirkurven over."
|
||||
],
|
||||
"Permission needed": [
|
||||
"Tilladelse nødvendig"
|
||||
],
|
||||
"Photo %d": [
|
||||
"Foto %d"
|
||||
],
|
||||
@@ -2841,9 +2859,6 @@
|
||||
"Preferred light theme": [
|
||||
"Foretrukket lyst tema"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
"Foretrukken udbyder af stemmeskrivning"
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
"Bevar farver, når du indsætter tekst i Rich Text Editor"
|
||||
],
|
||||
@@ -2943,6 +2958,9 @@
|
||||
"Publish/unpublish": [
|
||||
"Publicer/afpublicer"
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"QR Code": [
|
||||
"QR-kode"
|
||||
],
|
||||
@@ -3285,6 +3303,9 @@
|
||||
"Select notebook": [
|
||||
"Vælg notesbog"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select parent notebook": [
|
||||
"Vælg overordnet notesbog"
|
||||
],
|
||||
@@ -3621,6 +3642,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Skifter til [notebook] - alle fremtidige handlinger sker i denne notesbog."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Synkroniser så mange enheder, du vil"
|
||||
],
|
||||
@@ -3717,6 +3741,9 @@
|
||||
"Text editor command": [
|
||||
"Tekstredigeringskomando"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
@@ -4259,6 +4286,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
"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": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
"Brugersletninger"
|
||||
],
|
||||
@@ -4307,9 +4337,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
"Stemmeindtastning: Ordliste"
|
||||
],
|
||||
"Vosk": [
|
||||
"Vosk"
|
||||
],
|
||||
"Waiting for authorisation...": [
|
||||
"Venter på godkendelse..."
|
||||
],
|
||||
@@ -4367,12 +4394,12 @@
|
||||
"When creating a new to-do:": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
"Whisper": [
|
||||
"Whisper"
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
"Vinduet reagerer ikke."
|
||||
],
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
"[None]": [
|
||||
"[Nichts]"
|
||||
],
|
||||
"A brief description of the image:": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
@@ -335,6 +338,9 @@
|
||||
"An autosaved drawing was found. Attach a copy of it to the note?": [
|
||||
"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": [
|
||||
""
|
||||
],
|
||||
"An error occurred: %s": [
|
||||
"Ein Fehler trat auf: %s"
|
||||
],
|
||||
@@ -1171,6 +1177,9 @@
|
||||
"Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.": [
|
||||
"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?": [
|
||||
""
|
||||
],
|
||||
"Document scanner: Title template": [
|
||||
"Dokumentenscanner: Titelvorlage"
|
||||
],
|
||||
@@ -1591,6 +1600,9 @@
|
||||
"Feature flags": [
|
||||
"Funktionsargumente"
|
||||
],
|
||||
"Feedback": [
|
||||
""
|
||||
],
|
||||
"Fetched items: %d/%d.": [
|
||||
"Geladene Elemente: %d/%d."
|
||||
],
|
||||
@@ -1850,9 +1862,6 @@
|
||||
"In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.": [
|
||||
"Bei jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre ID angegeben werden oder durch die Abkürzungen `$n` oder `$b` für die aktuelle Notiz bzw. das aktuelle Notizbuch. `$c` kann benutzt werden, um auf momentan ausgewählte Element zu verweisen."
|
||||
],
|
||||
"In order to associate a geo-location with the note, the app needs your permission to access your location.\n\nYou may turn off this option at any time in the Configuration screen.": [
|
||||
"Um einen geografischen Standort mit der Notiz zu verknüpfen, benötigt die Anwendung die Berechtigung, auf Ihren Standort zuzugreifen.\n\nDiese Option kann jederzeit im Konfigurationsmenü deaktiviert werden."
|
||||
],
|
||||
"In order to do so, your entire data set will have to be encrypted and synchronised, so it is best to run it overnight.\n\nTo start, please follow these instructions:\n\n1. Synchronise all your devices.\n2. Click \"%s\".\n3. Let it run to completion. While it runs, avoid changing any note on your other devices, to avoid conflicts.\n4. Once sync is done on this device, sync all your other devices and let it run to completion.\n\nImportant: you only need to run this ONCE on one device.": [
|
||||
"Dazu muss dein gesamter Datensatz verschlüsselt und erneut synchronisiert werden. Starte diese Prozedur daher am besten nachts.\n\nBeachte dazu folgende Hinweise:\n\n1. Synchronisiere alle deine Geräte.\n2. Klicke auf „%s“.\n3. Lass es komplett durchlaufen. Vermeide während der Ausführung das Ändern von Notizen auf anderen Geräten, um Konflikte zu vermeiden.\n4. Sobald die Synchronisation auf diesem Gerät abgeschlossen ist, synchronisiere alle anderen Geräte und lass es komplett durchlaufen.\n\nWichtig: Du musst diese Prozedur nur EINMAL auf einem Gerät starten."
|
||||
],
|
||||
@@ -2024,6 +2033,9 @@
|
||||
"Joplin SSO Authentication": [
|
||||
"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 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."
|
||||
],
|
||||
@@ -2054,6 +2066,9 @@
|
||||
"Keys that need upgrading": [
|
||||
"Hauptschlüssel, die aktualisiert werden müssen"
|
||||
],
|
||||
"Label": [
|
||||
""
|
||||
],
|
||||
"Landscape": [
|
||||
"Querformat"
|
||||
],
|
||||
@@ -2468,6 +2483,9 @@
|
||||
"Not now": [
|
||||
"Nicht jetzt"
|
||||
],
|
||||
"Not useful": [
|
||||
""
|
||||
],
|
||||
"note": [
|
||||
"Notiz"
|
||||
],
|
||||
@@ -2675,6 +2693,9 @@
|
||||
"Ordered list": [
|
||||
"Geordnete Liste"
|
||||
],
|
||||
"Other": [
|
||||
""
|
||||
],
|
||||
"Other applications...": [
|
||||
"Andere Anwendungen..."
|
||||
],
|
||||
@@ -2736,9 +2757,6 @@
|
||||
"Permanently deletes the notebook, skipping the trash.": [
|
||||
"Löscht das Notizbuch dauerhaft, überspringt den Papierkorb."
|
||||
],
|
||||
"Permission needed": [
|
||||
"Berechtigung benötigt"
|
||||
],
|
||||
"Photo %d": [
|
||||
"Foto %d"
|
||||
],
|
||||
@@ -2841,9 +2859,6 @@
|
||||
"Preferred light theme": [
|
||||
"Bevorzugtes helles Erscheinungsbild"
|
||||
],
|
||||
"Preferred voice typing provider": [
|
||||
"Bevorzugter Anbieter für Spracheingabe"
|
||||
],
|
||||
"Preserve colours when pasting text in Rich Text Editor": [
|
||||
"Farben beim Einfügen von Text im Rich Text-Editor erhalten"
|
||||
],
|
||||
@@ -2943,6 +2958,9 @@
|
||||
"Publish/unpublish": [
|
||||
"Veröffentlichen / nicht veröffentlichen"
|
||||
],
|
||||
"Publishes a note to Joplin Server or Joplin Cloud": [
|
||||
""
|
||||
],
|
||||
"QR Code": [
|
||||
"QR-Code"
|
||||
],
|
||||
@@ -3130,7 +3148,7 @@
|
||||
"Notiz wiederherstellen"
|
||||
],
|
||||
"Restore notebook": [
|
||||
"Notizbuch wiederherstsellen"
|
||||
"Notizbuch wiederherstellen"
|
||||
],
|
||||
"Restore the items matching <pattern> from the trash.": [
|
||||
"Stellt die Elemente aus dem Papierkorb wieder her, die mit <pattern> übereinstimmen."
|
||||
@@ -3285,6 +3303,9 @@
|
||||
"Select notebook": [
|
||||
"Notizbuch auswählen"
|
||||
],
|
||||
"Select one of the other supported sync targets.": [
|
||||
""
|
||||
],
|
||||
"Select parent notebook": [
|
||||
"Eltern-Notizbuch auswählen"
|
||||
],
|
||||
@@ -3621,6 +3642,9 @@
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": [
|
||||
"Wechselt zu [notebook] - alle weiteren Aktionen werden in diesem Notizbuch ausgeführt."
|
||||
],
|
||||
"Sync": [
|
||||
""
|
||||
],
|
||||
"Sync as many devices as you want": [
|
||||
"Synchronisiere mit beliebig vielen Geräten"
|
||||
],
|
||||
@@ -3717,6 +3741,9 @@
|
||||
"Text editor command": [
|
||||
"Texteditor-Befehl"
|
||||
],
|
||||
"Thank you for the feedback!\nDo you have time to complete a short survey?": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
@@ -4259,6 +4286,9 @@
|
||||
"Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.": [
|
||||
"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": [
|
||||
""
|
||||
],
|
||||
"User deletions": [
|
||||
"Benutzer-Löschungen"
|
||||
],
|
||||
@@ -4307,9 +4337,6 @@
|
||||
"Voice typing: Glossary": [
|
||||
"Spracheingabe: Glossar"
|
||||
],
|
||||
"Vosk": [
|
||||
"Vosk"
|
||||
],
|
||||
"Waiting for authorisation...": [
|
||||
"Warte auf Autorisierung..."
|
||||
],
|
||||
@@ -4367,12 +4394,12 @@
|
||||
"When creating a new to-do:": [
|
||||
"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.": [
|
||||
""
|
||||
],
|
||||
"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."
|
||||
],
|
||||
"Whisper": [
|
||||
"Whisper"
|
||||
],
|
||||
"Window unresponsive.": [
|
||||
"Fenster antwortet nicht."
|
||||
],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user