You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-01-17 00:33:59 +02:00
Compare commits
92 Commits
logger_fix
...
paste_from
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377d02e19d | ||
|
|
b92cb7deb7 | ||
|
|
0edc66da49 | ||
|
|
e96ad7ccfa | ||
|
|
817ef7bbed | ||
|
|
5bd0c9b3a0 | ||
|
|
46d9cd34a8 | ||
|
|
c3e08237fd | ||
|
|
b406f05241 | ||
|
|
2cbee6d8af | ||
|
|
c859ad48c1 | ||
|
|
1141b1c2a1 | ||
|
|
39c118be90 | ||
|
|
f9ac4e112b | ||
|
|
87e51aa8e6 | ||
|
|
41fdc0d44d | ||
|
|
a754a8d772 | ||
|
|
41d0363fd0 | ||
|
|
2a4c7a334e | ||
|
|
df1b0a96f4 | ||
|
|
0030681cb4 | ||
|
|
e7014492c5 | ||
|
|
4804c1c0c3 | ||
|
|
270d96ad07 | ||
|
|
5ed3d94faa | ||
|
|
d0e943630d | ||
|
|
406e933407 | ||
|
|
7108a4243d | ||
|
|
135e2e4a21 | ||
|
|
c68c0bf501 | ||
|
|
cf3d86698d | ||
|
|
3251c4c40e | ||
|
|
bd5e0fd42a | ||
|
|
7d0b7122f0 | ||
|
|
c50052ac04 | ||
|
|
357c23b588 | ||
|
|
eca1afb6d5 | ||
|
|
85eddbfe22 | ||
|
|
c6c2733726 | ||
|
|
5d87b4ca3e | ||
|
|
89f550ca48 | ||
|
|
9e55d90736 | ||
|
|
8d0d9b58de | ||
|
|
ac5e484d4e | ||
|
|
bce18a1614 | ||
|
|
950b16370f | ||
|
|
4337e2b79a | ||
|
|
90d75ce80e | ||
|
|
72d34788dc | ||
|
|
6a2e6173ab | ||
|
|
65bb9fa3c4 | ||
|
|
90e1502e73 | ||
|
|
3cc990e1a2 | ||
|
|
10fd8454f7 | ||
|
|
56d7030222 | ||
|
|
8696052e27 | ||
|
|
5f7e130ff9 | ||
|
|
434c890686 | ||
|
|
5ab1b0bfd0 | ||
|
|
b9f632b634 | ||
|
|
ae8f32e6b4 | ||
|
|
ce8470ee7c | ||
|
|
3b1a726a23 | ||
|
|
3d4740203f | ||
|
|
7a74271e6a | ||
|
|
2cfae43fce | ||
|
|
a143b980ee | ||
|
|
329c385358 | ||
|
|
1543c651aa | ||
|
|
9cdaff4f1e | ||
|
|
8e3247221a | ||
|
|
ad975a473f | ||
|
|
9a8c0e9813 | ||
|
|
9d16dd22be | ||
|
|
c0501fc4e0 | ||
|
|
1acbb5dc9a | ||
|
|
2f6e146841 | ||
|
|
ba2bfa5b5c | ||
|
|
95ca89bf5b | ||
|
|
16d8a78d8a | ||
|
|
f116504e88 | ||
|
|
4d0b6ae382 | ||
|
|
13c4eba3af | ||
|
|
9756f64c11 | ||
|
|
807384cfac | ||
|
|
2329e321ec | ||
|
|
6c2e0d9262 | ||
|
|
887c271853 | ||
|
|
ea7c7f6447 | ||
|
|
29931c05ad | ||
|
|
17c227024e | ||
|
|
406a1496db |
@@ -133,6 +133,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
@@ -259,6 +260,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
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/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -267,13 +269,36 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/NoteListSource.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultLeftToRightListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteList/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteRevisionViewer.js
|
||||
@@ -368,6 +393,7 @@ packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
@@ -408,6 +434,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
@@ -514,6 +541,8 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
|
||||
15
.github/scripts/run_ci.sh
vendored
15
.github/scripts/run_ci.sh
vendored
@@ -171,6 +171,21 @@ if [ "$IS_PULL_REQUEST" == "1" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Check that the website still builds
|
||||
# =============================================================================
|
||||
|
||||
if [ "$IS_PULL_REQUEST" == "1" ] || [ "$IS_DEV_BRANCH" = "1" ]; then
|
||||
echo "Step: Check that the website still builds..."
|
||||
|
||||
mkdir -p ../joplin-website/docs
|
||||
SKIP_SPONSOR_PROCESSING=1 yarn run buildWebsite
|
||||
testResult=$?
|
||||
if [ $testResult -ne 0 ]; then
|
||||
exit $testResult
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Find out if we should run the build or not. Electron-builder gets stuck when
|
||||
# building PRs so we disable it in this case. The Linux build should provide
|
||||
|
||||
5
.github/workflows/build-android.yml
vendored
5
.github/workflows/build-android.yml
vendored
@@ -25,17 +25,18 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update || true
|
||||
sudo apt-get install -y libsecret-1-dev
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
corepack enable
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install
|
||||
run: yarn install
|
||||
|
||||
|
||||
1
.github/workflows/build-macos-m1.yml
vendored
1
.github/workflows/build-macos-m1.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
|
||||
2
.github/workflows/github-actions-main.yml
vendored
2
.github/workflows/github-actions-main.yml
vendored
@@ -75,6 +75,7 @@ jobs:
|
||||
# We need to pin the version to 18.15, because 18.16+ fails with this error:
|
||||
# https://github.com/facebook/react-native/issues/36440
|
||||
node-version: '18.15.0'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
@@ -160,6 +161,7 @@ jobs:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Yarn
|
||||
run: |
|
||||
|
||||
32
.gitignore
vendored
32
.gitignore
vendored
@@ -50,6 +50,7 @@ packages/tools/github_oauth_token.txt
|
||||
lerna-debug.log
|
||||
.env
|
||||
docs/**/*.mustache
|
||||
.idea
|
||||
|
||||
# Yarn stuff
|
||||
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||
@@ -118,6 +119,7 @@ packages/app-desktop/gui/ClipperConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
|
||||
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
|
||||
packages/app-desktop/gui/ConfigScreen/Sidebar.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginBox.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/PluginsStates.js
|
||||
packages/app-desktop/gui/ConfigScreen/controls/plugins/SearchPlugins.js
|
||||
@@ -244,6 +246,7 @@ packages/app-desktop/gui/NoteEditor/utils/types.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useDropHandler.js
|
||||
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/useMarkupToHtml.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useMessageHandler.js
|
||||
@@ -252,13 +255,36 @@ packages/app-desktop/gui/NoteEditor/utils/usePluginServiceRegistration.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useSearchMarkers.js
|
||||
packages/app-desktop/gui/NoteEditor/utils/useWindowCommandHandler.js
|
||||
packages/app-desktop/gui/NoteList/NoteList.js
|
||||
packages/app-desktop/gui/NoteList/NoteList2.js
|
||||
packages/app-desktop/gui/NoteList/NoteListSource.js
|
||||
packages/app-desktop/gui/NoteList/commands/focusElementNoteList.js
|
||||
packages/app-desktop/gui/NoteList/commands/index.js
|
||||
packages/app-desktop/gui/NoteList/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/canManuallySortNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultLeftToRightListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.js
|
||||
packages/app-desktop/gui/NoteList/utils/prepareViewProps.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.js
|
||||
packages/app-desktop/gui/NoteList/utils/useFocusNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useMoveNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.js
|
||||
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.js
|
||||
packages/app-desktop/gui/NoteList/utils/useScroll.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.test.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
packages/app-desktop/gui/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/NoteListItem.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/types.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemElement.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useItemEventHandlers.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useOnContextMenu.js
|
||||
packages/app-desktop/gui/NoteListItem/utils/useRootElement.js
|
||||
packages/app-desktop/gui/NoteListWrapper/NoteListWrapper.js
|
||||
packages/app-desktop/gui/NotePropertiesDialog.js
|
||||
packages/app-desktop/gui/NoteRevisionViewer.js
|
||||
@@ -353,6 +379,7 @@ packages/app-desktop/services/spellChecker/SpellCheckerServiceDriverNative.js
|
||||
packages/app-desktop/tools/notarizeMacApp.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.test.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtils.js
|
||||
packages/app-desktop/utils/checkForUpdatesUtilsTestData.js
|
||||
packages/app-desktop/utils/markupLanguageUtils.js
|
||||
packages/app-mobile/PluginAssetsLoader.js
|
||||
packages/app-mobile/components/ActionButton.js
|
||||
@@ -393,6 +420,7 @@ packages/app-mobile/components/NoteEditor/MarkdownToolbar/Toolbar.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarButton.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/ToolbarOverflowRows.js
|
||||
packages/app-mobile/components/NoteEditor/MarkdownToolbar/types.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.test.js
|
||||
packages/app-mobile/components/NoteEditor/NoteEditor.js
|
||||
packages/app-mobile/components/NoteEditor/SearchPanel.js
|
||||
packages/app-mobile/components/NoteEditor/SelectionFormatting.js
|
||||
@@ -499,6 +527,8 @@ packages/lib/commands/index.js
|
||||
packages/lib/commands/openMasterPasswordDialog.js
|
||||
packages/lib/commands/synchronize.js
|
||||
packages/lib/components/EncryptionConfigScreen/utils.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.js
|
||||
packages/lib/components/shared/config/shouldShowMissingPasswordWarning.js
|
||||
packages/lib/components/shared/note-screen-shared.js
|
||||
packages/lib/components/shared/reduxSharedMiddleware.js
|
||||
packages/lib/database-driver-better-sqlite.js
|
||||
|
||||
BIN
Assets/Aide.png
Normal file
BIN
Assets/Aide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -112,7 +112,7 @@
|
||||
}).then(async function(result) {
|
||||
if (!result.ok) {
|
||||
console.error('Could not create Stripe checkout session', await result.text());
|
||||
alert('The checkout session could not be created. Please contact support@joplincloud.com for support.');
|
||||
alert('The checkout session could not be created. Please contact us on the forum for support.');
|
||||
} else {
|
||||
return result.json();
|
||||
}
|
||||
|
||||
84
README.md
84
README.md
@@ -75,11 +75,10 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
| | | | |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[avanderberg](https://github.com/avanderberg) | <img width="50" src="https://avatars2.githubusercontent.com/u/67130?s=96&v=4"/></br>[chr15m](https://github.com/chr15m) | <img width="50" src="https://avatars2.githubusercontent.com/u/2793530?s=96&v=4"/></br>[CyberXZT](https://github.com/CyberXZT) | <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[dbrandonjohnson](https://github.com/dbrandonjohnson) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/49439044?s=96&v=4"/></br>[fourstepper](https://github.com/fourstepper) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/3266447?s=96&v=4"/></br>[iamwillbar](https://github.com/iamwillbar) | <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/5588131?s=96&v=4"/></br>[kianenigma](https://github.com/kianenigma) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) | <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/29300939?s=96&v=4"/></br>[mcejp](https://github.com/mcejp) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) | <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | | |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/14873877?s=96&v=4"/></br>[dchecks](https://github.com/dchecks) | <img width="50" src="https://avatars2.githubusercontent.com/u/56287?s=96&v=4"/></br>[fats](https://github.com/fats) | <img width="50" src="https://avatars2.githubusercontent.com/u/8030470?s=96&v=4"/></br>[Galliver7](https://github.com/Galliver7) | <img width="50" src="https://avatars2.githubusercontent.com/u/64712218?s=96&v=4"/></br>[Hegghammer](https://github.com/Hegghammer) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1310474?s=96&v=4"/></br>[jknowles](https://github.com/jknowles) | <img width="50" src="https://avatars2.githubusercontent.com/u/11947658?s=96&v=4"/></br>[KentBrockman](https://github.com/KentBrockman) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[konishi-t](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/42319182?s=96&v=4"/></br>[marcdw1289](https://github.com/marcdw1289) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/126279083?s=96&v=4"/></br>[matmoly](https://github.com/matmoly) | <img width="50" src="https://avatars2.githubusercontent.com/u/1788010?s=96&v=4"/></br>[maxtruxa](https://github.com/maxtruxa) | <img width="50" src="https://avatars2.githubusercontent.com/u/31054972?s=96&v=4"/></br>[saarantras](https://github.com/saarantras) | <img width="50" src="https://avatars2.githubusercontent.com/u/327998?s=96&v=4"/></br>[sif](https://github.com/sif) |
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/765564?s=96&v=4"/></br>[taskcruncher](https://github.com/taskcruncher) | <img width="50" src="https://avatars2.githubusercontent.com/u/333944?s=96&v=4"/></br>[tateisu](https://github.com/tateisu) | | |
|
||||
<!-- SPONSORS-GITHUB -->
|
||||
|
||||
<!-- TOC -->
|
||||
@@ -159,6 +158,7 @@ A community maintained list of these distributions can be found here: [Unofficia
|
||||
- [Guiding principles](https://github.com/laurent22/joplin/blob/dev/readme/principles.md)
|
||||
- [Stats](https://github.com/laurent22/joplin/blob/dev/readme/stats.md)
|
||||
- [Brand guidelines](https://joplinapp.org/brand)
|
||||
- [Release cycle](https://github.com/laurent22/joplin/blob/dev/readme/release_cycle.md)
|
||||
- [Donate](https://github.com/laurent22/joplin/blob/dev/readme/donate.md)
|
||||
<!-- TOC -->
|
||||
|
||||
@@ -232,9 +232,9 @@ Joplin is also capable of exporting to a number of other formats including HTML
|
||||
|
||||
# Synchronisation
|
||||
|
||||
One of the goals of Joplin is to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
|
||||
One of the goals of Joplin is to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another.
|
||||
|
||||
Currently, synchronisation is possible with Nextcloud, WebDAV, Dropbox, OneDrive or the local filesystem. To enable synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually. Joplin will background sync automatically after any content change is made on the local application.
|
||||
Currently, synchronisation is possible with Joplin Cloud, Nextcloud, S3, WebDAV, Dropbox, OneDrive or the local filesystem. To enable synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually. Joplin will background sync automatically after any content change is made on the local application.
|
||||
|
||||
If the **terminal client** has been installed, it is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at a regular interval. For example, this would do it every 30 minutes:
|
||||
|
||||
@@ -537,47 +537,47 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 79%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/arableague.png" width="16px"/> | Arabic | [ar](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ar.po) | [Whaell O](mailto:Whaell@protonmail.com) | 77%
|
||||
<img src="https://joplinapp.org/images/flags/es/basque_country.png" width="16px"/> | Basque | [eu](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eu.po) | juan.abasolo@ehu.eus | 22%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 57%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 45%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [MrKanister](mailto:pueblos_spatulas@aleeas.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 44%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ba.png" width="16px"/> | Bosnian (Bosna i Hercegovina) | [bs_BA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bs_BA.po) | [Derviš T.](mailto:dervis.t@pm.me) | 56%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/bg.png" width="16px"/> | Bulgarian (България) | [bg_BG](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/bg_BG.po) | | 44%
|
||||
<img src="https://joplinapp.org/images/flags/es/catalonia.png" width="16px"/> | Catalan | [ca](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ca.po) | [Xavi Ivars](mailto:xavi.ivars@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hr.png" width="16px"/> | Croatian (Hrvatska) | [hr_HR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hr_HR.po) | [Milo Ivir](mailto:mail@milotype.de) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cz.png" width="16px"/> | Czech (Česká republika) | [cs_CZ](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/cs_CZ.po) | Fejby | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/dk.png" width="16px"/> | Dansk (Danmark) | [da_DK](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/da_DK.po) | ERYpTION | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/de.png" width="16px"/> | Deutsch (Deutschland) | [de_DE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/de_DE.po) | [Mr-Kanister](mailto:viger_gtrc@simplelogin.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ee.png" width="16px"/> | Eesti Keel (Eesti) | [et_EE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/et_EE.po) | | 43%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gb.png" width="16px"/> | English (United Kingdom) | [en_GB](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_GB.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/us.png" width="16px"/> | English (United States of America) | [en_US](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/en_US.po) | | 100%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/es.png" width="16px"/> | Español (España) | [es_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/es_ES.po) | [Francisco Villaverde](mailto:teko.gr@gmail.com) | 95%
|
||||
<img src="https://joplinapp.org/images/flags/esperanto.png" width="16px"/> | Esperanto | [eo](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/eo.po) | Marton Paulo | 25%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fi.png" width="16px"/> | Finnish (Suomi) | [fi_FI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fi_FI.po) | mrkaato0 | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/fr.png" width="16px"/> | Français (France) | [fr_FR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 29%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 80%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 77%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 54%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 89%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Douglas Leão](mailto:djlsplays@gmail.com) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 72%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 50%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 79%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 36%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 77%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 71%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 87%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy K](mailto:dmitry@atsip.ru) | 99%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 64%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [wh201906](mailto:wh201906@yandex.com) | 96%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/es/galicia.png" width="16px"/> | Galician (España) | [gl_ES](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/gl_ES.po) | [Marcos Lans](mailto:marcoslansgarza@gmail.com) | 28%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/id.png" width="16px"/> | Indonesian (Indonesia) | [id_ID](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/id_ID.po) | [Wisnu Adi Santoso](mailto:waditos@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/it.png" width="16px"/> | Italiano (Italia) | [it_IT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/it_IT.po) | [Manuel Tassi](mailto:mannivuwiki@gmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/hu.png" width="16px"/> | Magyar (Magyarország) | [hu_HU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/hu_HU.po) | [Magyari Balázs](mailto:balmag@gmail.com) | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/be.png" width="16px"/> | Nederlands (België, Belgique, Belgien) | [nl_BE](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_BE.po) | | 76%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/nl.png" width="16px"/> | Nederlands (Nederland) | [nl_NL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nl_NL.po) | [MHolkamp](mailto:mholkamp@users.noreply.github.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/no.png" width="16px"/> | Norwegian (Norge, Noreg) | [nb_NO](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/nb_NO.po) | [Mats Estensen](mailto:code@mxe.no) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ir.png" width="16px"/> | Persian | [fa](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/fa.po) | [Kourosh Firoozbakht](mailto:kourox@protonmail.com) | 53%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pl.png" width="16px"/> | Polski (Polska) | [pl_PL](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pl_PL.po) | [X3NO](mailto:X3NO@disroot.org) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/br.png" width="16px"/> | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_BR.po) | [Douglas Leão](mailto:djlsplays@gmail.com) | 85%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/pt.png" width="16px"/> | Português (Portugal) | [pt_PT](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/pt_PT.po) | [Diogo Caveiro](mailto:dcaveiro@yahoo.com) | 70%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ro.png" width="16px"/> | Română | [ro](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ro.po) | [Cristi Duluta](mailto:cristi.duluta@gmail.com) | 49%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/si.png" width="16px"/> | Slovenian (Slovenija) | [sl_SI](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sl_SI.po) | [Martin Korelič](mailto:martin.korelic@protonmail.com) | 78%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/se.png" width="16px"/> | Svenska | [sv](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sv.po) | [Jonatan Nyberg](mailto:jonatan@autistici.org) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/th.png" width="16px"/> | Thai (ประเทศไทย) | [th_TH](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/th_TH.po) | | 35%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/vn.png" width="16px"/> | Tiếng Việt | [vi](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/vi.po) | | 75%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tr.png" width="16px"/> | Türkçe (Türkiye) | [tr_TR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/tr_TR.po) | [Arda Kılıçdağı](mailto:arda@kilicdagi.com) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ua.png" width="16px"/> | Ukrainian (Україна) | [uk_UA](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/uk_UA.po) | [Vyacheslav Andreykiv](mailto:vandreykiv@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/gr.png" width="16px"/> | Ελληνικά (Ελλάδα) | [el_GR](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/el_GR.po) | [Harris Arvanitis](mailto:xaris@tuta.io) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/ru.png" width="16px"/> | Русский (Россия) | [ru_RU](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ru_RU.po) | [Dmitriy K](mailto:dmitry@atsip.ru) | 97%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/rs.png" width="16px"/> | српски језик (Србија) | [sr_RS](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/sr_RS.po) | | 63%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/cn.png" width="16px"/> | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_CN.po) | [qx100](mailto:ztymaxwell@gmail.com) | 98%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/tw.png" width="16px"/> | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/zh_TW.po) | [Kevin Hsu](mailto:kevin.hsu.hws@gmail.com) | 86%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/jp.png" width="16px"/> | 日本語 (日本) | [ja_JP](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ja_JP.po) | [genneko](mailto:genneko217@gmail.com) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 88%
|
||||
<img src="https://joplinapp.org/images/flags/country-4x3/kr.png" width="16px"/> | 한국어 | [ko](https://github.com/laurent22/joplin/blob/dev/packages/tools/locales/ko.po) | [Ji-Hyeon Gim](mailto:potatogim@potatogim.net) | 86%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Contributors
|
||||
|
||||
@@ -452,6 +452,8 @@ class Application extends BaseApplication {
|
||||
type: 'FOLDER_SELECT',
|
||||
id: Setting.value('activeFolderId'),
|
||||
});
|
||||
|
||||
this.startRotatingLogMaintenance(Setting.value('profileDir'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local');
|
||||
const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default;
|
||||
const envFromArgs = require('@joplin/lib/envFromArgs');
|
||||
const nodeSqlite = require('sqlite3');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
|
||||
const env = envFromArgs(process.argv);
|
||||
|
||||
@@ -67,6 +68,10 @@ function appVersion() {
|
||||
|
||||
shimInit({ sharp, keytar, appVersion, nodeSqlite });
|
||||
|
||||
const logger = new Logger();
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
initLib(logger);
|
||||
|
||||
const application = app();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"proper-lockfile": "4.1.2",
|
||||
"read-chunk": "2.1.0",
|
||||
"server-destroy": "1.0.1",
|
||||
"sharp": "0.32.2",
|
||||
"sharp": "0.32.4",
|
||||
"sprintf-js": "1.1.2",
|
||||
"sqlite3": "5.1.6",
|
||||
"string-padding": "1.0.2",
|
||||
@@ -66,7 +66,7 @@
|
||||
"terminal-kit": "3.0.0",
|
||||
"tkwidgets": "0.5.27",
|
||||
"url-parse": "1.5.10",
|
||||
"word-wrap": "1.2.3",
|
||||
"word-wrap": "1.2.5",
|
||||
"yargs-parser": "21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -174,7 +174,7 @@ export default class ElectronAppWrapper {
|
||||
// so that it can tell us if we can really close the app or not.
|
||||
// Search for "appClose" event for closing logic on renderer side.
|
||||
event.preventDefault();
|
||||
this.win_.webContents.send('appClose');
|
||||
if (this.win_) this.win_.webContents.send('appClose');
|
||||
} else {
|
||||
// If the renderer process has responded, check if we can close or not
|
||||
if (this.rendererProcessQuitReply_.canClose) {
|
||||
|
||||
@@ -566,6 +566,8 @@ class Application extends BaseApplication {
|
||||
|
||||
await SpellCheckerService.instance().initialize(new SpellCheckerServiceDriverNative());
|
||||
|
||||
this.startRotatingLogMaintenance(Setting.value('profileDir'));
|
||||
|
||||
// await populateDatabase(reg.db(), {
|
||||
// clearDatabase: true,
|
||||
// folderCount: 1000,
|
||||
|
||||
@@ -23,8 +23,8 @@ function onCheckEnded() {
|
||||
isCheckingForUpdate_ = false;
|
||||
}
|
||||
|
||||
async function fetchLatestRelease() {
|
||||
const response = await shim.fetch('https://api.github.com/repos/laurent22/joplin/releases');
|
||||
async function fetchLatestReleases() {
|
||||
const response = await shim.fetch('https://objects.joplinusercontent.com/r/releases');
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
@@ -76,8 +76,8 @@ export default async function checkForUpdates(inBackground: boolean, parentWindo
|
||||
logger.info(`Checking with options ${JSON.stringify(options)}`);
|
||||
|
||||
try {
|
||||
const releases = await fetchLatestRelease();
|
||||
const release = extractVersionInfo(releases, process.platform, options);
|
||||
const releases = await fetchLatestReleases();
|
||||
const release = extractVersionInfo(releases, process.platform, process.arch, shim.isPortable(), options);
|
||||
|
||||
logger.info(`Current version: ${packageInfo.version}`);
|
||||
logger.info(`Latest version: ${release.version}`);
|
||||
|
||||
@@ -12,13 +12,17 @@ const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const pathUtils = require('@joplin/lib/path-utils');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
import restart from '../../services/restart';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { getDefaultPluginsInstallState, updateDefaultPluginsInstallState } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import getDefaultPluginsInfo from '@joplin/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo';
|
||||
import JoplinCloudConfigScreen from '../JoplinCloudConfigScreen';
|
||||
import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButton';
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import StyledLink from '../style/StyledLink';
|
||||
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
|
||||
|
||||
const settingKeyToControl: any = {
|
||||
@@ -180,6 +184,34 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
if (section.name === 'sync') {
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
const statusStyle = { ...theme.textStyle, marginTop: 10 };
|
||||
const warningStyle = { ...theme.textStyle, color: theme.colorWarn };
|
||||
|
||||
// Don't show the missing password warning if the user just changed the sync target (but hasn't
|
||||
// saved yet).
|
||||
const matchesSavedTarget = settings['sync.target'] === this.props.settings['sync.target'];
|
||||
if (matchesSavedTarget && shouldShowMissingPasswordWarning(settings['sync.target'], settings)) {
|
||||
const openMissingPasswordFAQ = () =>
|
||||
bridge().openExternal('https://joplinapp.org/faq#why-did-my-sync-and-encryption-passwords-disappear-after-updating-joplin');
|
||||
|
||||
const macInfoLink = (
|
||||
<StyledLink href="#"
|
||||
onClick={openMissingPasswordFAQ}
|
||||
style={theme.linkStyle}
|
||||
>
|
||||
{_('Help')}
|
||||
</StyledLink>
|
||||
);
|
||||
|
||||
// The FAQ section related to missing passwords is specific to MacOS/ARM -- only show it
|
||||
// in that case.
|
||||
const showMacInfoLink = shim.isMac() && process.arch === 'arm64';
|
||||
|
||||
settingComps.push(
|
||||
<p key='missing-password-warning' style={warningStyle}>
|
||||
{_('Warning: Missing password.')}{' '}{showMacInfoLink ? macInfoLink : null}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
@@ -208,17 +240,11 @@ class ConfigScreenComponent extends React.Component<any, any> {
|
||||
const advancedSettingsSectionStyle = { display: 'none' };
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
const iconName = this.state.showAdvancedSettings ? 'fa fa-angle-down' : 'fa fa-angle-right';
|
||||
// const advancedSettingsButtonStyle = { ...theme.buttonStyle, marginBottom: 10 };
|
||||
advancedSettingsButton = (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Button
|
||||
level={ButtonLevel.Secondary}
|
||||
onClick={() => shared.advancedSettingsButton_click(this)}
|
||||
iconName={iconName}
|
||||
title={_('Show Advanced Settings')}
|
||||
/>
|
||||
</div>
|
||||
<ToggleAdvancedSettingsButton
|
||||
onClick={() => shared.advancedSettingsButton_click(this)}
|
||||
advancedSettingsVisible={this.state.showAdvancedSettings}
|
||||
/>
|
||||
);
|
||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import Button, { ButtonLevel } from '../../Button/Button';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
onClick: ()=> void;
|
||||
advancedSettingsVisible: boolean;
|
||||
}
|
||||
|
||||
const ToggleAdvancedSettingsButton: React.FunctionComponent<Props> = props => {
|
||||
const iconName = props.advancedSettingsVisible ? 'fa fa-angle-down' : 'fa fa-angle-right';
|
||||
return (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Button
|
||||
level={ButtonLevel.Secondary}
|
||||
onClick={props.onClick}
|
||||
iconName={iconName}
|
||||
title={_('Show Advanced Settings')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default ToggleAdvancedSettingsButton;
|
||||
@@ -10,12 +10,13 @@ import { MasterKeyEntity } from '@joplin/lib/services/e2ee/types';
|
||||
import { getEncryptionEnabled, masterKeyEnabled, SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils';
|
||||
import { getDefaultMasterKey, getMasterPasswordStatusMessage, masterPasswordIsValid, toggleAndSetupEncryption } from '@joplin/lib/services/e2ee/utils';
|
||||
import Button, { ButtonLevel } from '../Button/Button';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { PublicPrivateKeyPair } from '@joplin/lib/services/e2ee/ppk';
|
||||
import ToggleAdvancedSettingsButton from '../ConfigScreen/controls/ToggleAdvancedSettingsButton';
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
@@ -83,34 +84,6 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderReencryptData = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!props.shouldReencrypt) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\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.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{_('Re-encryption')}</h2>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMasterKey = (mk: MasterKeyEntity) => {
|
||||
const theme = themeStyle(props.themeId);
|
||||
|
||||
@@ -121,6 +94,12 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
const missingPasswordCellStyle = {
|
||||
...theme.textStyle,
|
||||
border: '3px solid',
|
||||
borderColor: theme.colorError,
|
||||
};
|
||||
|
||||
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
|
||||
const isActive = props.activeMasterKeyId === mk.id;
|
||||
const activeIcon = isActive ? '✔' : '';
|
||||
@@ -135,8 +114,15 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<td style={theme.textStyle}>
|
||||
<input type="password" style={passwordStyle} value={password} onChange={event => onInputPasswordChange(mk, event.target.value)} />{' '}
|
||||
<td style={missingPasswordCellStyle}>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={_('Enter password')}
|
||||
style={passwordStyle}
|
||||
value={password}
|
||||
onChange={event => onInputPasswordChange(mk, event.target.value)}
|
||||
/>
|
||||
{' '}
|
||||
<button style={theme.buttonStyle} onClick={() => onSavePasswordClick(mk, { ...props.passwords, ...inputPasswords })}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
@@ -239,7 +225,6 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
/>
|
||||
);
|
||||
const needUpgradeSection = renderNeedUpgradeSection();
|
||||
const reencryptDataSection = renderReencryptData();
|
||||
|
||||
return (
|
||||
<div className="section">
|
||||
@@ -254,7 +239,6 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{needUpgradeSection}
|
||||
{props.shouldReencrypt ? reencryptDataSection : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -338,6 +322,56 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
return nonExistingMasterKeySection;
|
||||
};
|
||||
|
||||
const renderReencryptData = () => {
|
||||
if (!shim.isElectron()) return null;
|
||||
if (!props.encryptionEnabled) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const buttonLabel = _('Re-encrypt data');
|
||||
|
||||
const intro = props.shouldReencrypt ? _('The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.') : _('You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.');
|
||||
|
||||
let t = `${intro}\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.', buttonLabel)}`;
|
||||
|
||||
t = t.replace(/\n\n/g, '</p><p>');
|
||||
t = t.replace(/\n/g, '<br>');
|
||||
t = `<p>${t}</p>`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{_('Re-encryption')}</h2>
|
||||
<p style={theme.textStyle} dangerouslySetInnerHTML={{ __html: t }}></p>
|
||||
<span style={{ marginRight: 10 }}>
|
||||
<button onClick={() => void reencryptData()} style={theme.buttonStyle}>{buttonLabel}</button>
|
||||
</span>
|
||||
|
||||
{ !props.shouldReencrypt ? null : <button onClick={() => dontReencryptData()} style={theme.buttonStyle}>{_('Ignore')}</button> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// If the user should re-encrypt, ensure that the section is visible initially.
|
||||
const [showAdvanced, setShowAdvanced] = useState<boolean>(props.shouldReencrypt);
|
||||
const toggleAdvanced = useCallback(() => {
|
||||
setShowAdvanced(!showAdvanced);
|
||||
}, [showAdvanced]);
|
||||
|
||||
const renderAdvancedSection = () => {
|
||||
const reEncryptSection = renderReencryptData();
|
||||
|
||||
if (!reEncryptSection) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ToggleAdvancedSettingsButton
|
||||
onClick={toggleAdvanced}
|
||||
advancedSettingsVisible={showAdvanced}/>
|
||||
{ showAdvanced ? reEncryptSection : null }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="config-screen-content">
|
||||
{renderDebugSection()}
|
||||
@@ -346,6 +380,7 @@ const EncryptionConfigScreen = (props: Props) => {
|
||||
{renderMasterKeySection(props.masterKeys.filter(mk => masterKeyEnabled(mk)), true)}
|
||||
{renderMasterKeySection(props.masterKeys.filter(mk => !masterKeyEnabled(mk)), false)}
|
||||
{renderNonExistingMasterKeysSection()}
|
||||
{renderAdvancedSection()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ interface State {
|
||||
|
||||
interface Props {
|
||||
message?: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<Props, State> {
|
||||
|
||||
@@ -20,6 +20,7 @@ import NoteListWrapper from '../NoteListWrapper/NoteListWrapper';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import { saveLayout, loadLayout } from '../ResizableLayout/utils/persist';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
|
||||
import produce from 'immer';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import bridge from '../../services/bridge';
|
||||
@@ -67,6 +68,7 @@ interface Props {
|
||||
shouldUpgradeSyncTarget: boolean;
|
||||
hasDisabledSyncItems: boolean;
|
||||
hasDisabledEncryptionItems: boolean;
|
||||
hasMissingSyncCredentials: boolean;
|
||||
showMissingMasterKeyMessage: boolean;
|
||||
showNeedUpgradingMasterKeyMessage: boolean;
|
||||
showShouldReencryptMessage: boolean;
|
||||
@@ -561,6 +563,16 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
const onViewSyncSettingsScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
props: {
|
||||
defaultSection: 'sync',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onViewPluginScreen = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -598,6 +610,12 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
_('Disable safe mode and restart'),
|
||||
onDisableSafeModeAndRestart
|
||||
);
|
||||
} else if (this.props.hasMissingSyncCredentials) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('The synchronisation password is missing.'),
|
||||
_('Set the password'),
|
||||
onViewSyncSettingsScreen
|
||||
);
|
||||
} else if (this.props.shouldUpgradeSyncTarget) {
|
||||
msg = this.renderNotificationMessage(
|
||||
_('The sync target needs to be upgraded before Joplin can sync. The operation may take a few minutes to complete and the app needs to be restarted. To proceed please click on the link.'),
|
||||
@@ -662,7 +680,7 @@ class MainScreenComponent extends React.Component<Props, State> {
|
||||
|
||||
public messageBoxVisible(props: Props = null) {
|
||||
if (!props) props = this.props;
|
||||
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
|
||||
return props.hasDisabledSyncItems || props.showMissingMasterKeyMessage || props.hasMissingSyncCredentials || props.showNeedUpgradingMasterKeyMessage || props.showShouldReencryptMessage || props.hasDisabledEncryptionItems || this.props.shouldUpgradeSyncTarget || props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || this.props.showInstallTemplatesPlugin;
|
||||
}
|
||||
|
||||
public registerCommands() {
|
||||
@@ -875,6 +893,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
showNeedUpgradingMasterKeyMessage: showNeedUpgradingEnabledMasterKeyMessage,
|
||||
showShouldReencryptMessage: state.settings['encryption.shouldReencrypt'] >= Setting.SHOULD_REENCRYPT_YES,
|
||||
shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO,
|
||||
hasMissingSyncCredentials: shouldShowMissingPasswordWarning(state.settings['sync.target'], state.settings),
|
||||
pluginsLegacy: state.pluginsLegacy,
|
||||
plugins: state.pluginService.plugins,
|
||||
pluginHtmlContents: state.pluginService.pluginHtmlContents,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import * as React from 'react';
|
||||
import NoteListUtils from './utils/NoteListUtils';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const { buildStyle } = require('@joplin/lib/theme');
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
@@ -9,8 +10,7 @@ interface MultiNoteActionsProps {
|
||||
themeId: number;
|
||||
selectedNoteIds: string[];
|
||||
notes: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
inConflictFolder: boolean;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHand
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { commandAttachFileToBody, getResourcesFromPasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { CommandValue } from '../../utils/types';
|
||||
import { usePrevious, cursorPositionToTextOffset } from './utils';
|
||||
@@ -99,7 +99,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addListItem = useCallback((string1, defaultText = '') => {
|
||||
const addListItem = useCallback((string1: string, defaultText = '') => {
|
||||
if (editorRef.current) {
|
||||
if (editorRef.current.somethingSelected()) {
|
||||
editorRef.current.wrapSelectionsByLine(string1);
|
||||
@@ -268,7 +268,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
}, [props.content, props.visiblePanes, addListItem, wrapSelectionWithStrings, setEditorPercentScroll, setViewerPercentScroll, resetScroll]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
if (editorRef.current) {
|
||||
editorRef.current.replaceSelection(resourceMds.join('\n'));
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function useScrollHandler(editorRef: any, webviewRef: any, onScro
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, []);
|
||||
|
||||
const editor_resize = useCallback((cm) => {
|
||||
const editor_resize = useCallback((cm: any) => {
|
||||
if (isCodeMirrorReady(cm)) {
|
||||
// This handler is called when resized and refreshed.
|
||||
// Only when resized, the scroll position is restored.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import { ScrollOptions, ScrollOptionTypes, EditorCommand, NoteBodyEditorProps, ResourceInfos } from '../../utils/types';
|
||||
import { resourcesStatus, commandAttachFileToBody, handlePasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import { resourcesStatus, commandAttachFileToBody, getResourcesFromPasteEvent, processPastedHtml, attachedResources } from '../../utils/resourceHandling';
|
||||
import useScroll from './utils/useScroll';
|
||||
import styles_ from './styles';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
@@ -1064,38 +1064,43 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
|
||||
// to be processed in various ways.
|
||||
event.preventDefault();
|
||||
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else {
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
const pastedText = event.clipboardData.getData('text/plain');
|
||||
|
||||
// event.clipboardData.getData('text/html') wraps the
|
||||
// content with <html><body></body></html>, which seems to
|
||||
// be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be
|
||||
// ignored. In this case,
|
||||
// event.clopboardData.getData('text/html') returns an empty
|
||||
// string, but the clipboard.readHTML() still returns the
|
||||
// formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
|
||||
// We should only process the images if there is no plain text or
|
||||
// HTML text in the clipboard. This is because certain applications,
|
||||
// such as Word, are going to add multiple versions of the copied
|
||||
// data to the clipboard - one with the text formatted as HTML, and
|
||||
// one with the text as an image. In that case, we need to ignore
|
||||
// the image and only process the HTML.
|
||||
|
||||
if (!pastedText && !pastedHtml) {
|
||||
const resourceMds = await getResourcesFromPasteEvent(event);
|
||||
if (resourceMds.length) {
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, resourceMds.join('\n'), markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
}
|
||||
} else {
|
||||
if (BaseItem.isMarkdownTag(pastedText)) { // Paste a link to a note
|
||||
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
|
||||
editor.insertContent(result.html);
|
||||
} else { // Paste regular text
|
||||
// event.clipboardData.getData('text/html') wraps the content with <html><body></body></html>,
|
||||
// which seems to be not supported in editor.insertContent().
|
||||
//
|
||||
// when pasting text with Ctrl+Shift+V, the format should be ignored.
|
||||
// In this case, event.clopboardData.getData('text/html') returns an empty string, but the clipboard.readHTML() still returns the formatted text.
|
||||
const pastedHtml = event.clipboardData.getData('text/html') ? clipboard.readHTML() : '';
|
||||
if (pastedHtml) { // Handles HTML
|
||||
const modifiedHtml = await processPastedHtml(pastedHtml);
|
||||
editor.insertContent(modifiedHtml);
|
||||
} else { // Handles plain text
|
||||
pasteAsPlainText(pastedText);
|
||||
}
|
||||
|
||||
// This code before was necessary to get undo working after
|
||||
// pasting but it seems it's no longer necessary, so
|
||||
// removing it for now. We also couldn't do it immediately
|
||||
// it seems, or else nothing is added to the stack, so do it
|
||||
// on the next frame.
|
||||
//
|
||||
// window.requestAnimationFrame(() =>
|
||||
// editor.undoManager.add()); onChangeHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -40,7 +40,7 @@ import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import NoteRevisionViewer from '../NoteRevisionViewer';
|
||||
import { readFromSettings } from '@joplin/lib/services/share/reducer';
|
||||
import { parseShareCache } from '@joplin/lib/services/share/reducer';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { ModelType } from '@joplin/lib/BaseModel';
|
||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||
@@ -78,6 +78,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
const { formNote, setFormNote, isNewNote, resourceInfos } = useFormNote({
|
||||
syncStarted: props.syncStarted,
|
||||
decryptionStarted: props.decryptionStarted,
|
||||
noteId: effectiveNoteId,
|
||||
isProvisional: props.isProvisional,
|
||||
titleInputRef: titleInputRef,
|
||||
@@ -286,11 +287,15 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
// }
|
||||
// }, [props.dispatch]);
|
||||
|
||||
const shareCache = useMemo(() => {
|
||||
return parseShareCache(props.shareCacheSetting);
|
||||
}, [props.shareCacheSetting]);
|
||||
|
||||
useAsyncEffect(async event => {
|
||||
if (!formNote.id) return;
|
||||
|
||||
try {
|
||||
const result = await itemIsReadOnly(BaseItem, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, formNote.id, props.syncUserId, props.shareCache);
|
||||
const result = await itemIsReadOnly(BaseItem, ModelType.Note, ItemChange.SOURCE_UNSPECIFIED, formNote.id, props.syncUserId, shareCache);
|
||||
if (event.cancelled) return;
|
||||
setIsReadOnly(result);
|
||||
} catch (error) {
|
||||
@@ -301,7 +306,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, [formNote.id, props.syncUserId, props.shareCache]);
|
||||
}, [formNote.id, props.syncUserId, shareCache]);
|
||||
|
||||
const onBodyWillChange = useCallback((event: any) => {
|
||||
handleProvisionalFlag();
|
||||
@@ -324,7 +329,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
const onMessage = useMessageHandler(scrollWhenReady, setScrollWhenReady, editorRef, setLocalSearchResultCount, props.dispatch, formNote);
|
||||
|
||||
const externalEditWatcher_noteChange = useCallback((event) => {
|
||||
const externalEditWatcher_noteChange = useCallback((event: any) => {
|
||||
if (event.id === formNote.id) {
|
||||
const newFormNote = {
|
||||
...formNote,
|
||||
@@ -337,7 +342,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [formNote]);
|
||||
|
||||
const onNotePropertyChange = useCallback((event) => {
|
||||
const onNotePropertyChange = useCallback((event: any) => {
|
||||
setFormNote(formNote => {
|
||||
if (formNote.id !== event.note.id) return formNote;
|
||||
|
||||
@@ -629,6 +634,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
isProvisional: state.provisionalNoteIds.includes(noteId),
|
||||
editorNoteStatuses: state.editorNoteStatuses,
|
||||
syncStarted: state.syncStarted,
|
||||
decryptionStarted: state.decryptionWorker?.state !== 'idle',
|
||||
themeId: state.settings.theme,
|
||||
richTextBannerDismissed: state.settings.richTextBannerDismissed,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
@@ -656,7 +662,7 @@ const mapStateToProps = (state: AppState) => {
|
||||
isSafeMode: state.settings.isSafeMode,
|
||||
useCustomPdfViewer: false,
|
||||
syncUserId: state.settings['sync.userId'],
|
||||
shareCache: readFromSettings(state),
|
||||
shareCacheSetting: state.settings['sync.shareCache'],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import Resource from '@joplin/lib/models/Resource';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
|
||||
import htmlUtils from '@joplin/lib/htmlUtils';
|
||||
import rendererHtmlUtils from '@joplin/renderer/htmlUtils';
|
||||
import rendererHtmlUtils, { extractHtmlBody } from '@joplin/renderer/htmlUtils';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { fileUriToPath } = require('@joplin/lib/urlUtils');
|
||||
const joplinRendererUtils = require('@joplin/renderer').utils;
|
||||
@@ -78,7 +78,7 @@ export async function commandAttachFileToBody(body: string, filePaths: string[]
|
||||
logger.info(`Attaching ${filePath}`);
|
||||
const newBody = await shim.attachFileToNoteBody(body, filePath, options.position, {
|
||||
createFileURL: options.createFileURL,
|
||||
resizeLargeImages: 'ask',
|
||||
resizeLargeImages: Setting.value('imageResizing'),
|
||||
});
|
||||
|
||||
if (!newBody) {
|
||||
@@ -107,7 +107,7 @@ export function resourcesStatus(resourceInfos: any) {
|
||||
return joplinRendererUtils.resourceStatusName(lowestIndex);
|
||||
}
|
||||
|
||||
export async function handlePasteEvent(event: any) {
|
||||
export async function getResourcesFromPasteEvent(event: any) {
|
||||
const output = [];
|
||||
const formats = clipboard.availableFormats();
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
@@ -176,9 +176,9 @@ export async function processPastedHtml(html: string) {
|
||||
}
|
||||
}
|
||||
|
||||
return rendererHtmlUtils.sanitizeHtml(
|
||||
return extractHtmlBody(rendererHtmlUtils.sanitizeHtml(
|
||||
htmlUtils.replaceImageUrls(html, (src: string) => {
|
||||
return mappedResources[src];
|
||||
})
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { State as ShareState } from '@joplin/lib/services/share/reducer';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export interface AllAssetsOptions {
|
||||
contentMaxWidthTarget?: string;
|
||||
@@ -16,11 +15,9 @@ export interface ToolbarButtonInfos {
|
||||
}
|
||||
|
||||
export interface NoteEditorProps {
|
||||
// style: any;
|
||||
noteId: string;
|
||||
themeId: number;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
selectedNoteIds: string[];
|
||||
selectedFolderId: string;
|
||||
notes: any[];
|
||||
@@ -28,6 +25,7 @@ export interface NoteEditorProps {
|
||||
isProvisional: boolean;
|
||||
editorNoteStatuses: any;
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
bodyEditor: string;
|
||||
notesParentType: string;
|
||||
selectedNoteTags: any[];
|
||||
@@ -46,7 +44,7 @@ export interface NoteEditorProps {
|
||||
contentMaxWidth: number;
|
||||
isSafeMode: boolean;
|
||||
useCustomPdfViewer: boolean;
|
||||
shareCache: ShareState;
|
||||
shareCacheSetting: string;
|
||||
syncUserId: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import useFormNote, { HookDependencies } from './useFormNote';
|
||||
|
||||
|
||||
describe('useFormNote', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should update note when decryption completes', async () => {
|
||||
const testNote = await Note.save({ title: 'Test Note!' });
|
||||
|
||||
const makeFormNoteProps = (syncStarted: boolean, decryptionStarted: boolean): HookDependencies => {
|
||||
return {
|
||||
syncStarted,
|
||||
decryptionStarted,
|
||||
noteId: testNote.id,
|
||||
isProvisional: false,
|
||||
titleInputRef: null,
|
||||
editorRef: null,
|
||||
onBeforeLoad: ()=>{},
|
||||
onAfterLoad: ()=>{},
|
||||
};
|
||||
};
|
||||
|
||||
const formNote = renderHook(props => useFormNote(props), {
|
||||
initialProps: makeFormNoteProps(true, false),
|
||||
});
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: testNote.title,
|
||||
});
|
||||
});
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_cipher_text: 'cipher_text',
|
||||
encryption_applied: 1,
|
||||
});
|
||||
|
||||
// Sync starting should cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 1,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
formNote.rerender(makeFormNoteProps(false, true));
|
||||
|
||||
await Note.save({
|
||||
id: testNote.id,
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
|
||||
// Ending decryption should also cause a re-render
|
||||
formNote.rerender(makeFormNoteProps(false, false));
|
||||
|
||||
await formNote.waitFor(() => {
|
||||
expect(formNote.result.current.formNote).toMatchObject({
|
||||
encryption_applied: 0,
|
||||
title: 'Test Note!',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,8 +18,9 @@ export interface OnLoadEvent {
|
||||
formNote: FormNote;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
export interface HookDependencies {
|
||||
syncStarted: boolean;
|
||||
decryptionStarted: boolean;
|
||||
noteId: string;
|
||||
isProvisional: boolean;
|
||||
titleInputRef: any;
|
||||
@@ -61,14 +62,21 @@ function resourceInfosChanged(a: ResourceInfos, b: ResourceInfos): boolean {
|
||||
}
|
||||
|
||||
export default function useFormNote(dependencies: HookDependencies) {
|
||||
const { syncStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad } = dependencies;
|
||||
const {
|
||||
syncStarted, decryptionStarted, noteId, isProvisional, titleInputRef, editorRef, onBeforeLoad, onAfterLoad,
|
||||
} = dependencies;
|
||||
|
||||
const [formNote, setFormNote] = useState<FormNote>(defaultFormNote());
|
||||
const [isNewNote, setIsNewNote] = useState(false);
|
||||
const prevSyncStarted = usePrevious(syncStarted);
|
||||
const prevDecryptionStarted = usePrevious(decryptionStarted);
|
||||
const previousNoteId = usePrevious(formNote.id);
|
||||
const [resourceInfos, setResourceInfos] = useState<ResourceInfos>({});
|
||||
|
||||
// Increasing the value of this counter cancels any ongoing note refreshes and starts
|
||||
// a new refresh.
|
||||
const [formNoteRefeshScheduled, setFormNoteRefreshScheduled] = useState<number>(0);
|
||||
|
||||
async function initNoteState(n: any) {
|
||||
let originalCss = '';
|
||||
|
||||
@@ -106,14 +114,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
|
||||
if (!prevSyncStarted) return () => {};
|
||||
if (syncStarted) return () => {};
|
||||
if (formNote.hasChanged) return () => {};
|
||||
if (formNoteRefeshScheduled <= 0) return () => {};
|
||||
|
||||
reg.logger().info('Sync has finished and note has never been changed - reloading it');
|
||||
|
||||
@@ -132,6 +133,7 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
}
|
||||
|
||||
await initNoteState(n);
|
||||
setFormNoteRefreshScheduled(0);
|
||||
};
|
||||
|
||||
void loadNote();
|
||||
@@ -139,8 +141,34 @@ export default function useFormNote(dependencies: HookDependencies) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [prevSyncStarted, syncStarted, formNote]);
|
||||
}, [formNoteRefeshScheduled, noteId]);
|
||||
|
||||
const refreshFormNote = useCallback(() => {
|
||||
// Increase the counter to cancel any ongoing refresh attempts
|
||||
// and start a new one.
|
||||
setFormNoteRefreshScheduled(formNoteRefeshScheduled + 1);
|
||||
}, [formNoteRefeshScheduled]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check that synchronisation has just finished - and
|
||||
// if the note has never been changed, we reload it.
|
||||
// If the note has already been changed, it's a conflict
|
||||
// that's already been handled by the synchronizer.
|
||||
const decryptionJustEnded = prevDecryptionStarted && !decryptionStarted;
|
||||
const syncJustEnded = prevSyncStarted && !syncStarted;
|
||||
|
||||
if (!decryptionJustEnded && !syncJustEnded) return;
|
||||
if (formNote.hasChanged) return;
|
||||
|
||||
// Refresh the form note.
|
||||
// This is kept separate from the above logic so that when prevSyncStarted is changed
|
||||
// from true to false, it doesn't cancel the note from loading.
|
||||
refreshFormNote();
|
||||
}, [
|
||||
prevSyncStarted, syncStarted,
|
||||
prevDecryptionStarted, decryptionStarted,
|
||||
formNote.hasChanged, refreshFormNote,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteId) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import ItemList from '../ItemList';
|
||||
const { connect } = require('react-redux');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { Props } from './types';
|
||||
import { Props } from './utils/types';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
|
||||
304
packages/app-desktop/gui/NoteList/NoteList2.tsx
Normal file
304
packages/app-desktop/gui/NoteList/NoteList2.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useMemo, useRef, useEffect } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import { ItemFlow, Props } from './utils/types';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import NoteListItem from '../NoteListItem/NoteListItem';
|
||||
import useRenderedNotes from './utils/useRenderedNotes';
|
||||
import useItemCss from './utils/useItemCss';
|
||||
import useOnContextMenu from '../NoteListItem/utils/useOnContextMenu';
|
||||
import useVisibleRange from './utils/useVisibleRange';
|
||||
import useScroll from './utils/useScroll';
|
||||
import useFocusNote from './utils/useFocusNote';
|
||||
import useOnNoteClick from './utils/useOnNoteClick';
|
||||
import useMoveNote from './utils/useMoveNote';
|
||||
import useOnKeyDown from './utils/useOnKeyDown';
|
||||
import * as focusElementNoteList from './commands/focusElementNoteList';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import useDragAndDrop from './utils/useDragAndDrop';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
// import defaultLeftToRightItemRenderer from './utils/defaultLeftToRightListRenderer';
|
||||
import defaultListRenderer from './utils/defaultListRenderer';
|
||||
const { connect } = require('react-redux');
|
||||
|
||||
const commands = {
|
||||
focusElementNoteList,
|
||||
};
|
||||
|
||||
const NoteList = (props: Props) => {
|
||||
const listRef = useRef(null);
|
||||
const itemRefs = useRef<Record<string, HTMLDivElement>>({});
|
||||
// const listRenderer = defaultLeftToRightItemRenderer;
|
||||
const listRenderer = defaultListRenderer;
|
||||
|
||||
const itemSize: Size = useMemo(() => {
|
||||
return {
|
||||
width: listRenderer.itemSize.width ? listRenderer.itemSize.width : props.size.width,
|
||||
height: listRenderer.itemSize.height,
|
||||
};
|
||||
}, [listRenderer.itemSize, props.size.width]);
|
||||
|
||||
const itemsPerLine = useMemo(() => {
|
||||
if (listRenderer.flow === ItemFlow.TopToBottom) {
|
||||
return 1;
|
||||
} else {
|
||||
return Math.max(1, Math.floor(props.size.width / itemSize.width));
|
||||
}
|
||||
}, [listRenderer.flow, props.size.width, itemSize.width]);
|
||||
|
||||
const { scrollTop, onScroll, makeItemIndexVisible } = useScroll(
|
||||
itemsPerLine,
|
||||
props.notes.length,
|
||||
itemSize,
|
||||
props.size,
|
||||
listRef
|
||||
);
|
||||
|
||||
const [startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount] = useVisibleRange(
|
||||
itemsPerLine,
|
||||
scrollTop,
|
||||
props.size,
|
||||
itemSize,
|
||||
props.notes.length
|
||||
);
|
||||
|
||||
const focusNote = useFocusNote(itemRefs);
|
||||
|
||||
const moveNote = useMoveNote(
|
||||
props.notesParentType,
|
||||
props.noteSortOrder,
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
props.uncompletedTodosOnTop,
|
||||
props.showCompletedTodos,
|
||||
props.notes
|
||||
);
|
||||
|
||||
const renderedNotes = useRenderedNotes(
|
||||
startNoteIndex,
|
||||
endNoteIndex,
|
||||
props.notes,
|
||||
props.selectedNoteIds,
|
||||
listRenderer,
|
||||
props.highlightedWords,
|
||||
props.watchedNoteFiles
|
||||
);
|
||||
|
||||
const noteItemStyle = useMemo(() => {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: itemSize.height,
|
||||
};
|
||||
}, [itemSize.height]);
|
||||
|
||||
const noteListStyle = useMemo(() => {
|
||||
return {
|
||||
width: props.size.width,
|
||||
height: props.size.height,
|
||||
};
|
||||
}, [props.size]);
|
||||
|
||||
const onNoteClick = useOnNoteClick(props.dispatch, focusNote);
|
||||
|
||||
const onKeyDown = useOnKeyDown(
|
||||
props.selectedNoteIds,
|
||||
moveNote,
|
||||
makeItemIndexVisible,
|
||||
focusNote,
|
||||
props.notes,
|
||||
props.dispatch,
|
||||
visibleItemCount,
|
||||
props.notes.length,
|
||||
listRenderer.flow,
|
||||
itemsPerLine
|
||||
);
|
||||
|
||||
useItemCss(listRenderer.itemCss);
|
||||
|
||||
useEffect(() => {
|
||||
CommandService.instance().registerRuntime(commands.focusElementNoteList.declaration.name, commands.focusElementNoteList.runtime(focusNote));
|
||||
return () => {
|
||||
CommandService.instance().unregisterRuntime(commands.focusElementNoteList.declaration.name);
|
||||
};
|
||||
}, [focusNote]);
|
||||
|
||||
const onItemContextMenu = useOnContextMenu(
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
props.notes,
|
||||
props.dispatch,
|
||||
props.watchedNoteFiles,
|
||||
props.plugins,
|
||||
props.customCss
|
||||
);
|
||||
|
||||
const { onDragStart, onDragOver, onDrop, dragOverTargetNoteIndex } = useDragAndDrop(props.parentFolderIsReadOnly,
|
||||
props.selectedNoteIds,
|
||||
props.selectedFolderId,
|
||||
listRef,
|
||||
scrollTop,
|
||||
itemSize,
|
||||
props.notesParentType,
|
||||
props.noteSortOrder,
|
||||
props.uncompletedTodosOnTop,
|
||||
props.showCompletedTodos,
|
||||
listRenderer.flow,
|
||||
itemsPerLine
|
||||
);
|
||||
|
||||
const previousSelectedNoteIds = usePrevious(props.selectedNoteIds, []);
|
||||
const previousNoteCount = usePrevious(props.notes.length, 0);
|
||||
const previousVisible = usePrevious(props.visible, false);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousSelectedNoteIds !== props.selectedNoteIds && props.selectedNoteIds.length === 1) {
|
||||
const id = props.selectedNoteIds[0];
|
||||
const doRefocus = props.notes.length < previousNoteCount && !props.focusedField;
|
||||
|
||||
for (let i = 0; i < props.notes.length; i++) {
|
||||
if (props.notes[i].id === id) {
|
||||
makeItemIndexVisible(i);
|
||||
if (doRefocus) {
|
||||
const ref = itemRefs.current[id];
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [makeItemIndexVisible, previousSelectedNoteIds, previousNoteCount, previousVisible, props.selectedNoteIds, props.notes, props.focusedField, props.visible]);
|
||||
|
||||
const highlightedWords = useMemo(() => {
|
||||
if (props.notesParentType === 'Search') {
|
||||
const query = BaseModel.byId(props.searches, props.selectedSearchId);
|
||||
if (query) return props.highlightedWords;
|
||||
}
|
||||
return [];
|
||||
}, [props.notesParentType, props.searches, props.selectedSearchId, props.highlightedWords]);
|
||||
|
||||
const renderEmptyList = () => {
|
||||
if (props.notes.length) return null;
|
||||
return <div className="emptylist">{props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
|
||||
};
|
||||
|
||||
const renderFiller = (key: string, style: React.CSSProperties) => {
|
||||
if (!props.notes.length) return null;
|
||||
if (style.height as number <= 0) return null;
|
||||
return <div key={key} style={style}></div>;
|
||||
};
|
||||
|
||||
const renderNotes = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
const output: JSX.Element[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
const note = props.notes[i];
|
||||
const renderedNote = renderedNotes[note.id];
|
||||
|
||||
output.push(
|
||||
<NoteListItem
|
||||
key={note.id}
|
||||
ref={el => itemRefs.current[note.id] = el}
|
||||
index={i}
|
||||
dragIndex={dragOverTargetNoteIndex}
|
||||
noteCount={props.notes.length}
|
||||
itemSize={itemSize}
|
||||
noteHtml={renderedNote ? renderedNote.html : ''}
|
||||
noteId={note.id}
|
||||
onChange={listRenderer.onChange}
|
||||
onClick={onNoteClick}
|
||||
onContextMenu={onItemContextMenu}
|
||||
onDragStart={onDragStart}
|
||||
onDragOver={onDragOver}
|
||||
style={noteItemStyle}
|
||||
highlightedWords={highlightedWords}
|
||||
isProvisional={props.provisionalNoteIds.includes(note.id)}
|
||||
flow={listRenderer.flow}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
const topFillerHeight = startLineIndex * itemSize.height;
|
||||
const bottomFillerHeight = (totalLineCount - endLineIndex - 1) * itemSize.height;
|
||||
|
||||
const fillerBaseStyle = useMemo(() => {
|
||||
// return { width: 'auto', border: '1px solid red', backgroundColor: 'green' };
|
||||
return { width: 'auto' };
|
||||
}, []);
|
||||
|
||||
const topFillerStyle = useMemo(() => {
|
||||
return { ...fillerBaseStyle, height: topFillerHeight };
|
||||
}, [fillerBaseStyle, topFillerHeight]);
|
||||
|
||||
const bottomFillerStyle = useMemo(() => {
|
||||
return { ...fillerBaseStyle, height: bottomFillerHeight };
|
||||
}, [fillerBaseStyle, bottomFillerHeight]);
|
||||
|
||||
const notesStyle = useMemo(() => {
|
||||
const output: React.CSSProperties = {};
|
||||
|
||||
if (listRenderer.flow === ItemFlow.LeftToRight) {
|
||||
output.flexFlow = 'row wrap';
|
||||
} else {
|
||||
output.flexDirection = 'column';
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [listRenderer.flow]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="note-list"
|
||||
style={noteListStyle}
|
||||
ref={listRef}
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
{renderEmptyList()}
|
||||
{renderFiller('top', topFillerStyle)}
|
||||
<div className="notes" style={notesStyle}>
|
||||
{renderNotes()}
|
||||
</div>
|
||||
{renderFiller('bottom', bottomFillerStyle)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NoteList);
|
||||
113
packages/app-desktop/gui/NoteList/NoteListSource.tsx
Normal file
113
packages/app-desktop/gui/NoteList/NoteListSource.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState, useRef, useCallback } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
import NoteListItem from '../NoteListItem';
|
||||
import styled from 'styled-components';
|
||||
import ItemList from '../ItemList';
|
||||
const { connect } = require('react-redux');
|
||||
import { Props } from './utils/types';
|
||||
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
|
||||
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
|
||||
const StyledRoot = styled.div``;
|
||||
|
||||
const NoteListComponent = (props: Props) => {
|
||||
const [width] = useState(0);
|
||||
|
||||
const itemHeight = 34;
|
||||
|
||||
const noteListRef = useRef(null);
|
||||
const itemListRef = useRef(null);
|
||||
|
||||
const style = useMemo(() => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback((item: any, index: number) => {
|
||||
return <NoteListItem
|
||||
key={item.id}
|
||||
style={style}
|
||||
item={item}
|
||||
index={index}
|
||||
themeId={props.themeId}
|
||||
width={width}
|
||||
height={itemHeight}
|
||||
dragItemIndex={0}
|
||||
highlightedWords={[]}
|
||||
isProvisional={props.provisionalNoteIds.includes(item.id)}
|
||||
isSelected={props.selectedNoteIds.indexOf(item.id) >= 0}
|
||||
isWatched={props.watchedNoteFiles.indexOf(item.id) < 0}
|
||||
itemCount={props.notes.length}
|
||||
onCheckboxClick={() => {}}
|
||||
onDragStart={()=>{}}
|
||||
onNoteDragOver={()=>{}}
|
||||
onTitleClick={() => {}}
|
||||
onContextMenu={() => {}}
|
||||
draggable={!props.parentFolderIsReadOnly}
|
||||
/>;
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
}, [style, props.themeId, width, itemHeight, props.provisionalNoteIds, props.selectedNoteIds, props.watchedNoteFiles,
|
||||
props.notes,
|
||||
props.notesParentType,
|
||||
props.searches,
|
||||
props.selectedSearchId,
|
||||
props.highlightedWords,
|
||||
props.parentFolderIsReadOnly,
|
||||
]);
|
||||
const renderItemList = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
return (
|
||||
<ItemList
|
||||
ref={itemListRef}
|
||||
disabled={props.isInsertingNotes}
|
||||
itemHeight={32}
|
||||
className={'note-list'}
|
||||
items={props.notes}
|
||||
style={props.size}
|
||||
itemRenderer={renderItem}
|
||||
onKeyDown={() => {}}
|
||||
onNoteDrop={()=>{}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (!props.size) throw new Error('props.size is required');
|
||||
|
||||
return (
|
||||
<StyledRoot ref={noteListRef}>
|
||||
{renderItemList()}
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
|
||||
const userId = state.settings['sync.userId'];
|
||||
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
themeId: state.settings.theme,
|
||||
notesParentType: state.notesParentType,
|
||||
searches: state.searches,
|
||||
selectedSearchId: state.selectedSearchId,
|
||||
watchedNoteFiles: state.watchedNoteFiles,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
plugins: state.pluginService.plugins,
|
||||
customCss: state.customCss,
|
||||
focusedField: state.focusedField,
|
||||
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NoteListComponent);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { stateUtils } from '@joplin/lib/reducer';
|
||||
import { itemAnchorRef } from '../NoteList';
|
||||
import { FocusNote } from '../utils/useFocusNote';
|
||||
|
||||
export const declaration: CommandDeclaration = {
|
||||
name: 'focusElementNoteList',
|
||||
@@ -9,15 +9,11 @@ export const declaration: CommandDeclaration = {
|
||||
parentLabel: () => _('Focus'),
|
||||
};
|
||||
|
||||
export const runtime = (): CommandRuntime => {
|
||||
export const runtime = (focusNote: FocusNote): CommandRuntime => {
|
||||
return {
|
||||
execute: async (context: CommandContext, noteId: string = null) => {
|
||||
noteId = noteId || stateUtils.selectedNoteId(context.state);
|
||||
|
||||
if (noteId) {
|
||||
const ref = itemAnchorRef(noteId);
|
||||
if (ref) ref.focus();
|
||||
}
|
||||
focusNote(noteId);
|
||||
},
|
||||
enabledCondition: 'noteListHasNotes',
|
||||
};
|
||||
|
||||
43
packages/app-desktop/gui/NoteList/style.scss
Normal file
43
packages/app-desktop/gui/NoteList/style.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
.note-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--joplin-background-color3);
|
||||
border-right: 1px solid var(--joplin-divider-color);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
> .notes {
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
> .emptylist {
|
||||
padding: 10px;
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
background-color: var(--joplin-background-color);
|
||||
font-family: var(--joplin-font-family);
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.note-list-item-wrapper {
|
||||
border-color: var(--joplin-color);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
> .dragcursor {
|
||||
background-color: var(--joplin-color);
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item-wrapper.-provisional {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
|
||||
export interface Props {
|
||||
themeId: any;
|
||||
selectedNoteIds: string[];
|
||||
notes: NoteEntity[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
watchedNoteFiles: any[];
|
||||
plugins: PluginStates;
|
||||
selectedFolderId: string;
|
||||
customCss: string;
|
||||
notesParentType: string;
|
||||
noteSortOrder: string;
|
||||
uncompletedTodosOnTop: boolean;
|
||||
showCompletedTodos: boolean;
|
||||
resizableLayoutEventEmitter: any;
|
||||
isInsertingNotes: boolean;
|
||||
folders: FolderEntity[];
|
||||
size: any;
|
||||
searches: any[];
|
||||
selectedSearchId: string;
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
parentFolderIsReadOnly: boolean;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import bridge from '../../../services/bridge';
|
||||
|
||||
const canManuallySortNotes = (notesParentType: string, noteSortOrder: string) => {
|
||||
if (notesParentType !== 'Folder') return false;
|
||||
|
||||
if (noteSortOrder !== 'order') {
|
||||
const doIt = bridge().showConfirmMessageBox(_('To manually sort the notes, the sort order must be changed to "%s" in the menu "%s" > "%s"', _('Custom order'), _('View'), _('Sort notes by')), {
|
||||
buttons: [_('Do it now'), _('Cancel')],
|
||||
});
|
||||
if (!doIt) return false;
|
||||
|
||||
Setting.setValue('notes.sortOrder.field', 'order');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default canManuallySortNotes;
|
||||
@@ -0,0 +1,165 @@
|
||||
import { MarkupLanguage, MarkupToHtml } from '@joplin/renderer';
|
||||
import { ItemFlow, ListRenderer } from './types';
|
||||
|
||||
interface Props {
|
||||
note: {
|
||||
id: string;
|
||||
title: string;
|
||||
is_todo: number;
|
||||
todo_completed: number;
|
||||
body: string;
|
||||
};
|
||||
item: {
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
selected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultLeftToRightItemRenderer: ListRenderer = {
|
||||
flow: ItemFlow.LeftToRight,
|
||||
|
||||
itemSize: {
|
||||
width: 150,
|
||||
height: 150,
|
||||
},
|
||||
|
||||
dependencies: [
|
||||
'item.selected',
|
||||
'item.size.width',
|
||||
'item.size.height',
|
||||
'note.body',
|
||||
'note.id',
|
||||
'note.is_shared',
|
||||
'note.is_todo',
|
||||
'note.isWatched',
|
||||
'note.titleHtml',
|
||||
'note.todo_completed',
|
||||
],
|
||||
|
||||
itemCss: // css
|
||||
`
|
||||
&:before {
|
||||
content: '';
|
||||
border-bottom: 1px solid var(--joplin-divider-color);
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
> .content.-selected {
|
||||
background-color: var(--joplin-selected-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
align-items: flex-start;
|
||||
overflow-y: hidden;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
|
||||
> .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> input {
|
||||
margin: 0px 10px 1px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
|
||||
> .checkbox {
|
||||
margin: 0 6px 0 0;
|
||||
}
|
||||
|
||||
> .watchedicon {
|
||||
display: none;
|
||||
padding-right: 4px;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
|
||||
> .titlecontent {
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
> .preview {
|
||||
overflow-y: hidden;
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-shared {
|
||||
> .title {
|
||||
color: var(--joplin-color-warn3);
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-completed {
|
||||
> .title {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-watched {
|
||||
> .title {
|
||||
> .watchedicon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
itemTemplate: // html
|
||||
`
|
||||
<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}">
|
||||
<div style="width: {{titleWidth}}px;" class="title" data-id="{{note.id}}">
|
||||
{{#note.is_todo}}
|
||||
<input class="checkbox" data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>
|
||||
{{/note.is_todo}}
|
||||
<i class="watchedicon fa fa-share-square"></i>
|
||||
<div class="titlecontent">{{{note.titleHtml}}}</div>
|
||||
</div>
|
||||
<div class="preview">{{notePreview}}</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
onRenderNote: async (props: Props) => {
|
||||
const markupToHtml_ = new MarkupToHtml();
|
||||
|
||||
return {
|
||||
...props,
|
||||
notePreview: markupToHtml_.stripMarkup(MarkupLanguage.Markdown, props.note.body).substring(0, 200),
|
||||
titleWidth: props.item.size.width - 32,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultLeftToRightItemRenderer;
|
||||
134
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.ts
Normal file
134
packages/app-desktop/gui/NoteList/utils/defaultListRenderer.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { ItemFlow, ListRenderer } from './types';
|
||||
|
||||
interface Props {
|
||||
note: {
|
||||
id: string;
|
||||
title: string;
|
||||
is_todo: number;
|
||||
todo_completed: number;
|
||||
};
|
||||
item: {
|
||||
size: {
|
||||
height: number;
|
||||
};
|
||||
selected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultItemRenderer: ListRenderer = {
|
||||
flow: ItemFlow.TopToBottom,
|
||||
|
||||
itemSize: {
|
||||
width: 0,
|
||||
height: 34,
|
||||
},
|
||||
|
||||
dependencies: [
|
||||
'item.selected',
|
||||
'item.size.height',
|
||||
'note.id',
|
||||
'note.is_shared',
|
||||
'note.is_todo',
|
||||
'note.isWatched',
|
||||
'note.titleHtml',
|
||||
'note.todo_completed',
|
||||
],
|
||||
|
||||
itemCss: // css
|
||||
`
|
||||
&:before {
|
||||
content: '';
|
||||
border-bottom: 1px solid var(--joplin-divider-color);
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 5%;
|
||||
}
|
||||
|
||||
> .content.-selected {
|
||||
background-color: var(--joplin-selected-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--joplin-background-color-hover3);
|
||||
}
|
||||
|
||||
> .content {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 16px;
|
||||
|
||||
> .checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> input {
|
||||
margin: 0px 10px 1px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
font-family: var(--joplin-font-family);
|
||||
font-size: var(--joplin-font-size);
|
||||
text-decoration: none;
|
||||
color: var(--joplin-color);
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 0%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> .watchedicon {
|
||||
display: none;
|
||||
padding-right: 4px;
|
||||
color: var(--joplin-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-shared {
|
||||
> .title {
|
||||
color: var(--joplin-color-warn3);
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-completed {
|
||||
> .title {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
> .content.-watched {
|
||||
> .title {
|
||||
> .watchedicon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
itemTemplate: // html
|
||||
`
|
||||
<div class="content {{#item.selected}}-selected{{/item.selected}} {{#note.is_shared}}-shared{{/note.is_shared}} {{#note.todo_completed}}-completed{{/note.todo_completed}} {{#note.isWatched}}-watched{{/note.isWatched}}">
|
||||
{{#note.is_todo}}
|
||||
<div class="checkbox">
|
||||
<input data-id="todo-checkbox" type="checkbox" {{#note.todo_completed}}checked="checked"{{/note.todo_completed}}>
|
||||
</div>
|
||||
{{/note.is_todo}}
|
||||
<div class="title" data-id="{{note.id}}">
|
||||
<i class="watchedicon fa fa-share-square"></i>
|
||||
<span>{{{note.titleHtml}}}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
onRenderNote: async (props: Props) => {
|
||||
return props;
|
||||
},
|
||||
};
|
||||
|
||||
export default defaultItemRenderer;
|
||||
45
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.ts
Normal file
45
packages/app-desktop/gui/NoteList/utils/getNoteTitleHtml.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { htmlentities } from '@joplin/utils/html';
|
||||
const Mark = require('mark.js/dist/mark.min.js');
|
||||
const markJsUtils = require('@joplin/lib/markJsUtils');
|
||||
const { replaceRegexDiacritics, pregQuote } = require('@joplin/lib/string-utils');
|
||||
|
||||
const getNoteTitleHtml = (highlightedWords: string[], displayTitle: string) => {
|
||||
if (highlightedWords.length) {
|
||||
const titleElement = document.createElement('span');
|
||||
titleElement.textContent = displayTitle;
|
||||
const mark = new Mark(titleElement, {
|
||||
exclude: ['img'],
|
||||
acrossElements: true,
|
||||
});
|
||||
|
||||
mark.unmark();
|
||||
|
||||
try {
|
||||
for (const wordToBeHighlighted of highlightedWords) {
|
||||
markJsUtils.markKeyword(mark, wordToBeHighlighted, {
|
||||
pregQuote: pregQuote,
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'SyntaxError') {
|
||||
throw error;
|
||||
}
|
||||
// An error of 'Regular expression too large' might occour in the markJs library
|
||||
// when the input is really big, this catch is here to avoid the application crashing
|
||||
// https://github.com/laurent22/joplin/issues/7634
|
||||
// console.error('Error while trying to highlight words from search: ', error);
|
||||
}
|
||||
|
||||
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement
|
||||
// is a span tag that we created and that contains data that's been inserted as plain text
|
||||
// with `textContent` so it cannot contain any XSS attacks. We use this feature because
|
||||
// mark.js can only deal with DOM elements.
|
||||
// https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
|
||||
return titleElement.outerHTML;
|
||||
} else {
|
||||
return htmlentities(displayTitle);
|
||||
}
|
||||
};
|
||||
|
||||
export default getNoteTitleHtml;
|
||||
51
packages/app-desktop/gui/NoteList/utils/prepareViewProps.ts
Normal file
51
packages/app-desktop/gui/NoteList/utils/prepareViewProps.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ListRendererDepependency } from './types';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean, noteTitleHtml: string, noteIsWatched: boolean) => {
|
||||
const output: any = {};
|
||||
|
||||
for (const dep of dependencies) {
|
||||
|
||||
if (dep.startsWith('note.')) {
|
||||
const splitted = dep.split('.');
|
||||
if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
const propName = splitted.pop();
|
||||
if (!output.note) output.note = {};
|
||||
if (dep === 'note.titleHtml') {
|
||||
output.note.titleHtml = noteTitleHtml;
|
||||
} else if (dep === 'note.isWatched') {
|
||||
output.note.isWatched = noteIsWatched;
|
||||
} else {
|
||||
// The notes in the state only contain the properties defined in
|
||||
// Note.previewFields(). It means that if a view request a
|
||||
// property not present there, we need to load the full note.
|
||||
// One such missing property is the note body, which we don't
|
||||
// load by default.
|
||||
if (!(propName in note)) note = await Note.load(note.id);
|
||||
if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
output.note[propName] = (note as any)[propName];
|
||||
}
|
||||
}
|
||||
|
||||
if (dep.startsWith('item.size.')) {
|
||||
const splitted = dep.split('.');
|
||||
if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
const propName = splitted.pop();
|
||||
if (!output.item) output.item = {};
|
||||
if (!output.item.size) output.item.size = {};
|
||||
if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`);
|
||||
output.item.size[propName] = (itemSize as any)[propName];
|
||||
}
|
||||
|
||||
if (dep === 'item.selected') {
|
||||
if (!output.item) output.item = {};
|
||||
output.item.selected = selected;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export default prepareViewProps;
|
||||
64
packages/app-desktop/gui/NoteList/utils/types.ts
Normal file
64
packages/app-desktop/gui/NoteList/utils/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { FolderEntity, ItemRendererDatabaseDependency, NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export interface Props {
|
||||
themeId: any;
|
||||
selectedNoteIds: string[];
|
||||
notes: NoteEntity[];
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
selectedFolderId: string;
|
||||
customCss: string;
|
||||
notesParentType: string;
|
||||
noteSortOrder: string;
|
||||
uncompletedTodosOnTop: boolean;
|
||||
showCompletedTodos: boolean;
|
||||
resizableLayoutEventEmitter: any;
|
||||
isInsertingNotes: boolean;
|
||||
folders: FolderEntity[];
|
||||
size: Size;
|
||||
searches: any[];
|
||||
selectedSearchId: string;
|
||||
highlightedWords: string[];
|
||||
provisionalNoteIds: string[];
|
||||
visible: boolean;
|
||||
focusedField: string;
|
||||
parentFolderIsReadOnly: boolean;
|
||||
}
|
||||
|
||||
export enum ItemFlow {
|
||||
TopToBottom = 'topToBottom',
|
||||
LeftToRight = 'leftToRight',
|
||||
}
|
||||
|
||||
export type RenderNoteView = Record<string, any>;
|
||||
|
||||
export interface OnChangeEvent {
|
||||
elementId: string;
|
||||
value: any;
|
||||
noteId: string;
|
||||
}
|
||||
|
||||
export type OnRenderNoteHandler = (props: any)=> Promise<RenderNoteView>;
|
||||
export type OnChangeHandler = (event: OnChangeEvent)=> Promise<void>;
|
||||
|
||||
export type ListRendererDepependency =
|
||||
ItemRendererDatabaseDependency |
|
||||
'item.size.width' |
|
||||
'item.size.height' |
|
||||
'item.selected' |
|
||||
'note.titleHtml' |
|
||||
'note.isWatched';
|
||||
|
||||
export interface ListRenderer {
|
||||
flow: ItemFlow;
|
||||
itemSize: Size;
|
||||
itemCss?: string;
|
||||
dependencies: ListRendererDepependency[];
|
||||
itemTemplate: string;
|
||||
onRenderNote: OnRenderNoteHandler;
|
||||
onChange?: OnChangeHandler;
|
||||
}
|
||||
102
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.ts
Normal file
102
packages/app-desktop/gui/NoteList/utils/useDragAndDrop.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, DragEventHandler, MutableRefObject, useState, useEffect } from 'react';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import canManuallySortNotes from './canManuallySortNotes';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { ItemFlow } from './types';
|
||||
|
||||
const useDragAndDrop = (
|
||||
parentFolderIsReadOnly: boolean,
|
||||
selectedNoteIds: string[],
|
||||
selectedFolderId: string,
|
||||
listRef: MutableRefObject<HTMLDivElement>,
|
||||
scrollTop: number,
|
||||
itemSize: Size,
|
||||
notesParentType: string,
|
||||
noteSortOrder: string,
|
||||
uncompletedTodosOnTop: boolean,
|
||||
showCompletedTodos: boolean,
|
||||
flow: ItemFlow,
|
||||
itemsPerLine: number
|
||||
) => {
|
||||
const [dragOverTargetNoteIndex, setDragOverTargetNoteIndex] = useState(null);
|
||||
|
||||
const onGlobalDrop = useCallback(() => {
|
||||
setDragOverTargetNoteIndex(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('dragend', onGlobalDrop);
|
||||
return () => {
|
||||
document.removeEventListener('dragend', onGlobalDrop);
|
||||
};
|
||||
}, [onGlobalDrop]);
|
||||
|
||||
const onDragStart: DragEventHandler = useCallback(event => {
|
||||
if (parentFolderIsReadOnly) return false;
|
||||
|
||||
let noteIds = [];
|
||||
|
||||
// Here there is two cases:
|
||||
// - If multiple notes are selected, we drag the group
|
||||
// - If only one note is selected, we drag the note that was clicked on
|
||||
// (which might be different from the currently selected note)
|
||||
if (selectedNoteIds.length >= 2) {
|
||||
noteIds = selectedNoteIds;
|
||||
} else {
|
||||
const clickedNoteId = event.currentTarget.getAttribute('data-id');
|
||||
if (clickedNoteId) noteIds.push(clickedNoteId);
|
||||
}
|
||||
|
||||
if (!noteIds.length) return false;
|
||||
|
||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||
event.dataTransfer.clearData();
|
||||
event.dataTransfer.setData('text/x-jop-note-ids', JSON.stringify(noteIds));
|
||||
return true;
|
||||
}, [parentFolderIsReadOnly, selectedNoteIds]);
|
||||
|
||||
|
||||
const dragTargetNoteIndex = useCallback((event: React.DragEvent) => {
|
||||
const rect = listRef.current.getBoundingClientRect();
|
||||
const lineIndexFloat = (event.clientY - rect.top + scrollTop) / itemSize.height;
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
return Math.abs(Math.round(lineIndexFloat));
|
||||
} else {
|
||||
const lineIndex = Math.floor(lineIndexFloat);
|
||||
const rowIndexFloat = (event.clientX - rect.left) / itemSize.width;
|
||||
const rowIndex = Math.round(rowIndexFloat);
|
||||
return lineIndex * itemsPerLine + rowIndex;
|
||||
}
|
||||
}, [listRef, itemSize, scrollTop, flow, itemsPerLine]);
|
||||
|
||||
const onDragOver: DragEventHandler = useCallback(event => {
|
||||
if (notesParentType !== 'Folder') return;
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
|
||||
if (dt.types.indexOf('text/x-jop-note-ids') >= 0) {
|
||||
event.preventDefault();
|
||||
const newIndex = dragTargetNoteIndex(event);
|
||||
if (dragOverTargetNoteIndex === newIndex) return;
|
||||
setDragOverTargetNoteIndex(newIndex);
|
||||
}
|
||||
}, [notesParentType, dragTargetNoteIndex, dragOverTargetNoteIndex]);
|
||||
|
||||
const onDrop: DragEventHandler = useCallback(async (event: any) => {
|
||||
// TODO: check that parent type is folder
|
||||
if (!canManuallySortNotes(notesParentType, noteSortOrder)) return;
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
setDragOverTargetNoteIndex(null);
|
||||
|
||||
const targetNoteIndex = dragTargetNoteIndex(event);
|
||||
const noteIds: string[] = JSON.parse(dt.getData('text/x-jop-note-ids'));
|
||||
|
||||
await Note.insertNotesAt(selectedFolderId, noteIds, targetNoteIndex, uncompletedTodosOnTop, showCompletedTodos);
|
||||
}, [notesParentType, dragTargetNoteIndex, noteSortOrder, selectedFolderId, uncompletedTodosOnTop, showCompletedTodos]);
|
||||
|
||||
return { onDragStart, onDragOver, onDrop, dragOverTargetNoteIndex };
|
||||
};
|
||||
|
||||
export default useDragAndDrop;
|
||||
33
packages/app-desktop/gui/NoteList/utils/useFocusNote.ts
Normal file
33
packages/app-desktop/gui/NoteList/utils/useFocusNote.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { useRef, useCallback, MutableRefObject } from 'react';
|
||||
|
||||
export type FocusNote = (noteId: string)=> void;
|
||||
|
||||
const useFocusNote = (itemRefs: MutableRefObject<Record<string, HTMLDivElement>>) => {
|
||||
const focusItemIID = useRef(null);
|
||||
|
||||
const focusNote: FocusNote = useCallback((noteId: string) => {
|
||||
// - We need to focus the item manually otherwise focus might be lost when the
|
||||
// list is scrolled and items within it are being rebuilt.
|
||||
// - We need to use an interval because when leaving the arrow pressed, the rendering
|
||||
// of items might lag behind and so the ref is not yet available at this point.
|
||||
|
||||
if (!itemRefs.current[noteId]) {
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
focusItemIID.current = shim.setInterval(() => {
|
||||
if (itemRefs.current[noteId]) {
|
||||
itemRefs.current[noteId].focus();
|
||||
shim.clearInterval(focusItemIID.current);
|
||||
focusItemIID.current = null;
|
||||
}
|
||||
}, 10);
|
||||
} else {
|
||||
if (focusItemIID.current) shim.clearInterval(focusItemIID.current);
|
||||
itemRefs.current[noteId].focus();
|
||||
}
|
||||
}, [itemRefs]);
|
||||
|
||||
return focusNote;
|
||||
};
|
||||
|
||||
export default useFocusNote;
|
||||
19
packages/app-desktop/gui/NoteList/utils/useItemCss.ts
Normal file
19
packages/app-desktop/gui/NoteList/utils/useItemCss.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useItemCss = (itemCss: string) => {
|
||||
useEffect(() => {
|
||||
const element = document.createElement('style');
|
||||
element.setAttribute('type', 'text/css');
|
||||
element.appendChild(document.createTextNode(`
|
||||
.note-list-item {
|
||||
${itemCss};
|
||||
}
|
||||
`));
|
||||
document.head.appendChild(element);
|
||||
return () => {
|
||||
element.remove();
|
||||
};
|
||||
}, [itemCss]);
|
||||
};
|
||||
|
||||
export default useItemCss;
|
||||
25
packages/app-desktop/gui/NoteList/utils/useMoveNote.ts
Normal file
25
packages/app-desktop/gui/NoteList/utils/useMoveNote.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback } from 'react';
|
||||
import canManuallySortNotes from './canManuallySortNotes';
|
||||
|
||||
const useMoveNote = (notesParentType: string, noteSortOrder: string, selectedNoteIds: string[], selectedFolderId: string, uncompletedTodosOnTop: boolean, showCompletedTodos: boolean, notes: NoteEntity[]) => {
|
||||
const moveNote = useCallback((direction: number, inc: number) => {
|
||||
if (!canManuallySortNotes(notesParentType, noteSortOrder)) return;
|
||||
|
||||
const noteId = selectedNoteIds[0];
|
||||
let targetNoteIndex = BaseModel.modelIndexById(notes, noteId);
|
||||
if ((direction === 1)) {
|
||||
targetNoteIndex += inc + 1;
|
||||
}
|
||||
if ((direction === -1)) {
|
||||
targetNoteIndex -= inc;
|
||||
}
|
||||
void Note.insertNotesAt(selectedFolderId, selectedNoteIds, targetNoteIndex, uncompletedTodosOnTop, showCompletedTodos);
|
||||
}, [selectedFolderId, noteSortOrder, notes, notesParentType, selectedNoteIds, uncompletedTodosOnTop, showCompletedTodos]);
|
||||
|
||||
return moveNote;
|
||||
};
|
||||
|
||||
export default useMoveNote;
|
||||
150
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts
Normal file
150
packages/app-desktop/gui/NoteList/utils/useOnKeyDown.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as React from 'react';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { FocusNote } from './useFocusNote';
|
||||
import { ItemFlow } from './types';
|
||||
import { KeyboardEventKey } from '@joplin/lib/dom';
|
||||
|
||||
const useOnKeyDown = (
|
||||
selectedNoteIds: string[],
|
||||
moveNote: (direction: number, inc: number)=> void,
|
||||
makeItemIndexVisible: (itemIndex: number)=> void,
|
||||
focusNote: FocusNote,
|
||||
notes: NoteEntity[],
|
||||
dispatch: Dispatch,
|
||||
visibleItemCount: number,
|
||||
noteCount: number,
|
||||
flow: ItemFlow,
|
||||
itemsPerLine: number
|
||||
) => {
|
||||
const scrollNoteIndex = useCallback((visibleItemCount: number, key: KeyboardEventKey, ctrlKey: boolean, metaKey: boolean, noteIndex: number) => {
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
if (key === 'PageUp') {
|
||||
noteIndex -= (visibleItemCount - 1);
|
||||
} else if (key === 'PageDown') {
|
||||
noteIndex += (visibleItemCount - 1);
|
||||
} else if ((key === 'End' && ctrlKey) || (key === 'ArrowDown' && metaKey)) {
|
||||
noteIndex = noteCount - 1;
|
||||
} else if ((key === 'Home' && ctrlKey) || (key === 'ArrowUp' && metaKey)) {
|
||||
noteIndex = 0;
|
||||
} else if (key === 'ArrowUp' && !metaKey) {
|
||||
noteIndex -= 1;
|
||||
} else if (key === 'ArrowDown' && !metaKey) {
|
||||
noteIndex += 1;
|
||||
}
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > noteCount - 1) noteIndex = noteCount - 1;
|
||||
}
|
||||
|
||||
if (flow === ItemFlow.LeftToRight) {
|
||||
if (key === 'PageUp') {
|
||||
noteIndex -= (visibleItemCount - itemsPerLine);
|
||||
} else if (key === 'PageDown') {
|
||||
noteIndex += (visibleItemCount - itemsPerLine);
|
||||
} else if ((key === 'End' && ctrlKey) || (key === 'ArrowDown' && metaKey)) {
|
||||
noteIndex = noteCount - 1;
|
||||
} else if ((key === 'Home' && ctrlKey) || (key === 'ArrowUp' && metaKey)) {
|
||||
noteIndex = 0;
|
||||
} else if (key === 'ArrowUp' && !metaKey) {
|
||||
noteIndex -= itemsPerLine;
|
||||
} else if (key === 'ArrowDown' && !metaKey) {
|
||||
noteIndex += itemsPerLine;
|
||||
} else if (key === 'ArrowLeft' && !metaKey) {
|
||||
noteIndex -= 1;
|
||||
} else if (key === 'ArrowRight' && !metaKey) {
|
||||
noteIndex += 1;
|
||||
}
|
||||
if (noteIndex < 0) noteIndex = 0;
|
||||
if (noteIndex > noteCount - 1) noteIndex = noteCount - 1;
|
||||
}
|
||||
|
||||
return noteIndex;
|
||||
}, [noteCount, flow, itemsPerLine]);
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(async (event) => {
|
||||
const noteIds = selectedNoteIds;
|
||||
const key = event.key as KeyboardEventKey;
|
||||
|
||||
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(key) && event.altKey) {
|
||||
if (flow === ItemFlow.TopToBottom) {
|
||||
await moveNote(key === 'ArrowDown' ? 1 : -1, 1);
|
||||
} else {
|
||||
if (key === 'ArrowRight') {
|
||||
await moveNote(1, 1);
|
||||
} else if (key === 'ArrowLeft') {
|
||||
await moveNote(-1, 1);
|
||||
} else if (key === 'ArrowUp') {
|
||||
await moveNote(-1, itemsPerLine);
|
||||
} else if (key === 'ArrowDown') {
|
||||
await moveNote(1, itemsPerLine);
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
} else if (noteIds.length > 0 && (key === 'ArrowDown' || key === 'ArrowUp' || key === 'ArrowLeft' || key === 'ArrowRight' || key === 'PageDown' || key === 'PageUp' || key === 'End' || key === 'Home')) {
|
||||
const noteId = noteIds[0];
|
||||
let noteIndex = BaseModel.modelIndexById(notes, noteId);
|
||||
|
||||
noteIndex = scrollNoteIndex(visibleItemCount, key, event.ctrlKey, event.metaKey, noteIndex);
|
||||
|
||||
const newSelectedNote = notes[noteIndex];
|
||||
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: newSelectedNote.id,
|
||||
});
|
||||
|
||||
makeItemIndexVisible(noteIndex);
|
||||
|
||||
focusNote(newSelectedNote.id);
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (noteIds.length && (key === 'Delete' || (key === 'Backspace' && event.metaKey))) {
|
||||
event.preventDefault();
|
||||
void CommandService.instance().execute('deleteNote', noteIds);
|
||||
}
|
||||
|
||||
if (noteIds.length && key === ' ') {
|
||||
event.preventDefault();
|
||||
|
||||
const selectedNotes = BaseModel.modelsByIds(notes, noteIds);
|
||||
const todos = selectedNotes.filter((n: any) => !!n.is_todo);
|
||||
if (!todos.length) return;
|
||||
|
||||
for (let i = 0; i < todos.length; i++) {
|
||||
const toggledTodo = Note.toggleTodoCompleted(todos[i]);
|
||||
await Note.save(toggledTodo);
|
||||
}
|
||||
|
||||
focusNote(todos[0].id);
|
||||
}
|
||||
|
||||
if (key === 'Tab') {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.shiftKey) {
|
||||
void CommandService.instance().execute('focusElement', 'sideBar');
|
||||
} else {
|
||||
void CommandService.instance().execute('focusElement', 'noteTitle');
|
||||
}
|
||||
}
|
||||
|
||||
if (key.toUpperCase() === 'A' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_ALL',
|
||||
});
|
||||
}
|
||||
}, [moveNote, focusNote, visibleItemCount, scrollNoteIndex, makeItemIndexVisible, notes, selectedNoteIds, dispatch, flow, itemsPerLine]);
|
||||
|
||||
|
||||
return onKeyDown;
|
||||
};
|
||||
|
||||
export default useOnKeyDown;
|
||||
41
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.ts
Normal file
41
packages/app-desktop/gui/NoteList/utils/useOnNoteClick.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { FocusNote } from './useFocusNote';
|
||||
|
||||
const useOnNoteClick = (dispatch: Dispatch, focusNote: FocusNote) => {
|
||||
const onNoteClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const noteId = event.currentTarget.getAttribute('data-id');
|
||||
|
||||
const targetTagName = event.target ? (event.target as any).tagName : '';
|
||||
|
||||
// If we are for example on a checkbox, don't process the click since it
|
||||
// should be handled by the checkbox onChange handler.
|
||||
if (['INPUT'].includes(targetTagName)) return;
|
||||
|
||||
focusNote(noteId);
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_TOGGLE',
|
||||
id: noteId,
|
||||
});
|
||||
} else if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT_EXTEND',
|
||||
id: noteId,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: noteId,
|
||||
});
|
||||
}
|
||||
}, [dispatch, focusNote]);
|
||||
|
||||
return onNoteClick;
|
||||
};
|
||||
|
||||
export default useOnNoteClick;
|
||||
82
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.ts
Normal file
82
packages/app-desktop/gui/NoteList/utils/useRenderedNotes.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react';
|
||||
import { ListRenderer } from './types';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import * as Mustache from 'mustache';
|
||||
import { createHash } from 'crypto';
|
||||
import getNoteTitleHtml from './getNoteTitleHtml';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import prepareViewProps from './prepareViewProps';
|
||||
|
||||
interface RenderedNote {
|
||||
id: string;
|
||||
hash: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
const hashContent = (content: any) => {
|
||||
return createHash('sha1').update(JSON.stringify(content)).digest('hex');
|
||||
};
|
||||
|
||||
const useRenderedNotes = (startNoteIndex: number, endNoteIndex: number, notes: NoteEntity[], selectedNoteIds: string[], listRenderer: ListRenderer, highlightedWords: string[], watchedNoteFiles: string[]) => {
|
||||
const [renderedNotes, setRenderedNotes] = useState<Record<string, RenderedNote>>({});
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
if (event.cancelled) return;
|
||||
|
||||
const renderNote = async (note: NoteEntity): Promise<void> => {
|
||||
const isSelected = selectedNoteIds.includes(note.id);
|
||||
const isWatched = watchedNoteFiles.includes(note.id);
|
||||
|
||||
// Note: with this hash we're assuming that the list renderer
|
||||
// properties never changes. It means that later if we support
|
||||
// dynamic list renderers, we should include these into the hash.
|
||||
const viewHash = hashContent([
|
||||
note.updated_time,
|
||||
isSelected,
|
||||
isWatched,
|
||||
highlightedWords,
|
||||
]);
|
||||
|
||||
if (renderedNotes[note.id] && renderedNotes[note.id].hash === viewHash) return null;
|
||||
|
||||
const titleHtml = getNoteTitleHtml(highlightedWords, Note.displayTitle(note));
|
||||
const viewProps = await prepareViewProps(
|
||||
listRenderer.dependencies,
|
||||
note,
|
||||
listRenderer.itemSize,
|
||||
isSelected,
|
||||
titleHtml,
|
||||
isWatched
|
||||
);
|
||||
const view = await listRenderer.onRenderNote(viewProps);
|
||||
|
||||
if (event.cancelled) return null;
|
||||
|
||||
setRenderedNotes(prev => {
|
||||
if (prev[note.id] && prev[note.id].hash === viewHash) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[note.id]: {
|
||||
id: note.id,
|
||||
hash: viewHash,
|
||||
html: Mustache.render(listRenderer.itemTemplate, view),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
promises.push(renderNote(notes[i]));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}, [startNoteIndex, endNoteIndex, notes, selectedNoteIds, listRenderer, renderedNotes, watchedNoteFiles]);
|
||||
|
||||
return renderedNotes;
|
||||
};
|
||||
|
||||
export default useRenderedNotes;
|
||||
99
packages/app-desktop/gui/NoteList/utils/useScroll.ts
Normal file
99
packages/app-desktop/gui/NoteList/utils/useScroll.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as React from 'react';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useCallback, useState, useRef, useEffect, useMemo } from 'react';
|
||||
|
||||
const useScroll = (itemsPerLine: number, noteCount: number, itemSize: Size, listSize: Size, listRef: React.MutableRefObject<HTMLDivElement>) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const lastScrollSetTime = useRef(0);
|
||||
|
||||
const maxScrollTop = useMemo(() => {
|
||||
return Math.max(0, itemSize.height * noteCount - listSize.height);
|
||||
}, [itemSize.height, noteCount, listSize.height]);
|
||||
|
||||
// This ugly hack is necessary because setting scrollTop at a high
|
||||
// frequency, while scrolling with the keyboard, is unreliable - the
|
||||
// property will appear to be set (reading it back gives the correct value),
|
||||
// but the scrollbar will not be at the expected position. That can be
|
||||
// verified by moving the scrollbar a little and reading the event value -
|
||||
// it will be different from what was set, and what was read.
|
||||
//
|
||||
// As a result, since we can't rely on setting or reading that value (to
|
||||
// check if it's correct), we forcefully set it multiple times over the next
|
||||
// few milliseconds, hoping that maybe one of these attempts will stick.
|
||||
//
|
||||
// This is most likely a race condition in either Chromimum or Electron
|
||||
// although I couldn't find an upstream issue.
|
||||
//
|
||||
// Setting the value only once after a short time, for example 10ms, helps
|
||||
// but still fails now and then. Setting it after 500ms would probably work
|
||||
// reliably but it's too slow so it makes sense to do it in an interval.
|
||||
|
||||
const setScrollTopLikeYouMeanItTimer = useRef(null);
|
||||
const setScrollTopLikeYouMeanItStartTime = useRef(0);
|
||||
const setScrollTopLikeYouMeanIt = useCallback((newScrollTop: number) => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItStartTime.current = Date.now();
|
||||
|
||||
setScrollTopLikeYouMeanItTimer.current = shim.setInterval(() => {
|
||||
if (!listRef.current) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
listRef.current.scrollTop = newScrollTop;
|
||||
lastScrollSetTime.current = Date.now();
|
||||
|
||||
if (Date.now() - setScrollTopLikeYouMeanItStartTime.current > 500) {
|
||||
shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}
|
||||
}, 10);
|
||||
}, [listRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (setScrollTopLikeYouMeanItTimer.current) shim.clearInterval(setScrollTopLikeYouMeanItTimer.current);
|
||||
setScrollTopLikeYouMeanItTimer.current = null;
|
||||
}, []);
|
||||
|
||||
const makeItemIndexVisible = useCallback((itemIndex: number) => {
|
||||
const lineTopFloat = scrollTop / itemSize.height;
|
||||
const topFloat = lineTopFloat * itemsPerLine; // scrollTop / itemSize.height;
|
||||
const lineBottomFloat = (scrollTop + listSize.height - itemSize.height) / itemSize.height;
|
||||
const bottomFloat = lineBottomFloat * itemsPerLine; // (scrollTop + listSize.height - itemSize.height) / itemSize.height;
|
||||
const top = Math.min(noteCount - 1, Math.floor(topFloat) + 1);
|
||||
const bottom = Math.max(0, Math.floor(bottomFloat));
|
||||
|
||||
if (itemIndex >= top && itemIndex <= bottom) return;
|
||||
|
||||
const lineIndex = Math.floor(itemIndex / itemsPerLine);
|
||||
|
||||
let newScrollTop = 0;
|
||||
if (itemIndex < top) {
|
||||
newScrollTop = itemSize.height * lineIndex;
|
||||
} else {
|
||||
newScrollTop = itemSize.height * (lineIndex + 1) - listSize.height;
|
||||
}
|
||||
|
||||
if (newScrollTop < 0) newScrollTop = 0;
|
||||
if (newScrollTop > maxScrollTop) newScrollTop = maxScrollTop;
|
||||
|
||||
setScrollTop(newScrollTop);
|
||||
setScrollTopLikeYouMeanIt(newScrollTop);
|
||||
}, [itemsPerLine, noteCount, itemSize.height, scrollTop, listSize.height, maxScrollTop, setScrollTopLikeYouMeanIt]);
|
||||
|
||||
const onScroll = useCallback((event: any) => {
|
||||
// Ignore the scroll event if it has just been set programmatically.
|
||||
if (Date.now() - lastScrollSetTime.current < 100) return;
|
||||
setScrollTop(event.target.scrollTop);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
scrollTop,
|
||||
onScroll,
|
||||
makeItemIndexVisible,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScroll;
|
||||
@@ -0,0 +1,61 @@
|
||||
import useVisibleRange from './useVisibleRange';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
|
||||
describe('useVisibleRange', () => {
|
||||
|
||||
test('should calculate indexes', () => {
|
||||
|
||||
// IN: scrollTop, listSize, itemSize, noteCount, flow
|
||||
//
|
||||
// OUT: [itemsPerLine, startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount]
|
||||
|
||||
const testCases: [number, number, Size, Size, number, ReturnType<typeof useVisibleRange>][] = [
|
||||
[
|
||||
1,
|
||||
150,
|
||||
{ width: 100, height: 400 },
|
||||
{ width: 100, height: 100 },
|
||||
8,
|
||||
[1, 5, 1, 5, 8, 5],
|
||||
],
|
||||
[
|
||||
2,
|
||||
100,
|
||||
{ width: 220, height: 380 },
|
||||
{ width: 100, height: 100 },
|
||||
12,
|
||||
[2, 9, 1, 4, 6, 8],
|
||||
],
|
||||
[
|
||||
2,
|
||||
50,
|
||||
{ width: 220, height: 300 },
|
||||
{ width: 100, height: 100 },
|
||||
9,
|
||||
[0, 7, 0, 3, 5, 8],
|
||||
],
|
||||
[
|
||||
4,
|
||||
0,
|
||||
{ width: 410, height: 450 },
|
||||
{ width: 100, height: 100 },
|
||||
30,
|
||||
[0, 19, 0, 4, 8, 20],
|
||||
],
|
||||
];
|
||||
|
||||
for (const [scrollTop, listSize, itemSize, noteCount, flow, expected] of testCases) {
|
||||
const { result } = renderHook(() => useVisibleRange(
|
||||
scrollTop,
|
||||
listSize,
|
||||
itemSize,
|
||||
noteCount,
|
||||
flow
|
||||
));
|
||||
|
||||
expect(result.current).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
57
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
57
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const useVisibleRange = (itemsPerLine: number, scrollTop: number, listSize: Size, itemSize: Size, noteCount: number) => {
|
||||
const startLineIndexFloat = useMemo(() => {
|
||||
return scrollTop / itemSize.height;
|
||||
}, [scrollTop, itemSize.height]);
|
||||
|
||||
const endLineIndexFloat = useMemo(() => {
|
||||
return startLineIndexFloat + (listSize.height / itemSize.height);
|
||||
}, [startLineIndexFloat, listSize.height, itemSize.height]);
|
||||
|
||||
const startLineIndex = useMemo(() => {
|
||||
return Math.floor(startLineIndexFloat);
|
||||
}, [startLineIndexFloat]);
|
||||
|
||||
const endLineIndex = useMemo(() => {
|
||||
return Math.floor(endLineIndexFloat);
|
||||
}, [endLineIndexFloat]);
|
||||
|
||||
const visibleLineCount = useMemo(() => {
|
||||
return endLineIndex - startLineIndex + 1;
|
||||
}, [endLineIndex, startLineIndex]);
|
||||
|
||||
const visibleItemCount = useMemo(() => {
|
||||
return visibleLineCount * itemsPerLine;
|
||||
}, [visibleLineCount, itemsPerLine]);
|
||||
|
||||
const startNoteIndex = useMemo(() => {
|
||||
return itemsPerLine * startLineIndex;
|
||||
}, [itemsPerLine, startLineIndex]);
|
||||
|
||||
const endNoteIndex = useMemo(() => {
|
||||
let output = (endLineIndex + 1) * itemsPerLine - 1;
|
||||
if (output >= noteCount) output = noteCount - 1;
|
||||
return output;
|
||||
}, [endLineIndex, itemsPerLine, noteCount]);
|
||||
|
||||
const totalLineCount = useMemo(() => {
|
||||
return Math.ceil(noteCount / itemsPerLine);
|
||||
}, [noteCount, itemsPerLine]);
|
||||
|
||||
// console.info('itemsPerLine', itemsPerLine);
|
||||
// console.info('startLineIndexFloat', startLineIndexFloat);
|
||||
// console.info('endLineIndexFloat', endLineIndexFloat);
|
||||
// console.info('visibleLineCount', visibleLineCount);
|
||||
// console.info('startNoteIndex', startNoteIndex);
|
||||
// console.info('endNoteIndex', endNoteIndex);
|
||||
// console.info('startLineIndex', startLineIndex);
|
||||
// console.info('endLineIndex', endLineIndex);
|
||||
// console.info('totalLineCount', totalLineCount);
|
||||
// console.info('visibleItemCount', visibleItemCount);
|
||||
|
||||
return [startNoteIndex, endNoteIndex, startLineIndex, endLineIndex, totalLineCount, visibleItemCount];
|
||||
};
|
||||
|
||||
export default useVisibleRange;
|
||||
@@ -84,11 +84,11 @@ function NoteListItem(props: NoteListItemProps, ref: any) {
|
||||
dragItemPosition = 'bottom';
|
||||
}
|
||||
|
||||
const onTitleClick = useCallback((event) => {
|
||||
const onTitleClick = useCallback((event: any) => {
|
||||
props.onTitleClick(event, props.item);
|
||||
}, [props.onTitleClick, props.item]);
|
||||
|
||||
const onCheckboxClick = useCallback((event) => {
|
||||
const onCheckboxClick = useCallback((event: any) => {
|
||||
props.onCheckboxClick(event, props.item);
|
||||
}, [props.onCheckboxClick, props.item]);
|
||||
|
||||
|
||||
143
packages/app-desktop/gui/NoteListItem/NoteListItem.tsx
Normal file
143
packages/app-desktop/gui/NoteListItem/NoteListItem.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback, forwardRef, LegacyRef, ChangeEvent, CSSProperties, MouseEventHandler, DragEventHandler, useMemo, memo } from 'react';
|
||||
import { ItemFlow, OnChangeEvent, OnChangeHandler } from '../NoteList/utils/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import useRootElement from './utils/useRootElement';
|
||||
import useItemElement from './utils/useItemElement';
|
||||
import useItemEventHandlers from './utils/useItemEventHandlers';
|
||||
import { OnCheckboxChange } from './utils/types';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
|
||||
interface NoteItemProps {
|
||||
dragIndex: number;
|
||||
flow: ItemFlow;
|
||||
highlightedWords: string[];
|
||||
index: number;
|
||||
isProvisional: boolean;
|
||||
itemSize: Size;
|
||||
noteCount: number;
|
||||
noteHtml: string;
|
||||
noteId: string;
|
||||
onChange: OnChangeHandler;
|
||||
onClick: MouseEventHandler<HTMLDivElement>;
|
||||
onContextMenu: MouseEventHandler;
|
||||
onDragOver: DragEventHandler;
|
||||
onDragStart: DragEventHandler;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
|
||||
const elementId = `list-note-${props.noteId}`;
|
||||
|
||||
const onCheckboxChange: OnCheckboxChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const changeEvent: OnChangeEvent = {
|
||||
noteId: props.noteId,
|
||||
elementId: event.currentTarget.getAttribute('data-id'),
|
||||
value: event.currentTarget.checked,
|
||||
};
|
||||
|
||||
if (changeEvent.elementId === 'todo-checkbox') {
|
||||
await Note.save({
|
||||
id: changeEvent.noteId,
|
||||
todo_completed: changeEvent.value ? Date.now() : 0,
|
||||
}, { userSideValidation: true });
|
||||
} else {
|
||||
if (props.onChange) await props.onChange(changeEvent);
|
||||
}
|
||||
}, [props.onChange, props.noteId]);
|
||||
|
||||
const rootElement = useRootElement(elementId);
|
||||
|
||||
const itemElement = useItemElement(
|
||||
rootElement,
|
||||
props.noteId,
|
||||
props.noteHtml,
|
||||
props.style,
|
||||
props.itemSize,
|
||||
props.onClick,
|
||||
props.flow
|
||||
);
|
||||
|
||||
useItemEventHandlers(rootElement, itemElement, onCheckboxChange);
|
||||
|
||||
const className = useMemo(() => {
|
||||
return [
|
||||
'note-list-item-wrapper',
|
||||
|
||||
// This is not used by the app, but kept here because it may be used
|
||||
// by users for custom CSS.
|
||||
(props.index + 1) % 2 === 0 ? 'even' : 'odd',
|
||||
|
||||
props.isProvisional && '-provisional',
|
||||
].filter(e => !!e).join(' ');
|
||||
}, [props.index, props.isProvisional]);
|
||||
|
||||
const isActiveDragItem = props.dragIndex === props.index;
|
||||
const isLastActiveDragItem = props.index === props.noteCount - 1 && props.dragIndex >= props.noteCount;
|
||||
|
||||
const dragCursorStyle = useMemo(() => {
|
||||
if (props.flow === ItemFlow.TopToBottom) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'top';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'bottom';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
width: props.itemSize.width,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
left: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'top') {
|
||||
output.top = 0;
|
||||
} else {
|
||||
output.bottom = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
if (props.flow === ItemFlow.LeftToRight) {
|
||||
let dragItemPosition = '';
|
||||
if (isActiveDragItem) {
|
||||
dragItemPosition = 'left';
|
||||
} else if (isLastActiveDragItem) {
|
||||
dragItemPosition = 'right';
|
||||
}
|
||||
|
||||
const output: React.CSSProperties = {
|
||||
height: props.itemSize.height,
|
||||
display: dragItemPosition ? 'block' : 'none',
|
||||
top: 0,
|
||||
};
|
||||
|
||||
if (dragItemPosition === 'left') {
|
||||
output.left = 0;
|
||||
} else {
|
||||
output.right = 0;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
throw new Error('Unreachable');
|
||||
}, [isActiveDragItem, isLastActiveDragItem, props.flow, props.itemSize]);
|
||||
|
||||
return <div
|
||||
id={elementId}
|
||||
ref={ref}
|
||||
draggable={true}
|
||||
tabIndex={0}
|
||||
className={className}
|
||||
data-id={props.noteId}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDragStart={props.onDragStart}
|
||||
onDragOver={props.onDragOver}
|
||||
>
|
||||
<div className="dragcursor" style={dragCursorStyle}></div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default memo(forwardRef(NoteListItem));
|
||||
3
packages/app-desktop/gui/NoteListItem/utils/types.ts
Normal file
3
packages/app-desktop/gui/NoteListItem/utils/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type OnCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>)=> void;
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ItemFlow } from '../../NoteList/utils/types';
|
||||
|
||||
const useItemElement = (rootElement: HTMLDivElement, noteId: string, noteHtml: string, style: any, itemSize: Size, onClick: React.MouseEventHandler<HTMLDivElement>, flow: ItemFlow) => {
|
||||
const [itemElement, setItemElement] = useState<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootElement) return () => {};
|
||||
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('data-id', noteId);
|
||||
element.className = 'note-list-item';
|
||||
for (const [n, v] of Object.entries(style)) {
|
||||
(element.style as any)[n] = v;
|
||||
}
|
||||
if (flow === ItemFlow.LeftToRight) element.style.width = `${itemSize.width}px`;
|
||||
element.style.height = `${itemSize.height}px`;
|
||||
element.innerHTML = noteHtml;
|
||||
element.addEventListener('click', onClick as any);
|
||||
|
||||
rootElement.appendChild(element);
|
||||
|
||||
setItemElement(element);
|
||||
|
||||
return () => {
|
||||
element.remove();
|
||||
};
|
||||
}, [rootElement, itemSize, noteHtml, noteId, style, onClick, flow]);
|
||||
|
||||
return itemElement;
|
||||
};
|
||||
|
||||
export default useItemElement;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { OnCheckboxChange } from './types';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useItemEventHandlers = (rootElement: HTMLDivElement, itemElement: HTMLDivElement, onCheckboxChange: OnCheckboxChange) => {
|
||||
useEffect(() => {
|
||||
if (!itemElement) return () => {};
|
||||
|
||||
const inputs = itemElement.getElementsByTagName('input');
|
||||
|
||||
const mods: HTMLInputElement[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.addEventListener('change', onCheckboxChange as any);
|
||||
mods.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const input of mods) {
|
||||
input.removeEventListener('change', onCheckboxChange as any);
|
||||
}
|
||||
};
|
||||
}, [itemElement, rootElement, onCheckboxChange]);
|
||||
};
|
||||
|
||||
export default useItemEventHandlers;
|
||||
@@ -0,0 +1,44 @@
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { useCallback } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import bridge from '../../../services/bridge';
|
||||
import NoteListUtils from '../../utils/NoteListUtils';
|
||||
|
||||
const useOnContextMenu = (
|
||||
selectedNoteIds: string[],
|
||||
selectedFolderId: string,
|
||||
notes: NoteEntity[],
|
||||
dispatch: Dispatch,
|
||||
watchedNoteFiles: string[],
|
||||
plugins: PluginStates,
|
||||
customCss: string
|
||||
) => {
|
||||
return useCallback((event: any) => {
|
||||
const currentNoteId = event.currentTarget.getAttribute('data-id');
|
||||
if (!currentNoteId) return;
|
||||
|
||||
let noteIds = [];
|
||||
if (selectedNoteIds.indexOf(currentNoteId) < 0) {
|
||||
noteIds = [currentNoteId];
|
||||
} else {
|
||||
noteIds = selectedNoteIds;
|
||||
}
|
||||
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||
notes: notes,
|
||||
dispatch: dispatch,
|
||||
watchedNoteFiles: watchedNoteFiles,
|
||||
plugins: plugins,
|
||||
inConflictFolder: selectedFolderId === Folder.conflictFolderId(),
|
||||
customCss: customCss,
|
||||
});
|
||||
|
||||
menu.popup({ window: bridge().window() });
|
||||
}, [selectedNoteIds, notes, dispatch, watchedNoteFiles, plugins, selectedFolderId, customCss]);
|
||||
};
|
||||
|
||||
export default useOnContextMenu;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { waitForElement } from '@joplin/lib/dom';
|
||||
|
||||
const useRootElement = (elementId: string) => {
|
||||
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
const element = await waitForElement(document, elementId);
|
||||
if (event.cancelled) return;
|
||||
setRootElement(element);
|
||||
}, [document, elementId]);
|
||||
|
||||
return rootElement;
|
||||
};
|
||||
|
||||
export default useRootElement;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import * as React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import NoteList from '../NoteList/NoteList';
|
||||
// import NoteList from '../NoteList/NoteList';
|
||||
import NoteList2 from '../NoteList/NoteList2';
|
||||
import NoteListControls from '../NoteListControls/NoteListControls';
|
||||
import { Size } from '../ResizableLayout/utils/types';
|
||||
import styled from 'styled-components';
|
||||
@@ -39,10 +40,12 @@ export default function NoteListWrapper(props: Props) {
|
||||
};
|
||||
}, [props.size, controlHeight]);
|
||||
|
||||
// <NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<NoteListControls height={controlHeight} width={noteListSize.width} onContentHeightChange={onContentHeightChange}/>
|
||||
<NoteList resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
<NoteList2 resizableLayoutEventEmitter={props.resizableLayoutEventEmitter} size={noteListSize} visible={props.visible}/>
|
||||
</StyledRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import themeToCss from '@joplin/lib/services/style/themeToCss';
|
||||
import { addExtraStyles, themeById } from '@joplin/lib/theme';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
||||
interface Props {
|
||||
themeId: any;
|
||||
@@ -21,7 +21,7 @@ export default function(props: Props): any {
|
||||
const [styleSheetContent, setStyleSheetContent] = useState('');
|
||||
|
||||
useAsyncEffect(async (event: AsyncEffectEvent) => {
|
||||
const theme = addExtraStyles(themeById(props.themeId));
|
||||
const theme = themeStyle(props.themeId);
|
||||
const themeCss = themeToCss(theme);
|
||||
if (event.cancelled) return;
|
||||
setStyleSheetContent(themeCss);
|
||||
|
||||
@@ -7,19 +7,19 @@ import InteropServiceHelper from '../../InteropServiceHelper';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuItemLocation } from '@joplin/lib/services/plugins/api/types';
|
||||
import { getNoteCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||
|
||||
import bridge from '../../services/bridge';
|
||||
import BaseModel from '@joplin/lib/BaseModel';
|
||||
const bridge = require('@electron/remote').require('./bridge').default;
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
const { clipboard } = require('electron');
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
|
||||
interface ContextMenuProps {
|
||||
notes: any[];
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
dispatch: Function;
|
||||
dispatch: Dispatch;
|
||||
watchedNoteFiles: string[];
|
||||
plugins: PluginStates;
|
||||
inConflictFolder: boolean;
|
||||
@@ -45,26 +45,26 @@ export default class NoteListUtils {
|
||||
|
||||
if (!hasEncrypted) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('setTags', noteIds) as any)
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('moveToFolder', noteIds) as any)
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('duplicateNote', noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem('duplicateNote', noteIds) as any)
|
||||
);
|
||||
|
||||
if (singleNoteId) {
|
||||
const cmd = props.watchedNoteFiles.includes(singleNoteId) ? 'stopExternalEditing' : 'startExternalEditing';
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, singleNoteId)));
|
||||
menu.append(new MenuItem(menuUtils.commandToStatefulMenuItem(cmd, singleNoteId) as any));
|
||||
}
|
||||
|
||||
if (noteIds.length <= 1) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('toggleNoteType', noteIds)
|
||||
menuUtils.commandToStatefulMenuItem('toggleNoteType', noteIds) as any
|
||||
)
|
||||
);
|
||||
} else {
|
||||
@@ -125,7 +125,7 @@ export default class NoteListUtils {
|
||||
if ([9, 10].includes(Setting.value('sync.target'))) {
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
|
||||
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice()) as any
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ export default class NoteListUtils {
|
||||
|
||||
exportMenu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', noteIds)
|
||||
menuUtils.commandToStatefulMenuItem('exportPdf', noteIds) as any
|
||||
)
|
||||
);
|
||||
|
||||
@@ -167,7 +167,7 @@ export default class NoteListUtils {
|
||||
|
||||
menu.append(
|
||||
new MenuItem(
|
||||
menuUtils.commandToStatefulMenuItem('deleteNote', noteIds)
|
||||
menuUtils.commandToStatefulMenuItem('deleteNote', noteIds) as any
|
||||
)
|
||||
);
|
||||
|
||||
@@ -179,7 +179,7 @@ export default class NoteListUtils {
|
||||
|
||||
if (cmdService.isEnabled(info.view.commandName)) {
|
||||
menu.append(
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds))
|
||||
new MenuItem(menuUtils.commandToStatefulMenuItem(info.view.commandName, noteIds) as any)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
/* eslint-disable jest/require-top-level-describe */
|
||||
|
||||
const { default: Logger, TargetType } = require('@joplin/utils/Logger');
|
||||
const initLib = require('@joplin/lib/initLib').default;
|
||||
|
||||
// TODO: Some libraries required by test-utils.js seem to fail to import with the
|
||||
// jsdom environment.
|
||||
//
|
||||
// Thus, require('@joplin/lib/testing/test-utils.js') fails and some setup must be
|
||||
// copied.
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget(TargetType.Console);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
Logger.initializeGlobalLogger(logger);
|
||||
initLib(logger);
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node');
|
||||
const sqlite3 = require('sqlite3');
|
||||
const SyncTargetNone = require('@joplin/lib/SyncTargetNone').default;
|
||||
|
||||
// Mock the S3 sync target -- the @aws-s3 libraries depend on an old version
|
||||
// of uuid that doesn't work with jest without additional configuration.
|
||||
jest.doMock('@joplin/lib/SyncTargetAmazonS3', () => {
|
||||
return SyncTargetNone;
|
||||
});
|
||||
|
||||
// @electron/remote requires electron to be running. Mock it.
|
||||
jest.mock('@electron/remote', () => {
|
||||
@@ -25,3 +20,18 @@ jest.mock('@electron/remote', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Import after mocking problematic libraries
|
||||
const { afterEachCleanUp, afterAllCleanUp } = require('@joplin/lib/testing/test-utils.js');
|
||||
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllCleanUp();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.12.10",
|
||||
"version": "2.12.11",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
@@ -112,7 +112,7 @@
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/node": "18.16.18",
|
||||
"@types/react": "16.14.43",
|
||||
"@types/react": "18.0.24",
|
||||
"@types/react-redux": "7.1.25",
|
||||
"@types/styled-components": "5.1.26",
|
||||
"electron": "25.3.1",
|
||||
@@ -139,6 +139,7 @@
|
||||
"@joplin/lib": "~2.12",
|
||||
"@joplin/renderer": "~2.12",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@types/mustache": "4.2.2",
|
||||
"async-mutex": "0.4.0",
|
||||
"codemirror": "5.65.9",
|
||||
"color": "3.2.1",
|
||||
@@ -154,6 +155,7 @@
|
||||
"mark.js": "8.11.1",
|
||||
"md5": "2.3.0",
|
||||
"moment": "2.29.4",
|
||||
"mustache": "4.2.0",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-notifier": "10.0.1",
|
||||
"node-rsa": "1.1.1",
|
||||
@@ -162,8 +164,8 @@
|
||||
"react": "18.2.0",
|
||||
"react-datetime": "3.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-redux": "8.1.1",
|
||||
"react-select": "5.7.3",
|
||||
"react-redux": "8.1.2",
|
||||
"react-select": "5.7.4",
|
||||
"react-toggle-button": "2.2.0",
|
||||
"react-tooltip": "4.5.1",
|
||||
"redux": "4.2.1",
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
@use 'gui/JoplinCloudConfigScreen.scss' as joplin-cloud-config-screen;
|
||||
@use 'gui/Dropdown/style.scss' as dropdown-control;
|
||||
@use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog;
|
||||
@use 'gui/NoteList/style.scss' as note-list;
|
||||
@use 'main.scss' as main;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,23 @@
|
||||
import { fileExtension } from '@joplin/lib/path-utils';
|
||||
import shim from '@joplin/lib/shim';
|
||||
|
||||
export interface CheckForUpdateOptions {
|
||||
includePreReleases?: boolean;
|
||||
}
|
||||
|
||||
interface GitHubReleaseAsset {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
tag_name: string;
|
||||
prerelease: boolean;
|
||||
body: string;
|
||||
assets: {
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}[];
|
||||
assets: GitHubReleaseAsset[];
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
export interface Release {
|
||||
version: string;
|
||||
prerelease: boolean;
|
||||
downloadUrl: string;
|
||||
@@ -24,13 +25,17 @@ interface Release {
|
||||
pageUrl: string;
|
||||
}
|
||||
|
||||
export type Platform = typeof process.platform;
|
||||
|
||||
export type Architecture = typeof process.arch;
|
||||
|
||||
function getMajorMinorTagName(tagName: string) {
|
||||
const s = tagName.split('.');
|
||||
s.pop();
|
||||
return s.join('.');
|
||||
}
|
||||
|
||||
export const extractVersionInfo = (releases: GitHubRelease[], platform: typeof process.platform, options: CheckForUpdateOptions) => {
|
||||
export const extractVersionInfo = (releases: GitHubRelease[], platform: Platform, arch: Architecture, portable: boolean, options: CheckForUpdateOptions) => {
|
||||
options = { includePreReleases: false, ...options };
|
||||
|
||||
if (!releases.length) throw new Error('Cannot get latest release info (JSON)');
|
||||
@@ -67,28 +72,43 @@ export const extractVersionInfo = (releases: GitHubRelease[], platform: typeof p
|
||||
}
|
||||
}
|
||||
|
||||
let downloadUrl = null;
|
||||
for (let i = 0; i < release.assets.length; i++) {
|
||||
const asset = release.assets[i];
|
||||
let found = false;
|
||||
const ext = fileExtension(asset.name);
|
||||
if (platform === 'win32' && ext === 'exe') {
|
||||
if (shim.isPortable()) {
|
||||
found = asset.name === 'JoplinPortable.exe';
|
||||
} else {
|
||||
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
|
||||
}
|
||||
} else if (platform === 'darwin' && ext === 'dmg' && !asset.name.endsWith('arm64.dmg')) { // We don't return the arm64 version for now
|
||||
found = true;
|
||||
} else if (platform === 'linux' && ext === '.AppImage') {
|
||||
found = true;
|
||||
}
|
||||
let foundAsset: GitHubReleaseAsset = null;
|
||||
|
||||
if (found) {
|
||||
downloadUrl = asset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
|
||||
downloadUrl.concat('?source=DesktopApp&type=Update');
|
||||
break;
|
||||
}
|
||||
if (platform === 'win32' && portable) {
|
||||
foundAsset = release.assets.find(asset => {
|
||||
return asset.name === 'JoplinPortable.exe';
|
||||
});
|
||||
}
|
||||
|
||||
if (!foundAsset && platform === 'win32') {
|
||||
foundAsset = release.assets.find(asset => {
|
||||
return !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
|
||||
});
|
||||
}
|
||||
|
||||
if (platform === 'darwin' && arch === 'arm64') {
|
||||
foundAsset = release.assets.find(asset => {
|
||||
return asset.name.endsWith('arm64.dmg');
|
||||
});
|
||||
}
|
||||
|
||||
if (!foundAsset && platform === 'darwin') {
|
||||
foundAsset = release.assets.find(asset => {
|
||||
return fileExtension(asset.name) === 'dmg' && !asset.name.endsWith('arm64.dmg');
|
||||
});
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
foundAsset = release.assets.find(asset => {
|
||||
return fileExtension(asset.name) === 'AppImage';
|
||||
});
|
||||
}
|
||||
|
||||
let downloadUrl: string = null;
|
||||
|
||||
if (foundAsset) {
|
||||
downloadUrl = foundAsset.browser_download_url.replace('github.com/laurent22/joplin/releases/download', 'objects.joplinusercontent.com');
|
||||
downloadUrl.concat('?source=DesktopApp&type=Update');
|
||||
}
|
||||
|
||||
function cleanUpReleaseNotes(releaseNotes: string[]) {
|
||||
|
||||
5155
packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts
Normal file
5155
packages/app-desktop/utils/checkForUpdatesUtilsTestData.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -110,8 +110,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097717
|
||||
versionName "2.12.0"
|
||||
versionCode 2097718
|
||||
versionName "2.12.1"
|
||||
// ndk {
|
||||
// abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const React = require('react');
|
||||
|
||||
import { ReactElement, useCallback, useState } from 'react';
|
||||
import { ReactElement, useCallback, useMemo, useState } from 'react';
|
||||
import { LayoutChangeEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
import ToggleOverflowButton from './ToggleOverflowButton';
|
||||
import ToolbarButton, { buttonSize } from './ToolbarButton';
|
||||
@@ -18,19 +18,22 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
const [overflowButtonsVisible, setOverflowPopupVisible] = useState(false);
|
||||
const [maxButtonsEachSide, setMaxButtonsEachSide] = useState(0);
|
||||
|
||||
const allButtonSpecs = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
const allButtonSpecs = useMemo(() => {
|
||||
const buttons = props.buttons.reduce((accumulator: ButtonSpec[], current: ButtonGroup) => {
|
||||
const newItems: ButtonSpec[] = [];
|
||||
for (const item of current.items) {
|
||||
if (item.visible ?? true) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
return accumulator.concat(...newItems);
|
||||
}, []);
|
||||
|
||||
// Sort from highest priority to lowest
|
||||
allButtonSpecs.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
// Sort from highest priority to lowest
|
||||
buttons.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
||||
return buttons;
|
||||
}, [props.buttons]);
|
||||
|
||||
const allButtonComponents: ReactElement[] = [];
|
||||
let key = 0;
|
||||
@@ -67,7 +70,9 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
);
|
||||
|
||||
const mainButtons: ReactElement[] = [];
|
||||
if (maxButtonsEachSide < allButtonComponents.length) {
|
||||
if (maxButtonsEachSide >= allButtonComponents.length) {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
} else if (maxButtonsEachSide > 0) {
|
||||
// We want the menu to look something like this:
|
||||
// B I (…) 🔍 ⌨
|
||||
// where (…) shows/hides overflow.
|
||||
@@ -77,7 +82,7 @@ const Toolbar = (props: ToolbarProps) => {
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
mainButtons.push(...allButtonComponents.slice(-maxButtonsEachSide));
|
||||
} else {
|
||||
mainButtons.push(...allButtonComponents);
|
||||
mainButtons.push(toggleOverflowButton);
|
||||
}
|
||||
|
||||
const styles = props.styleSheet.styles;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
|
||||
import '@testing-library/jest-native';
|
||||
|
||||
import NoteEditor from './NoteEditor';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { MenuProvider } from 'react-native-popup-menu';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
describe('NoteEditor', () => {
|
||||
beforeEach(async () => {
|
||||
// Required to use ExtendedWebView
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
});
|
||||
|
||||
it('should hide the markdown toolbar when the window is small', async () => {
|
||||
const wrappedNoteEditor = render(
|
||||
<MenuProvider>
|
||||
<NoteEditor
|
||||
themeId={Setting.THEME_ARITIM_DARK}
|
||||
initialText='Testing...'
|
||||
style={{}}
|
||||
toolbarEnabled={true}
|
||||
readOnly={false}
|
||||
onChange={()=>{}}
|
||||
onSelectionChange={()=>{}}
|
||||
onUndoRedoDepthChange={()=>{}}
|
||||
onAttach={()=>{}}
|
||||
/>
|
||||
</MenuProvider>
|
||||
);
|
||||
|
||||
// Maps from screen height to whether the markdown toolbar should be visible.
|
||||
const testCases: [number, boolean][] = [
|
||||
[10, false],
|
||||
[1000, true],
|
||||
[100, false],
|
||||
[80, false],
|
||||
[600, true],
|
||||
];
|
||||
|
||||
const noteEditorRoot = await wrappedNoteEditor.findByTestId('note-editor-root');
|
||||
|
||||
const setRootHeight = (height: number) => {
|
||||
act(() => {
|
||||
// See https://stackoverflow.com/a/61774123
|
||||
fireEvent(noteEditorRoot, 'layout', {
|
||||
nativeEvent: {
|
||||
layout: { height },
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
for (const [height, visible] of testCases) {
|
||||
setRootHeight(height);
|
||||
|
||||
await waitFor(async () => {
|
||||
const showMoreButton = await screen.queryByLabelText(_('Show more actions'));
|
||||
if (visible) {
|
||||
expect(showMoreButton).not.toBeNull();
|
||||
} else {
|
||||
expect(showMoreButton).toBeNull();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import ExtendedWebView from '../ExtendedWebView';
|
||||
const React = require('react');
|
||||
import { forwardRef, RefObject, useImperativeHandle } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { LayoutChangeEvent, View, ViewStyle } from 'react-native';
|
||||
const { editorFont } = require('../global-style');
|
||||
|
||||
import SelectionFormatting from './SelectionFormatting';
|
||||
@@ -368,6 +368,19 @@ function NoteEditor(props: Props, ref: any) {
|
||||
console.error('NoteEditor: webview error');
|
||||
}, []);
|
||||
|
||||
const [hasSpaceForToolbar, setHasSpaceForToolbar] = useState(true);
|
||||
const toolbarEnabled = props.toolbarEnabled && hasSpaceForToolbar;
|
||||
|
||||
const onContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
const containerHeight = event.nativeEvent.layout.height;
|
||||
|
||||
if (containerHeight < 140) {
|
||||
setHasSpaceForToolbar(false);
|
||||
} else {
|
||||
setHasSpaceForToolbar(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const toolbar = <MarkdownToolbar
|
||||
style={{
|
||||
// Don't show the markdown toolbar if there isn't enough space
|
||||
@@ -385,10 +398,14 @@ function NoteEditor(props: Props, ref: any) {
|
||||
// - `scrollEnabled` prevents iOS from scrolling the document (has no effect on Android)
|
||||
// when an editable region (e.g. a the full-screen NoteEditor) is focused.
|
||||
return (
|
||||
<View style={{
|
||||
...props.style,
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<View
|
||||
testID='note-editor-root'
|
||||
onLayout={onContainerLayout}
|
||||
style={{
|
||||
...props.style,
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<EditLinkDialog
|
||||
visible={linkDialogVisible}
|
||||
themeId={props.themeId}
|
||||
@@ -419,7 +436,7 @@ function NoteEditor(props: Props, ref: any) {
|
||||
searchState={searchState}
|
||||
/>
|
||||
|
||||
{props.toolbarEnabled ? toolbar : null}
|
||||
{toolbarEnabled ? toolbar : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const { _ } = require('@joplin/lib/locale');
|
||||
const { BaseScreenComponent } = require('../../base-screen.js');
|
||||
const { Dropdown } = require('../../Dropdown');
|
||||
const { themeStyle } = require('../../global-style.js');
|
||||
const shared = require('@joplin/lib/components/shared/config-shared.js');
|
||||
const shared = require('@joplin/lib/components/shared/config/config-shared.js');
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import { openDocumentTree } from '@joplin/react-native-saf-x';
|
||||
import biometricAuthenticate from '../../biometrics/biometricAuthenticate';
|
||||
|
||||
@@ -36,7 +36,7 @@ const { BaseScreenComponent } = require('../base-screen.js');
|
||||
const { themeStyle, editorFont } = require('../global-style.js');
|
||||
const { dialogs } = require('../../utils/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
import ImageResizer from '@bam.tech/react-native-image-resizer';
|
||||
import shared from '@joplin/lib/components/shared/note-screen-shared';
|
||||
import { ImagePickerResponse, launchImageLibrary } from 'react-native-image-picker';
|
||||
import SelectDateTimeDialog from '../SelectDateTimeDialog';
|
||||
@@ -572,34 +572,30 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
public async resizeImage(localFilePath: string, targetPath: string, mimeType: string) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
|
||||
const dimensions: any = await this.imageDimensions(localFilePath);
|
||||
|
||||
reg.logger().info('Original dimensions ', dimensions);
|
||||
|
||||
let mustResize = dimensions.width > maxSize || dimensions.height > maxSize;
|
||||
|
||||
if (mustResize) {
|
||||
const buttonId = await dialogs.pop(this, _('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize), [
|
||||
{ text: _('Yes'), id: 'yes' },
|
||||
{ text: _('No'), id: 'no' },
|
||||
{ text: _('Cancel'), id: 'cancel' },
|
||||
]);
|
||||
|
||||
if (buttonId === 'cancel') return false;
|
||||
|
||||
mustResize = buttonId === 'yes';
|
||||
}
|
||||
|
||||
if (mustResize) {
|
||||
const saveOriginalImage = async () => {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
return true;
|
||||
};
|
||||
const saveResizedImage = async () => {
|
||||
dimensions.width = maxSize;
|
||||
dimensions.height = maxSize;
|
||||
|
||||
reg.logger().info('New dimensions ', dimensions);
|
||||
|
||||
const format = mimeType === 'image/png' ? 'PNG' : 'JPEG';
|
||||
reg.logger().info(`Resizing image ${localFilePath}`);
|
||||
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); // , 0, targetPath);
|
||||
const resizedImage = await ImageResizer.createResizedImage(
|
||||
localFilePath,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
format,
|
||||
85, // quality
|
||||
undefined, // rotation
|
||||
undefined, // outputPath
|
||||
true // keep metadata
|
||||
);
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
@@ -612,11 +608,27 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error when unlinking cached file: ', error);
|
||||
}
|
||||
} else {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
return true;
|
||||
};
|
||||
|
||||
const canResize = dimensions.width > maxSize || dimensions.height > maxSize;
|
||||
if (canResize) {
|
||||
const resizeLargeImages = Setting.value('imageResizing');
|
||||
if (resizeLargeImages === 'alwaysAsk') {
|
||||
const userAnswer = await dialogs.pop(this, `${_('You are about to attach a large image (%dx%d pixels). Would you like to resize it down to %d pixels before attaching it?', dimensions.width, dimensions.height, maxSize)}\n\n${_('(You may disable this prompt in the options)')}`, [
|
||||
{ text: _('Yes'), id: 'yes' },
|
||||
{ text: _('No'), id: 'no' },
|
||||
{ text: _('Cancel'), id: 'cancel' },
|
||||
]);
|
||||
if (userAnswer === 'yes') return await saveResizedImage();
|
||||
if (userAnswer === 'no') return await saveOriginalImage();
|
||||
if (userAnswer === 'cancel') return false;
|
||||
} else if (resizeLargeImages === 'alwaysResize') {
|
||||
return await saveResizedImage();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return await saveOriginalImage();
|
||||
}
|
||||
|
||||
public async attachFile(pickerResponse: any, fileType: string) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Banner, ActivityIndicator, Modal } from 'react-native-paper';
|
||||
import { Banner, ActivityIndicator } from 'react-native-paper';
|
||||
import { _, languageName } from '@joplin/lib/locale';
|
||||
import useAsyncEffect, { AsyncEffectEvent } from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import { getVosk, Recorder, startRecording, Vosk } from '../../services/voiceTyping/vosk';
|
||||
@@ -107,18 +107,16 @@ export default (props: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal visible={true} style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end' }}>
|
||||
<Banner
|
||||
visible={true}
|
||||
icon={renderIcon()}
|
||||
actions={[
|
||||
{
|
||||
label: _('Done'),
|
||||
onPress: onDismiss,
|
||||
},
|
||||
]}>
|
||||
{`${_('Voice typing...')}\n${renderContent()}`}
|
||||
</Banner>
|
||||
</Modal>
|
||||
<Banner
|
||||
visible={true}
|
||||
icon={renderIcon()}
|
||||
actions={[
|
||||
{
|
||||
label: _('Done'),
|
||||
onPress: onDismiss,
|
||||
},
|
||||
]}>
|
||||
{`${_('Voice typing...')}\n${renderContent()}`}
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -343,7 +343,7 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-camera/RN (4.2.1):
|
||||
- React-Core
|
||||
- react-native-document-picker (8.2.1):
|
||||
- react-native-document-picker (9.0.1):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
- React
|
||||
@@ -353,20 +353,16 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-image-picker (5.6.0):
|
||||
- React-Core
|
||||
- react-native-image-resizer (1.4.5):
|
||||
- react-native-image-resizer (3.0.5):
|
||||
- React-Core
|
||||
- react-native-netinfo (9.3.11):
|
||||
- react-native-netinfo (9.4.1):
|
||||
- React-Core
|
||||
- react-native-rsa-native (2.0.5):
|
||||
- React
|
||||
- react-native-saf-x (2.12.0):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.6.4):
|
||||
- RCT-Folly
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- react-native-safe-area-context (4.7.1):
|
||||
- React-Core
|
||||
- ReactCommon/turbomodule/core
|
||||
- react-native-slider (4.4.2):
|
||||
- React-Core
|
||||
- react-native-sqlite-storage (6.0.1):
|
||||
@@ -465,9 +461,9 @@ PODS:
|
||||
- React-Core
|
||||
- RNCPushNotificationIOS (1.11.0):
|
||||
- React-Core
|
||||
- RNDateTimePicker (7.2.0):
|
||||
- RNDateTimePicker (7.4.1):
|
||||
- React-Core
|
||||
- RNDeviceInfo (10.6.1):
|
||||
- RNDeviceInfo (10.7.0):
|
||||
- React-Core
|
||||
- RNExitApp (1.1.0):
|
||||
- React
|
||||
@@ -475,7 +471,7 @@ PODS:
|
||||
- React-Core
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.12.0):
|
||||
- RNGestureHandler (2.12.1):
|
||||
- React-Core
|
||||
- RNLocalize (3.0.2):
|
||||
- React-Core
|
||||
@@ -584,7 +580,7 @@ DEPENDENCIES:
|
||||
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
- react-native-image-resizer (from `../node_modules/react-native-image-resizer`)
|
||||
- "react-native-image-resizer (from `../node_modules/@bam.tech/react-native-image-resizer`)"
|
||||
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
|
||||
- react-native-rsa-native (from `../node_modules/react-native-rsa-native`)
|
||||
- "react-native-saf-x (from `../node_modules/@joplin/react-native-saf-x`)"
|
||||
@@ -703,7 +699,7 @@ EXTERNAL SOURCES:
|
||||
react-native-image-picker:
|
||||
:path: "../node_modules/react-native-image-picker"
|
||||
react-native-image-resizer:
|
||||
:path: "../node_modules/react-native-image-resizer"
|
||||
:path: "../node_modules/@bam.tech/react-native-image-resizer"
|
||||
react-native-netinfo:
|
||||
:path: "../node_modules/@react-native-community/netinfo"
|
||||
react-native-rsa-native:
|
||||
@@ -819,16 +815,16 @@ SPEC CHECKSUMS:
|
||||
React-logger: ef2269b3afa6ba868da90496c3e17a4ec4f4cee0
|
||||
react-native-alarm-notification: 0732f97be04975a23ba60e675bdb961a0aaf6aa6
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
|
||||
react-native-document-picker: 2b8f18667caee73a96708a82b284a4f40b30a156
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 0f7fe8a4c2de477e278b0365cce27d089a8c5903
|
||||
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
|
||||
react-native-image-picker: db60857e03d63721f19b6f4027de20429ddd9cba
|
||||
react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f
|
||||
react-native-netinfo: 3a48f51c18dbd9253440621955e11de71bc51b32
|
||||
react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa
|
||||
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
|
||||
react-native-rsa-native: 12132eb627797529fdb1f0d22fd0f8f9678df64a
|
||||
react-native-saf-x: 129cd2ddf120a1f6164c724b2846d172666b33de
|
||||
react-native-safe-area-context: 68b07eabfb0d14547d36f6929c0e98d818064f02
|
||||
react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2
|
||||
react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d
|
||||
react-native-sqlite-storage: f6d515e1c446d1e6d026aa5352908a25d4de3261
|
||||
react-native-version-info: a106f23009ac0db4ee00de39574eb546682579b9
|
||||
@@ -849,12 +845,12 @@ SPEC CHECKSUMS:
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8
|
||||
RNDateTimePicker: 3942382593f104af226ad9c56e16166960c7ae30
|
||||
RNDeviceInfo: ab292735ad4fccc5f2aec0c773f7a7f03c7073ae
|
||||
RNDateTimePicker: 9b4091348e53f540180abdc54984d839a556f593
|
||||
RNDeviceInfo: 25d818c85db769cc0e7083d39efaa01a6f450df3
|
||||
RNExitApp: c4e052df2568b43bec8a37c7cd61194d4cfee2c3
|
||||
RNFileViewer: ce7ca3ac370e18554d35d6355cffd7c30437c592
|
||||
RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
|
||||
RNGestureHandler: dec4645026e7401a0899f2846d864403478ff6a5
|
||||
RNGestureHandler: c0d04458598fcb26052494ae23dda8f8f5162b13
|
||||
RNLocalize: dbea38dcb344bf80ff18a1757b1becf11f70cae4
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNReanimated: 9976fbaaeb8a188c36026154c844bf374b3b7eeb
|
||||
|
||||
@@ -33,6 +33,21 @@ document.createRange = () => {
|
||||
|
||||
shimInit({ nodeSqlite: sqlite3 });
|
||||
|
||||
// This library has the following error when running within Jest:
|
||||
// Invariant Violation: `new NativeEventEmitter()` requires a non-null argument.
|
||||
jest.mock('react-native-device-info', () => {
|
||||
return {
|
||||
hasNotch: () => false,
|
||||
};
|
||||
});
|
||||
|
||||
// react-native-webview expects native iOS/Android code so needs to be mocked.
|
||||
jest.mock('react-native-webview', () => {
|
||||
const { View } = require('react-native');
|
||||
return {
|
||||
WebView: View,
|
||||
};
|
||||
});
|
||||
|
||||
// react-native-fs's CachesDirectoryPath export doesn't work in a testing environment.
|
||||
// Use a temporary folder instead.
|
||||
|
||||
@@ -18,15 +18,16 @@
|
||||
"postinstall": "jetify && yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "3.0.5",
|
||||
"@joplin/lib": "~2.12",
|
||||
"@joplin/react-native-alarm-notification": "~2.12",
|
||||
"@joplin/react-native-saf-x": "~2.12",
|
||||
"@joplin/renderer": "~2.12",
|
||||
"@joplin/utils": "~2.12",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "7.2.0",
|
||||
"@react-native-community/datetimepicker": "7.4.1",
|
||||
"@react-native-community/geolocation": "3.0.6",
|
||||
"@react-native-community/netinfo": "9.3.11",
|
||||
"@react-native-community/netinfo": "9.4.1",
|
||||
"@react-native-community/push-notification-ios": "1.11.0",
|
||||
"@react-native-community/slider": "4.4.2",
|
||||
"assert-browserify": "2.0.0",
|
||||
@@ -43,29 +44,27 @@
|
||||
"punycode": "2.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.71.10",
|
||||
"react-native-action-button": "2.8.5",
|
||||
"react-native-camera": "4.2.1",
|
||||
"react-native-device-info": "10.6.1",
|
||||
"react-native-device-info": "10.7.0",
|
||||
"react-native-dialogbox": "0.6.10",
|
||||
"react-native-document-picker": "8.2.1",
|
||||
"react-native-document-picker": "9.0.1",
|
||||
"react-native-drawer-layout": "3.2.1",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
"react-native-exit-app": "1.1.0",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.12.0",
|
||||
"react-native-gesture-handler": "2.12.1",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-image-picker": "5.6.0",
|
||||
"react-native-image-resizer": "1.4.5",
|
||||
"react-native-localize": "3.0.2",
|
||||
"react-native-modal-datetime-picker": "15.0.1",
|
||||
"react-native-paper": "5.8.0",
|
||||
"react-native-paper": "5.9.1",
|
||||
"react-native-popup-menu": "0.16.1",
|
||||
"react-native-quick-actions": "0.3.13",
|
||||
"react-native-reanimated": "3.3.0",
|
||||
"react-native-rsa-native": "2.0.5",
|
||||
"react-native-safe-area-context": "4.6.4",
|
||||
"react-native-safe-area-context": "4.7.1",
|
||||
"react-native-securerandom": "1.0.1",
|
||||
"react-native-share": "8.2.2",
|
||||
"react-native-sqlite-storage": "6.0.1",
|
||||
@@ -75,7 +74,7 @@
|
||||
"react-native-vosk": "0.1.12",
|
||||
"react-native-webview": "12.4.0",
|
||||
"react-native-zip-archive": "6.0.9",
|
||||
"react-redux": "8.1.1",
|
||||
"react-redux": "8.1.2",
|
||||
"redux": "4.2.1",
|
||||
"rn-fetch-blob": "0.12.0",
|
||||
"stream": "0.0.2",
|
||||
@@ -105,7 +104,7 @@
|
||||
"@joplin/tools": "~2.12",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@testing-library/jest-native": "5.4.2",
|
||||
"@testing-library/react-native": "12.1.2",
|
||||
"@testing-library/react-native": "12.1.3",
|
||||
"@tsconfig/react-native": "2.0.2",
|
||||
"@types/fs-extra": "11.0.1",
|
||||
"@types/jest": "29.5.3",
|
||||
|
||||
@@ -351,7 +351,7 @@ export default class FsDriverRN extends FsDriverBase {
|
||||
} else {
|
||||
// the result is an array
|
||||
if (multiple) {
|
||||
result = await DocumentPicker.pickMultiple();
|
||||
result = await DocumentPicker.pick({ allowMultiSelection: true });
|
||||
} else {
|
||||
result = [await DocumentPicker.pick()];
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"standard": "17.1.0",
|
||||
"tap": "16.3.7"
|
||||
"tap": "16.3.8"
|
||||
},
|
||||
"gitHead": "eb4b0e64eab40a51b0895d3a40a9d8c3cb7b1b14"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"typedoc": "0.17.8",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ export default class BaseApplication {
|
||||
const newState = store.getState();
|
||||
|
||||
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(15 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
@@ -735,6 +735,20 @@ export default class BaseApplication {
|
||||
return toSystemSlashes(output, 'linux');
|
||||
}
|
||||
|
||||
protected startRotatingLogMaintenance(profileDir: string) {
|
||||
this.rotatingLogs = new RotatingLogs(profileDir);
|
||||
const processLogs = async () => {
|
||||
try {
|
||||
await this.rotatingLogs.cleanActiveLogFile();
|
||||
await this.rotatingLogs.deleteNonActiveLogFiles();
|
||||
} catch (error) {
|
||||
appLogger.error(error);
|
||||
}
|
||||
};
|
||||
shim.setTimeout(() => { void processLogs(); }, 60000);
|
||||
shim.setInterval(() => { void processLogs(); }, 24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
public async start(argv: string[], options: StartOptions = null): Promise<any> {
|
||||
options = {
|
||||
keychainEnabled: true,
|
||||
@@ -932,18 +946,6 @@ export default class BaseApplication {
|
||||
|
||||
await MigrationService.instance().run();
|
||||
|
||||
this.rotatingLogs = new RotatingLogs(profileDir);
|
||||
const processLogs = async () => {
|
||||
try {
|
||||
await this.rotatingLogs.cleanActiveLogFile();
|
||||
await this.rotatingLogs.deleteNonActiveLogFiles();
|
||||
} catch (error) {
|
||||
appLogger.error(error);
|
||||
}
|
||||
};
|
||||
shim.setTimeout(() => { void processLogs(); }, 60000);
|
||||
shim.setInterval(() => { void processLogs(); }, 24 * 60 * 60 * 1000);
|
||||
|
||||
return argv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ export default class BaseSyncTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the sync target expects a non-empty sync.{id}.password
|
||||
// setting.
|
||||
public static requiresPassword() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static description(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const shim_1 = require("./shim");
|
||||
class RotatingLogs {
|
||||
constructor(logFilesDir, maxFileSize = null, inactiveMaxAge = null) {
|
||||
this.maxFileSize = 1024 * 1024 * 100;
|
||||
this.inactiveMaxAge = 90 * 24 * 60 * 60 * 1000;
|
||||
this.logFilesDir = logFilesDir;
|
||||
if (maxFileSize)
|
||||
this.maxFileSize = maxFileSize;
|
||||
if (inactiveMaxAge)
|
||||
this.inactiveMaxAge = inactiveMaxAge;
|
||||
}
|
||||
cleanActiveLogFile() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const stats = yield this.fsDriver().stat(this.logFileFullpath());
|
||||
if (stats.size >= this.maxFileSize) {
|
||||
const newLogFile = this.logFileFullpath(this.getNameToNonActiveLogFile());
|
||||
yield this.fsDriver().move(this.logFileFullpath(), newLogFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
getNameToNonActiveLogFile() {
|
||||
return `log-${Date.now()}.txt`;
|
||||
}
|
||||
deleteNonActiveLogFiles() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const files = yield this.fsDriver().readDirStats(this.logFilesDir);
|
||||
for (const file of files) {
|
||||
if (!file.path.match(/^log-[0-9]+.txt$/gi))
|
||||
continue;
|
||||
const ageOfTheFile = Date.now() - file.birthtime;
|
||||
if (ageOfTheFile >= this.inactiveMaxAge) {
|
||||
yield this.fsDriver().remove(this.logFileFullpath(file.path));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
logFileFullpath(fileName = 'log.txt') {
|
||||
return `${this.logFilesDir}/${fileName}`;
|
||||
}
|
||||
fsDriver() {
|
||||
return shim_1.default.fsDriver();
|
||||
}
|
||||
}
|
||||
exports.default = RotatingLogs;
|
||||
//# sourceMappingURL=RotatingLogs.js.map
|
||||
@@ -1,56 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fs_extra_1 = require("fs-extra");
|
||||
const test_utils_1 = require("./testing/test-utils");
|
||||
const RotatingLogs_1 = require("./RotatingLogs");
|
||||
const createTestLogFile = (dir) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
yield (0, fs_extra_1.writeFile)(`${dir}/log.txt`, 'some content');
|
||||
});
|
||||
describe('RotatingLogs', () => {
|
||||
test('should rename log.txt to log-TIMESTAMP.txt', () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
let dir;
|
||||
try {
|
||||
dir = yield (0, test_utils_1.createTempDir)();
|
||||
yield createTestLogFile(dir);
|
||||
let files = yield (0, fs_extra_1.readdir)(dir);
|
||||
expect(files.find(file => file.match(/^log.txt$/gi))).toBeTruthy();
|
||||
expect(files.length).toBe(1);
|
||||
const rotatingLogs = new RotatingLogs_1.default(dir, 1, 1);
|
||||
yield rotatingLogs.cleanActiveLogFile();
|
||||
files = yield (0, fs_extra_1.readdir)(dir);
|
||||
expect(files.find(file => file.match(/^log.txt$/gi))).toBeFalsy();
|
||||
expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeTruthy();
|
||||
expect(files.length).toBe(1);
|
||||
}
|
||||
finally {
|
||||
yield (0, fs_extra_1.remove)(dir);
|
||||
}
|
||||
}));
|
||||
test('should delete inative log file after 1ms', () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
let dir;
|
||||
try {
|
||||
dir = yield (0, test_utils_1.createTempDir)();
|
||||
yield createTestLogFile(dir);
|
||||
const rotatingLogs = new RotatingLogs_1.default(dir, 1, 1);
|
||||
yield rotatingLogs.cleanActiveLogFile();
|
||||
yield (0, test_utils_1.msleep)(1);
|
||||
yield rotatingLogs.deleteNonActiveLogFiles();
|
||||
const files = yield (0, fs_extra_1.readdir)(dir);
|
||||
expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeFalsy();
|
||||
expect(files.length).toBe(0);
|
||||
}
|
||||
finally {
|
||||
yield (0, fs_extra_1.remove)(dir);
|
||||
}
|
||||
}));
|
||||
});
|
||||
//# sourceMappingURL=RotatingLogs.test.js.map
|
||||
@@ -12,7 +12,7 @@ describe('RotatingLogs', () => {
|
||||
try {
|
||||
dir = await createTempDir();
|
||||
await createTestLogFile(dir);
|
||||
let files: string[] = await readdir(dir);
|
||||
let files = await readdir(dir);
|
||||
expect(files.find(file => file.match(/^log.txt$/gi))).toBeTruthy();
|
||||
expect(files.length).toBe(1);
|
||||
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 1);
|
||||
@@ -26,7 +26,7 @@ describe('RotatingLogs', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should delete inative log file after 1ms', async () => {
|
||||
test('should delete inactive log file after 1ms', async () => {
|
||||
let dir: string;
|
||||
try {
|
||||
dir = await createTempDir();
|
||||
@@ -42,4 +42,21 @@ describe('RotatingLogs', () => {
|
||||
await remove(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not delete the log-timestamp.txt right after its be created', async () => {
|
||||
let dir: string;
|
||||
try {
|
||||
dir = await createTempDir();
|
||||
await createTestLogFile(dir);
|
||||
await msleep(100);
|
||||
const rotatingLogs: RotatingLogs = new RotatingLogs(dir, 1, 100);
|
||||
await rotatingLogs.cleanActiveLogFile();
|
||||
await rotatingLogs.deleteNonActiveLogFiles();
|
||||
const files = await readdir(dir);
|
||||
expect(files.find(file => file.match(/^log-[0-9]+.txt$/gi))).toBeTruthy();
|
||||
expect(files.length).toBe(1);
|
||||
} finally {
|
||||
await remove(dir);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,8 @@ export default class RotatingLogs {
|
||||
const files: Stat[] = await this.fsDriver().readDirStats(this.logFilesDir);
|
||||
for (const file of files) {
|
||||
if (!file.path.match(/^log-[0-9]+.txt$/gi)) continue;
|
||||
const ageOfTheFile: number = Date.now() - file.birthtime;
|
||||
const timestamp: number = parseInt(file.path.match(/[0-9]+/g)[0], 10);
|
||||
const ageOfTheFile: number = Date.now() - timestamp;
|
||||
if (ageOfTheFile >= this.inactiveMaxAge) {
|
||||
await this.fsDriver().remove(this.logFileFullpath(file.path));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ class SyncTargetAmazonS3 extends BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
static requiresPassword() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static s3BucketName() {
|
||||
return Setting.value('sync.8.path');
|
||||
}
|
||||
|
||||
@@ -49,6 +49,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static requiresPassword() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async fileApi(): Promise<FileApi> {
|
||||
return super.fileApi();
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static requiresPassword() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async fileApi(): Promise<FileApi> {
|
||||
return super.fileApi();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
static requiresPassword() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async checkConfig(options) {
|
||||
return SyncTargetWebDAV.checkConfig(options);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@ class SyncTargetWebDAV extends BaseSyncTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
static requiresPassword() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async newFileApi_(syncTargetId, options) {
|
||||
const apiOptions = {
|
||||
baseUrl: () => options.path(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Setting = require('../../models/Setting').default;
|
||||
const SyncTargetRegistry = require('../../SyncTargetRegistry').default;
|
||||
const ObjectUtils = require('../../ObjectUtils');
|
||||
const { _ } = require('../../locale');
|
||||
const Setting = require('../../../models/Setting').default;
|
||||
const SyncTargetRegistry = require('../../../SyncTargetRegistry').default;
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
const { _ } = require('../../../locale');
|
||||
const { createSelector } = require('reselect');
|
||||
const Logger = require('@joplin/utils/Logger').default;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import SyncTargetRegistry from '../../../SyncTargetRegistry';
|
||||
import shouldShowMissingPasswordWarning from './shouldShowMissingPasswordWarning';
|
||||
|
||||
// Maps targets to whether each target requires a password.
|
||||
// A subset of all sync targets.
|
||||
const targetToRequiresPassword: Record<string, boolean> = {
|
||||
'nextcloud': true,
|
||||
'webdav': true,
|
||||
'amazon_s3': true,
|
||||
'joplinServer': true,
|
||||
'joplinCloud': true,
|
||||
'onedrive': false,
|
||||
'dropbox': false,
|
||||
};
|
||||
|
||||
describe('shouldShowMissingPasswordWarning', () => {
|
||||
it('should return true when sync target requires a password and the password is missing', () => {
|
||||
for (const targetName in targetToRequiresPassword) {
|
||||
const targetId = SyncTargetRegistry.nameToId(targetName);
|
||||
const expected = targetToRequiresPassword[targetName];
|
||||
|
||||
expect(shouldShowMissingPasswordWarning(targetId, {})).toBe(expected);
|
||||
|
||||
// Should also consider an empty string to be missing
|
||||
const settings = {
|
||||
[`sync.${targetId}.password`]: '',
|
||||
};
|
||||
expect(shouldShowMissingPasswordWarning(targetId, settings)).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return false when a password is present', () => {
|
||||
for (const targetName in targetToRequiresPassword) {
|
||||
const targetId = SyncTargetRegistry.nameToId(targetName);
|
||||
const settings = {
|
||||
[`sync.${targetId}.password`]: 'some nonempty',
|
||||
};
|
||||
|
||||
expect(shouldShowMissingPasswordWarning(targetId, settings)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import SyncTargetRegistry from '../../../SyncTargetRegistry';
|
||||
|
||||
const shouldShowMissingPasswordWarning = (syncTargetId: number, settings: any) => {
|
||||
const syncTargetClass = SyncTargetRegistry.classById(syncTargetId);
|
||||
|
||||
return syncTargetClass.requiresPassword() && !settings[`sync.${syncTargetId}.password`];
|
||||
};
|
||||
|
||||
export default shouldShowMissingPasswordWarning;
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export const isInsideContainer = (node: any, className: string): boolean => {
|
||||
while (node) {
|
||||
if (node.classList && node.classList.contains(className)) return true;
|
||||
@@ -7,3 +5,49 @@ export const isInsideContainer = (node: any, className: string): boolean => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const waitForElement = async (parent: any, id: string): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const iid = setInterval(() => {
|
||||
try {
|
||||
const element = parent.getElementById(id);
|
||||
if (element) {
|
||||
clearInterval(iid);
|
||||
resolve(element);
|
||||
}
|
||||
} catch (error) {
|
||||
clearInterval(iid);
|
||||
reject(error);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Imported from https://github.com/Moh-Snoussi/keyboard-event-key-type
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
type NumericKeypadKeys = 'Decimal' | 'Key11' | 'Key12' | 'Multiply' | 'Add' | 'Clear' | 'Divide' | 'Subtract' | 'Separator' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
|
||||
type UpperAlpha = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
|
||||
type LowerAlpha = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
|
||||
type ModifierKeys = 'Alt' | 'AltGraph' | 'CapsLock' | 'Control' | 'Fn' | 'FnLock' | 'Hyper' | 'Meta' | 'NumLock' | 'ScrollLock' | 'Shift' | 'Super' | 'Symbol' | 'SymbolLock';
|
||||
type WhitespaceKeys = 'Enter' | 'Tab' | ' ';
|
||||
type NavigationKeys = 'ArrowDown' | 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'End' | 'Home' | 'PageDown' | 'PageUp';
|
||||
type EditingKeys = 'Backspace' | 'Clear' | 'Copy' | 'CrSel' | 'Cut' | 'Delete' | 'EraseEof' | 'ExSel' | 'Insert' | 'Paste' | 'Redo' | 'Undo';
|
||||
type UIKeys = 'Accept' | 'Again' | 'Attn' | 'Cancel' | 'ContextMenu' | 'Escape' | 'Execute' | 'Find' | 'Finish' | 'Help' | 'Pause' | 'Play' | 'Props' | 'Select' | 'ZoomIn' | 'ZoomOut';
|
||||
type DeviceKeys = 'BrightnessDown' | 'BrightnessUp' | 'Eject' | 'LogOff' | 'Power' | 'PowerOff' | 'PrintScreen' | 'Hibernate' | 'Standby' | 'WakeUp';
|
||||
type IMECompositionKeys = 'AllCandidates' | 'Alphanumeric' | 'CodeInput' | 'Compose' | 'Convert' | 'Dead' | 'FinalMode' | 'GroupFirst' | 'GroupLast' | 'GroupNext' | 'GroupPrevious' | 'ModeChange' | 'NextCandidate' | 'NonConvert' | 'PreviousCandidate' | 'Process' | 'SingleCandidate';
|
||||
type LinuxDeadKeys = 'GDK_KEY_dead_grave' | 'GDK_KEY_dead_acute' | 'GDK_KEY_dead_circumflex' | 'GDK_KEY_dead_tilde' | 'GDK_KEY_dead_perispomeni' | 'GDK_KEY_dead_macron' | 'GDK_KEY_dead_breve' | 'GDK_KEY_dead_abovedot' | 'GDK_KEY_dead_diaeresis' | 'GDK_KEY_dead_abovering' | 'GDK_KEY_dead_doubleacute' | 'GDK_KEY_dead_caron' | 'GDK_KEY_dead_cedilla' | 'GDK_KEY_dead_ogonek' | 'GDK_KEY_dead_iota' | 'GDK_KEY_dead_voiced_sound' | 'GDK_KEY_dead_semivoiced_sound' | 'GDK_KEY_dead_belowdot' | 'GDK_KEY_dead_hook' | 'GDK_KEY_dead_horn' | 'GDK_KEY_dead_stroke' | 'GDK_KEY_dead_abovecomma' | 'GDK_KEY_dead_psili' | 'GDK_KEY_dead_abovereversedcomma' | 'GDK_KEY_dead_dasia' | 'GDK_KEY_dead_doublegrave' | 'GDK_KEY_dead_belowring' | 'GDK_KEY_dead_belowmacron' | 'GDK_KEY_dead_belowcircumflex' | 'GDK_KEY_dead_belowtilde' | 'GDK_KEY_dead_belowbreve' | 'GDK_KEY_dead_belowdiaeresis' | 'GDK_KEY_dead_invertedbreve' | 'GDK_KEY_dead_belowcomma' | 'GDK_KEY_dead_currency' | 'GDK_KEY_dead_a' | 'GDK_KEY_dead_A' | 'GDK_KEY_dead_e' | 'GDK_KEY_dead_E' | 'GDK_KEY_dead_i' | 'GDK_KEY_dead_I' | 'GDK_KEY_dead_o' | 'GDK_KEY_dead_O' | 'GDK_KEY_dead_u' | 'GDK_KEY_dead_U' | 'GDK_KEY_dead_small_schwa' | 'GDK_KEY_dead_capital_schwa' | 'GDK_KEY_dead_greek';
|
||||
type FunctionKeys = 'F1' | 'F2' | 'F3' | 'F4' | 'F5' | 'F6' | 'F7' | 'F8' | 'F9' | 'F10' | 'F11' | 'F12' | 'F13' | 'F14' | 'F15' | 'F16' | 'F17' | 'F18' | 'F19' | 'F20' | 'Soft1' | 'Soft2' | 'Soft3' | 'Soft4';
|
||||
type PhoneKeys = 'AppSwitch' | 'Call' | 'Camera' | 'CameraFocus' | 'EndCall' | 'GoBack' | 'GoHome' | 'HeadsetHook' | 'LastNumberRedial' | 'Notification' | 'MannerMode' | 'VoiceDial';
|
||||
type MultimediaKeys = 'ChannelDown' | 'ChannelUp' | 'MediaFastForward' | 'MediaPause' | 'MediaPlay' | 'MediaPlayPause' | 'MediaRecord' | 'MediaRewind' | 'MediaStop' | 'MediaTrackNext' | 'MediaTrackPrevious';
|
||||
type TVControlKeys = 'TV' | 'TV3DMode' | 'TVAntennaCable' | 'TVAudioDescription' | 'TVAudioDescriptionMixDown' | 'TVAudioDescriptionMixUp' | 'TVContentsMenu' | 'TVDataService' | 'TVInput' | 'TVInputComponent1' | 'TVInputComponent2' | 'TVInputComposite1' | 'TVInputComposite2' | 'TVInputHDMI1' | 'TVInputHDMI2' | 'TVInputHDMI3' | 'TVInputHDMI4' | 'TVInputVGA1' | 'TVMediaContext' | 'TVNetwork' | 'TVNumberEntry' | 'TVPower' | 'TVRadioService' | 'TVSatellite' | 'TVSatelliteBS' | 'TVSatelliteCS' | 'TVSatelliteToggle' | 'TVTerrestrialAnalog' | 'TVTerrestrialDigital' | 'TVTimer';
|
||||
type MediaControllerKeys = 'AVRInput' | 'AVRPower' | 'ColorF0Red' | 'ColorF1Green' | 'ColorF2Yellow' | 'ColorF3Blue' | 'ColorF4Grey' | 'ColorF5Brown' | 'ClosedCaptionToggle' | 'Dimmer' | 'DisplaySwap' | 'DVR' | 'Exit' | 'FavoriteClear0' | 'FavoriteClear1' | 'FavoriteClear2' | 'FavoriteClear3' | 'FavoriteRecall0' | 'FavoriteRecall1' | 'FavoriteRecall2' | 'FavoriteRecall3' | 'FavoriteStore0' | 'FavoriteStore1' | 'FavoriteStore2' | 'FavoriteStore3' | 'Guide' | 'GuideNextDay' | 'GuidePreviousDay' | 'Info' | 'InstantReplay' | 'Link' | 'ListProgram' | 'LiveContent' | 'Lock' | 'MediaApps' | 'MediaAudioTrack' | 'MediaLast' | 'MediaSkipBackward' | 'MediaSkipForward' | 'MediaStepBackward' | 'MediaStepForward' | 'MediaTopMenu' | 'NavigateIn' | 'NavigateNext' | 'NavigateOut' | 'NavigatePrevious' | 'NextFavoriteChannel' | 'NextUserProfile' | 'OnDemand' | 'Pairing' | 'PinPDown' | 'PinPMove' | 'PinPToggle' | 'PinPUp' | 'PlaySpeedDown' | 'PlaySpeedReset' | 'PlaySpeedUp' | 'RandomToggle' | 'RcLowBattery' | 'RecordSpeedNext' | 'RfBypass' | 'ScanChannelsToggle' | 'ScreenModeNext' | 'Settings' | 'SplitScreenToggle' | 'STBInput' | 'STBPower' | 'Subtitle' | 'Teletext' | 'VideoModeNext' | 'Wink' | 'ZoomToggle';
|
||||
type SpeechRecognitionKeys = 'SpeechCorrectionList' | 'SpeechInputToggle';
|
||||
type DocumentKeys = 'Close' | 'New' | 'Open' | 'Print' | 'Save' | 'SpellCheck' | 'MailForward' | 'MailReply' | 'MailSend';
|
||||
type ApplicationSelectorKeys = 'LaunchCalculator' | 'LaunchCalendar' | 'LaunchContacts' | 'LaunchMail' | 'LaunchMediaPlayer' | 'LaunchMusicPlayer' | 'LaunchMyComputer' | 'LaunchPhone' | 'LaunchScreenSaver' | 'LaunchSpreadsheet' | 'LaunchWebBrowser' | 'LaunchWebCam' | 'LaunchWordProcessor' | 'LaunchApplication1' | 'LaunchApplication2' | 'LaunchApplication3' | 'LaunchApplication4' | 'LaunchApplication5' | 'LaunchApplication6' | 'LaunchApplication7' | 'LaunchApplication8' | 'LaunchApplication9' | 'LaunchApplication10' | 'LaunchApplication11' | 'LaunchApplication12' | 'LaunchApplication13' | 'LaunchApplication14' | 'LaunchApplication15' | 'LaunchApplication16';
|
||||
type BrowserControlKeys = 'BrowserBack' | 'BrowserFavorites' | 'BrowserForward' | 'BrowserHome' | 'BrowserRefresh' | 'BrowserSearch' | 'BrowserStop';
|
||||
type KoreanKeyboardsOnly = 'HangulMode' | 'HanjaMode' | 'JunjaMode';
|
||||
type SpecialValueKey = 'Unidentified';
|
||||
|
||||
export declare type KeyboardEventKey = SpecialValueKey | ModifierKeys | WhitespaceKeys | NavigationKeys | EditingKeys | UIKeys | DeviceKeys | IMECompositionKeys | LinuxDeadKeys | FunctionKeys | PhoneKeys | MultimediaKeys | TVControlKeys | MediaControllerKeys | SpeechRecognitionKeys | DocumentKeys | ApplicationSelectorKeys | BrowserControlKeys | NumericKeypadKeys | UpperAlpha | LowerAlpha | KoreanKeyboardsOnly;
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"Application": "Anwendung",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure you want to renew the authorisation token?": "Bist du sicher, dass du den Berechtigungstoken erneuern möchtest?",
|
||||
"Are you sure you want to return to the default layout? The current layout configuration will be lost.": "Sicher, dass du zum Standardlayout zurückkehren möchtest? Das aktuelle Layout geht dabei verloren",
|
||||
"Are you sure you want to return to the default layout? The current layout configuration will be lost.": "Sicher, dass du zum Standardlayout zurückkehren möchtest? Das aktuelle Layout geht dabei verloren.",
|
||||
"Arguments:": "Kommandozeilenargumente:",
|
||||
"Aritim Dark": "Aritim dunkel",
|
||||
"Attach": "Anhängen",
|
||||
@@ -99,6 +99,7 @@
|
||||
"Automatically switch theme to match system theme": "Automatisch das Design ändern, um es dem System-Design anzupassen",
|
||||
"Back": "Zurück",
|
||||
"Basic": "Standard",
|
||||
"Biometric unlock is not setup on the device. Please set it up in order to unlock Joplin. If the device is on lockout, consider switching it off and on to reset biometrics scanning.": "Biometrisches Entsperren ist auf diesem Gerät nicht eingerichtet. Bitte richte es ein, um Joplin zu entsperren. Falls das Gerät gesperrt ist, solltest du es aus- und wieder einschalten, um das biometrische Scannen zurückzusetzen.",
|
||||
"Bold": "Fett",
|
||||
"Browse all plugins": "Alle Erweiterungen durchsuchen",
|
||||
"Browse...": "Durchsuchen ...",
|
||||
@@ -169,6 +170,7 @@
|
||||
"Continue": "Fortfahren",
|
||||
"Convert to note": "In eine Notiz umwandeln",
|
||||
"Convert to todo": "In eine Aufgabe umwandeln",
|
||||
"Converting speech to text...": "Wandle Sprache in Text um...",
|
||||
"Copy": "Kopieren",
|
||||
"Copy dev mode command to clipboard": "Entwicklermodus-Befehl in Zwischenablage kopieren",
|
||||
"Copy external link": "Externen Link kopieren",
|
||||
@@ -187,6 +189,7 @@
|
||||
"Could not switch profile: %s": "Konnte das Profil nicht wechseln: %s",
|
||||
"Could not upgrade master key: %s": "Konnte Hauptschlüssel nicht aktualisieren: %s",
|
||||
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Der Freigabestatus dieses Notizbuchs konnte nicht überprüft werden - Vorgang wird abgebrochen. Bitte versuche es erneut, wenn eine Internetverbindung besteht.",
|
||||
"Could not verify your identify: %s": "Konnte deine Identität nicht verifizieren: %s",
|
||||
"Create": "Erstellen",
|
||||
"Create a new notebook under a parent notebook.": "Erstelle ein neues Notizbuch unter einem übergeordneten Notizbuch.",
|
||||
"Create a notebook": "Notizbuch erstellen",
|
||||
@@ -277,6 +280,7 @@
|
||||
"Downloaded and decrypted": "Heruntergeladen und entschlüsselt",
|
||||
"Downloaded and encrypted": "Heruntergeladen und verschlüsselt",
|
||||
"Downloading": "Wird heruntergeladen",
|
||||
"Downloading %s language files...": "Lade %s Sprachdateien herunter...",
|
||||
"Downloading resources...": "Anhänge werden heruntergeladen ...",
|
||||
"Dracula": "Dracula",
|
||||
"Drop notes or files here": "Notizen oder Dateien hierher ziehen",
|
||||
@@ -326,6 +330,7 @@
|
||||
"Enable soft breaks": "Weiche Zeilenumbrüche aktivieren",
|
||||
"Enable spellcheck in the text editor": "Aktiviere die Rechtschreibprüfung im Text-Editor",
|
||||
"Enable table of contents extension": "Inhaltsverzeichnis-Erweiterung aktivieren",
|
||||
"Enable the Markdown toolbar": "Markdown Werkzeugleiste aktivieren",
|
||||
"Enable typographer support": "Typographie-Unterstützung aktivieren",
|
||||
"Enable video player": "Videospieler aktivieren",
|
||||
"Enable Web Clipper Service": "Web-Clipper-Dienst aktivieren",
|
||||
@@ -406,6 +411,7 @@
|
||||
"Headers": "Kopfzeilen",
|
||||
"Heading": "Überschrift",
|
||||
"Help": "Hilfe",
|
||||
"Hermes enabled: %d": "Hermes aktiviert: %d",
|
||||
"Hide %s": "%s ausblenden",
|
||||
"Hide advanced": "Erweitertes verstecken",
|
||||
"Hide disabled": "Deaktiviertes verstecken",
|
||||
@@ -489,6 +495,7 @@
|
||||
"Later": "Später",
|
||||
"Layout": "Layout",
|
||||
"Layout button sequence": "Layout-Reihenfolge",
|
||||
"Leave it blank to download the language files from the default website": "Lasse es leer, um die Sprachdateien von der Standardseite herunterzuladen.",
|
||||
"Leave notebook...": "Verlasse Notizbuch...",
|
||||
"Legal": "Legal",
|
||||
"Letter": "Letter",
|
||||
@@ -502,6 +509,7 @@
|
||||
"List item": "Listeneintrag",
|
||||
"Lists": "Listen",
|
||||
"Loaded": "Geladen",
|
||||
"Loading...": "Lade...",
|
||||
"Location": "Standort",
|
||||
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine Synchronisation im Gange ist, kannst du die Sperrdatei „%s“ löschen und den Vorgang fortsetzen.",
|
||||
"Log": "Protokoll",
|
||||
@@ -642,6 +650,7 @@
|
||||
"Please enter your password in the master key list below before upgrading the key.": "Bitte gib zuerst dein Passwort in der unten stehenden Liste der Hauptschlüssel ein, bevor du den Schlüssel aktualisierst.",
|
||||
"Please note that if it is a large notebook, it may take a few minutes for all the notes to show up on the recipient's device.": "Wenn das Notizbuch sehr groß ist, kann es ein paar Minuten dauern, bis alle Notizen auf dem Empfängergerät erscheinen.",
|
||||
"Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.": "Bitte öffne die folgende URL in deinem Browser, um die Anwendung zu authentifizieren. Die Anwendung erstellt ein Verzeichnis in „Apps/Joplin“ und kann nur Dateien in diesem Verzeichnis lesen und schreiben. Sie hat weder Zugriff auf Dateien außerhalb dieses Verzeichnisses noch auf andere persönliche Daten. Es werden keine Daten an Dritte weitergegeben.",
|
||||
"Please record your voice...": "Bitte nehme deine Stimme auf...",
|
||||
"Please select a notebook first.": "Bitte wähle erst ein Notizbuch aus.",
|
||||
"Please select the note or notebook to be deleted first.": "Bitte wähle zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden soll.",
|
||||
"Please select where the sync status should be exported to": "Bitte wähle aus, wohin der Synchronisations-Status exportiert werden soll",
|
||||
@@ -751,6 +760,7 @@
|
||||
"Select all": "Alles auswählen",
|
||||
"Select emoji...": "Emoji auswählen...",
|
||||
"Select file...": "Datei auswählen...",
|
||||
"Select parent notebook": "Eltern-Notizbuch auswählen",
|
||||
"Server is already running on port %d": "Server läuft bereits auf Port %d",
|
||||
"Server is not running.": "Server läuft nicht.",
|
||||
"Server is running on port %d": "Server läuft auf Port %d",
|
||||
@@ -996,6 +1006,8 @@
|
||||
"View them now": "Zeige sie jetzt an",
|
||||
"Viewer": "Vorschau",
|
||||
"Vim": "Vim",
|
||||
"Voice typing language files (URL)": "Sprachdateien für die Spracheingabe (URL)",
|
||||
"Voice typing...": "Spracheingabe...",
|
||||
"Warning": "Warnung",
|
||||
"Warning: not all resources shown for performance reasons (limit: %s).": "Warnung: Aus Leistungsgründen werden nicht alle Anhänge angezeigt (Obergrenze: %s).",
|
||||
"Web Clipper": "Web Clipper",
|
||||
@@ -1005,6 +1017,7 @@
|
||||
"WebDAV username": "WebDAV-Benutzername",
|
||||
"Website and documentation": "Webseite und Dokumentation",
|
||||
"Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.": "Willkommen bei Joplin!\n\nTippe `:help shortcuts` für eine Liste der Shortcuts oder `:help` für Informationen zur Benutzung ein.\n\nUm zum Beispiel ein Notizbuch zu erstellen, drücke `mb`; um eine Notiz zu erstellen drücke `mn`.",
|
||||
"Welcome!": "Willkommen!",
|
||||
"When creating a new note:": "Wenn eine neue Notiz erstellt wird:",
|
||||
"When creating a new to-do:": "Wenn eine neue Aufgabe erstellt wird:",
|
||||
"Words": "Wörter",
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"\"%s\" is missing the required \"%s\" property.": "Το \"%s\" δεν διαθέτει την απαιτούμενη ιδιότητα \"%s\".",
|
||||
"%d days": "%d ημέρες",
|
||||
"%d GB": "%d GB",
|
||||
"%d GB storage space": "%d GB αποθηκευτικού χώρου",
|
||||
"%d hour": "%d ώρα",
|
||||
"%d hours": "%d ώρες",
|
||||
"%d MB": "%d MB",
|
||||
"%d MB per note or attachment": "%d MB ανά σημείωση ή συνημμένο",
|
||||
"%d minutes": "%d΄λεπτά",
|
||||
"%d notes match this pattern. Delete them?": "%d σημειώσεις ταιριάζουν. Θέλετε να διαγραφούν;",
|
||||
"%s": "%s",
|
||||
"%s %s (%s, %s)": "%s %s (%s, %s)",
|
||||
"%s (%s) could not be uploaded: %s": "%s (%s) δεν μπορούν να ανέβουν: %s",
|
||||
"%s (%s) would like to share a notebook with you.": "%s (%s) θα ήθελε να μοιραστεί μαζί σας ένα σημειωματάριο.",
|
||||
@@ -27,6 +32,7 @@
|
||||
"&Tools": "&Εργαλεία",
|
||||
"&View": "&Εμφάνιση",
|
||||
"(%s)": "(%s)",
|
||||
"(In plugin: %s)": "(Στο plugin: %s)",
|
||||
"(None)": "(Κανένας)",
|
||||
"(wysiwyg: %s)": "(wysiwyg: %s)",
|
||||
"- Camera: to allow taking a picture and attaching it to a note.": "- Φωτογραφική μηχανή: για να μπορείτε να τραβήξετε μια φωτογραφία και να την προσαρτήσετε σε μια σημείωση.",
|
||||
@@ -42,11 +48,13 @@
|
||||
"Accelerator \"%s\" is not valid.": "Ο επιταχυντής \"%s\" δεν είναι έγκυρος.",
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "Ο επιταχυντής \"%s\" χρησιμοποιείται για εντολές \"%s\" και \"%s\". Αυτό μπορεί να οδηγήσει σε απροσδόκητη συμπεριφορά.",
|
||||
"Accept": "Αποδοχή",
|
||||
"Account": "Λογαριασμός",
|
||||
"Action": "Ενέργεια",
|
||||
"Actions": "Ενέργειες",
|
||||
"Active": "Ενεργό",
|
||||
"Actual Size": "Πραγματικό μέγεθος",
|
||||
"Add body": "Προσθήκη σώματος κειμένου",
|
||||
"Add new": "Προσθήκη νέου",
|
||||
"Add or remove tags:": "Προσθήκη ή διαγραφή ετικετών:",
|
||||
"Add recipient:": "Προσθήκη παραλήπτη:",
|
||||
"Add title": "Προσθήκη τίτλου",
|
||||
@@ -55,17 +63,23 @@
|
||||
"Admin dashboard": "Πίνακας ελέγχου διαχειριστή",
|
||||
"Advanced options": "Επιλογές για προχωρημένους",
|
||||
"Advanced tools": "Προηγμένα εργαλεία",
|
||||
"All data, including notes, notebooks and tags will be permanently deleted.": "Όλα τα δεδομένα, συμπεριλαμβανομένων των σημειώσεων, των σημειωματάριων και των ετικετών, θα διαγραφούν οριστικά.",
|
||||
"All notes": "Όλες οι σημειώσεις",
|
||||
"All potential ports are in use - please report the issue at %s": "Όλες οι πιθανές θύρες χρησιμοποιούνται - αναφέρετε το ζήτημα στο %s",
|
||||
"Also displays unset and hidden config variables.": "Εμφανίζει επίσης μη ορισμένες και κρυφές μεταβλητές.",
|
||||
"Also publish linked notes": "Δημοσίευση σημειώσεων στο διαδίκτυο",
|
||||
"Always": "Πάντα",
|
||||
"Ambiguous notebook \"%s\". Please use notebook id instead - press \"ti\" to see the short notebook id or use $b for current selected notebook": "Διφορούμενο σημειωματάριο \"%s\". Παρακαλούμε χρησιμοποιήστε το id του σημειωματάριου - πατήστε \"ti\" για να δείτε το σύντομο id του σημειωματάριου ή χρησιμοποιήστε $b για το τρέχον επιλεγμένο σημειωματάριο.",
|
||||
"Ambiguous notebook \"%s\". Please use short notebook id instead - press \"ti\" to see the short notebook id": "Διφορούμενο σημειωματάριο \"%s\". Παρακαλούμε χρησιμοποιήστε το σύντομο αναγνωριστικό σημειωματάριου - πατήστε \"ti\" για να δείτε το σύντομο αναγνωριστικό σημειωματάριου",
|
||||
"An update is available, do you want to download it now?": "Υπάρχει διαθέσιμη μια ενημέρωση, θέλετε να την κατεβάσετε τώρα;",
|
||||
"Appearance": "Εμφάνιση",
|
||||
"Application": "Εφαρμογή",
|
||||
"Apply": "Εφαρμογή",
|
||||
"Are you sure you want to renew the authorisation token?": "Είστε βέβαιοι ότι θέλετε να ανανεώσετε το διακριτικό εξουσιοδότησης;",
|
||||
"Are you sure you want to return to the default layout? The current layout configuration will be lost.": "Είστε σίγουροι ότι θέλετε να επιστρέψετε στην προεπιλεγμένη διάταξη; Η τρέχουσα διαμόρφωση της διάταξης θα χαθεί.",
|
||||
"Arguments:": "Παράμετροι:",
|
||||
"Aritim Dark": "Aritim Dark",
|
||||
"Attach": "Επισύναψη",
|
||||
"Attach file": "Επισύναψη αρχείου",
|
||||
"Attach photo": "Επισυνάψτε μία φωτογραφία",
|
||||
"Attach...": "Επισύναψη...",
|
||||
@@ -84,10 +98,13 @@
|
||||
"Automatically check for updates": "Αυτόματος έλεγχος για ενημερώσεις",
|
||||
"Automatically switch theme to match system theme": "Αυτόματη εναλλαγή θέματος ώστε να ταιριάζει με το θέμα συστήματος",
|
||||
"Back": "Πίσω",
|
||||
"Basic": "Βασικό",
|
||||
"Biometric unlock is not setup on the device. Please set it up in order to unlock Joplin. If the device is on lockout, consider switching it off and on to reset biometrics scanning.": "Το βιομετρικό ξεκλείδωμα δεν έχει ρυθμιστεί στη συσκευή. Παρακαλούμε ρυθμίστε το για να ξεκλειδώσετε το Joplin. Εάν η συσκευή είναι κλειδωμένη, σκεφτείτε να την απενεργοποιήσετε και να την ενεργοποιήσετε για να επαναφέρετε τη βιομετρική σάρωση.",
|
||||
"Bold": "Έντονη γραφή",
|
||||
"Browse all plugins": "Αναζήτηση όλων των plugin",
|
||||
"Browse...": "Αναζήτηση...",
|
||||
"Bulleted List": "Λίστα με κουκκίδες",
|
||||
"Can Share": "Μπορεί να μοιραστεί",
|
||||
"Cancel": "Άκυρο",
|
||||
"Cancelling background synchronisation... Please wait.": "Ακύρωση συγχρονισμού ... Παρακαλώ περιμένετε.",
|
||||
"Cancelling...": "Ακύρωση...",
|
||||
@@ -96,6 +113,7 @@
|
||||
"Cannot change encrypted item": "Δεν είναι δυνατή η αλλαγή κρυπτογραφημένου στοιχείου",
|
||||
"Cannot copy note to \"%s\" notebook": "Δεν είναι δυνατή η αντιγραφή της σημείωσης στο σημειωματάριο \"%s\"",
|
||||
"Cannot find \"%s\".": "Δεν είναι δυνατή η εύρεση του \"%s\".",
|
||||
"Cannot find: \"%s\"": "Δεν είναι δυνατή η εύρεση του: \"%s\".",
|
||||
"Cannot initialise synchroniser.": "Δεν είναι δυνατή η προετοιμασία του συγχρονιστή.",
|
||||
"Cannot load \"%s\" module for format \"%s\" and output \"%s\"": "Δεν είναι δυνατή η φόρτωση του \"%s\" module με μορφή \"%s\" και έξοδο \"%s\"",
|
||||
"Cannot load \"%s\" module for format \"%s\" and target \"%s\"": "Δεν είναι δυνατή η φόρτωση του module \"%s\" για τη μορφοποίηση σε \"%s\" και την εξαγωγή σε \"%s\"",
|
||||
@@ -105,6 +123,7 @@
|
||||
"Cannot save %s \"%s\" because it is larger than the allowed limit (%s)": "Δεν είναι δυνατή η αποθήκευση του %s \"%s\" επειδή είναι μεγαλύτερο από το επιτρεπόμενο όριο (%s)",
|
||||
"Cannot save %s \"%s\" because it would go over the total allowed size (%s) for this account": "Δεν είναι δυνατή η αποθήκευση του %s \"%s\" επειδή υπερβαίνει το συνολικό επιτρεπόμενο μέγεθος (%s) για αυτόν τον λογαριασμό",
|
||||
"Cannot share encrypted notebook with recipient %s because they have not enabled end-to-end encryption. They may do so from the screen Configuration > Encryption.": "Δεν είναι δυνατή η κοινή χρήση κρυπτογραφημένου σημειωματάριου με τον παραλήπτη %s επειδή δεν έχει ενεργοποιήσει την κρυπτογράφηση από άκρο σε άκρο. Μπορούν να το κάνουν από την οθόνη Διαμόρφωση > Κρυπτογράφηση.",
|
||||
"Case sensitive": "Case sensitive",
|
||||
"Change application layout": "Αλλαγή διάταξης εφαρμογής",
|
||||
"Change language": "Αλλαγή γλώσσας",
|
||||
"Characters": "Χαρακτήρες",
|
||||
@@ -122,11 +141,13 @@
|
||||
"Click to add tags...": "Κάντε κλικ για να προσθέσετε ετικέτες...",
|
||||
"Client ID: %s": "Αναγνωριστικό προγράμματος-πελάτη: %s",
|
||||
"Close": "Κλείσιμο",
|
||||
"Close dropdown": "Κλείσιμο πτυσσόμενου μενού",
|
||||
"Close Window": "Κλείσιμο παραθύρου",
|
||||
"Code": "Κώδικας",
|
||||
"Code Block": "Μπλοκ Κώδικα",
|
||||
"Code View": "Προβολή κώδικα",
|
||||
"Collaborate on notebooks with others": "Συνεργασία σε σημειωματάρια με άλλους",
|
||||
"Collapse": "Collapse",
|
||||
"Coming alarms": "Προσεχείς ειδοποιήσεις",
|
||||
"Comma-separated list of paths to directories to load the certificates from, or path to individual cert files. For example: /my/cert_dir, /other/custom.pem. Note that if you make changes to the TLS settings, you must save your changes before clicking on \"Check synchronisation configuration\".": "Λίστα διαδρομών από φακέλους, διαχωρισμένων με κόμμα, για τη φόρτωση των πιστοποιητικών, ή διαδρομή των μεμονωμένων αρχείων πιστοποιητικών. Για παράδειγμα: /my/cert_dir, /other/custom.pem. Σημειώστε ότι αν κάνετε αλλαγές στις ρυθμίσεις TLS, πρέπει να αποθηκεύσετε τις αλλαγές σας πριν κάνετε κλικ στο \"Έλεγχος ρύθμισης παραμέτρων συγχρονισμού\".",
|
||||
"command": "εντολή",
|
||||
@@ -144,9 +165,12 @@
|
||||
"Conflicted: %d": "Με διένεξη: %d",
|
||||
"Conflicts": "Διενέξεις",
|
||||
"Conflicts (attachments)": "Διενέξεις (συνημμένα)",
|
||||
"Consolidated billing": "Ενοποιημένη χρέωση",
|
||||
"Content provided by %s": "Περιεχόμενο που παρέχεται από %s",
|
||||
"Continue": "Συνεχίστε",
|
||||
"Convert to note": "Μετατροπή σε σημείωση",
|
||||
"Convert to todo": "Μετατροπή σε to-do",
|
||||
"Converting speech to text...": "Μετατροπή της ομιλίας σε κείμενο...",
|
||||
"Copy": "Αντιγραφή",
|
||||
"Copy dev mode command to clipboard": "Αντιγραφή dev mode εντολής στο πρόχειρο",
|
||||
"Copy external link": "Αντιγραφή εξωτερικού συνδέσμου",
|
||||
@@ -162,9 +186,14 @@
|
||||
"Could not export notes: %s": "Δεν ήταν δυνατή η εξαγωγή σημειώσεων: %s",
|
||||
"Could not install plugin: %s": "Δεν ήταν δυνατή η εγκατάσταση του plugin: %s",
|
||||
"Could not respond to the invitation. Please try again, or check with the notebook owner if they are still sharing it.\n\nThe error was: \"%s\"": "Δεν μπόρεσε να ανταποκριθεί στην πρόσκληση. Προσπαθήστε ξανά ή επικοινωνήστε με τον ιδιοκτήτη του φορητού υπολογιστή, αν εξακολουθεί να το μοιράζεται.\n\nΤο σφάλμα ήταν: \"%s\"",
|
||||
"Could not switch profile: %s": "Δεν ήταν δυνατή η αλλαγή του plugin: %s",
|
||||
"Could not upgrade master key: %s": "Δεν ήταν δυνατή η αναβάθμιση του master κλειδιού: %s",
|
||||
"Could not verify the share status of this notebook - aborting. Please try again when you are connected to the internet.": "Δεν ήταν δυνατή η επαλήθευση της κατάστασης κοινής χρήσης αυτού του φορητού υπολογιστή - διακοπή. Προσπαθήστε ξανά όταν συνδεθείτε στο διαδίκτυο.",
|
||||
"Could not verify your identify: %s": "Δεν ήταν δυνατή η επαλήθευση της ταυτότητάς σας: %s",
|
||||
"Create": "Δημιουργία",
|
||||
"Create a new notebook under a parent notebook.": "Δημιουργήστε ένα νέο σημειωματάριο κάτω από ένα γονικό σημειωματάριο.",
|
||||
"Create a notebook": "Δημιουργία σημειωματάριου",
|
||||
"Create new profile...": "Δημιουργία νέου προφίλ...",
|
||||
"Create notebook": "Δημιουργία σημειωματαρίου",
|
||||
"Create user": "Δημιουργία χρήστη",
|
||||
"Created": "Δημιουργήθηκε",
|
||||
@@ -193,6 +222,7 @@
|
||||
"Date": "Ημερομηνία",
|
||||
"Date format": "Μορφή ημερομηνίας",
|
||||
"days": "ημέρες",
|
||||
"Decrease indent level": "Μείωση του επιπέδου εσοχής",
|
||||
"Decrypted items: %d": "Αποκρυπτογραφημένα στοιχεία: %d",
|
||||
"Decrypted items: %s / %s": "Αποκρυπτογραφημένα στοιχεία: %s / %s",
|
||||
"Decrypting items: %d/%d": "Αποκρυπτογράφηση αντικειμένων: %d/%d",
|
||||
@@ -209,8 +239,11 @@
|
||||
"Delete notebook \"%s\"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.": "Διαγραφή σημειωματάριου \"%s\";\n\nΌλες οι σημειώσεις και τα υπο-σημειωματάρια που περιέχει θα διαγραφούν επίσης.",
|
||||
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "Διαγραφή σημειωματάριου; Όλες οι σημειώσεις και τα υπο-σημειωματάρια θα διαγραφούν επίσης.",
|
||||
"Delete plugin \"%s\"?": "Διαγραφή plugin \"%s\";",
|
||||
"Delete profile \"%s\"": "Διαγραφή προφίλ \"%s\";",
|
||||
"Delete selected notes": "Διαγραφή επιλεγμένων σημειώσεων",
|
||||
"Delete these %d notes?": "Διαγραφή των σημειώσεων %d;",
|
||||
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "Να διαγραφεί αυτή την πρόσκληση; Ο παραλήπτης δεν θα έχει πλέον πρόσβαση σε αυτό το κοινόχρηστο σημειωματάριο.",
|
||||
"Delete this profile?": "Να διαγραφεί αυτό το προφίλ;",
|
||||
"Deleted local items: %d.": "Διαγραμμένα τοπικά στοιχεία: %d.",
|
||||
"Deleted remote items: %d.": "Διαγραμμένα απομακρυσμένα στοιχεία: %d.",
|
||||
"Deletes the given notebook.": "Διαγράφει το καθορισμένο σημειωματάριο.",
|
||||
@@ -240,6 +273,7 @@
|
||||
"Do it now": "Κάνε το τώρα",
|
||||
"Do not ask for confirmation.": "Χωρίς να ζητείται επιβεβαίωση.",
|
||||
"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.": "Μην χάσετε τον κωδικό πρόσβασης, καθώς, για λόγους ασφαλείας, αυτός θα είναι ο *μόνος* τρόπος αποκρυπτογράφησης των δεδομένων! Για να ενεργοποιήσετε την κρυπτογράφηση, εισαγάγετε τον κωδικό πρόσβασής σας παρακάτω.",
|
||||
"Done": "Ολοκληρώθηκε",
|
||||
"Download": "Λήψη",
|
||||
"Download and install the relevant extension for your browser:": "Κάντε λήψη και εγκατάσταση της αντίστοιχης επέκτασης για το πρόγραμμα περιήγησής σας:",
|
||||
"Downloaded": "Έχουν ληφθεί",
|
||||
@@ -253,17 +287,21 @@
|
||||
"Dropbox Login": "Σύνδεση Dropbox",
|
||||
"Duplicate": "Δημιουργία αντιγράφου",
|
||||
"Duplicate line": "Διπλασιασμός γραμμής",
|
||||
"Duplicate selected notes": "Διπλασιασμός επιλεγμένων γραμμών",
|
||||
"Duplicates the notes matching <note> to [notebook]. If no notebook is specified the note is duplicated in the current notebook.": "Αντιγράφει τις σημειώσεις που ταιριάζουν με τη <note> στο [σημειωματάριο]. Εάν δεν έχει οριστεί κανένα σημειωματάριο, η σημείωση αντιγράφεται στον τρέχον.",
|
||||
"Edit": "Επεξεργασία",
|
||||
"Edit in external editor": "Επεξεργασία σε εξωτερικό editor",
|
||||
"Edit link": "Επεξεργασία συνδέσμου",
|
||||
"Edit note.": "Eπεξεργασία σημείωσης.",
|
||||
"Edit notebook": "Επεξεργασία σημειωματάριου",
|
||||
"Edit profile": "Επεξεργασία προφίλ",
|
||||
"Editor": "Επεξεργασία",
|
||||
"Editor font": "Γραμματοσειρά editor",
|
||||
"Editor font family": "Οικογένεια γραμματοσειράς editor",
|
||||
"Editor font size": "Μέγεθος γραμματοσειράς editor",
|
||||
"Editor maximum width": "Μέγιστο πλάτος editor",
|
||||
"Editor monospace font family": "Οικογένεια μονοδιαστημικών γραμματοσειρών επεξεργαστή",
|
||||
"Editor: %s": "Συντάκτης: %s",
|
||||
"Either \"text\" or \"json\"": "Είτε \"text\" ή \"json\"",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "Email",
|
||||
@@ -275,6 +313,7 @@
|
||||
"Enable ^sup^ syntax": "Ενεργοποίηση ^sup^ syntax",
|
||||
"Enable abbreviation syntax": "Ενεργοποίηση abbreviation syntax",
|
||||
"Enable audio player": "Ενεργοποίηση προγράμματος αναπαραγωγής ήχου",
|
||||
"Enable biometrics authentication?": "Ενεργοποίηση βιομετρικού ελέγχου ταυτότητας;",
|
||||
"Enable deflist syntax": "Ενεργοποίηση deflist syntax",
|
||||
"Enable encryption": "Ενεργοποίηση κρυπτογράφησης",
|
||||
"Enable footnotes": "Ενεργοποίηση footnotes",
|
||||
@@ -287,7 +326,9 @@
|
||||
"Enable note history": "Ενεργοποίηση ιστορικού σημειώσεων",
|
||||
"Enable PDF viewer": "Ενεργοποίηση προβολής PDF",
|
||||
"Enable soft breaks": "Ενεργοποίηση soft breaks",
|
||||
"Enable spellcheck in the text editor": "Ενεργοποίηση του ορθογραφικού ελέγχου στον επεξεργαστή κειμένου",
|
||||
"Enable table of contents extension": "Ενεργοποίηση επέκτασης table of contents",
|
||||
"Enable the Markdown toolbar": "Ενεργοποίηση της γραμμής εργαλείων Markdown",
|
||||
"Enable typographer support": "Ενεργοποίηση υποστήριξης typographer",
|
||||
"Enable video player": "Ενεργοποίηση προγράμματος αναπαραγωγής βίντεο",
|
||||
"Enable Web Clipper Service": "Ενεργοποίηση υπηρεσίας Web Clipper",
|
||||
@@ -314,6 +355,7 @@
|
||||
"Evernote Export File (as HTML)": "Αρχείο εξαγωγής Evernote (σαν HTML)",
|
||||
"Evernote Export File (as Markdown)": "Αρχείο εξαγωγής Evernote (σαν Markdown)",
|
||||
"Exits the application.": "Βγαίνει από την εφαρμογή.",
|
||||
"Expand": "Επέκταση",
|
||||
"Export": "Εξαγωγή",
|
||||
"Export all": "Εξαγωγή όλων",
|
||||
"Export debug report": "Εξαγωγή Debug report",
|
||||
@@ -332,6 +374,9 @@
|
||||
"Fetching resources: %d/%d": "Λήψη πόρων: %d/%d",
|
||||
"File": "Αρχείο",
|
||||
"File system": "Σύστημα Αρχείων",
|
||||
"Filter tags": "Φίλτρο ετικετών",
|
||||
"Find and replace": "Εύρεση και αντικατάσταση",
|
||||
"Find: ": "Found: %d.",
|
||||
"Firefox Extension": "Επέκταση Firefox",
|
||||
"Fix search index": "Διόρθωση ευρετηρίων αναζήτησης",
|
||||
"Fixing search index...": "Διόρθωση ευρετηρίων αναζήτησης...",
|
||||
@@ -340,14 +385,17 @@
|
||||
"Focus title": "Eστίαση στον τίτλο",
|
||||
"Folders": "Φάκελοι",
|
||||
"For debugging purpose only: export your profile to an external SD card.": "Για τον εντοπισμό σφαλμάτων και μόνο: εξαγάγετε το προφίλ σας σε μια εξωτερική κάρτα SD.",
|
||||
"For example \"%s\"": "Για παράδειγμα \"%s\"",
|
||||
"For information on how to customise the shortcuts please visit %s": "Για πληροφορίες σχετικά με τον τρόπο προσαρμογής των συντομεύσεων, επισκεφτείτε το %s",
|
||||
"For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:": "Για περισσότερες πληροφορίες σχετικά με την κρυπτογράφηση End-To-End (E2EE) και συμβουλές σχετικά με τον τρόπο ενεργοποίησής της, ελέγξτε το εγχειρίδιο:",
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`": "Για τη λίστα των συντομεύσεων πληκτρολογίου και των ρυθμίσεων, πληκτρολογήστε `help keymap`",
|
||||
"Force path style": "Επιβολή path style",
|
||||
"Formatting": "Μορφοποίηση",
|
||||
"Forward": "Μπροστά",
|
||||
"Found: %d.": "Βρέθηκε: %d.",
|
||||
"FTS enabled: %d": "FTS ενεργοποιημένο: %d",
|
||||
"Full changelog": "Πλήρες αρχείο αλλαγών",
|
||||
"Full name": "Ονοματεπώνυμο",
|
||||
"General": "Γενικά",
|
||||
"Generated": "Δημιουργήθηκε",
|
||||
"Generating link...": "Δημιουργία συνδέσμου...",
|
||||
@@ -357,11 +405,18 @@
|
||||
"Go to source URL": "Μεταβείτε στη διεύθυνση URL προέλευσης",
|
||||
"Goto Anything...": "Γρήγορη Μετακίνηση...",
|
||||
"Grant authorisation": "Έγκριση εξουσιοδότησης",
|
||||
"Header %d": "Επικεφαλίδα %d",
|
||||
"Headers": "Κεφαλίδες",
|
||||
"Heading": "Επικεφαλίδα",
|
||||
"Help": "Βοήθεια",
|
||||
"Hermes enabled: %d": "Hermes ενεργοποιημένο: %d",
|
||||
"Hide %s": "Απόκρυψη %s",
|
||||
"Hide advanced": "Απόκρυψη προηγμένων",
|
||||
"Hide disabled": "Απόκρυψη απενεργοποιημένων",
|
||||
"Hide disabled keys": "Απόκρυψη απενεργοποιημένων κλειδιών",
|
||||
"Hide Joplin": "Κρύψε το Joplin",
|
||||
"Hide keyboard": "Απόκρυψη πληκτρολογίου",
|
||||
"Hide more actions": "Απόκρυψη περισσότερων ενεργειών",
|
||||
"Highlight": "Επισήμανση",
|
||||
"Home": "Home",
|
||||
"Horizontal Rule": "Οριζόντια Διαγράμμιση",
|
||||
@@ -386,12 +441,14 @@
|
||||
"In order to use the web clipper, you need to do the following:": "Για να χρησιμοποιήσετε το web clipper, πρέπει να κάνετε τα εξής:",
|
||||
"In progress": "Σε εξέλιξη",
|
||||
"In: %s": "Στο: %s",
|
||||
"Increase indent level": "Αύξηση του επιπέδου εσοχής",
|
||||
"Indent less": "Μείωση εσοχής",
|
||||
"Indent more": "Αύξηση εσοχής",
|
||||
"Information": "Πληροφορίες",
|
||||
"Inline Code": "Ενσωματωμένος κώδικας",
|
||||
"Insert": "Εισαγωγή",
|
||||
"Insert Hyperlink": "Εισαγωγή υπερσύνδεσης",
|
||||
"Insert time": "Εισαγωγή ώρας",
|
||||
"Install": "Εγκατάσταση",
|
||||
"Install from file": "Εγκατάσταση από αρχείο",
|
||||
"Installed": "Εγκαταστάθηκε",
|
||||
@@ -407,6 +464,7 @@
|
||||
"Items": "Στοιχεία",
|
||||
"Items that cannot be decrypted": "Στοιχεία που δεν μπορούν αποκρυπτογραφηθούν",
|
||||
"Items that cannot be synchronised": "Στοιχεία που δεν μπορούν να συγχρονιστούν",
|
||||
"Join us on Twitter": "Ακολουθήστε μας στο Twitter",
|
||||
"Joplin can synchronise your notes using various providers. Select one from the list below.": "Το Joplin μπορεί να συγχρονίσει τις σημειώσεις σας χρησιμοποιώντας διάφορους παρόχους. Επιλέξτε έναν από τον παρακάτω κατάλογο.",
|
||||
"Joplin Cloud": "Joplin Cloud",
|
||||
"Joplin Cloud email": "Joplin Cloud email",
|
||||
@@ -422,6 +480,7 @@
|
||||
"Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.": "Το Joplin Web Clipper επιτρέπει την αποθήκευση ιστοσελίδων και screenshots από το πρόγραμμα περιήγησης στο Joplin.",
|
||||
"Joplin website": "Joplin website",
|
||||
"Joplin's own sync service. Also gives access to Joplin-specific features such as publishing notes or collaborating on notebooks with others.": "Η υπηρεσία συγχρονισμού του Joplin. Παρέχει επίσης πρόσβαση σε ειδικές λειτουργίες του Joplin, όπως η δημοσίευση σημειώσεων ή η συνεργασία σε σημειωματάρια με άλλους.",
|
||||
"KaTeX": "KaTeX",
|
||||
"Keep note history for": "Διατήρηση ιστορικού σημειώσεων για",
|
||||
"Keyboard Mode": "Λειτουργία πληκτρολογίου",
|
||||
"Keyboard Shortcut": "Συντόμευση πληκτρολογίου",
|
||||
@@ -439,10 +498,15 @@
|
||||
"Letter": "Letter",
|
||||
"Light": "Light",
|
||||
"Lines": "Γραμμές",
|
||||
"Link": "Σύνδεσμος",
|
||||
"Link description": "Περιγραφή συνδέσμου",
|
||||
"Link has been copied to clipboard!": "Ο σύνδεσμος έχει αντιγραφεί στο πρόχειρο!",
|
||||
"Link text": "Κείμενο συνδέσμου",
|
||||
"Links with protocol \"%s\" are not supported": "Σύνδεσμοι με πρωτόκολλο \"%s\" δεν υποστηρίζονται",
|
||||
"List item": "Στοιχείο λίστας",
|
||||
"Lists": "Λίστες",
|
||||
"Loaded": "Φορτωμένα",
|
||||
"Loading...": "Φόρτωση...",
|
||||
"Location": "Τοποθεσία",
|
||||
"Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at \"%s\" and resume the operation.": "Το αρχείο κλειδώματος έχει ήδη δημιουργηθεί. Αν γνωρίζετε ότι δεν πραγματοποιείται συγχρονισμός, μπορείτε να διαγράψετε το αρχείο κλειδώματος στο \"%s\" και να συνεχίσετε τη λειτουργία.",
|
||||
"Log": "Log",
|
||||
@@ -455,6 +519,8 @@
|
||||
"Make a donation": "Κάντε μια δωρεά",
|
||||
"Manage master password": "Διαχείριση κύριου κωδικού πρόσβασης",
|
||||
"Manage master password...": "Διαχείριση κύριου κωδικού πρόσβασης...",
|
||||
"Manage multiple users": "Διαχείριση πολλαπλών χρηστών",
|
||||
"Manage profiles": "Διαχείριση προφίλ",
|
||||
"Manage your plugins": "Διαχείριση των επεκτάσεων",
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, and `target-status`.": "Διαχειρίζεται τη διαμόρφωση E2EE. Οι εντολές είναι `enable`, `disable`, `decrypt`, `status`, `decrypt-file`, και `target-status`.",
|
||||
"Manual": "Χειροκίνητα",
|
||||
@@ -467,9 +533,13 @@
|
||||
"Master password": "Κύριος κωδικός πρόσβασης",
|
||||
"Master password:": "Κύριος κωδικός πρόσβασης:",
|
||||
"Max concurrent connections": "Μέγιστος αριθμός ταυτόχρονων συνδέσεων",
|
||||
"Max Item Size": "Μέγιστο μέγεθος αντικειμένου",
|
||||
"Max note or attachment size": "Μέγιστο μέγεθος σημείωσης ή συνημμένου",
|
||||
"Max Total Size": "Μέγιστο συνολικό μέγεθος",
|
||||
"Missing keys": "Απουσία κλειδιών",
|
||||
"Missing Master Keys": "Απουσία κυρίως κλειδιών",
|
||||
"Missing required argument: %s": "Λείπει το απαιτούμενο όρισμα: %s",
|
||||
"Missing required flag value: %s": "Λείπει το απαιτούμενο flag: %s",
|
||||
"Mobile data - auto-sync disabled": "Δεδομένα κινητής τηλεφωνίας - ο αυτόματος συγχρονισμός απενεργοποιήθηκε",
|
||||
"More info": "Περισσότερες πληροφορίες",
|
||||
"More information": "Περισσότερες πληροφορίες",
|
||||
@@ -478,6 +548,7 @@
|
||||
"Move to notebook": "Μετακίνηση στο σημειωματάριο",
|
||||
"Move to notebook...": "Μετακίνηση στο σημειωματάριο...",
|
||||
"Move to notebook:": "Μετακίνηση στο σημειωματάριο:",
|
||||
"Moves the given <item> to [notebook]": "Μετακινεί το συγκεκριμένο <item> στο [notebook]",
|
||||
"n": "ο",
|
||||
"N": "Ο",
|
||||
"New note": "Νέα σημείωση",
|
||||
@@ -488,6 +559,7 @@
|
||||
"New tags:": "Νέες ετικέτες:",
|
||||
"New to-do": "Νέο to-do",
|
||||
"New version: %s": "Νέα έκδοση: %s",
|
||||
"Next match": "Επόμενη αντιστοίχιση",
|
||||
"Nextcloud": "Nextcloud",
|
||||
"Nextcloud password": "Nextcloud password",
|
||||
"Nextcloud username": "Nextcloud username",
|
||||
@@ -508,6 +580,7 @@
|
||||
"Not authentified with %s. Please provide any missing credentials.": "Δεν έγινε επαλήθευση με το %s. Παρακαλώ καταχωρίστε τα διαπιστευτήρια που λείπουν.",
|
||||
"Not downloaded": "Δεν έγινε λήψη",
|
||||
"Not generated": "Δεν δημιουργήθηκαν",
|
||||
"Not now": "Αργότερα",
|
||||
"note": "σημείωση",
|
||||
"Note": "Σημείωση",
|
||||
"Note area growth factor": "Συντελεστής ανάπτυξης περιοχής σημειώσεων",
|
||||
@@ -515,6 +588,7 @@
|
||||
"Note attachments...": "Επισυναπτόμενα...",
|
||||
"Note body": "Σώμα σημειώσεων",
|
||||
"Note does not exist: \"%s\". Create it?": "Δεν υπάρχει η σημείωση : \"%s\". Να δημιουργηθεί;",
|
||||
"Note editor": "Συντάκτης σημείωσης",
|
||||
"Note has been saved.": "Η σημείωση έχει αποθηκευτεί.",
|
||||
"Note History": "Ιστορικό Σημειώσεων",
|
||||
"Note is not a to-do: \"%s\"": "Η σημείωση δεν είναι to-do: \"%s\"",
|
||||
@@ -545,12 +619,14 @@
|
||||
"Only one note can be printed at a time.": "Μόνο μία σημείωση τη φορά μπορεί να εκτυπωθεί.",
|
||||
"Open": "Άνοιγμα",
|
||||
"Open %s": "Άνοιγμα %s",
|
||||
"Open PDF viewer": "Άνοιγμα προγράμματος προβολής PDF",
|
||||
"Open profile directory": "Άνοιγμα φακέλου προφίλ",
|
||||
"Open Sync Wizard...": "Άνοιγμα Οδηγού συγχρονισμού...",
|
||||
"Open...": "Άνοιγμα...",
|
||||
"Operation cancelled": "Η λειτουργία ακυρώθηκε",
|
||||
"Options": "Επιλογές",
|
||||
"Or create an account.": "Ή δημιουργείστε ένα λογαριασμό.",
|
||||
"Ordered list": "Ταξινομημένη λίστα",
|
||||
"Other applications...": "Άλλες εφαρμογές...",
|
||||
"Output format: %s": "Μορφή εξόδου: %s",
|
||||
"Page orientation for PDF export": "Προσανατολισμός σελίδας για εξαγωγή PDF",
|
||||
@@ -560,14 +636,18 @@
|
||||
"Password:": "Κωδικός:",
|
||||
"Passwords do not match!": "Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός!",
|
||||
"Paste": "Επικόλληση",
|
||||
"Paste as text": "Επικόλληση ως κείμενο",
|
||||
"Path:": "Διαδρομή:",
|
||||
"PDF File": "Αρχείο PDF",
|
||||
"Per user. Minimum of %d users.": "Ανά χρήστη. Τουλάχιστον %d χρήστες.",
|
||||
"Permission needed": "Απαιτείται άδεια",
|
||||
"Permission to use camera": "Άδεια χρήσης κάμερας",
|
||||
"Please click on \"%s\" to proceed, or set the passwords in the \"%s\" list below.": "Κάντε κλικ στο \"%s\" για να συνεχίσετε ή ορίστε τους κωδικούς πρόσβασης στην παρακάτω λίστα \"%s\".",
|
||||
"Please confirm that you would like to re-encrypt your complete database.": "Επιβεβαιώστε ότι θέλετε να ξανακρυπτογραφήσετε την πλήρη βάση δεδομένων σας.",
|
||||
"Please enter your password in the master key list below before upgrading the key.": "Εισαγάγετε τον κωδικό πρόσβασής σας στη λίστα του master κλειδιού πριν αναβαθμίσετε το κλειδί.",
|
||||
"Please note that if it is a large notebook, it may take a few minutes for all the notes to show up on the recipient's device.": "Λάβετε υπόψη ότι αν πρόκειται για ένα μεγάλο σημειωματάριο, μπορεί να χρειαστούν μερικά λεπτά για να εμφανιστούν όλες οι σημειώσεις στη συσκευή του παραλήπτη.",
|
||||
"Please open the following URL in your browser to authenticate the application. The application will create a directory in \"Apps/Joplin\" and will only read and write files in this directory. It will have no access to any files outside this directory nor to any other personal data. No data will be shared with any third party.": "Παρακαλώ ανοίξτε την ακόλουθη διεύθυνση URL στο πρόγραμμα περιήγησής σας για να πιστοποιήσετε την εφαρμογή. Η εφαρμογή θα δημιουργήσει έναν φάκελο στο \"Apps / Joplin\" και θα διαβάζει και θα γράφει μόνο αρχεία σε αυτόν. Δεν θα έχει πρόσβαση σε αρχεία εκτός αυτού του φακέλου ούτε σε άλλα προσωπικά δεδομένα. Δεν πρόκειται να κοινοποιηθούν δεδομένα σε τρίτους.",
|
||||
"Please record your voice...": "Παρακαλώ καταγράψτε τη φωνή σας...",
|
||||
"Please select a notebook first.": "Παρακαλώ επιλέξτε πρώτα σημειωματάριο.",
|
||||
"Please select the note or notebook to be deleted first.": "Παρακαλώ πρώτα επιλέξτε τη σημείωση ή το σημειωματάριο που θέλετε να διαγραφτεί.",
|
||||
"Please select where the sync status should be exported to": "Επιλέξτε την τοποθεσία στην οποία θα εξαχθεί η κατάσταση του συγχρονισμού",
|
||||
@@ -589,15 +669,24 @@
|
||||
"Press the shortcut": "Πατήστε τη συντόμευση",
|
||||
"Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.": "Πατήστε τη συντόμευση και, στη συνέχεια, πατήστε το πλήκτρο ENTER. Εναλλακτικά, πατήστε το πλήκτρο BACKSPACE για να καταργήσετε τη συντόμευση.",
|
||||
"Press to set the decryption password.": "Πατήστε για να ορίσετε τον κωδικό αποκρυπτογράφησης.",
|
||||
"Previous match": "Προηγούμενη αντιστοίχιση",
|
||||
"Previous versions of this note": "Προηγούμενες εκδόσεις αυτής της σημείωσης",
|
||||
"Print": "Εκτύπωση",
|
||||
"Priority support": "Υποστήριξη προτεραιότητας",
|
||||
"Privacy Policy": "Πολιτική Προστασίας Προσωπικών Δεδομένων",
|
||||
"Pro": "Pro",
|
||||
"Process failed payment subscriptions": "Επεξεργασία αποτυχημένων συνδρομών πληρωμής",
|
||||
"Process oversized accounts": "Επεξεργασία υπερμεγεθών λογαριασμών",
|
||||
"Process user deletions": "Επεξεργασία διαγραφών χρηστών",
|
||||
"Profile": "Προφιλ",
|
||||
"Profile name": "Όνομα προφίλ",
|
||||
"Profile name:": "Όνομα προφιλ:",
|
||||
"Profile Version: %s": "Έκδοση προφίλ: %s",
|
||||
"Profiles": "Προφιλ",
|
||||
"Properties": "Ιδιότητες",
|
||||
"Proxy enabled": "Ενεργοποίηση διακομιστή μεσολάβησης",
|
||||
"Proxy timeout (seconds)": "Χρονικό όριο μεσολάβησης (δευτερόλεπτα)",
|
||||
"Proxy URL": "URL διακομιστή μεσολάβησης",
|
||||
"Public-private key pair:": "Ζεύγος δημόσιου-ιδιωτικού κλειδιού:",
|
||||
"Publish note...": "Δημοσίευση σημείωσης...",
|
||||
"Publish Notes": "Δημοσίευση σημειώσεων",
|
||||
@@ -614,6 +703,7 @@
|
||||
"Recipients:": "Αποδέκτες:",
|
||||
"Redo": "Επανάληψη",
|
||||
"Refresh": "Ανανέωση",
|
||||
"Regular expression": "Regular expression",
|
||||
"Reject": "Απόρριψη",
|
||||
"Remove": "Κατάργηση",
|
||||
"Remove tag \"%s\" from all notes?": "Κατάργηση της ετικέτας \"%s\" από όλες τις σημειώσεις;",
|
||||
@@ -623,6 +713,11 @@
|
||||
"Rename tag:": "Μετονομασία ετικέτας:",
|
||||
"Renames the given <item> (note or notebook) to <name>.": "Μετονομάζει το καθορισμένο <item> (σημείωση ή σημειωματάριο) στο <name>.",
|
||||
"Renew token": "Ανανέωση token",
|
||||
"Replace": "Αντικατάσταση",
|
||||
"Replace all": "Αντικατάσταση όλων",
|
||||
"Replace with...": "Αντικατάσταση με...",
|
||||
"Replace: ": "Αντικατάσταση: ",
|
||||
"Reset application layout": "Αλλαγή διάταξης εφαρμογής",
|
||||
"Reset master password": "Επαναφορά κύριου κωδικού πρόσβασης",
|
||||
"Resources: %d.": "Πόροι: %d.",
|
||||
"Restart and upgrade": "Επανεκκίνηση και αναβάθμιση",
|
||||
@@ -645,11 +740,13 @@
|
||||
"Safe mode is currently active. Note rendering and all plugins are temporarily disabled.": "Η ασφαλής λειτουργία είναι ενεργή αυτήν τη στιγμή. Σημειώστε ότι η απόδοση και όλα τα πρόσθετα απενεργοποιούνται προσωρινά.",
|
||||
"Save": "Αποθήκευση",
|
||||
"Save alarm": "Αποθήκευση ειδοποίησης",
|
||||
"Save as %s": "Αποθήκευση ως %s",
|
||||
"Save as...": "Αποθήκευση ως...",
|
||||
"Save changes": "Αποθήκευση αλλαγών",
|
||||
"Save geo-location with notes": "Αποθήκευση της γεωγραφικής τοποθεσίας μαζί με της σημειώσεις",
|
||||
"Search": "Εύρεση",
|
||||
"Search for plugins...": "Αναζήτηση για plugins...",
|
||||
"Search for...": "Αναζήτηση για...",
|
||||
"Search in all the notes": "Αναζήτηση σε όλες τις σημειώσεις",
|
||||
"Search in current note": "Αναζήτηση στην τρέχουσα σημείωση",
|
||||
"Search...": "Αναζήτηση...",
|
||||
@@ -660,6 +757,7 @@
|
||||
"Select all": "Επιλογή όλων",
|
||||
"Select emoji...": "Επιλογή emoji...",
|
||||
"Select file...": "Επιλογή φακέλου...",
|
||||
"Select parent notebook": "Επιλέξτε γονικό σημειωματάριο",
|
||||
"Server is already running on port %d": "Ο διακομιστής εκτελείται ήδη στη θύρα %d",
|
||||
"Server is not running.": "Ο διακομιστής δεν εκτελείται.",
|
||||
"Server is running on port %d": "Ο διακομιστής εκτελείται στη θύρα %d",
|
||||
@@ -669,17 +767,23 @@
|
||||
"Set the password": "Ορισμός κωδικού πρόσβασης",
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s": "Ορίζει την ιδιότητα <name> του καθορισμένου <note> στην καθορισμένη [τιμή]. Πιθανές ιδιότητες είναι:\n\n%s",
|
||||
"Share": "Κοινοποίηση",
|
||||
"Share and collaborate on a notebook": "Μοιραστείτε και συνεργαστείτε σε ένα σημειωματάριο",
|
||||
"Share Notebook": "Κοινοποίηση Σημειωματάριου",
|
||||
"Share notebook...": "Κοινοποίηση σημειωματάριου...",
|
||||
"Sharing access control": "Έλεγχος πρόσβασης κοινής χρήσης",
|
||||
"Sharing notebook...": "Κοινοποίηση σημειωματάριου...",
|
||||
"Shortcuts are not available in CLI mode.": "Οι συντομεύσεις δεν είναι διαθέσιμες στη λειτουργία CLI.",
|
||||
"Show advanced": "Εμφάνιση επιλογών για προχωρημένους",
|
||||
"Show Advanced Settings": "Εμφάνιση επιλογών για προχωρημένους",
|
||||
"Show all": "Εμφάνιση όλων",
|
||||
"Show completed to-dos": "Εμφάνιση ολοκληρωμένων to-dos",
|
||||
"Show disabled": "Εμφάνιση απενεργοποιημένων",
|
||||
"Show disabled keys": "Εμφάνιση απενεργοποιημένων κλειδιών",
|
||||
"Show more actions": "Εμφάνιση περισσότερων ενεργειών",
|
||||
"Show note counts": "Εμφάνιση πλήθους σημειώσεων",
|
||||
"Show sort order buttons": "Εμφάνιση κουμπιών σειράς ταξινόμησης",
|
||||
"Show tray icon": "Εμφάνιση εικονιδίου στη γραμμή εργασιών",
|
||||
"Show/hide the sidebar": "Εμφάνιση/απόκρυψη της πλευρικής μπάρας",
|
||||
"Sidebar": "Πλευρική μπάρα",
|
||||
"Size": "Μέγεθος",
|
||||
"Skip this version": "Παραλείψτε αυτή την έκδοση",
|
||||
@@ -715,6 +819,7 @@
|
||||
"Step 2: Install the extension": "Βήμα 2: Εγκαταστήστε την επέκταση",
|
||||
"Stop": "Σταμάτημα",
|
||||
"Stop external editing": "Διακοπή εξωτερικής επεξεργασίας",
|
||||
"Storage space": "Χώρος αποθήκευσης",
|
||||
"Strikethrough": "Διακριτή διαγραφή",
|
||||
"strong text": "έντονη γραφή",
|
||||
"Submit": "Yποβολή",
|
||||
@@ -724,9 +829,12 @@
|
||||
"Swap line down": "Αλλαγή γραμμής με την κάτω",
|
||||
"Swap line up": "Αλλαγή γραμμής με την πανω",
|
||||
"Switch between note and to-do type": "Εναλλαγή μεταξύ σημείωσης και to-do",
|
||||
"Switch profile": "Αλλαγή προφίλ",
|
||||
"Switch to note type": "Αλλαγή σε σημείωση",
|
||||
"Switch to profile %d": "Αλλαγή στο προφίλ %d",
|
||||
"Switch to to-do type": "Αλλαγή σε to-do",
|
||||
"Switches to [notebook] - all further operations will happen within this notebook.": "Αλλάζει σε [σημειωματάριο] - όλες οι περαιτέρω λειτουργίες θα πραγματοποιηθούν μέσα σε αυτό το σημειωματάριο.",
|
||||
"Sync as many devices as you want": "Συγχρονίστε όσες συσκευές επιθυμείτε",
|
||||
"Sync Status": "Κατάσταση Συγχρονισμού",
|
||||
"Sync status (synced items / total items)": "Κατάσταση συγχρονισμού (συγχρονισμένα στοιχεία / σύνολο αρχείων)",
|
||||
"Sync target must be upgraded! Run `%s` to proceed.": "Ο προορισμός συγχρονισμού πρέπει να αναβαθμιστεί! Εκτελέστε το `%s` για να συνεχίσετε.",
|
||||
@@ -746,13 +854,18 @@
|
||||
"Synchronising...": "Συγχρονισμός...",
|
||||
"Synchronizing...": "Συγχρονισμός...",
|
||||
"Tabloid": "Tabloid",
|
||||
"tag1, tag2, ...": "tag1, tag2, ...",
|
||||
"Tagged: %d.": "Με ετικέτα: %d.",
|
||||
"Tags": "Ετικέτες",
|
||||
"Take photo": "Βγάλτε μια φωτογραφία",
|
||||
"Task list": "Λίστα εργασιών",
|
||||
"Tasks": "Εργασίες",
|
||||
"Teams": "Ομάδες",
|
||||
"Text editor command": "Εντολή \"Επεξεργαστή κειμένου\"",
|
||||
"Thank you! Your Joplin Cloud account is now setup and ready to use.": "Ευχαριστώ! Ο λογαριασμός σας στο Joplin Cloud είναι τώρα ρυθμισμένος και έτοιμος για χρήση.",
|
||||
"The active profile cannot be deleted. Switch to a different profile and try again.": "Το ενεργό προφίλ δεν μπορεί να διαγραφεί. Μεταβείτε σε ένα άλλο προφίλ και δοκιμάστε ξανά.",
|
||||
"The app is now going to close. Please relaunch it to complete the process.": "Η εφαρμογή θα κλείσει τώρα. Παρακαλώ ανοίξτε την ξανά για να ολοκληρώσετε τη διαδικασία.",
|
||||
"The application did not close properly. Would you like to start in safe mode?": "Η εφαρμογή δεν έκλεισε σωστά. Θα θέλατε να ξεκινήσετε σε ασφαλή λειτουργία;",
|
||||
"The application has been authorised - you may now close this browser tab.": "Η εφαρμογή έχει εξουσιοδοτηθεί - μπορείτε τώρα να κλείσετε αυτήν την καρτέλα του προγράμματος περιήγησης.",
|
||||
"The application has been authorised!": "Η εφαρμογή έχει εγκριθεί!",
|
||||
"The application has been successfully authorised.": "Η εφαρμογή έχει εξουσιοδοτηθεί με επιτυχία.",
|
||||
@@ -762,6 +875,7 @@
|
||||
"The default admin password is insecure and has not been changed! [Change it now](%s)": "Ο προεπιλεγμένος κωδικός πρόσβασης διαχειριστή δεν είναι ασφαλής και δεν έχει αλλάξει! [Αλλάξτε τον τώρα] (%s)",
|
||||
"The default encryption method has been changed to a more secure one and it is recommended that you apply it to your data.": "Η προεπιλεγμένη μέθοδος κρυπτογράφησης έχει αλλάξει μία σε πιο ασφαλή και συνιστάται να την εφαρμόσετε στα δεδομένα σας.",
|
||||
"The default encryption method has been changed, you should re-encrypt your data.": "Η προεπιλεγμένη μέθοδος κρυπτογράφησης έχει αλλάξει, θα πρέπει να κρυπτογραφήσετε εκ νέου τα δεδομένα σας.",
|
||||
"The default profile cannot be deleted": "Το προεπιλεγμένο προφίλ δεν μπορεί να διαγραφεί",
|
||||
"The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.": "Η εντολή του editor (που μπορεί να περιλαμβάνει ορίσματα) θα χρησιμοποιηθεί για το άνοιγμα μιας σημείωσης. Εάν δεν δωθεί καμία, θα προσπαθήσει να ανιχνεύσει αυτόματα το προεπιλεγμένο πρόγραμμα επεξεργασίας.",
|
||||
"The factor property sets how the item will grow or shrink to fit the available space in its container with respect to the other items. Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.Restart app to see changes.": "Ο συντελεστής ανάπτυξης καθορίζει το πόσο θα αναπτυχθεί ή θα συρρίκνωθεί το αντικειμένο ώστε να χωρά στον διαθέσιμο χώρο σε σχέση με τα άλλα αντικείμενα. Έτσι, ένα στοιχείο με συντελεστή 2 θα πάρει διπλάσιο χώρο από ένα στοιχείο με συντελεστή 1. Επανεκκινήστε την εφαρμογή για να δείτε αλλαγές.",
|
||||
"The following attachments are being watched for changes:": "Τα ακόλουθα συνημμένα παρακολουθούνται για αλλαγές:",
|
||||
@@ -783,8 +897,10 @@
|
||||
"The Web Clipper needs your authorisation to access your data.": "Το Web Clipper χρειάζεται την άδειά σας για να αποκτήσει πρόσβαση στα δεδομένα σας.",
|
||||
"The web clipper service is enabled and set to auto-start.": "Η υπηρεσία web clipper είναι ενεργοποιημένη και έχει ρυθμιστεί για αυτόματη εκκίνηση.",
|
||||
"The web clipper service is not enabled.": "Η υπηρεσία Web Clipper δεν είναι ενεργοποιημένη.",
|
||||
"The WebDAV implementation of %s is incompatible with Joplin, and as such is no longer supported. Please use a different sync method.": "Η υλοποίηση WebDAV του %s δεν είναι συμβατή με το Joplin και ως εκ τούτου δεν υποστηρίζεται πλέον. Παρακαλούμε χρησιμοποιήστε μια διαφορετική μέθοδο συγχρονισμού.",
|
||||
"Theme": "Θέμα",
|
||||
"There are currently no notes. Create one by clicking on the (+) button.": "Δεν υπάρχουν προς το παρόν σημειώσεις. Δημιουργήστε μία κάνοντας κλικ στο κουμπί (+).",
|
||||
"There are unsaved changes.": "Υπάρχουν μη αποθηκευμένες αλλαγές.",
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\".": "Δεν υπάρχει κανένα σημειωματάριο. Δημιουργήστε ένα πατώντας στο \"Νέο σημειωματάριο\".",
|
||||
"There is no data to export.": "Δεν υπάρχουν δεδομένα για εξαγωγή.",
|
||||
"There was a [conflict](%s) on the attachment below.\n\n%s": "Υπήρξε μια [διένεξη](%s) στο παρακάτω συνημμένο.\n\n%s",
|
||||
@@ -818,8 +934,10 @@
|
||||
"To maximise/minimise the console, press \"tc\".": "Για μεγιστοποιήση/ελαχιστοποιήση της κονσόλας, πατήστε \"tc\".",
|
||||
"To move from one pane to another, press Tab or Shift+Tab.": "Για να μετακινηθείτε από ένα παράθυρο σε άλλο, πατήστε Tab ή Shift + Tab.",
|
||||
"To retry decryption of these items. Run `e2ee decrypt --retry-failed-items`": "Για να επαναλάβετε την αποκρυπτογράφηση αυτών των στοιχείων. Τρέξτε `e2ee decrypt --retry-failed-items`",
|
||||
"To switch the profile, the app is going to close and you will need to restart it.": "Για να αλλάξετε το προφίλ, η εφαρμογή θα κλείσει και θα πρέπει να την επανεκκινήσετε.",
|
||||
"To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions": "Για να λειτουργήσει σωστά, η εφαρμογή χρειάζεται τα ακόλουθα δικαιώματα. Ενεργοποιήστε τα στις ρυθμίσεις του τηλεφώνου σας, στο Apps> Joplin> Permissions",
|
||||
"to-do": "to-do",
|
||||
"to-do: %s": "to-do: %s",
|
||||
"Toggle comment": "Εναλλαγή σχολίου",
|
||||
"Toggle development tools": "Εναλλαγή εργαλείων προγραμματισμού",
|
||||
"Toggle editor layout": "Εναλλαγή διάταξης editor",
|
||||
@@ -832,8 +950,10 @@
|
||||
"Toggle sort order field": "Εναλλαγή του πεδίου ταξινόμησης",
|
||||
"Token has been copied to the clipboard!": "Το Token έχει αντιγραφεί στο πρόχειρο!",
|
||||
"Tools": "Εργαλεία",
|
||||
"Total Size": "Συνολικό μέγεθος",
|
||||
"Total: %d/%d": "Σύνολο: %d/%d",
|
||||
"Try again": "Δοκιμάστε ξανά",
|
||||
"Try it now": "Δοκιμάστε το τώρα",
|
||||
"Type `help [command]` for more information about a command; or type `help all` for the complete usage information.": "Πληκτρολογήστε `help [command]` για περισσότερες πληροφορίες σχετικά με μια εντολή, ή πληκτρολογήστε `help all` για όλες τις πληροφορίες χρήσης.",
|
||||
"Type `joplin help` for usage information.": "Πληκτρολογήστε `joplin help` για πληροφορίες χρήσης.",
|
||||
"Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.": "Πληκτρολογήστε έναν τίτλο σημείωσης ή μέρος του περιεχομένου του για να μεταβείτε σε αυτόν. Ή πληκτρολογήστε # ακολουθούμενο από ένα όνομα ετικέτας ή @ ακολουθούμενο από ένα σημειωματάριο. Ή πληκτρολογίστε : για να ψάξετε για εντολές.",
|
||||
@@ -843,6 +963,7 @@
|
||||
"Undo": "Αναίρεση",
|
||||
"Unknown flag: %s": "Άγνωστο flag: %s",
|
||||
"Unknown item type downloaded - please upgrade Joplin to the latest version": "Λήψη άγνωστου τύπου στοιχείου - αναβαθμίστε το Joplin στην πιο πρόσφατη έκδοση",
|
||||
"Unordered list": "Μη ταξινομημένη λίστα",
|
||||
"Unpublish note": "Αποδημοσίευση σημείωσης",
|
||||
"Unshare": "Κατάργηση κοινής χρήσης",
|
||||
"Unshare this notebook? The recipients will no longer have access to its content.": "Διακόψτε την κοινή χρήση σε αυτό το σημειωματάριο; Οι παραλήπτες δεν θα έχουν πλέον πρόσβαση στο περιεχόμενό του.",
|
||||
@@ -864,21 +985,25 @@
|
||||
"Upgrade the sync target to the latest version.": "Αναβαθμίστε το στόχο συγχρονισμού στην πιο πρόσφατη έκδοση.",
|
||||
"URL": "URL",
|
||||
"Usage: %s": "Χρήση: %s",
|
||||
"Use biometrics to secure access to the app": "Χρησιμοποιήστε βιομετρικά στοιχεία για να διασφαλίσετε την πρόσβαση στην εφαρμογή",
|
||||
"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, TODO_CHECKED (for to-dos), TITLE": "Χρησιμοποιήστε τη μορφή μακράς λίστας. Η μορφή είναι ID, NOTE_COUNT (για σημειωματάριο), DATE, TODO_CHECKED (για to-dos), TITLE",
|
||||
"Use spell checker": "Χρήση ορθογραφικού ελέγχου",
|
||||
"Use the arrows and page up/down to scroll the lists and text areas (including this console).": "Χρησιμοποιήστε τα βελάκια πατώντας πάνω/κάτω για να μετακινηθείτε στις λίστες και τις περιοχές κειμένου (συμπεριλαμβανομένης αυτής της κονσόλας).",
|
||||
"Use the arrows to move the layout items. Press \"Escape\" to exit.": "Χρησιμοποιήστε τα βέλη για να μετακινήσετε τα στοιχεία διάταξης. Πατήστε \"Escape\" για έξοδο.",
|
||||
"Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.": "Χρησιμοποιήστε το για να ξαναφτιάξετε το ευρετήριο αναζήτησης, εάν υπάρχει πρόβλημα με την αναζήτηση. Μπορεί να χρειαστεί πολύς χρόνος ανάλογα με τον αριθμό των σημειώσεων.",
|
||||
"Use your biometrics to secure access to your application. You can always set it up later in Settings.": "Χρησιμοποιήστε τα βιομετρικά σας στοιχεία για να εξασφαλίσετε την πρόσβαση στην εφαρμογή σας. Μπορείτε πάντα να το ρυθμίσετε αργότερα στις Ρυθμίσεις.",
|
||||
"Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.": "Χρησιμοποιείται για το μεγαλύτερο μέρος του κειμένου στο πρόγραμμα επεξεργασίας σημειώσεων. Εάν δεν βρεθεί, χρησιμοποιείται μια γενική αναλογική γραμματοσειρά (μεταβλητού πλάτους).",
|
||||
"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.": "Χρησιμοποιείται όταν απαιτείται γραμματοσειρά σταθερού πλάτους για την ευανάκριβη δημιουργία κειμένου (π.χ. πίνακες, πλαίσια ελέγχου, κωδικός). Εάν δεν βρεθεί, χρησιμοποιείται μια γενική μονοδιάστημα (σταθερού πλάτους).",
|
||||
"User deletions": "Διαγραφές χρήστη",
|
||||
"Users": "Χρήστες",
|
||||
"Valid": "Έγκυρo",
|
||||
"Verify your identity": "Επαληθεύστε την ταυτότητα σας",
|
||||
"View": "Προβολή",
|
||||
"View on map": "Προβολή στο χάρτη",
|
||||
"View them now": "Προβολή τώρα",
|
||||
"Viewer": "Εμφάνιση",
|
||||
"Vim": "Vim",
|
||||
"Voice typing...": "Φωνητική πληκτρολόγηση...",
|
||||
"Warning": "Προειδοποίηση",
|
||||
"Warning: not all resources shown for performance reasons (limit: %s).": "Προειδοποίηση: δεν εμφανίζονται όλοι οι πόροι για λόγους απόδοσης (όριο:% s).",
|
||||
"Web Clipper": "Web Clipper",
|
||||
@@ -888,6 +1013,7 @@
|
||||
"WebDAV username": "WebDAV username",
|
||||
"Website and documentation": "Ιστοσελίδα και εγχειρίδια",
|
||||
"Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.": "Καλώς ήλθατε στο Joplin!\n\nΠληκτρολογίστε `:help shortcuts` για τη λίστα συντομεύσεων πληκτρολογίου ή απλά `:help` για πληροφορίες χρήσης.\n\nΓια παράδειγμα, για να δημιουργήσετε ένα σημειωματάριο πατήστε `mb` ενώ για να δημιουργήσετε μια σημείωση πατήστε `mn`.",
|
||||
"Welcome!": "Καλώς ήρθατε!",
|
||||
"When creating a new note:": "Κατά τη δημιουργία μιας νέας σημείωσης:",
|
||||
"When creating a new to-do:": "Κατά τη δημιουργία ενός νέου to-do:",
|
||||
"Words": "Λέξεις",
|
||||
@@ -902,6 +1028,7 @@
|
||||
"You may use the tool below to re-encrypt your data, for example if you know that some of your notes are encrypted with an obsolete encryption method.": "Μπορείτε να χρησιμοποιήσετε το παρακάτω εργαλείο για να κρυπτογραφήσετε εκ νέου τα δεδομένα σας, για παράδειγμα, εάν γνωρίζετε ότι ορισμένες από τις σημειώσεις σας είναι κρυπτογραφημένες με μια παρωχημένη μέθοδο κρυπτογράφησης.",
|
||||
"Your choice: ": "Η επιλογή σας: ",
|
||||
"Your data is going to be re-encrypted and synced again.": "Τα δεδομένα σας πρόκειται να κρυπτογραφηθούν και να συγχρονιστούν ξανά.",
|
||||
"Your password is needed to decrypt some of your data.": "Ο κωδικός πρόσβασής σας είναι απαραίτητος για την αποκρυπτογράφηση ορισμένων δεδομένων σας.",
|
||||
"Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.": "Ο κωδικός πρόσβασής σας είναι απαραίτητος για την αποκρυπτογράφηση ορισμένων δεδομένων σας. Πληκτρολογήστε `:e2ee decrypt` για να τον ορίσετε.",
|
||||
"Your permission to use your camera is required.": "Απαιτείται η άδειά σας για τη χρήση της φωτογραφικής σας μηχανής.",
|
||||
"Your version: %s": "Η έκδοσή σου: %s",
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
"Accelerator \"%s\" is not valid.": "Le raccourci \"%s\" n'est pas valide.",
|
||||
"Accelerator \"%s\" is used for \"%s\" and \"%s\" commands. This may lead to unexpected behaviour.": "Le raccourci \"%s\" est utilisé par les commandes \"%s\" et \"%s\", ce qui peut causer des problèmes.",
|
||||
"Accept": "Accepter",
|
||||
"Access denied: Please check your username and password": "Accès non-autorisé : Veuillez vérifier votre nom et mot de passe",
|
||||
"Access denied: Please re-enter your password and/or username": "Accès non-autorisé : Veuillez ré-entrer votre nom ou mot de passe",
|
||||
"Account": "Compte",
|
||||
"Action": "Action",
|
||||
"Actions": "Actions",
|
||||
@@ -72,6 +74,7 @@
|
||||
"Ambiguous notebook \"%s\". Please use notebook id instead - press \"ti\" to see the short notebook id or use $b for current selected notebook": "Titre ambigu \"%s\". Veuillez entrer l'identifiant du carnet - pressez \"ti\" pour afficher les identifiants, ou utilisez $b pour le carnet en cours",
|
||||
"Ambiguous notebook \"%s\". Please use short notebook id instead - press \"ti\" to see the short notebook id": "Titre ambigu \"%s\". Veuillez entrer l'identifiant du carnet - pressez \"ti\" pour afficher les identifiants",
|
||||
"An update is available, do you want to download it now?": "Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?",
|
||||
"Any email sent to this address will be converted into a note and added to your collection. The note will be saved into the Inbox notebook": "Un email envoyé à cette adresse sera converti en note et ajouté à votre collection. La note sera sauvé dans votre carnet \"Inbox\"",
|
||||
"Appearance": "Apparence",
|
||||
"Application": "Application",
|
||||
"Apply": "Appliquer",
|
||||
@@ -105,6 +108,8 @@
|
||||
"Browse...": "Parcourir…",
|
||||
"Bulleted List": "Liste à puces",
|
||||
"Can Share": "Peut partager",
|
||||
"Can view": "Lecture seule",
|
||||
"Can view and edit": "Lecture et édition",
|
||||
"Cancel": "Annuler",
|
||||
"Cancelling background synchronisation... Please wait.": "Annulation de la synchronisation… Veuillez patienter.",
|
||||
"Cancelling...": "Annulation…",
|
||||
@@ -112,6 +117,7 @@
|
||||
"Cannot access %s": "Impossible d'accéder à %s",
|
||||
"Cannot change encrypted item": "Un objet chiffré ne peut pas être modifié",
|
||||
"Cannot copy note to \"%s\" notebook": "Impossible de copier la note vers le carnet \"%s\"",
|
||||
"Cannot create a new note: %s": "Impossible de créer une nouvelle note : %s",
|
||||
"Cannot find \"%s\".": "Impossible de trouver \"%s\".",
|
||||
"Cannot find: \"%s\"": "Impossible de trouver : \"%s\"",
|
||||
"Cannot initialise synchroniser.": "Impossible d'initialiser la synchronisation.",
|
||||
@@ -179,6 +185,7 @@
|
||||
"Copy Markdown link": "Copier lien Markdown",
|
||||
"Copy path to clipboard": "Copier le chemin",
|
||||
"Copy Shareable Link": "Copier lien partageable",
|
||||
"Copy to clipboard": "Copier",
|
||||
"Copy token": "Copier le code",
|
||||
"Could not authorise application:\n\n%s\n\nPlease try again.": "Impossible d'autoriser le logiciel :\n\n%s\n\nVeuillez réessayer.",
|
||||
"Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s": "Impossible de se connecter au server Joplin. Veuillez vérifier la configuration. L'erreur complète était :\n\n%s",
|
||||
@@ -234,13 +241,16 @@
|
||||
"Delete expired tokens": "Supprimer les tokens expirés",
|
||||
"Delete line": "Supprimer la ligne",
|
||||
"Delete local data and re-download from sync target": "Supprimer les données locales et re‑télécharger depuis la cible de synchronisation",
|
||||
"Delete note": "Supprimer la note",
|
||||
"Delete note \"%s\"?": "Supprimer note \"%s\" ?",
|
||||
"Delete note?": "Supprimer la note ?",
|
||||
"Delete notebook": "Supprimer le carnet",
|
||||
"Delete notebook \"%s\"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.": "Effacer le carnet \"%s\" ?\n\nToutes les notes et sous‑carnets dans ce carnet seront également effacés.",
|
||||
"Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.": "Effacer le carnet ? Toutes les notes et sous‑carnets dans ce carnet seront également effacés.",
|
||||
"Delete plugin \"%s\"?": "Supprimer plugin \"%s\" ?",
|
||||
"Delete profile \"%s\"": "Supprimer profil \"%s\" ?",
|
||||
"Delete selected notes": "Supprimer les notes sélectionnées",
|
||||
"Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that's recently been sent to it may be lost.": "Supprimer le carnet \"Inbox\" ?\n\nSi vous le supprimez, tout email qui lui a été récemment envoyé peut être perdu.",
|
||||
"Delete these %d notes?": "Supprimer ces %d notes ?",
|
||||
"Delete this invitation? The recipient will no longer have access to this shared notebook.": "Supprimer cette invitation ? Le destinataire n'aura plus accès au carnet partagé.",
|
||||
"Delete this profile?": "Supprimer ce profil ?",
|
||||
@@ -307,6 +317,8 @@
|
||||
"Either \"text\" or \"json\"": "Soit \"text\" soit \"json\"",
|
||||
"Emacs": "Emacs",
|
||||
"Email": "Email",
|
||||
"Email to note": "Conversion email en note",
|
||||
"Email to Note": "Conversion email en note",
|
||||
"Emails": "Emails",
|
||||
"emphasised text": "texte en italique",
|
||||
"Enable": "Activer",
|
||||
@@ -360,11 +372,14 @@
|
||||
"Expand": "Ouvrir",
|
||||
"Export": "Exporter",
|
||||
"Export all": "Tout exporter",
|
||||
"Export all notes as JEX": "Exporter toutes les notes en JEX",
|
||||
"Export debug report": "Exporter rapport de débogage",
|
||||
"Export Debug Report": "Exporter rapport de débogage",
|
||||
"Export profile": "Exporter le profil",
|
||||
"Exported successfully!": "Exporté avec succès !",
|
||||
"Exporting profile...": "Exportation du profil…",
|
||||
"Exporting to \"%s\" as \"%s\" format. Please wait...": "Exportation en cours vers \"%s\" au format \"%s\". Veuillez patienter…",
|
||||
"Exporting...": "Exportation...",
|
||||
"Exports Joplin data to the given path. By default, it will export the complete database including notebooks, notes, tags and resources.": "Exporter les données de Joplin. Par défaut, la base de donnée complète sera exportée, y compris les carnets, notes, tags et ressources.",
|
||||
"Exports only the given note.": "Exporter uniquement la note spécifiée.",
|
||||
"Exports only the given notebook.": "Exporter uniquement le carnet spécifié.",
|
||||
@@ -770,6 +785,7 @@
|
||||
"Set the password": "Définir le mot de passe",
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s": "Assigner la valeur [value] à la propriété <name> de la <note> donnée. Les valeurs possibles sont :\n\n%s",
|
||||
"Share": "Partager",
|
||||
"Share a copy of all notes in a file format that can be imported by Joplin on a computer.": "Partager une copie de toutes vos notes dans un format qui peut être importé par Joplin sur un ordinateur.",
|
||||
"Share and collaborate on a notebook": "Partager et collaborer sur un carnet",
|
||||
"Share Notebook": "Partager le carnet",
|
||||
"Share notebook...": "Partager carnet…",
|
||||
@@ -915,6 +931,9 @@
|
||||
"This attachment is not downloaded or not decrypted yet.": "Cette pièce jointe n'est pas téléchargée ou pas encore déchiffrée.",
|
||||
"This authorisation token is only needed to allow third-party applications to access Joplin.": "Ce code d'authentification est nécessaire uniquement pour permettre aux logiciels tiers d'accéder aux données de Joplin.",
|
||||
"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.": "Ceci est un outil pour lister les fichiers qui sont attachés à vos notes. Veuillez noter que lorsqu'un fichier est supprimé ici, il ne peut pas être restauré par la suite.",
|
||||
"This note could not be deleted: %s": "Cette note n'a pas pu être supprimée : %s",
|
||||
"This note could not be duplicated: %s": "Cette note n'a pas pu être dupliquée : %s",
|
||||
"This note could not be moved: %s": "Cette note n'a pas pu être déplacée : %s",
|
||||
"This note does not have geolocation information.": "Cette note n'a pas d'information de géolocalisation.",
|
||||
"This note has been modified:": "Cette note a été modifiée :",
|
||||
"This note has no content. Click on \"%s\" to toggle the editor and edit the note.": "Cette note n'a pas de contenu. Cliquer sur \"%s\" pour basculer vers l'éditeur et éditer cette note.",
|
||||
@@ -962,6 +981,7 @@
|
||||
"Type a note title or part of its content to jump to it. Or type # followed by a tag name, or @ followed by a notebook name. Or type : to search for commands.": "Entrez le titre d’une note, ou entrez # suivi du nom d’une étiquette, ou @ suivi du nom d’un carnet. Ou entrez : pour chercher une commande.",
|
||||
"Type new tags or select from list": "Entrez de nouvelles étiquettes ou sélectionnez de la liste",
|
||||
"Type: %s.": "Type : %s.",
|
||||
"Unable to export or share data. Reason: %s": "Impossible d'exporter les données : %s",
|
||||
"Uncompleted to-dos on top": "Tâches non‑terminées en haut",
|
||||
"Undo": "Annuler",
|
||||
"Unknown flag: %s": "Paramètre inconnu : %s",
|
||||
@@ -1010,6 +1030,7 @@
|
||||
"Voice typing...": "Saisie vocale...",
|
||||
"Warning": "Avertissement",
|
||||
"Warning: not all resources shown for performance reasons (limit: %s).": "Attention : tous les fichiers ne sont pas affichés pour des raisons de performance (limite : %s).",
|
||||
"Warnings:\n%s": "Avertissements:\n%s",
|
||||
"Web Clipper": "Web Clipper",
|
||||
"WebDAV": "WebDAV",
|
||||
"WebDAV password": "WebDAV : Mot de passe",
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"Downloaded and decrypted": "Preuzeto i dešifrirano",
|
||||
"Downloaded and encrypted": "Preuzeto i šifrirano",
|
||||
"Downloading": "Preuzimanje",
|
||||
"Downloading %s language files...": "Preuzimanje jezičnih datoteka za %s …",
|
||||
"Downloading resources...": "Preuzimanje resursa …",
|
||||
"Dracula": "Drakula",
|
||||
"Drop notes or files here": "Ispusti bilješke ili dokumente ovdje",
|
||||
@@ -410,6 +411,7 @@
|
||||
"Headers": "Zaglavlja",
|
||||
"Heading": "Naslov",
|
||||
"Help": "Pomoć",
|
||||
"Hermes enabled: %d": "Hermes aktiviran: %d",
|
||||
"Hide %s": "Sakrij %s",
|
||||
"Hide advanced": "Sakrij napredne",
|
||||
"Hide disabled": "Sakrij deaktivirane",
|
||||
@@ -493,6 +495,7 @@
|
||||
"Later": "Kasnije",
|
||||
"Layout": "Raspored",
|
||||
"Layout button sequence": "Slijed gumbova rasporeda",
|
||||
"Leave it blank to download the language files from the default website": "Ostavi prazno za preuzimanje jezičnih datoteka sa standardne web stranice",
|
||||
"Leave notebook...": "Napusti bilježnicu …",
|
||||
"Legal": "Legal",
|
||||
"Letter": "Letter",
|
||||
@@ -716,7 +719,7 @@
|
||||
"Replace": "Zamijeni",
|
||||
"Replace all": "Zamijeni sve",
|
||||
"Replace with...": "Zamijeni sa …",
|
||||
"Replace: ": "Zamijeni:",
|
||||
"Replace: ": "Zamijeni: ",
|
||||
"Reset application layout": "Obnovi raspored programa",
|
||||
"Reset master password": "Obnovi glavnu lozinku",
|
||||
"Resources: %d.": "Resursi: %d.",
|
||||
@@ -757,6 +760,7 @@
|
||||
"Select all": "Označi sve",
|
||||
"Select emoji...": "Odaberi emoji …",
|
||||
"Select file...": "Odaberi datoteku …",
|
||||
"Select parent notebook": "Obriši nadređenu bilježnicu",
|
||||
"Server is already running on port %d": "Poslužitelj je već pokrenut na priključku %d",
|
||||
"Server is not running.": "Poslužitelj ne radi.",
|
||||
"Server is running on port %d": "Poslužitelj se pokreće na priključku %d",
|
||||
@@ -1002,7 +1006,8 @@
|
||||
"View them now": "Pogledaj ih sada",
|
||||
"Viewer": "Preglednik",
|
||||
"Vim": "Vim",
|
||||
"Voice typing...": "Tipkanje glasa …",
|
||||
"Voice typing language files (URL)": "Jezične datoteke za tipkanje glasom (URL)",
|
||||
"Voice typing...": "Tipkanje glasom …",
|
||||
"Warning": "Upozorenje",
|
||||
"Warning: not all resources shown for performance reasons (limit: %s).": "Upozorenje: za brže izvođenje, ne prikazuju se svi resursi (ograničenje: %s).",
|
||||
"Web Clipper": "Web Clipper",
|
||||
@@ -1012,6 +1017,7 @@
|
||||
"WebDAV username": "WebDAV korisničko ime",
|
||||
"Website and documentation": "Web-stranica i dokumentacija",
|
||||
"Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.": "Dobro došao, dobro došla u Joplin!\n\nUpiši `:help shortcuts` za popis tipkovnih prečaca ili samo`:help` za informacije o korištenju.\n\nNa primjer, za stvaranje bilježnice pritisni `mb`; za stvaranje bilješke pritisni `mn`.",
|
||||
"Welcome!": "Dobro došao, dobro došla!",
|
||||
"When creating a new note:": "Prilikom stvaranja nove bilješke:",
|
||||
"When creating a new to-do:": "Prilikom stvaranja novog zadatka:",
|
||||
"Words": "Broj riječi",
|
||||
|
||||
@@ -41,45 +41,45 @@ locales['uk_UA'] = require('./uk_UA.json');
|
||||
locales['vi'] = require('./vi.json');
|
||||
locales['zh_CN'] = require('./zh_CN.json');
|
||||
locales['zh_TW'] = require('./zh_TW.json');
|
||||
stats['ar'] = {"percentDone":79};
|
||||
stats['ar'] = {"percentDone":77};
|
||||
stats['eu'] = {"percentDone":22};
|
||||
stats['bs_BA'] = {"percentDone":57};
|
||||
stats['bg_BG'] = {"percentDone":45};
|
||||
stats['ca'] = {"percentDone":88};
|
||||
stats['hr_HR'] = {"percentDone":99};
|
||||
stats['cs_CZ'] = {"percentDone":98};
|
||||
stats['da_DK'] = {"percentDone":98};
|
||||
stats['bs_BA'] = {"percentDone":56};
|
||||
stats['bg_BG'] = {"percentDone":44};
|
||||
stats['ca'] = {"percentDone":86};
|
||||
stats['hr_HR'] = {"percentDone":98};
|
||||
stats['cs_CZ'] = {"percentDone":96};
|
||||
stats['da_DK'] = {"percentDone":96};
|
||||
stats['de_DE'] = {"percentDone":98};
|
||||
stats['et_EE'] = {"percentDone":44};
|
||||
stats['et_EE'] = {"percentDone":43};
|
||||
stats['en_GB'] = {"percentDone":100};
|
||||
stats['en_US'] = {"percentDone":100};
|
||||
stats['es_ES'] = {"percentDone":97};
|
||||
stats['es_ES'] = {"percentDone":95};
|
||||
stats['eo'] = {"percentDone":25};
|
||||
stats['fi_FI'] = {"percentDone":98};
|
||||
stats['fi_FI'] = {"percentDone":96};
|
||||
stats['fr_FR'] = {"percentDone":100};
|
||||
stats['gl_ES'] = {"percentDone":29};
|
||||
stats['id_ID'] = {"percentDone":88};
|
||||
stats['it_IT'] = {"percentDone":80};
|
||||
stats['hu_HU'] = {"percentDone":77};
|
||||
stats['nl_BE'] = {"percentDone":78};
|
||||
stats['nl_NL'] = {"percentDone":87};
|
||||
stats['nb_NO'] = {"percentDone":87};
|
||||
stats['fa'] = {"percentDone":54};
|
||||
stats['pl_PL'] = {"percentDone":89};
|
||||
stats['pt_BR'] = {"percentDone":87};
|
||||
stats['pt_PT'] = {"percentDone":72};
|
||||
stats['ro'] = {"percentDone":50};
|
||||
stats['sl_SI'] = {"percentDone":79};
|
||||
stats['sv'] = {"percentDone":99};
|
||||
stats['th_TH'] = {"percentDone":36};
|
||||
stats['vi'] = {"percentDone":77};
|
||||
stats['tr_TR'] = {"percentDone":99};
|
||||
stats['uk_UA'] = {"percentDone":71};
|
||||
stats['el_GR'] = {"percentDone":87};
|
||||
stats['ru_RU'] = {"percentDone":99};
|
||||
stats['sr_RS'] = {"percentDone":64};
|
||||
stats['zh_CN'] = {"percentDone":96};
|
||||
stats['zh_TW'] = {"percentDone":88};
|
||||
stats['gl_ES'] = {"percentDone":28};
|
||||
stats['id_ID'] = {"percentDone":86};
|
||||
stats['it_IT'] = {"percentDone":78};
|
||||
stats['hu_HU'] = {"percentDone":75};
|
||||
stats['nl_BE'] = {"percentDone":76};
|
||||
stats['nl_NL'] = {"percentDone":86};
|
||||
stats['nb_NO'] = {"percentDone":85};
|
||||
stats['fa'] = {"percentDone":53};
|
||||
stats['pl_PL'] = {"percentDone":88};
|
||||
stats['pt_BR'] = {"percentDone":85};
|
||||
stats['pt_PT'] = {"percentDone":70};
|
||||
stats['ro'] = {"percentDone":49};
|
||||
stats['sl_SI'] = {"percentDone":78};
|
||||
stats['sv'] = {"percentDone":97};
|
||||
stats['th_TH'] = {"percentDone":35};
|
||||
stats['vi'] = {"percentDone":75};
|
||||
stats['tr_TR'] = {"percentDone":97};
|
||||
stats['uk_UA'] = {"percentDone":98};
|
||||
stats['el_GR'] = {"percentDone":97};
|
||||
stats['ru_RU'] = {"percentDone":97};
|
||||
stats['sr_RS'] = {"percentDone":63};
|
||||
stats['zh_CN'] = {"percentDone":98};
|
||||
stats['zh_TW'] = {"percentDone":86};
|
||||
stats['ja_JP'] = {"percentDone":88};
|
||||
stats['ko'] = {"percentDone":88};
|
||||
stats['ko'] = {"percentDone":86};
|
||||
module.exports = { locales: locales, stats: stats };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user