You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
71 Commits
android-v1
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
314686bede | ||
|
af8845f209 | ||
|
c95d7f9d37 | ||
|
2510c659e6 | ||
|
b7523e1b21 | ||
|
e4e9e801a2 | ||
|
524ec12d8a | ||
|
1108e8c28a | ||
|
0f1156ab9c | ||
|
2a08cc332a | ||
|
5d2baa872e | ||
|
4b377589aa | ||
|
cf78204c85 | ||
|
c513cdd4eb | ||
|
5f410e80e6 | ||
|
2aa7eaa192 | ||
|
b24d060281 | ||
|
a014b9347e | ||
|
582ab4ac13 | ||
|
c9adccad4a | ||
|
f41ba67e15 | ||
|
1f70a76c7e | ||
|
5fe3732a38 | ||
|
445533cfcc | ||
|
a8e29249d6 | ||
|
e4a3cbd2ff | ||
|
96b7ce9d50 | ||
|
2bbc1e7ecd | ||
|
83619b279d | ||
|
8b5a99d494 | ||
|
67d4123608 | ||
|
a424e3c899 | ||
|
08d4b5a714 | ||
|
68aefd5e4c | ||
|
57d750bc9a | ||
|
fbe966903b | ||
|
652748f969 | ||
|
e108fdb1d8 | ||
|
a8296e2e37 | ||
|
0998fc0ad7 | ||
|
d5f3e860b9 | ||
|
4e624f7db5 | ||
|
5b697b7e16 | ||
|
30e0d69a74 | ||
|
652816fd26 | ||
|
3a33e5f416 | ||
|
277dac5512 | ||
|
81d97d9f9d | ||
|
a4873cd40d | ||
|
20cb2daf43 | ||
|
e5b5250a91 | ||
|
db7d617e2b | ||
|
a627884876 | ||
|
ed30d09e07 | ||
|
179e3f9aee | ||
|
a67aedba35 | ||
|
199c411a7d | ||
|
5cd7bb5bdb | ||
|
30b8f5e2aa | ||
|
44f2842820 | ||
|
569355a318 | ||
|
8464e16d5d | ||
|
874c1e3e82 | ||
|
2530ecfc86 | ||
|
6b49f1dfcc | ||
|
b1af25ea18 | ||
|
3086007a9c | ||
|
bdfb6b97f5 | ||
|
c01219e6be | ||
|
c903947704 | ||
|
e190d90832 |
@@ -71,6 +71,12 @@ ElectronClient/commands/stopExternalEditing.js
|
||||
ElectronClient/global.d.js
|
||||
ElectronClient/gui/ErrorBoundary.js
|
||||
ElectronClient/gui/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
|
||||
ElectronClient/gui/KeymapConfig/styles/index.js
|
||||
ElectronClient/gui/KeymapConfig/utils/getLabel.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
|
||||
ElectronClient/gui/MainScreen/commands/editAlarm.js
|
||||
ElectronClient/gui/MainScreen/commands/exportPdf.js
|
||||
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
|
||||
@@ -98,12 +104,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
@@ -152,6 +152,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/lib/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@@ -50,6 +50,8 @@ joplin-webclipper-source.zip
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
*.map
|
||||
ReactNativeClient/lib/sql-extensions/
|
||||
!ReactNativeClient/lib/sql-extensions/spellfix.dll
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
CliClient/app/LinkSelector.js
|
||||
@@ -62,6 +64,12 @@ ElectronClient/commands/stopExternalEditing.js
|
||||
ElectronClient/global.d.js
|
||||
ElectronClient/gui/ErrorBoundary.js
|
||||
ElectronClient/gui/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
|
||||
ElectronClient/gui/KeymapConfig/styles/index.js
|
||||
ElectronClient/gui/KeymapConfig/utils/getLabel.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
|
||||
ElectronClient/gui/MainScreen/commands/editAlarm.js
|
||||
ElectronClient/gui/MainScreen/commands/exportPdf.js
|
||||
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
|
||||
@@ -89,12 +97,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
@@ -143,6 +145,7 @@ ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/fence.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/mermaid.js
|
||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
ReactNativeClient/lib/ntpDate.js
|
||||
ReactNativeClient/lib/services/CommandService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
4
BUILD.md
4
BUILD.md
@@ -87,6 +87,10 @@ It still requires you to quit the application each time you want it to rebuild,
|
||||
2. Switch to the Electron app and <kbd>cmd</kbd>+<kbd>Q</kbd> to quit it.
|
||||
3. `watchman` immediately restarts the app for you (whereas usually you'd have to switch back to the terminal, type `"npm start"`, and hit enter).
|
||||
|
||||
# Updating Markdown renderer packages
|
||||
|
||||
The Markdown renderer is located under ReactNativeClient/lib/joplin-renderer. Whenever updating one of its dependencies, such as Mermaid or Katex, please run `npm run buildAssets` to make sure all assets such as fonts or CSS files are deployed correctly.
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
Please read for the [Build Troubleshooting Document](https://github.com/laurent22/joplin/blob/master/readme/build_troubleshooting.md) for various tips on how to get the build working.
|
@@ -2,6 +2,9 @@ const gulp = require('gulp');
|
||||
const fs = require('fs-extra');
|
||||
const utils = require('../Tools/gulp/utils');
|
||||
const tasks = {
|
||||
compileExtensions: {
|
||||
fn: require('../Tools/gulp/tasks/compileExtensions.js'),
|
||||
},
|
||||
copyLib: require('../Tools/gulp/tasks/copyLib'),
|
||||
tsc: require('../Tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
@@ -53,10 +56,12 @@ utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
gulp.task('build', gulp.series([
|
||||
'prepareBuild',
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
]));
|
||||
|
||||
gulp.task('buildTests', gulp.series([
|
||||
'prepareTestBuild',
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
]));
|
||||
|
@@ -15,6 +15,8 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.2.4\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
|
||||
#: CliClient/app/command-cp.js:13
|
||||
msgid ""
|
||||
@@ -106,9 +108,9 @@ msgid "Do not ask for confirmation."
|
||||
msgstr "Χωρίς να ζητείται επιβεβαίωση."
|
||||
|
||||
#: CliClient/app/command-import.js:27
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Output format: %s"
|
||||
msgstr "Μορφή προέλευσης: %s"
|
||||
msgstr "Μορφή εξόδου: %s"
|
||||
|
||||
#: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69
|
||||
#, javascript-format
|
||||
@@ -222,14 +224,16 @@ msgstr ""
|
||||
"αρκετά λεπτά ανάλογα με το μέγεθος αυτών που πρέπει να αποκρυπτογραφηθούν."
|
||||
|
||||
#: CliClient/app/command-e2ee.js:53
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Decrypted items: %d"
|
||||
msgstr "Αποκρυπτογραφημένα στοιχεία: %s / %s"
|
||||
msgstr "Αποκρυπτογραφημένα στοιχεία: %d"
|
||||
|
||||
#: CliClient/app/command-e2ee.js:54
|
||||
#, javascript-format
|
||||
msgid "Skipped items: %d (use --retry-failed-items to retry decrypting them)"
|
||||
msgstr ""
|
||||
"Στοιχεία που έχουν παραλειφθεί: %d (χρήσιμοποίησε --retry-failed-items για "
|
||||
"επανάληψη της αποκρυπτογράφησης τους)"
|
||||
|
||||
#: CliClient/app/command-e2ee.js:68
|
||||
msgid "Completed decryption."
|
||||
@@ -373,7 +377,6 @@ msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Στόχος συγχρονισμού: %s (%s)"
|
||||
|
||||
#: CliClient/app/command-sync.js:177
|
||||
#, fuzzy
|
||||
msgid "Cannot initialise synchroniser."
|
||||
msgstr "Δεν είναι δυνατή η προετοιμασία του συγχρονιστή."
|
||||
|
||||
@@ -839,14 +842,14 @@ msgid "Goto Anything..."
|
||||
msgstr "Γρήγορη Μετακίνηση..."
|
||||
|
||||
#: ElectronClient/InteropServiceHelper.js:147
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
|
||||
msgstr "Εισαγωγή από \"%s\" σε μορφή \"%s\". Παρακαλώ περιμένετε..."
|
||||
msgstr "Γίνεται εξαγωγή στο \"%s\" με μορφή \"%s\". Παρακαλώ περιμένετε..."
|
||||
|
||||
#: ElectronClient/InteropServiceHelper.js:164
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Could not export notes: %s"
|
||||
msgstr "Δεν ήταν δυνατή η αναβάθμιση του master κλειδιού: %s"
|
||||
msgstr "Δεν ήταν δυνατή η εξαγωγή σημειώσεων: %s"
|
||||
|
||||
#: ElectronClient/checkForUpdates.js:138
|
||||
msgid "Current version is up-to-date."
|
||||
@@ -959,9 +962,10 @@ msgid "Delete"
|
||||
msgstr "Διαγραφή"
|
||||
|
||||
#: ElectronClient/gui/SideBar/SideBar.min.js:276
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Remove tag \"%s\" and its descendant tags from all notes?"
|
||||
msgstr "Κατάργηση της ετικέτας \"%s\" από όλες τις σημειώσεις;"
|
||||
msgstr ""
|
||||
"Κατάργηση ετικέτας \"%s\" και των απογόνων ετικετών από όλες τις σημειώσεις;"
|
||||
|
||||
#: ElectronClient/gui/SideBar/SideBar.min.js:278
|
||||
#: ElectronClient/gui/SideBar.min.js:292
|
||||
@@ -1031,9 +1035,8 @@ msgstr "έντονη γραφή"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:145
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:261
|
||||
#, fuzzy
|
||||
msgid "emphasised text"
|
||||
msgstr "πλάγια γραφή"
|
||||
msgstr "κείμενο με έμφαση"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:263
|
||||
@@ -1091,9 +1094,8 @@ msgstr ""
|
||||
"επεξεργαστείτε τη σημείωση στον editor."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
|
||||
#, fuzzy
|
||||
msgid "Checkbox list"
|
||||
msgstr "Πλαίσιο ελέγχου"
|
||||
msgstr "Λίστα πλαισίων ελέγχου"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:470
|
||||
#: ElectronClient/gui/ConfigScreen.min.js:642
|
||||
@@ -1110,11 +1112,11 @@ msgstr "Επισύναψη αρχείου"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:535
|
||||
msgid "Code Block"
|
||||
msgstr ""
|
||||
msgstr "Μπλοκ Κώδικα"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:544
|
||||
msgid "Inline Code"
|
||||
msgstr ""
|
||||
msgstr "Ενσωματωμένος κώδικας"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:559
|
||||
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:80
|
||||
@@ -1124,13 +1126,12 @@ msgstr "Εισαγωγή Ημερομηνίας Ώρας"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||
msgid "Drop notes or files here"
|
||||
msgstr ""
|
||||
msgstr "Απόθεση σημειώσεων ή αρχείων εδώ"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||
#: ElectronClient/gui/MainScreen/MainScreen.min.js:401
|
||||
#, fuzzy
|
||||
msgid "Code View"
|
||||
msgstr "Κώδικας"
|
||||
msgstr "Προβολή κώδικα"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||
#, javascript-format
|
||||
@@ -1138,6 +1139,8 @@ msgid ""
|
||||
"Please wait for all attachments to be downloaded and decrypted. You may also "
|
||||
"switch to %s to edit the note."
|
||||
msgstr ""
|
||||
"Παρακαλώ περιμένετε να γίνει λήψη και αποκρυπτογράφηση όλων των συνημμένων. "
|
||||
"Μπορείτε επίσης να μεταβείτε στο %s για να επεξεργαστείτε τη σημείωση."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/utils/useMessageHandler.js:70
|
||||
#: ElectronClient/gui/NoteText.min.js:833
|
||||
@@ -1162,7 +1165,7 @@ msgstr "Αποθήκευση ως..."
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:65
|
||||
msgid "Reveal file in folder"
|
||||
msgstr ""
|
||||
msgstr "Αποκάλυψη αρχείου στο φάκελο"
|
||||
|
||||
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:73
|
||||
#: ElectronClient/gui/NoteText.min.js:797
|
||||
@@ -1342,7 +1345,7 @@ msgstr "Αναζήτηση..."
|
||||
|
||||
#: ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js:16
|
||||
msgid "Statistics..."
|
||||
msgstr ""
|
||||
msgstr "Στατιστικά..."
|
||||
|
||||
#: ElectronClient/gui/MainScreen/commands/renameFolder.js:17
|
||||
#: ElectronClient/gui/MainScreen/commands/renameTag.js:17
|
||||
@@ -1605,13 +1608,12 @@ msgstr "Yποβολή"
|
||||
#: ElectronClient/gui/NoteList.min.js:147
|
||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
||||
msgid "Custom order"
|
||||
msgstr ""
|
||||
msgstr "Προσαρμοσμένη σειρά"
|
||||
|
||||
#: ElectronClient/gui/NoteList.min.js:147
|
||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
||||
#, fuzzy
|
||||
msgid "View"
|
||||
msgstr "&Εμφάνιση"
|
||||
msgstr "Προβολή"
|
||||
|
||||
#: ElectronClient/gui/NoteList.min.js:147
|
||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
||||
@@ -1626,12 +1628,13 @@ msgid ""
|
||||
"To manually sort the notes, the sort order must be changed to \"%s\" in the "
|
||||
"menu \"%s\" > \"%s\""
|
||||
msgstr ""
|
||||
"Για να ταξινομήσετε τις σημειώσεις χειροκίνητα, η σειρά ταξινόμησης πρέπει "
|
||||
"να αλλάξει σε \"%s\" στο μενού \"%s\" - \"%s\""
|
||||
|
||||
#: ElectronClient/gui/NoteList.min.js:148
|
||||
#: ElectronClient/gui/NoteList/NoteList.min.js:153
|
||||
#, fuzzy
|
||||
msgid "Do it now"
|
||||
msgstr "Λήψη τώρα:"
|
||||
msgstr "Κάνε το τώρα"
|
||||
|
||||
#: ElectronClient/gui/NoteList.min.js:452
|
||||
#: ElectronClient/gui/NoteList/NoteList.min.js:425
|
||||
@@ -1870,14 +1873,13 @@ msgid "Viewer"
|
||||
msgstr "Εμφάνιση"
|
||||
|
||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:107
|
||||
#, fuzzy
|
||||
msgid "Statistics"
|
||||
msgstr "Κατάσταση"
|
||||
msgstr "Στατιστικά"
|
||||
|
||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:111
|
||||
#, javascript-format
|
||||
msgid "Read time: %s min"
|
||||
msgstr ""
|
||||
msgstr "Χρόνος ανάγνωσης: %s min"
|
||||
|
||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:112
|
||||
#: ElectronClient/gui/ShareNoteDialog.js:175
|
||||
@@ -2102,7 +2104,7 @@ msgstr "Έκδοση προφίλ: %s"
|
||||
#: ElectronClient/app.js:608
|
||||
#, javascript-format
|
||||
msgid "Keychain Supported: %s"
|
||||
msgstr ""
|
||||
msgstr "Υποστηριζόμενη κλειδοθήκη: %s"
|
||||
|
||||
#: ElectronClient/app.js:630 ElectronClient/app.js:706
|
||||
msgid "&File"
|
||||
@@ -2166,9 +2168,8 @@ msgid "Zoom Out"
|
||||
msgstr "Σμίκρινση"
|
||||
|
||||
#: ElectronClient/app.js:860
|
||||
#, fuzzy
|
||||
msgid "&Note"
|
||||
msgstr "Σημείωση"
|
||||
msgstr "&Σημείωση"
|
||||
|
||||
#: ElectronClient/app.js:870
|
||||
msgid "&Tools"
|
||||
@@ -2230,7 +2231,6 @@ msgstr ""
|
||||
"δεδομένα. Δεν πρόκειται να κοινοποιηθούν δεδομένα σε τρίτους."
|
||||
|
||||
#: ReactNativeClient/lib/registry.js:156
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Could not synchronise with OneDrive.\n"
|
||||
"\n"
|
||||
@@ -2241,10 +2241,10 @@ msgid ""
|
||||
msgstr ""
|
||||
"Δεν ήταν δυνατός ο συγχρονισμός με το OneDrive.\n"
|
||||
"\n"
|
||||
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive for Business, "
|
||||
"το οποίο δυστυχώς δεν υποστηρίζεται.\n"
|
||||
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive για "
|
||||
"επιχειρήσεις, το οποίο δυστυχώς δεν μπορεί να υποστηριχθεί.\n"
|
||||
"\n"
|
||||
"Παρακαλώ σκεφτείτε να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
|
||||
"Εξετάστε το ενδεχόμενο να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
|
||||
|
||||
#: ReactNativeClient/lib/logger.js:178
|
||||
#, javascript-format
|
||||
@@ -2258,7 +2258,7 @@ msgstr "Άγνωστο level ID: %s"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
|
||||
msgid "AWS S3"
|
||||
msgstr ""
|
||||
msgstr "AWS S3"
|
||||
|
||||
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
|
||||
msgid "Dropbox"
|
||||
@@ -2404,15 +2404,15 @@ msgstr "WebDAV password"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:189
|
||||
msgid "AWS S3 bucket"
|
||||
msgstr ""
|
||||
msgstr "AWS S3 bucket"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:200
|
||||
msgid "AWS key"
|
||||
msgstr ""
|
||||
msgstr "AWS key"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:210
|
||||
msgid "AWS secret"
|
||||
msgstr ""
|
||||
msgstr "AWS secret"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:224
|
||||
msgid "Attachment download behaviour"
|
||||
@@ -2463,15 +2463,15 @@ msgstr "Θέμα"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:315
|
||||
msgid "Automatically switch theme to match system theme"
|
||||
msgstr ""
|
||||
msgstr "Αυτόματη εναλλαγή θέματος ώστε να ταιριάζει με το θέμα συστήματος"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:327
|
||||
msgid "Preferred light theme"
|
||||
msgstr ""
|
||||
msgstr "Προτιμώμενο φωτεινό θέμα"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:341
|
||||
msgid "Preferred dark theme"
|
||||
msgstr ""
|
||||
msgstr "Προτιμώμενο σκοτεινό θέμα"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:346
|
||||
msgid "Show note counts"
|
||||
@@ -2510,6 +2510,7 @@ msgstr "Αυτόματη-σύζευξη αγκίστρων, παρενθέσεω
|
||||
#: ReactNativeClient/lib/models/Setting.js:395
|
||||
msgid "Use CodeMirror as the code editor (WARNING: BETA)."
|
||||
msgstr ""
|
||||
"Χρησιμοποιείστε το CodeMirror ως τον επεξεργαστή κώδικα (ΠΡΟΣΟΧΗ: BETA)"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:397
|
||||
#: ReactNativeClient/lib/models/Setting.js:415
|
||||
@@ -2637,15 +2638,14 @@ msgid "Editor font family"
|
||||
msgstr "Οικογένεια γραμματοσειράς editor"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:550
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"This should be a *monospace* font or some elements will render incorrectly. "
|
||||
"If the font is incorrect or empty, it will default to a generic monospace "
|
||||
"font."
|
||||
msgstr ""
|
||||
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς δεν θα λειτουργήσει "
|
||||
"σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα προεπιλεγεί μια γενική "
|
||||
"γραμματοσειρά *monospace*."
|
||||
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς κάποια στοιχεία δεν θα "
|
||||
"εμφανιστούν σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα "
|
||||
"προεπιλεγεί μια γενική γραμματοσειρά *monospace*."
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:589
|
||||
msgid "Custom stylesheet for Joplin-wide app styles"
|
||||
@@ -2816,7 +2816,7 @@ msgstr "Διατήρηση ιστορικού σημειώσεων για"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:741
|
||||
msgid "Notebook list growth factor"
|
||||
msgstr ""
|
||||
msgstr "Συντελεστής ανάπτυξης λίστας σημειωματάριων"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:743
|
||||
#: ReactNativeClient/lib/models/Setting.js:756
|
||||
@@ -2827,14 +2827,19 @@ msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Ο συντελεστής ανάπτυξης καθορίζει το πόσο θα αναπτυχθεί ή θα συρρίκνωθεί το "
|
||||
"αντικειμένο ώστε να χωρά στον διαθέσιμο χώρο σε σχέση με τα άλλα "
|
||||
"αντικείμενα. Έτσι, ένα στοιχείο με συντελεστή 2 θα πάρει διπλάσιο χώρο από "
|
||||
"ένα στοιχείο με συντελεστή 1. Επανεκκινήστε την εφαρμογή για να δείτε "
|
||||
"αλλαγές."
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:754
|
||||
msgid "Note list growth factor"
|
||||
msgstr ""
|
||||
msgstr "Συντελεστής ανάπτυξης λίστας σημειώσεων"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:767
|
||||
msgid "Note area growth factor"
|
||||
msgstr ""
|
||||
msgstr "Συντελεστής ανάπτυξης περιοχής σημειώσεων"
|
||||
|
||||
#: ReactNativeClient/lib/models/Setting.js:919
|
||||
#, javascript-format
|
||||
@@ -2910,30 +2915,32 @@ msgid "Downloaded"
|
||||
msgstr "Έχουν ληφθεί"
|
||||
|
||||
#: ReactNativeClient/lib/models/Resource.js:371
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Attachment conflict: \"%s\""
|
||||
msgstr "Επισυναπτόμενα"
|
||||
msgstr "Διένεξη συνημμένου: \"%s\""
|
||||
|
||||
#: ReactNativeClient/lib/models/Resource.js:372
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"There was a [conflict](%s) on the attachment below.\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Παρουσιάστηκε σφάλμα κατά τη λήψη αυτού του συνημμένου:"
|
||||
msgstr ""
|
||||
"Υπήρξε μια [διένεξη](%s) στο παρακάτω συνημμένο.\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
#: ReactNativeClient/lib/models/Tag.js:384
|
||||
#, fuzzy
|
||||
msgid "Cannot move tag to this location."
|
||||
msgstr "Δεν είναι δυνατή η μετακίνηση του σημειωματάριου σε αυτήν τη θέση"
|
||||
msgstr "Δεν είναι δυνατή η μετακίνηση της ετικέτας σε αυτήν τη θέση."
|
||||
|
||||
#: ReactNativeClient/lib/models/Tag.js:429
|
||||
msgid "Tag name cannot start or end with a `/`."
|
||||
msgstr ""
|
||||
msgstr "Το όνομα της ετικέτας δεν μπορεί να ξεκινάει ή να τελειώνει με `/`."
|
||||
|
||||
#: ReactNativeClient/lib/models/Tag.js:431
|
||||
msgid "Tag name cannot contain `//`."
|
||||
msgstr ""
|
||||
msgstr "Το όνομα της ετικέτας δεν μπορεί να περιέχει `//`."
|
||||
|
||||
#: ReactNativeClient/lib/models/Tag.js:482
|
||||
#, javascript-format
|
||||
@@ -2946,7 +2953,7 @@ msgstr "ημερομηνία δημιουργίας"
|
||||
|
||||
#: ReactNativeClient/lib/models/Note.js:28
|
||||
msgid "custom order"
|
||||
msgstr ""
|
||||
msgstr "προσαρμοσμένη σειρά"
|
||||
|
||||
#: ReactNativeClient/lib/models/Note.js:92
|
||||
msgid "This note does not have geolocation information."
|
||||
@@ -3564,12 +3571,11 @@ msgstr "File system"
|
||||
|
||||
#: ReactNativeClient/lib/commands/historyForward.js:16
|
||||
msgid "Forward"
|
||||
msgstr ""
|
||||
msgstr "Μπροστά"
|
||||
|
||||
#: ReactNativeClient/lib/commands/synchronize.js:17
|
||||
#, fuzzy
|
||||
msgid "Synchronize"
|
||||
msgstr "Συγχρονισμός"
|
||||
msgstr "Συγχρόνισε"
|
||||
|
||||
#: ReactNativeClient/lib/services/InteropService_Exporter_Jex.js:29
|
||||
msgid "There is no data to export."
|
||||
@@ -3737,9 +3743,10 @@ msgid "Directory"
|
||||
msgstr "Φάκελος"
|
||||
|
||||
#: ReactNativeClient/lib/services/InteropService.js:174
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
|
||||
msgstr "Δεν είναι δυνατή η φόρτωση του module \"%s\" για τη μορφή \"%s\""
|
||||
msgstr ""
|
||||
"Δεν είναι δυνατή η φόρτωση του \"%s\" module με μορφή \"%s\" και έξοδο \"%s\""
|
||||
|
||||
#: ReactNativeClient/lib/services/InteropService.js:232
|
||||
#, javascript-format
|
||||
|
File diff suppressed because it is too large
Load Diff
2
CliClient/package-lock.json
generated
2
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "1.0.166",
|
||||
"version": "1.0.167",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.166",
|
||||
"version": "1.1.167",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
@@ -4,7 +4,12 @@ require('app-module-path').addPath(__dirname);
|
||||
const filterParser = require('lib/services/searchengine/filterParser.js').default;
|
||||
// import filterParser from 'lib/services/searchengine/filterParser.js';
|
||||
|
||||
const makeTerm = (name, value, negated) => { return { name, value, negated }; };
|
||||
const makeTerm = (name, value, negated, quoted = false, wildcard = false) => {
|
||||
if (name === 'text') { return { name, value, negated, quoted, wildcard }; }
|
||||
if (name === 'title' | name === 'body') { return { name, value, negated, wildcard }; }
|
||||
return { name, value, negated };
|
||||
};
|
||||
|
||||
describe('filterParser should be correct filter for keyword', () => {
|
||||
it('title', () => {
|
||||
const searchString = 'title: something';
|
||||
@@ -65,7 +70,7 @@ describe('filterParser should be correct filter for keyword', () => {
|
||||
|
||||
it('phrase text search', () => {
|
||||
const searchString = '"scott joplin"';
|
||||
expect(filterParser(searchString)).toContain(makeTerm('text', '"scott joplin"', false));
|
||||
expect(filterParser(searchString)).toContain(makeTerm('text', '"scott joplin"', false, true));
|
||||
});
|
||||
|
||||
it('multi word body', () => {
|
||||
@@ -105,6 +110,9 @@ describe('filterParser should be correct filter for keyword', () => {
|
||||
|
||||
searchString = 'tag:bl*sphemy';
|
||||
expect(filterParser(searchString)).toContain(makeTerm('tag', 'bl%sphemy', false));
|
||||
|
||||
searchString = 'tag:"space travel"';
|
||||
expect(filterParser(searchString)).toContain(makeTerm('tag', 'space travel', false));
|
||||
});
|
||||
|
||||
it('wildcard notebooks', () => {
|
||||
|
@@ -24,7 +24,7 @@ describe('pathUtils', function() {
|
||||
['no space at the end ', 'no space at the end'],
|
||||
['nor dots...', 'nor dots'],
|
||||
[' no space before either', 'no space before either'],
|
||||
['thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong', 'thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong'],
|
||||
['thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong', 'thatsreallylongthatsreallylongthatsreallylongthats'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const { asyncTest, fileContentEqual, expectNotThrow, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const InteropService = require('lib/services/InteropService.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
@@ -442,4 +442,26 @@ describe('services_InteropService', function() {
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not try to export folders with a non-existing parent', asyncTest(async () => {
|
||||
// Handles and edge case where user has a folder but this folder with a parent
|
||||
// that doesn't exist. Can happen for example in this case:
|
||||
//
|
||||
// - folder1/folder2
|
||||
// - Client 1 sync folder2, but not folder1
|
||||
// - Client 2 sync and get folder2 only
|
||||
// - Client 2 export data
|
||||
// => Crash if we don't handle this case
|
||||
|
||||
await Folder.save({ title: 'orphan', parent_id: '0c5bbd8a1b5a48189484a412a7e534cc' });
|
||||
|
||||
const service = new InteropService();
|
||||
|
||||
const result = await service.export({
|
||||
path: exportDir(),
|
||||
format: 'md',
|
||||
});
|
||||
|
||||
expect(result.warnings.length).toBe(0);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -115,6 +115,7 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+T' },
|
||||
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Option+Shift+Cmd+B' },
|
||||
];
|
||||
|
||||
testCases_Darwin.forEach(({ command, accelerator }) => {
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
@@ -131,6 +132,7 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
|
||||
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
|
||||
];
|
||||
|
||||
testCases_Linux.forEach(({ command, accelerator }) => {
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
@@ -138,10 +140,10 @@ describe('services_KeymapService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAccelerator', () => {
|
||||
describe('getDefaultAccelerator', () => {
|
||||
beforeEach(() => keymapService.initialize());
|
||||
|
||||
it('should reset the Accelerator', () => {
|
||||
it('should return the default accelerator', () => {
|
||||
const testCases = [
|
||||
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
|
||||
{ command: 'synchronize', accelerator: null /* Disabled */ },
|
||||
@@ -154,26 +156,22 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
testCases.forEach(({ command, accelerator }) => {
|
||||
// Remember the default Accelerator value
|
||||
const prevAccelerator = keymapService.getAccelerator(command);
|
||||
// Remember the real default Accelerator value
|
||||
const defaultAccelerator = keymapService.getAccelerator(command);
|
||||
|
||||
// Update the Accelerator,
|
||||
// Update the Accelerator and then retrieve the default accelerator
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
|
||||
// and then reset it..
|
||||
keymapService.resetAccelerator(command);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(prevAccelerator);
|
||||
expect(keymapService.getDefaultAccelerator(command)).toEqual(defaultAccelerator);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setKeymap', () => {
|
||||
describe('overrideKeymap', () => {
|
||||
beforeEach(() => keymapService.initialize());
|
||||
|
||||
it('should update the keymap', () => {
|
||||
keymapService.initialize('darwin');
|
||||
const customKeymap_Darwin = [
|
||||
const customKeymapItems_Darwin = [
|
||||
{ command: 'newNote', accelerator: 'Option+Shift+Cmd+N' },
|
||||
{ command: 'synchronize', accelerator: 'F11' },
|
||||
{ command: 'textBold', accelerator: 'Shift+F5' },
|
||||
@@ -187,14 +185,14 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteList', accelerator: 'Shift+Cmd+S' /* Default of focusElementSideBar */ },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap_Darwin)).not.toThrow();
|
||||
customKeymap_Darwin.forEach(({ command, accelerator }) => {
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems_Darwin)).not.toThrow();
|
||||
customKeymapItems_Darwin.forEach(({ command, accelerator }) => {
|
||||
// Also check if keymap is updated or not
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
});
|
||||
|
||||
keymapService.initialize('win32');
|
||||
const customKeymap_Win32 = [
|
||||
const customKeymapItems_Win32 = [
|
||||
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
|
||||
{ command: 'synchronize', accelerator: 'F11' },
|
||||
{ command: 'textBold', accelerator: 'Shift+F5' },
|
||||
@@ -208,8 +206,8 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteList', accelerator: 'Ctrl+Shift+S' /* Default of focusElementSideBar */ },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap_Win32)).not.toThrow();
|
||||
customKeymap_Win32.forEach(({ command, accelerator }) => {
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems_Win32)).not.toThrow();
|
||||
customKeymapItems_Win32.forEach(({ command, accelerator }) => {
|
||||
// Also check if keymap is updated or not
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
});
|
||||
@@ -240,30 +238,30 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps.length; i++) {
|
||||
const customKeymap = customKeymaps[i];
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
const customKeymapItems = customKeymaps[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when the provided Accelerators are invalid', () => {
|
||||
// Only one test case is provided since KeymapService.validateAccelerator() is already tested
|
||||
const customKeymap = [
|
||||
const customKeymapItems = [
|
||||
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
|
||||
{ command: 'print', accelerator: 'Alt+P' },
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J+O+P+L+I+N' },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when the provided commands are invalid', () => {
|
||||
const customKeymap = [
|
||||
const customKeymapItems = [
|
||||
{ command: 'totallyInvalidCommand', accelerator: 'Ctrl+Shift+G' },
|
||||
{ command: 'print', accelerator: 'Alt+P' },
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when duplicate accelerators are provided', () => {
|
||||
@@ -281,14 +279,8 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps_Darwin.length; i++) {
|
||||
const customKeymap = customKeymaps_Darwin[i];
|
||||
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
// All items should be reset to default values
|
||||
for (let j = 0; j < customKeymap.length; j++) {
|
||||
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
|
||||
}
|
||||
const customKeymapItems = customKeymaps_Darwin[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
|
||||
const customKeymaps_Linux = [
|
||||
@@ -305,14 +297,8 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps_Linux.length; i++) {
|
||||
const customKeymap = customKeymaps_Linux[i];
|
||||
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
|
||||
for (let j = 0; j < customKeymap.length; j++) {
|
||||
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
|
||||
}
|
||||
const customKeymapItems = customKeymaps_Linux[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -426,7 +426,7 @@ describe('services_SearchEngine', function() {
|
||||
const t = testCases[i];
|
||||
const input = t[0];
|
||||
const expected = t[1];
|
||||
const actual = engine.parseQuery(input);
|
||||
const actual = await engine.parseQuery(input);
|
||||
|
||||
const _Values = actual.terms._ ? actual.terms._.map(v => v.value) : undefined;
|
||||
const titleValues = actual.terms.title ? actual.terms.title.map(v => v.value) : undefined;
|
||||
|
@@ -268,7 +268,7 @@ describe('services_SearchFilter', function() {
|
||||
|
||||
await Tag.setNoteTagsByTitles(n1.id, ['tag1', 'tag2']);
|
||||
await Tag.setNoteTagsByTitles(n2.id, ['tag2', 'tag3']);
|
||||
await Tag.setNoteTagsByTitles(n3.id, ['tag3', 'tag4']);
|
||||
await Tag.setNoteTagsByTitles(n3.id, ['tag3', 'tag4', 'space travel']);
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
@@ -304,6 +304,10 @@ describe('services_SearchFilter', function() {
|
||||
expect(rows.length).toBe(2);
|
||||
expect(ids(rows)).toContain(n1.id);
|
||||
expect(ids(rows)).toContain(n3.id);
|
||||
|
||||
rows = await engine.search('tag:"space travel"');
|
||||
expect(rows.length).toBe(1);
|
||||
expect(ids(rows)).toContain(n3.id);
|
||||
}));
|
||||
|
||||
it('should support filtering by notebook', asyncTest(async () => {
|
||||
|
163
CliClient/tests/services_SearchFuzzy.js
Normal file
163
CliClient/tests/services_SearchFuzzy.js
Normal file
@@ -0,0 +1,163 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint prefer-const: 0*/
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const ItemChange = require('lib/models/ItemChange');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const ResourceService = require('lib/services/ResourceService.js');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
let engine = null;
|
||||
|
||||
const ids = (array) => array.map(a => a.id);
|
||||
|
||||
describe('services_SearchFuzzy', function() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
engine = new SearchEngine();
|
||||
engine.setDb(db());
|
||||
|
||||
Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('should return note almost matching title', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
const n2 = await Note.save({ title: 'Important note' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('Broke', { fuzzy: false });
|
||||
expect(rows.length).toBe(0);
|
||||
|
||||
rows = await engine.search('Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n2.id);
|
||||
}));
|
||||
|
||||
|
||||
it('should order results by min fuzziness', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
const n4 = await Note.save({ title: 'No drama for me' });
|
||||
const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('demand', { fuzzy: false });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('demand', { fuzzy: true });
|
||||
expect(rows.length).toBe(3);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
expect(rows[1].id).toBe(n3.id);
|
||||
expect(rows[2].id).toBe(n2.id);
|
||||
}));
|
||||
|
||||
it('should consider any:1', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'cat' });
|
||||
const n2 = await Note.save({ title: 'cats' });
|
||||
const n3 = await Note.save({ title: 'cot' });
|
||||
|
||||
const n4 = await Note.save({ title: 'defenestrate' });
|
||||
const n5 = await Note.save({ title: 'defenstrate' });
|
||||
const n6 = await Note.save({ title: 'defenestrated' });
|
||||
|
||||
const n7 = await Note.save({ title: 'he defenestrated the cat' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
|
||||
rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(7);
|
||||
}));
|
||||
|
||||
it('should leave phrase searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'def ghi' });
|
||||
const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
const n4 = await Note.save({ title: 'def abc' });
|
||||
const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc def', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
|
||||
rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
}));
|
||||
|
||||
it('should leave wild card searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
const n5 = await Note.save({ title: 'wxy zzz' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc*', { fuzzy: true });
|
||||
|
||||
expect(rows.length).toBe(4);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n2.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
}));
|
||||
|
||||
});
|
@@ -1,5 +1,6 @@
|
||||
import LockHandler from 'lib/services/synchronizer/LockHandler';
|
||||
import MigrationHandler from 'lib/services/synchronizer/MigrationHandler';
|
||||
import { Dirnames } from 'lib/services/synchronizer/utils/types';
|
||||
|
||||
// To create a sync target snapshot for the current syncVersion:
|
||||
// - In test-utils, set syncTargetName_ to "filesystem"
|
||||
@@ -70,6 +71,14 @@ describe('synchronizer_MigrationHandler', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should init a new sync target', asyncTest(async () => {
|
||||
// Check that basic folders "locks" and "temp" are created for new sync targets.
|
||||
await migrationHandler().upgrade(1);
|
||||
const result = await fileApi().list();
|
||||
expect(result.items.filter((i:any) => i.path === Dirnames.Locks).length).toBe(1);
|
||||
expect(result.items.filter((i:any) => i.path === Dirnames.Temp).length).toBe(1);
|
||||
}), specTimeout);
|
||||
|
||||
it('should not allow syncing if the sync target is out-dated', asyncTest(async () => {
|
||||
await synchronizer().start();
|
||||
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);
|
||||
|
@@ -50,6 +50,7 @@ const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriv
|
||||
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
|
||||
const md5 = require('md5');
|
||||
const S3 = require('aws-sdk/clients/s3');
|
||||
const { Dirnames } = require('lib/services/synchronizer/utils/types');
|
||||
|
||||
const databases_ = [];
|
||||
let synchronizers_ = [];
|
||||
@@ -438,6 +439,7 @@ async function initFileApi() {
|
||||
|
||||
fileApi.setLogger(logger);
|
||||
fileApi.setSyncTargetId(syncTargetId_);
|
||||
fileApi.setTempDirName(Dirnames.Temp);
|
||||
fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0;
|
||||
|
||||
fileApis_[syncTargetId_] = fileApi;
|
||||
|
@@ -8,9 +8,10 @@ const { ipcMain } = require('electron');
|
||||
|
||||
class ElectronAppWrapper {
|
||||
|
||||
constructor(electronApp, env, profilePath) {
|
||||
constructor(electronApp, env, profilePath, isDebugMode) {
|
||||
this.electronApp_ = electronApp;
|
||||
this.env_ = env;
|
||||
this.isDebugMode_ = isDebugMode;
|
||||
this.profilePath_ = profilePath;
|
||||
this.win_ = null;
|
||||
this.willQuitApp_ = false;
|
||||
@@ -41,7 +42,7 @@ class ElectronAppWrapper {
|
||||
|
||||
createWindow() {
|
||||
// Set to true to view errors if the application does not start
|
||||
const debugEarlyBugs = this.env_ === 'dev';
|
||||
const debugEarlyBugs = this.env_ === 'dev' || this.isDebugMode_;
|
||||
|
||||
const windowStateKeeper = require('electron-window-state');
|
||||
|
||||
@@ -104,7 +105,17 @@ class ElectronAppWrapper {
|
||||
// Waiting for one of the ready events might work but they might not be triggered if there's an error, so
|
||||
// the easiest is to use a timeout. Keep in mind that if you get a white window on Windows it might be due
|
||||
// to this line though.
|
||||
if (debugEarlyBugs) setTimeout(() => this.win_.webContents.openDevTools(), 3000);
|
||||
if (debugEarlyBugs) {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.win_.webContents.openDevTools();
|
||||
} catch (error) {
|
||||
// This will throw an exception "Object has been destroyed" if the app is closed
|
||||
// in less that the timeout interval. It can be ignored.
|
||||
console.warn('Error opening dev tools', error);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
this.win_.on('close', (event) => {
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
|
@@ -30,7 +30,7 @@ const PluginManager = require('lib/services/PluginManager');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const KeymapService = require('lib/services/KeymapService.js').default;
|
||||
const KeymapService = require('lib/services/KeymapService').default;
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
|
||||
@@ -110,6 +110,8 @@ class Application extends BaseApplication {
|
||||
|
||||
this.commandService_commandsEnabledStateChange = this.commandService_commandsEnabledStateChange.bind(this);
|
||||
CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
|
||||
|
||||
KeymapService.instance().on('keymapChange', this.refreshMenu.bind(this));
|
||||
}
|
||||
|
||||
commandService_commandsEnabledStateChange() {
|
||||
@@ -569,7 +571,7 @@ class Application extends BaseApplication {
|
||||
const toolsItemsWindowsLinux = toolsItemsFirst.concat([{
|
||||
label: _('Options'),
|
||||
visible: !shim.isMac(),
|
||||
accelerator: shim.isMac() ? null : keymapService.getAccelerator('config'),
|
||||
accelerator: !shim.isMac() && keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -631,7 +633,7 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Preferences...'),
|
||||
visible: shim.isMac() ? true : false,
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('config') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -663,7 +665,7 @@ class Application extends BaseApplication {
|
||||
visible: shim.isMac() ? false : true,
|
||||
submenu: importItems,
|
||||
}, {
|
||||
label: _('Export'),
|
||||
label: _('Export all'),
|
||||
visible: shim.isMac() ? false : true,
|
||||
submenu: exportItems,
|
||||
}, {
|
||||
@@ -680,7 +682,7 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('hideApp') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('hideApp'),
|
||||
click: () => { bridge().electronApp().hide(); },
|
||||
}, {
|
||||
type: 'separator',
|
||||
@@ -700,7 +702,7 @@ class Application extends BaseApplication {
|
||||
newNotebookItem, {
|
||||
label: _('Close Window'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('closeWindow') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'),
|
||||
selector: 'performClose:',
|
||||
}, {
|
||||
type: 'separator',
|
||||
@@ -1040,11 +1042,9 @@ class Application extends BaseApplication {
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
|
||||
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const ace_css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
styleTag.appendChild(document.createTextNode(ace_css));
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
@@ -1074,10 +1074,16 @@ class Application extends BaseApplication {
|
||||
|
||||
argv = await super.start(argv);
|
||||
|
||||
await this.applySettingsSideEffects();
|
||||
|
||||
if (Setting.value('sync.upgradeState') === Setting.SYNC_UPGRADE_STATE_MUST_DO) {
|
||||
reg.logger().info('app.start: doing upgradeSyncTarget action');
|
||||
bridge().window().show();
|
||||
return { action: 'upgradeSyncTarget' };
|
||||
}
|
||||
|
||||
reg.logger().info('app.start: doing regular boot');
|
||||
|
||||
const dir = Setting.value('profileDir');
|
||||
|
||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||
@@ -1087,7 +1093,7 @@ class Application extends BaseApplication {
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
try {
|
||||
await KeymapService.instance().loadKeymap(`${dir}/keymap-desktop.json`);
|
||||
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
}
|
||||
@@ -1206,7 +1212,7 @@ class Application extends BaseApplication {
|
||||
if (Setting.value('env') === 'dev') {
|
||||
AlarmService.updateAllNotifications();
|
||||
} else {
|
||||
reg.scheduleSync().then(() => {
|
||||
reg.scheduleSync(1000).then(() => {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
AlarmService.updateAllNotifications();
|
||||
@@ -1237,9 +1243,14 @@ class Application extends BaseApplication {
|
||||
this.updateMenuItemStates();
|
||||
|
||||
// Make it available to the console window - useful to call revisionService.collectRevisions()
|
||||
window.revisionService = RevisionService.instance();
|
||||
window.migrationService = MigrationService.instance();
|
||||
window.decryptionWorker = DecryptionWorker.instance();
|
||||
window.joplin = () => {
|
||||
return {
|
||||
revisionService: RevisionService.instance(),
|
||||
migrationService: MigrationService.instance(),
|
||||
decryptionWorker: DecryptionWorker.instance(),
|
||||
bridge: bridge(),
|
||||
};
|
||||
};
|
||||
|
||||
bridge().addEventListener('nativeThemeUpdated', this.bridge_nativeThemeUpdated);
|
||||
}
|
||||
|
@@ -184,12 +184,17 @@ class Bridge {
|
||||
}
|
||||
|
||||
restart() {
|
||||
const { app } = require('electron');
|
||||
|
||||
// Note that in this case we are not sending the "appClose" event
|
||||
// to notify services and component that the app is about to close
|
||||
// but for the current use-case it's not really needed.
|
||||
if (shim.isLinux()) {
|
||||
const { app } = require('electron');
|
||||
|
||||
if (shim.isPortable()) {
|
||||
const options = {
|
||||
execPath: process.env.PORTABLE_EXECUTABLE_FILE,
|
||||
};
|
||||
app.relaunch(options);
|
||||
} else if (shim.isLinux()) {
|
||||
this.showInfoMessageBox(_('The app is now going to close. Please relaunch it to complete the process.'));
|
||||
} else {
|
||||
app.relaunch();
|
||||
|
@@ -10,6 +10,7 @@ const shared = require('lib/components/shared/config-shared.js');
|
||||
const ConfigMenuBar = require('./ConfigMenuBar.min.js');
|
||||
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min');
|
||||
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min');
|
||||
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
@@ -68,6 +69,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
screenFromName(screenName) {
|
||||
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>;
|
||||
|
||||
throw new Error(`Invalid screen name: ${screenName}`);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ const { themeStyle } = require('lib/theme');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const commands = [
|
||||
require('./commands/focusSearch'),
|
||||
@@ -28,7 +29,7 @@ class HeaderComponent extends React.Component {
|
||||
|
||||
const triggerOnQuery = query => {
|
||||
clearTimeout(this.scheduleSearchChangeEventIid_);
|
||||
if (this.searchOnQuery_) this.searchOnQuery_(query);
|
||||
if (this.searchOnQuery_) this.searchOnQuery_(query, Setting.value('db.fuzzySearchEnabled'));
|
||||
this.scheduleSearchChangeEventIid_ = null;
|
||||
};
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
192
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx
Normal file
192
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import KeymapService, { KeymapItem } from '../../lib/services/KeymapService';
|
||||
import { ShortcutRecorder } from './ShortcutRecorder';
|
||||
import getLabel from './utils/getLabel';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useCommandStatus from './utils/useCommandStatus';
|
||||
import styles_ from './styles';
|
||||
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { shim } = require('lib/shim');
|
||||
const { _ } = require('lib/locale');
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
export interface KeymapConfigScreenProps {
|
||||
themeId: number
|
||||
}
|
||||
|
||||
export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
|
||||
const styles = styles_(themeId);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator] = useKeymap();
|
||||
const [recorderError, setRecorderError] = useState<Error>(null);
|
||||
const [editing, enableEditing, disableEditing] = useCommandStatus();
|
||||
const [hovering, enableHovering, disableHovering] = useCommandStatus();
|
||||
|
||||
const handleSave = (event: { commandName: string, accelerator: string }) => {
|
||||
const { commandName, accelerator } = event;
|
||||
setAccelerator(commandName, accelerator);
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleReset = (event: { commandName: string }) => {
|
||||
const { commandName } = event;
|
||||
resetAccelerator(commandName);
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleCancel = (event: { commandName: string }) => {
|
||||
const { commandName } = event;
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleError = (event: { recorderError: Error }) => {
|
||||
const { recorderError } = event;
|
||||
setRecorderError(recorderError);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
const filePath = bridge().showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
defaultPath: 'keymap-desktop',
|
||||
filters: [{ name: 'Joplin Keymaps (keymap-desktop.json)', extensions: ['json'] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
const actualFilePath = filePath[0];
|
||||
try {
|
||||
const keymapFile = await shim.fsDriver().readFile(actualFilePath, 'utf-8');
|
||||
overrideKeymapItems(JSON.parse(keymapFile));
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(`${_('An unexpected error occured while importing the keymap!')}\n${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const filePath = bridge().showSaveDialog({
|
||||
defaultPath: 'keymap-desktop',
|
||||
filters: [{ name: 'Joplin Keymaps (keymap-desktop.json)', extensions: ['json'] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
try {
|
||||
// KeymapService is already synchronized with the in-state keymap
|
||||
await keymapService.saveCustomKeymap(filePath);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccelerator = (accelerator: string) => {
|
||||
return (
|
||||
<div>
|
||||
{accelerator.split('+').map(part => <kbd style={styles.kbd} key={part}>{part}</kbd>).reduce(
|
||||
(accumulator, part) => (accumulator.length ? [...accumulator, ' + ', part] : [part]),
|
||||
[]
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatus = (commandName: string) => {
|
||||
if (editing[commandName]) {
|
||||
return (recorderError && <i className="fa fa-exclamation-triangle" title={recorderError.message} />);
|
||||
} else if (hovering[commandName]) {
|
||||
return (<i className="fa fa-pen" />);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderError = (error: Error) => {
|
||||
return (
|
||||
<div style={styles.warning}>
|
||||
<p style={styles.text}>
|
||||
<span>
|
||||
{error.message}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderKeymapRow = ({ command, accelerator }: KeymapItem) => {
|
||||
const handleClick = () => enableEditing(command);
|
||||
const handleMouseEnter = () => enableHovering(command);
|
||||
const handleMouseLeave = () => disableHovering(command);
|
||||
const cellContent =
|
||||
<div style={styles.tableCell} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{editing[command] ?
|
||||
<ShortcutRecorder
|
||||
onSave={handleSave}
|
||||
onReset={handleReset}
|
||||
onCancel={handleCancel}
|
||||
onError={handleError}
|
||||
initialAccelerator={accelerator || '' /* Because accelerator is null if disabled */}
|
||||
commandName={command}
|
||||
themeId={themeId}
|
||||
/> :
|
||||
<div style={styles.tableCellContent} onClick={handleClick}>
|
||||
{accelerator
|
||||
? renderAccelerator(accelerator)
|
||||
: <div style={styles.disabled}>{_('Disabled')}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div style={styles.tableCellStatus} onClick={handleClick}>
|
||||
{renderStatus(command)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<tr key={command}>
|
||||
<td style={styles.tableCommandColumn}>
|
||||
{getLabel(command)}
|
||||
</td>
|
||||
<td style={styles.tableShortcutColumn}>
|
||||
{cellContent}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{keymapError && renderError(keymapError)}
|
||||
<div style={styles.container}>
|
||||
<div style={styles.actionsContainer}>
|
||||
<input
|
||||
value={filter}
|
||||
onChange={event => setFilter(event.target.value)}
|
||||
placeholder={_('Search...')}
|
||||
style={styles.filterInput}
|
||||
/>
|
||||
<button style={styles.inlineButton} onClick={handleImport}>{_('Import')}</button>
|
||||
<button style={styles.inlineButton} onClick={handleExport}>{_('Export')}</button>
|
||||
</div>
|
||||
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={styles.tableCommandColumn}>{_('Command')}</th>
|
||||
<th style={styles.tableShortcutColumn}>{_('Keyboard Shortcut')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keymapItems.filter(({ command }) => {
|
||||
const filterLowerCase = filter.toLowerCase();
|
||||
return (command.toLowerCase().includes(filterLowerCase) || getLabel(command).toLowerCase().includes(filterLowerCase));
|
||||
}).map(item => renderKeymapRow(item))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,51 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const React = require('react');
|
||||
const react_1 = require('react');
|
||||
const KeymapService_1 = require('../../lib/services/KeymapService');
|
||||
const styles_1 = require('./styles');
|
||||
const { _ } = require('lib/locale');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
exports.ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId }) => {
|
||||
const styles = styles_1.default(themeId);
|
||||
const [accelerator, setAccelerator] = react_1.useState(initialAccelerator);
|
||||
const [saveAllowed, setSaveAllowed] = react_1.useState(true);
|
||||
react_1.useEffect(() => {
|
||||
try {
|
||||
// Only perform validations if there's an accelerator provided
|
||||
// Otherwise performing a save means that it's going to be disabled
|
||||
if (accelerator) {
|
||||
keymapService.validateAccelerator(accelerator);
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
// Discard previous errors
|
||||
onError({ recorderError: null });
|
||||
setSaveAllowed(true);
|
||||
} catch (recorderError) {
|
||||
onError({ recorderError });
|
||||
setSaveAllowed(false);
|
||||
}
|
||||
}, [accelerator]);
|
||||
const handleKeydown = (event) => {
|
||||
event.preventDefault();
|
||||
const newAccelerator = keymapService.domToElectronAccelerator(event);
|
||||
switch (newAccelerator) {
|
||||
case 'Enter':
|
||||
if (saveAllowed) { return onSave({ commandName, accelerator }); }
|
||||
break;
|
||||
case 'Escape':
|
||||
return onCancel({ commandName });
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
return setAccelerator('');
|
||||
default:
|
||||
setAccelerator(newAccelerator);
|
||||
}
|
||||
};
|
||||
return (React.createElement('div', { style: styles.recorderContainer },
|
||||
React.createElement('input', { value: accelerator, placeholder: _('Press the shortcut'), onKeyDown: handleKeydown, style: styles.recorderInput, readOnly: true, autoFocus: true }),
|
||||
React.createElement('button', { style: styles.inlineButton, disabled: !saveAllowed, onClick: () => onSave({ commandName, accelerator }) }, _('Save')),
|
||||
React.createElement('button', { style: styles.inlineButton, onClick: () => onReset({ commandName }) }, _('Restore')),
|
||||
React.createElement('button', { style: styles.inlineButton, onClick: () => onCancel({ commandName }) }, _('Cancel'))));
|
||||
};
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2hvcnRjdXRSZWNvcmRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIlNob3J0Y3V0UmVjb3JkZXIudHN4Il0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsK0JBQStCO0FBQy9CLGlDQUEyRDtBQUUzRCxvRUFBNkQ7QUFDN0QscUNBQStCO0FBRS9CLE1BQU0sRUFBRSxDQUFDLEVBQUUsR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDcEMsTUFBTSxhQUFhLEdBQUcsdUJBQWEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztBQVlsQyxRQUFBLGdCQUFnQixHQUFHLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsV0FBVyxFQUFFLE9BQU8sRUFBeUIsRUFBRSxFQUFFO0lBQzNJLE1BQU0sTUFBTSxHQUFHLGdCQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFFaEMsTUFBTSxDQUFDLFdBQVcsRUFBRSxjQUFjLENBQUMsR0FBRyxnQkFBUSxDQUFDLGtCQUFrQixDQUFDLENBQUM7SUFDbkUsTUFBTSxDQUFDLFdBQVcsRUFBRSxjQUFjLENBQUMsR0FBRyxnQkFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBRXJELGlCQUFTLENBQUMsR0FBRyxFQUFFO1FBQ2QsSUFBSTtZQUNILDhEQUE4RDtZQUM5RCxtRUFBbUU7WUFDbkUsSUFBSSxXQUFXLEVBQUU7Z0JBQ2hCLGFBQWEsQ0FBQyxtQkFBbUIsQ0FBQyxXQUFXLENBQUMsQ0FBQztnQkFDL0MsYUFBYSxDQUFDLGNBQWMsQ0FBQyxFQUFFLFdBQVcsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQzthQUNwRTtZQUVELDBCQUEwQjtZQUMxQixPQUFPLENBQUMsRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNqQyxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDckI7UUFBQyxPQUFPLGFBQWEsRUFBRTtZQUN2QixPQUFPLENBQUMsRUFBRSxhQUFhLEVBQUUsQ0FBQyxDQUFDO1lBQzNCLGNBQWMsQ0FBQyxLQUFLLENBQUMsQ0FBQztTQUN0QjtJQUNGLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUM7SUFFbEIsTUFBTSxhQUFhLEdBQUcsQ0FBQyxLQUFvQyxFQUFFLEVBQUU7UUFDOUQsS0FBSyxDQUFDLGNBQWMsRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sY0FBYyxHQUFHLGFBQWEsQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUVyRSxRQUFRLGNBQWMsRUFBRTtZQUN4QixLQUFLLE9BQU87Z0JBQ1gsSUFBSSxXQUFXO29CQUFFLE9BQU8sTUFBTSxDQUFDLEVBQUUsV0FBVyxFQUFFLFdBQVcsRUFBRSxDQUFDLENBQUM7Z0JBQzdELE1BQU07WUFDUCxLQUFLLFFBQVE7Z0JBQ1osT0FBTyxRQUFRLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxDQUFDO1lBQ2xDLEtBQUssV0FBVyxDQUFDO1lBQ2pCLEtBQUssUUFBUTtnQkFDWixPQUFPLGNBQWMsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUMzQjtnQkFDQyxjQUFjLENBQUMsY0FBYyxDQUFDLENBQUM7U0FDL0I7SUFDRixDQUFDLENBQUM7SUFFRixPQUFPLENBQ04sNkJBQUssS0FBSyxFQUFFLE1BQU0sQ0FBQyxpQkFBaUI7UUFDbkMsK0JBQ0MsS0FBSyxFQUFFLFdBQVcsRUFDbEIsV0FBVyxFQUFFLENBQUMsQ0FBQyxvQkFBb0IsQ0FBQyxFQUNwQyxTQUFTLEVBQUUsYUFBYSxFQUN4QixLQUFLLEVBQUUsTUFBTSxDQUFDLGFBQWEsRUFDM0IsUUFBUSxRQUNSLFNBQVMsU0FDUjtRQUVGLGdDQUFRLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWSxFQUFFLFFBQVEsRUFBRSxDQUFDLFdBQVcsRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUFDLEVBQUUsV0FBVyxFQUFFLFdBQVcsRUFBRSxDQUFDLElBQzdHLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FDRjtRQUNULGdDQUFRLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWSxFQUFFLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxJQUN6RSxDQUFDLENBQUMsU0FBUyxDQUFDLENBQ0w7UUFDVCxnQ0FBUSxLQUFLLEVBQUUsTUFBTSxDQUFDLFlBQVksRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsV0FBVyxFQUFFLENBQUMsSUFDMUUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUNKLENBQ0osQ0FDTixDQUFDO0FBQ0gsQ0FBQyxDQUFDIn0=
|
85
ElectronClient/gui/KeymapConfig/ShortcutRecorder.tsx
Normal file
85
ElectronClient/gui/KeymapConfig/ShortcutRecorder.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, KeyboardEvent } from 'react';
|
||||
|
||||
import KeymapService from '../../lib/services/KeymapService';
|
||||
import styles_ from './styles';
|
||||
|
||||
const { _ } = require('lib/locale');
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
export interface ShortcutRecorderProps {
|
||||
onSave: (event: { commandName: string, accelerator: string }) => void,
|
||||
onReset: (event: { commandName: string }) => void,
|
||||
onCancel: (event: { commandName: string }) => void,
|
||||
onError: (event: { recorderError: Error }) => void,
|
||||
initialAccelerator: string
|
||||
commandName: string,
|
||||
themeId: number
|
||||
}
|
||||
|
||||
export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId }: ShortcutRecorderProps) => {
|
||||
const styles = styles_(themeId);
|
||||
|
||||
const [accelerator, setAccelerator] = useState(initialAccelerator);
|
||||
const [saveAllowed, setSaveAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Only perform validations if there's an accelerator provided
|
||||
// Otherwise performing a save means that it's going to be disabled
|
||||
if (accelerator) {
|
||||
keymapService.validateAccelerator(accelerator);
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
|
||||
// Discard previous errors
|
||||
onError({ recorderError: null });
|
||||
setSaveAllowed(true);
|
||||
} catch (recorderError) {
|
||||
onError({ recorderError });
|
||||
setSaveAllowed(false);
|
||||
}
|
||||
}, [accelerator]);
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const newAccelerator = keymapService.domToElectronAccelerator(event);
|
||||
|
||||
switch (newAccelerator) {
|
||||
case 'Enter':
|
||||
if (saveAllowed) return onSave({ commandName, accelerator });
|
||||
break;
|
||||
case 'Escape':
|
||||
return onCancel({ commandName });
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
return setAccelerator('');
|
||||
default:
|
||||
setAccelerator(newAccelerator);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.recorderContainer}>
|
||||
<input
|
||||
value={accelerator}
|
||||
placeholder={_('Press the shortcut')}
|
||||
onKeyDown={handleKeydown}
|
||||
style={styles.recorderInput}
|
||||
title={_('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.')}
|
||||
readOnly
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<button style={styles.inlineButton} disabled={!saveAllowed} onClick={() => onSave({ commandName, accelerator })}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
<button style={styles.inlineButton} onClick={() => onReset({ commandName })}>
|
||||
{_('Restore')}
|
||||
</button>
|
||||
<button style={styles.inlineButton} onClick={() => onCancel({ commandName })}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,53 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const { buildStyle } = require('lib/theme');
|
||||
function styles(themeId) {
|
||||
return buildStyle('KeymapConfigScreen', themeId, (theme) => {
|
||||
return {
|
||||
container: Object.assign(Object.assign({}, theme.containerStyle), { padding: 16 }),
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
recorderContainer: {
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
},
|
||||
filterInput: Object.assign(Object.assign({}, theme.inputStyle), { flexGrow: 1, minHeight: 29, alignSelf: 'center' }),
|
||||
recorderInput: Object.assign(Object.assign({}, theme.inputStyle), { minHeight: 29 }),
|
||||
label: Object.assign(Object.assign({}, theme.textStyle), { alignSelf: 'center', marginRight: 10 }),
|
||||
table: Object.assign(Object.assign({}, theme.containerStyle), { marginTop: 16, overflow: 'auto', width: '100%' }),
|
||||
tableShortcutColumn: Object.assign(Object.assign({}, theme.textStyle), { width: '60%' }),
|
||||
tableCommandColumn: Object.assign(Object.assign({}, theme.textStyle), { width: 'auto' }),
|
||||
tableCell: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableCellContent: {
|
||||
flexGrow: 1,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tableCellStatus: {
|
||||
height: '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
kbd: {
|
||||
fontFamily: 'sans-serif',
|
||||
border: '1px solid',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
padding: 2,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
},
|
||||
disabled: {
|
||||
color: theme.colorFaded,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
inlineButton: Object.assign(Object.assign({}, theme.buttonStyle), { marginLeft: 12 }),
|
||||
warning: Object.assign(Object.assign({}, theme.textStyle), { backgroundColor: theme.warningBackgroundColor, paddingLeft: 16, paddingRight: 16, paddingTop: 2, paddingBottom: 2 }),
|
||||
};
|
||||
});
|
||||
}
|
||||
exports.default = styles;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLE1BQU0sRUFBRSxVQUFVLEVBQUUsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUM7QUFFNUMsU0FBd0IsTUFBTSxDQUFDLE9BQWU7SUFDN0MsT0FBTyxVQUFVLENBQUMsb0JBQW9CLEVBQUUsT0FBTyxFQUFFLENBQUMsS0FBVSxFQUFFLEVBQUU7UUFDL0QsT0FBTztZQUNOLFNBQVMsa0NBQ0wsS0FBSyxDQUFDLGNBQWMsS0FDdkIsT0FBTyxFQUFFLEVBQUUsR0FDWDtZQUNELGdCQUFnQixFQUFFO2dCQUNqQixPQUFPLEVBQUUsTUFBTTtnQkFDZixhQUFhLEVBQUUsS0FBSzthQUNwQjtZQUNELGlCQUFpQixFQUFFO2dCQUNsQixPQUFPLEVBQUUsQ0FBQztnQkFDVixRQUFRLEVBQUUsQ0FBQzthQUNYO1lBQ0QsV0FBVyxrQ0FDUCxLQUFLLENBQUMsVUFBVSxLQUNuQixRQUFRLEVBQUUsQ0FBQyxFQUNYLFNBQVMsRUFBRSxFQUFFLEVBQ2IsU0FBUyxFQUFFLFFBQVEsR0FDbkI7WUFDRCxhQUFhLGtDQUNULEtBQUssQ0FBQyxVQUFVLEtBQ25CLFNBQVMsRUFBRSxFQUFFLEdBQ2I7WUFDRCxLQUFLLGtDQUNELEtBQUssQ0FBQyxTQUFTLEtBQ2xCLFNBQVMsRUFBRSxRQUFRLEVBQ25CLFdBQVcsRUFBRSxFQUFFLEdBQ2Y7WUFDRCxLQUFLLGtDQUNELEtBQUssQ0FBQyxjQUFjLEtBQ3ZCLFNBQVMsRUFBRSxFQUFFLEVBQ2IsUUFBUSxFQUFFLE1BQU0sRUFDaEIsS0FBSyxFQUFFLE1BQU0sR0FDYjtZQUNELG1CQUFtQixrQ0FDZixLQUFLLENBQUMsU0FBUyxLQUNsQixLQUFLLEVBQUUsS0FBSyxHQUNaO1lBQ0Qsa0JBQWtCLGtDQUNkLEtBQUssQ0FBQyxTQUFTLEtBQ2xCLEtBQUssRUFBRSxNQUFNLEdBQ2I7WUFDRCxTQUFTLEVBQUU7Z0JBQ1YsT0FBTyxFQUFFLE1BQU07Z0JBQ2YsYUFBYSxFQUFFLEtBQUs7YUFDcEI7WUFDRCxnQkFBZ0IsRUFBRTtnQkFDakIsUUFBUSxFQUFFLENBQUM7Z0JBQ1gsU0FBUyxFQUFFLFFBQVE7YUFDbkI7WUFDRCxlQUFlLEVBQUU7Z0JBQ2hCLE1BQU0sRUFBRSxNQUFNO2dCQUNkLFNBQVMsRUFBRSxRQUFRO2FBQ25CO1lBQ0QsR0FBRyxFQUFFO2dCQUNKLFVBQVUsRUFBRSxZQUFZO2dCQUN4QixNQUFNLEVBQUUsV0FBVztnQkFDbkIsWUFBWSxFQUFFLENBQUM7Z0JBQ2YsZUFBZSxFQUFFLEtBQUssQ0FBQyxxQkFBcUI7Z0JBQzVDLE9BQU8sRUFBRSxDQUFDO2dCQUNWLFdBQVcsRUFBRSxDQUFDO2dCQUNkLFlBQVksRUFBRSxDQUFDO2FBQ2Y7WUFDRCxRQUFRLEVBQUU7Z0JBQ1QsS0FBSyxFQUFFLEtBQUssQ0FBQyxVQUFVO2dCQUN2QixTQUFTLEVBQUUsUUFBUTthQUNuQjtZQUNELFlBQVksa0NBQ1IsS0FBSyxDQUFDLFdBQVcsS0FDcEIsVUFBVSxFQUFFLEVBQUUsR0FDZDtZQUNELE9BQU8sa0NBQ0gsS0FBSyxDQUFDLFNBQVMsS0FDbEIsZUFBZSxFQUFFLEtBQUssQ0FBQyxzQkFBc0IsRUFDN0MsV0FBVyxFQUFFLEVBQUUsRUFDZixZQUFZLEVBQUUsRUFBRSxFQUNoQixVQUFVLEVBQUUsQ0FBQyxFQUNiLGFBQWEsRUFBRSxDQUFDLEdBQ2hCO1NBQ0QsQ0FBQztJQUNILENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQW5GRCx5QkFtRkMifQ==
|
86
ElectronClient/gui/KeymapConfig/styles/index.ts
Normal file
86
ElectronClient/gui/KeymapConfig/styles/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
export default function styles(themeId: number) {
|
||||
return buildStyle('KeymapConfigScreen', themeId, (theme: any) => {
|
||||
return {
|
||||
container: {
|
||||
...theme.containerStyle,
|
||||
padding: 16,
|
||||
},
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
recorderContainer: {
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
},
|
||||
filterInput: {
|
||||
...theme.inputStyle,
|
||||
flexGrow: 1,
|
||||
minHeight: 29,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
recorderInput: {
|
||||
...theme.inputStyle,
|
||||
minHeight: 29,
|
||||
},
|
||||
label: {
|
||||
...theme.textStyle,
|
||||
alignSelf: 'center',
|
||||
marginRight: 10,
|
||||
},
|
||||
table: {
|
||||
...theme.containerStyle,
|
||||
marginTop: 16,
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
},
|
||||
tableShortcutColumn: {
|
||||
...theme.textStyle,
|
||||
width: '60%',
|
||||
},
|
||||
tableCommandColumn: {
|
||||
...theme.textStyle,
|
||||
width: 'auto',
|
||||
},
|
||||
tableCell: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableCellContent: {
|
||||
flexGrow: 1,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tableCellStatus: {
|
||||
height: '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
kbd: {
|
||||
fontFamily: 'sans-serif',
|
||||
border: '1px solid',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
padding: 2,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
},
|
||||
disabled: {
|
||||
color: theme.colorFaded,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
inlineButton: {
|
||||
...theme.buttonStyle,
|
||||
marginLeft: 12,
|
||||
},
|
||||
warning: {
|
||||
...theme.textStyle,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const CommandService_1 = require('../../../lib/services/CommandService');
|
||||
const { _ } = require('lib/locale');
|
||||
const { shim } = require('lib/shim');
|
||||
const commandService = CommandService_1.default.instance();
|
||||
const getLabel = (commandName) => {
|
||||
if (commandService.exists(commandName)) { return commandService.label(commandName); }
|
||||
// Some commands are not registered in CommandService at the moment
|
||||
// Following hard-coded labels are used as a workaround
|
||||
switch (commandName) {
|
||||
case 'quit':
|
||||
return _('Quit');
|
||||
case 'insertTemplate':
|
||||
return _('Insert template');
|
||||
case 'zoomActualSize':
|
||||
return _('Actual Size');
|
||||
case 'gotoAnything':
|
||||
return _('Goto Anything...');
|
||||
case 'help':
|
||||
return _('Website and documentation');
|
||||
case 'hideApp':
|
||||
return _('Hide Joplin');
|
||||
case 'closeWindow':
|
||||
return _('Close Window');
|
||||
case 'config':
|
||||
return shim.isMac() ? _('Preferences') : _('Options');
|
||||
default:
|
||||
throw new Error(`Command: ${commandName} is unknown`);
|
||||
}
|
||||
};
|
||||
exports.default = getLabel;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0TGFiZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnZXRMYWJlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlFQUFrRTtBQUVsRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQyxDQUFDO0FBQ3BDLE1BQU0sRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUM7QUFFckMsTUFBTSxjQUFjLEdBQUcsd0JBQWMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztBQUVqRCxNQUFNLFFBQVEsR0FBRyxDQUFDLFdBQW1CLEVBQUUsRUFBRTtJQUN4QyxJQUFJLGNBQWMsQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDO1FBQUUsT0FBTyxjQUFjLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRWpGLG1FQUFtRTtJQUNuRSx1REFBdUQ7SUFFdkQsUUFBUSxXQUFXLEVBQUU7UUFDckIsS0FBSyxNQUFNO1lBQ1YsT0FBTyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDbEIsS0FBSyxnQkFBZ0I7WUFDcEIsT0FBTyxDQUFDLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUM3QixLQUFLLGdCQUFnQjtZQUNwQixPQUFPLENBQUMsQ0FBQyxhQUFhLENBQUMsQ0FBQztRQUN6QixLQUFLLGNBQWM7WUFDbEIsT0FBTyxDQUFDLENBQUMsa0JBQWtCLENBQUMsQ0FBQztRQUM5QixLQUFLLE1BQU07WUFDVixPQUFPLENBQUMsQ0FBQywyQkFBMkIsQ0FBQyxDQUFDO1FBQ3ZDLEtBQUssU0FBUztZQUNiLE9BQU8sQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3pCLEtBQUssYUFBYTtZQUNqQixPQUFPLENBQUMsQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUMxQixLQUFLLFFBQVE7WUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDdkQ7WUFDQyxNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksV0FBVyxhQUFhLENBQUMsQ0FBQztLQUN0RDtBQUNGLENBQUMsQ0FBQztBQUVGLGtCQUFlLFFBQVEsQ0FBQyJ9
|
36
ElectronClient/gui/KeymapConfig/utils/getLabel.ts
Normal file
36
ElectronClient/gui/KeymapConfig/utils/getLabel.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import CommandService from '../../../lib/services/CommandService';
|
||||
|
||||
const { _ } = require('lib/locale');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
const commandService = CommandService.instance();
|
||||
|
||||
const getLabel = (commandName: string) => {
|
||||
if (commandService.exists(commandName)) return commandService.label(commandName);
|
||||
|
||||
// Some commands are not registered in CommandService at the moment
|
||||
// Following hard-coded labels are used as a workaround
|
||||
|
||||
switch (commandName) {
|
||||
case 'quit':
|
||||
return _('Quit');
|
||||
case 'insertTemplate':
|
||||
return _('Insert template');
|
||||
case 'zoomActualSize':
|
||||
return _('Actual Size');
|
||||
case 'gotoAnything':
|
||||
return _('Goto Anything...');
|
||||
case 'help':
|
||||
return _('Website and documentation');
|
||||
case 'hideApp':
|
||||
return _('Hide Joplin');
|
||||
case 'closeWindow':
|
||||
return _('Close Window');
|
||||
case 'config':
|
||||
return shim.isMac() ? _('Preferences') : _('Options');
|
||||
default:
|
||||
throw new Error(`Command: ${commandName} is unknown`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getLabel;
|
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const react_1 = require('react');
|
||||
const KeymapService_1 = require('../../../lib/services/KeymapService');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
const useCommandStatus = () => {
|
||||
const [status, setStatus] = react_1.useState(() => keymapService.getCommandNames().reduce((accumulator, command) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {}));
|
||||
const disableStatus = (commandName) => setStatus(prevStatus => (Object.assign(Object.assign({}, prevStatus), { [commandName]: false })));
|
||||
const enableStatus = (commandName) => setStatus(prevStatus => {
|
||||
// Falsify all the commands; Only one command should be truthy at any given time
|
||||
const newStatus = Object.keys(prevStatus).reduce((accumulator, command) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {});
|
||||
// Make the appropriate command truthful
|
||||
newStatus[commandName] = true;
|
||||
return newStatus;
|
||||
});
|
||||
return [status, enableStatus, disableStatus];
|
||||
};
|
||||
exports.default = useCommandStatus;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNlQ29tbWFuZFN0YXR1cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInVzZUNvbW1hbmRTdGF0dXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxpQ0FBaUM7QUFDakMsdUVBQWdFO0FBRWhFLE1BQU0sYUFBYSxHQUFHLHVCQUFhLENBQUMsUUFBUSxFQUFFLENBQUM7QUFNL0MsTUFBTSxnQkFBZ0IsR0FBRyxHQUFrRixFQUFFO0lBQzVHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsU0FBUyxDQUFDLEdBQUcsZ0JBQVEsQ0FBZ0IsR0FBRyxFQUFFLENBQ3hELGFBQWEsQ0FBQyxlQUFlLEVBQUUsQ0FBQyxNQUFNLENBQUMsQ0FBQyxXQUEwQixFQUFFLE9BQWUsRUFBRSxFQUFFO1FBQ3RGLFdBQVcsQ0FBQyxPQUFPLENBQUMsR0FBRyxLQUFLLENBQUM7UUFDN0IsT0FBTyxXQUFXLENBQUM7SUFDcEIsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUNOLENBQUM7SUFFRixNQUFNLGFBQWEsR0FBRyxDQUFDLFdBQW1CLEVBQUUsRUFBRSxDQUFDLFNBQVMsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDLGlDQUFNLFVBQVUsS0FBRSxDQUFDLFdBQVcsQ0FBQyxFQUFFLEtBQUssSUFBRyxDQUFDLENBQUM7SUFDbEgsTUFBTSxZQUFZLEdBQUcsQ0FBQyxXQUFtQixFQUFFLEVBQUUsQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLEVBQUU7UUFDcEUsZ0ZBQWdGO1FBQ2hGLE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsV0FBMEIsRUFBRSxPQUFlLEVBQUUsRUFBRTtZQUNoRyxXQUFXLENBQUMsT0FBTyxDQUFDLEdBQUcsS0FBSyxDQUFDO1lBQzdCLE9BQU8sV0FBVyxDQUFDO1FBQ3BCLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVQLHdDQUF3QztRQUN4QyxTQUFTLENBQUMsV0FBVyxDQUFDLEdBQUcsSUFBSSxDQUFDO1FBQzlCLE9BQU8sU0FBUyxDQUFDO0lBQ2xCLENBQUMsQ0FBQyxDQUFDO0lBRUgsT0FBTyxDQUFDLE1BQU0sRUFBRSxZQUFZLEVBQUUsYUFBYSxDQUFDLENBQUM7QUFDOUMsQ0FBQyxDQUFDO0FBRUYsa0JBQWUsZ0JBQWdCLENBQUMifQ==
|
34
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.ts
Normal file
34
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useState } from 'react';
|
||||
import KeymapService from '../../../lib/services/KeymapService';
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
interface CommandStatus {
|
||||
[commandName: string]: boolean
|
||||
}
|
||||
|
||||
const useCommandStatus = (): [CommandStatus, (commandName: string) => void, (commandName: string) => void] => {
|
||||
const [status, setStatus] = useState<CommandStatus>(() =>
|
||||
keymapService.getCommandNames().reduce((accumulator: CommandStatus, command: string) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const disableStatus = (commandName: string) => setStatus(prevStatus => ({ ...prevStatus, [commandName]: false }));
|
||||
const enableStatus = (commandName: string) => setStatus(prevStatus => {
|
||||
// Falsify all the commands; Only one command should be truthy at any given time
|
||||
const newStatus = Object.keys(prevStatus).reduce((accumulator: CommandStatus, command: string) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
// Make the appropriate command truthful
|
||||
newStatus[commandName] = true;
|
||||
return newStatus;
|
||||
});
|
||||
|
||||
return [status, enableStatus, disableStatus];
|
||||
};
|
||||
|
||||
export default useCommandStatus;
|
@@ -1,68 +0,0 @@
|
||||
'use strict';
|
||||
const __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 react_1 = require('react');
|
||||
const KeymapService_1 = require('../../../lib/services/KeymapService');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
// This custom hook provides a synchronized snapshot of the keymap residing at KeymapService
|
||||
// All the logic regarding altering and interacting with the keymap is isolated from the components
|
||||
const useKeymap = () => {
|
||||
const [keymapItems, setKeymapItems] = react_1.useState(() => keymapService.getKeymapItems());
|
||||
const [keymapError, setKeymapError] = react_1.useState(null);
|
||||
const setAccelerator = (commandName, accelerator) => {
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
newKeymap.find(item => item.command === commandName).accelerator = accelerator || null /* Disabled */;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
const resetAccelerator = (commandName) => {
|
||||
const defaultAccelerator = keymapService.getDefaultAccelerator(commandName);
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
newKeymap.find(item => item.command === commandName).accelerator = defaultAccelerator;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
const overrideKeymapItems = (customKeymapItems) => {
|
||||
const oldKeymapItems = [...customKeymapItems];
|
||||
keymapService.initialize(); // Start with a fresh keymap
|
||||
try {
|
||||
// First, try to update the in-memory keymap of KeymapService
|
||||
// This function will throw if there are any issues with the new custom keymap
|
||||
keymapService.overrideKeymap(customKeymapItems);
|
||||
// Then, update the state with the data from KeymapService
|
||||
// Side-effect: Changes will also be saved to the disk
|
||||
setKeymapItems(keymapService.getKeymapItems());
|
||||
} catch (err) {
|
||||
// oldKeymapItems includes even the unchanged keymap items
|
||||
// However, it is not an issue because the logic accounts for such scenarios
|
||||
keymapService.overrideKeymap(oldKeymapItems);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const exportCustomKeymap = (customKeymapPath) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
// KeymapService is already synchronized automatically with the in-state keymap
|
||||
yield keymapService.saveCustomKeymap(customKeymapPath);
|
||||
});
|
||||
react_1.useEffect(() => {
|
||||
try {
|
||||
keymapService.overrideKeymap(keymapItems);
|
||||
keymapService.saveCustomKeymap();
|
||||
setKeymapError(null);
|
||||
} catch (err) {
|
||||
setKeymapError(err);
|
||||
}
|
||||
}, [keymapItems]);
|
||||
return [keymapItems, keymapError, overrideKeymapItems, exportCustomKeymap, setAccelerator, resetAccelerator];
|
||||
};
|
||||
exports.default = useKeymap;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNlS2V5bWFwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidXNlS2V5bWFwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBQUEsaUNBQTRDO0FBQzVDLHVFQUFnRjtBQUVoRixNQUFNLGFBQWEsR0FBRyx1QkFBYSxDQUFDLFFBQVEsRUFBRSxDQUFDO0FBRS9DLDRGQUE0RjtBQUM1RixtR0FBbUc7QUFFbkcsTUFBTSxTQUFTLEdBQUcsR0FPaEIsRUFBRTtJQUNILE1BQU0sQ0FBQyxXQUFXLEVBQUUsY0FBYyxDQUFDLEdBQUcsZ0JBQVEsQ0FBZSxHQUFHLEVBQUUsQ0FBQyxhQUFhLENBQUMsY0FBYyxFQUFFLENBQUMsQ0FBQztJQUNuRyxNQUFNLENBQUMsV0FBVyxFQUFFLGNBQWMsQ0FBQyxHQUFHLGdCQUFRLENBQVEsSUFBSSxDQUFDLENBQUM7SUFFNUQsTUFBTSxjQUFjLEdBQUcsQ0FBQyxXQUFtQixFQUFFLFdBQW1CLEVBQUUsRUFBRTtRQUNuRSxjQUFjLENBQUMsVUFBVSxDQUFDLEVBQUU7WUFDM0IsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFHLFVBQVUsQ0FBQyxDQUFDO1lBRWxDLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsT0FBTyxLQUFLLFdBQVcsQ0FBQyxDQUFDLFdBQVcsR0FBRyxXQUFXLElBQUksSUFBSSxDQUFDLGNBQWMsQ0FBQztZQUN0RyxPQUFPLFNBQVMsQ0FBQztRQUNsQixDQUFDLENBQUMsQ0FBQztJQUNKLENBQUMsQ0FBQztJQUVGLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxXQUFtQixFQUFFLEVBQUU7UUFDaEQsTUFBTSxrQkFBa0IsR0FBRyxhQUFhLENBQUMscUJBQXFCLENBQUMsV0FBVyxDQUFDLENBQUM7UUFDNUUsY0FBYyxDQUFDLFVBQVUsQ0FBQyxFQUFFO1lBQzNCLE1BQU0sU0FBUyxHQUFHLENBQUMsR0FBRyxVQUFVLENBQUMsQ0FBQztZQUVsQyxTQUFTLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sS0FBSyxXQUFXLENBQUMsQ0FBQyxXQUFXLEdBQUcsa0JBQWtCLENBQUM7WUFDdEYsT0FBTyxTQUFTLENBQUM7UUFDbEIsQ0FBQyxDQUFDLENBQUM7SUFDSixDQUFDLENBQUM7SUFFRixNQUFNLG1CQUFtQixHQUFHLENBQUMsaUJBQStCLEVBQUUsRUFBRTtRQUMvRCxNQUFNLGNBQWMsR0FBRyxDQUFDLEdBQUcsaUJBQWlCLENBQUMsQ0FBQztRQUM5QyxhQUFhLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQyw0QkFBNEI7UUFFeEQsSUFBSTtZQUNILDZEQUE2RDtZQUM3RCw4RUFBOEU7WUFDOUUsYUFBYSxDQUFDLGNBQWMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1lBQ2hELDBEQUEwRDtZQUMxRCxzREFBc0Q7WUFDdEQsY0FBYyxDQUFDLGFBQWEsQ0FBQyxjQUFjLEVBQUUsQ0FBQyxDQUFDO1NBQy9DO1FBQUMsT0FBTyxHQUFHLEVBQUU7WUFDYiwwREFBMEQ7WUFDMUQsNEVBQTRFO1lBQzVFLGFBQWEsQ0FBQyxjQUFjLENBQUMsY0FBYyxDQUFDLENBQUM7WUFDN0MsTUFBTSxHQUFHLENBQUM7U0FDVjtJQUNGLENBQUMsQ0FBQztJQUVGLE1BQU0sa0JBQWtCLEdBQUcsQ0FBTyxnQkFBd0IsRUFBRSxFQUFFO1FBQzdELCtFQUErRTtRQUMvRSxNQUFNLGFBQWEsQ0FBQyxnQkFBZ0IsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBQ3hELENBQUMsQ0FBQSxDQUFDO0lBRUYsaUJBQVMsQ0FBQyxHQUFHLEVBQUU7UUFDZCxJQUFJO1lBQ0gsYUFBYSxDQUFDLGNBQWMsQ0FBQyxXQUFXLENBQUMsQ0FBQztZQUMxQyxhQUFhLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztZQUNqQyxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDckI7UUFBQyxPQUFPLEdBQUcsRUFBRTtZQUNiLGNBQWMsQ0FBQyxHQUFHLENBQUMsQ0FBQztTQUNwQjtJQUNGLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUM7SUFFbEIsT0FBTyxDQUFDLFdBQVcsRUFBRSxXQUFXLEVBQUUsbUJBQW1CLEVBQUUsa0JBQWtCLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixDQUFDLENBQUM7QUFDOUcsQ0FBQyxDQUFDO0FBRUYsa0JBQWUsU0FBUyxDQUFDIn0=
|
70
ElectronClient/gui/KeymapConfig/utils/useKeymap.ts
Normal file
70
ElectronClient/gui/KeymapConfig/utils/useKeymap.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import KeymapService, { KeymapItem } from '../../../lib/services/KeymapService';
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
// This custom hook provides a synchronized snapshot of the keymap residing at KeymapService
|
||||
// All the logic regarding altering and interacting with the keymap is isolated from the components
|
||||
|
||||
const useKeymap = (): [
|
||||
KeymapItem[],
|
||||
Error,
|
||||
(keymapItems: KeymapItem[]) => void,
|
||||
(commandName: string, accelerator: string) => void,
|
||||
(commandName: string) => void
|
||||
] => {
|
||||
const [keymapItems, setKeymapItems] = useState<KeymapItem[]>(() => keymapService.getKeymapItems());
|
||||
const [keymapError, setKeymapError] = useState<Error>(null);
|
||||
|
||||
const setAccelerator = (commandName: string, accelerator: string) => {
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
|
||||
newKeymap.find(item => item.command === commandName).accelerator = accelerator || null /* Disabled */;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
|
||||
const resetAccelerator = (commandName: string) => {
|
||||
const defaultAccelerator = keymapService.getDefaultAccelerator(commandName);
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
|
||||
newKeymap.find(item => item.command === commandName).accelerator = defaultAccelerator;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
|
||||
const overrideKeymapItems = (customKeymapItems: KeymapItem[]) => {
|
||||
const oldKeymapItems = [...customKeymapItems];
|
||||
keymapService.initialize(); // Start with a fresh keymap
|
||||
|
||||
try {
|
||||
// First, try to update the in-memory keymap of KeymapService
|
||||
// This function will throw if there are any issues with the new custom keymap
|
||||
keymapService.overrideKeymap(customKeymapItems);
|
||||
// Then, update the state with the data from KeymapService
|
||||
// Side-effect: Changes will also be saved to the disk
|
||||
setKeymapItems(keymapService.getKeymapItems());
|
||||
} catch (err) {
|
||||
// oldKeymapItems includes even the unchanged keymap items
|
||||
// However, it is not an issue because the logic accounts for such scenarios
|
||||
keymapService.overrideKeymap(oldKeymapItems);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
keymapService.overrideKeymap(keymapItems);
|
||||
keymapService.saveCustomKeymap();
|
||||
setKeymapError(null);
|
||||
} catch (err) {
|
||||
setKeymapError(err);
|
||||
}
|
||||
}, [keymapItems]);
|
||||
|
||||
return [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator];
|
||||
};
|
||||
|
||||
export default useKeymap;
|
@@ -434,7 +434,7 @@ class MainScreenComponent extends React.Component {
|
||||
// A bit of a hack, but for now don't allow changing code view
|
||||
// while a note is being saved as it will cause a problem with
|
||||
// TinyMCE because it won't have time to send its content before
|
||||
// being switch to Ace Editor.
|
||||
// being switch to the Code Editor.
|
||||
if (this.props.hasNotesBeingSaved) return;
|
||||
Setting.toggle('editor.codeView');
|
||||
},
|
||||
@@ -445,8 +445,8 @@ class MainScreenComponent extends React.Component {
|
||||
headerItems.push({
|
||||
title: _('Search...'),
|
||||
iconName: 'fa-search',
|
||||
onQuery: query => {
|
||||
CommandService.instance().execute('search', { query });
|
||||
onQuery: (query, fuzzy = false) => {
|
||||
CommandService.instance().execute('search', { query, fuzzy });
|
||||
},
|
||||
type: 'search',
|
||||
});
|
||||
@@ -468,8 +468,7 @@ class MainScreenComponent extends React.Component {
|
||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
|
||||
const codeEditor = Setting.value('editor.betaCodeMirror') ? 'CodeMirror' : 'AceEditor';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? codeEditor : 'TinyMCE';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
|
@@ -10,7 +10,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ query }:any) => {
|
||||
execute: async ({ query, fuzzy }:any) => {
|
||||
console.info('RUNTIME', query);
|
||||
|
||||
if (!comp.searchId_) comp.searchId_ = uuid.create();
|
||||
@@ -23,6 +23,7 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
query_pattern: query,
|
||||
query_folder_id: null,
|
||||
type_: BaseModel.TYPE_SEARCH,
|
||||
fuzzy: fuzzy,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,654 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRange, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText } from './utils';
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
|
||||
const AceEditorReact = require('react-ace').default;
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const NoteTextViewer = require('../../../NoteTextViewer.min');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const { _ } = require('lib/locale');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const dialogs = require('../../../dialogs');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
require('brace/theme/chrome');
|
||||
require('brace/theme/solarized_light');
|
||||
require('brace/theme/solarized_dark');
|
||||
require('brace/theme/twilight');
|
||||
require('brace/theme/dracula');
|
||||
require('brace/theme/chaos');
|
||||
require('brace/theme/tomorrow');
|
||||
require('brace/keybinding/vim');
|
||||
require('brace/keybinding/emacs');
|
||||
require('brace/theme/terminal');
|
||||
|
||||
// TODO: Could not get below code to work
|
||||
|
||||
// @ts-ignore Ace global variable
|
||||
// const aceGlobal = (ace as any);
|
||||
|
||||
// class CustomHighlightRules extends aceGlobal.acequire(
|
||||
// 'ace/mode/markdown_highlight_rules'
|
||||
// ).MarkdownHighlightRules {
|
||||
// constructor() {
|
||||
// super();
|
||||
// if (Setting.value('markdown.plugin.mark')) {
|
||||
// this.$rules.start.push({
|
||||
// // This is actually a highlight `mark`, but Ace has no token name for
|
||||
// // this so we made up our own. Reference for common tokens here:
|
||||
// // https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#common-tokens
|
||||
// token: 'highlight_mark',
|
||||
// regex: '==[^ ](?:.*?[^ ])?==',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /* eslint-disable-next-line no-undef */
|
||||
// class CustomMdMode extends aceGlobal.acequire('ace/mode/markdown').Mode {
|
||||
// constructor() {
|
||||
// super();
|
||||
// this.HighlightRules = CustomHighlightRules;
|
||||
// }
|
||||
// }
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
function AceEditor(props: NoteBodyEditorProps, ref: any) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
const previousContentKey = usePrevious(props.contentKey);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
editorRef.current = editor;
|
||||
const rootRef = useRef(null);
|
||||
const webviewRef = useRef(null);
|
||||
const props_onChangeRef = useRef<Function>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
const contentKeyHasChangedRef = useRef(false);
|
||||
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
|
||||
|
||||
const rootWidth = useRootWidth({ rootRef });
|
||||
|
||||
const { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll } = useScrollHandler(editor, webviewRef, props.onScroll);
|
||||
|
||||
useListIdent({ editor });
|
||||
|
||||
const aceEditor_change = useCallback((newBody: string) => {
|
||||
// Throw an error early to know what part of the code set the body to the
|
||||
// wrong value. Otherwise it will trigger an error somewhere deep in React-Ace
|
||||
// which will be hard to debug.
|
||||
if (typeof newBody !== 'string') throw new Error('Body is not a string');
|
||||
props_onChangeRef.current({ changeId: null, content: newBody });
|
||||
}, []);
|
||||
|
||||
const wrapSelectionWithStrings = useCallback((string1: string, string2 = '', defaultText = '', replacementText: string = null, byLine = false) => {
|
||||
if (!editor) return;
|
||||
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
|
||||
let newBody = props.content;
|
||||
|
||||
if (selection && selection.start !== selection.end) {
|
||||
const selectedLines = replacementText !== null ? replacementText : props.content.substr(selection.start, selection.end - selection.start);
|
||||
const selectedStrings = byLine ? selectedLines.split(/\r?\n/) : [selectedLines];
|
||||
|
||||
newBody = props.content.substr(0, selection.start);
|
||||
|
||||
let startCursorPos, endCursorPos;
|
||||
|
||||
for (let i = 0; i < selectedStrings.length; i++) {
|
||||
if (byLine == false) {
|
||||
const start = selectedStrings[i].search(/[^\s]/);
|
||||
const end = selectedStrings[i].search(/[^\s](?=[\s]*$)/);
|
||||
newBody += selectedStrings[i].substr(0, start) + string1 + selectedStrings[i].substr(start, end - start + 1) + string2 + selectedStrings[i].substr(end + 1);
|
||||
// Getting position for correcting offset in highlighted text when surrounded by white spaces
|
||||
startCursorPos = textOffsetToCursorPosition(selection.start + start, newBody);
|
||||
endCursorPos = textOffsetToCursorPosition(selection.start + end + 1, newBody);
|
||||
|
||||
} else { newBody += string1 + selectedStrings[i] + string2; }
|
||||
|
||||
}
|
||||
|
||||
newBody += props.content.substr(selection.end);
|
||||
|
||||
const r = selectionRange(editor);
|
||||
|
||||
// Because some insertion strings will have newlines, we'll need to account for them
|
||||
const str1Split = string1.split(/\r?\n/);
|
||||
|
||||
// Add the number of newlines to the row
|
||||
// and add the length of the final line to the column (for strings with no newlines this is the string length)
|
||||
|
||||
let newRange: any = {};
|
||||
if (!byLine) {
|
||||
// Correcting offset in Highlighted text when surrounded by white spaces
|
||||
newRange = {
|
||||
start: {
|
||||
row: startCursorPos.row,
|
||||
column: startCursorPos.column + string1.length,
|
||||
},
|
||||
end: {
|
||||
row: endCursorPos.row,
|
||||
column: endCursorPos.column + string1.length,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newRange = {
|
||||
start: {
|
||||
row: r.start.row + str1Split.length - 1,
|
||||
column: r.start.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
end: {
|
||||
row: r.end.row + str1Split.length - 1,
|
||||
column: r.end.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (replacementText !== null) {
|
||||
const diff = replacementText.length - (selection.end - selection.start);
|
||||
newRange.end.column += diff;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
} else {
|
||||
const middleText = replacementText !== null ? replacementText : defaultText;
|
||||
const textOffset = currentTextOffset(editor, props.content);
|
||||
const s1 = props.content.substr(0, textOffset);
|
||||
const s2 = props.content.substr(textOffset);
|
||||
newBody = s1 + string1 + middleText + string2 + s2;
|
||||
|
||||
const p = textOffsetToCursorPosition(textOffset + string1.length, newBody);
|
||||
const newRange = {
|
||||
start: { row: p.row, column: p.column },
|
||||
end: { row: p.row, column: p.column + middleText.length },
|
||||
};
|
||||
|
||||
// BUG!! If replacementText contains newline characters, the logic
|
||||
// to select the new text will not work.
|
||||
|
||||
setTimeout(() => {
|
||||
if (middleText && newRange) {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
} else {
|
||||
for (let i = 0; i < string1.length; i++) {
|
||||
editor.getSession().getSelection().moveCursorRight();
|
||||
}
|
||||
}
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
aceEditor_change(newBody);
|
||||
}, [editor, props.content, aceEditor_change]);
|
||||
|
||||
const addListItem = useCallback((string1, string2 = '', defaultText = '', byLine = false) => {
|
||||
let newLine = '\n';
|
||||
const range = selectionRange(editor);
|
||||
if (!range || (range.start.row === range.end.row && !selectionRangeCurrentLine(range, props.content))) {
|
||||
newLine = '';
|
||||
}
|
||||
wrapSelectionWithStrings(newLine + string1, string2, defaultText, null, byLine);
|
||||
}, [wrapSelectionWithStrings, props.content, editor]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
content: () => props.content,
|
||||
resetScroll: () => {
|
||||
resetScroll();
|
||||
},
|
||||
scrollTo: (options:ScrollOptions) => {
|
||||
if (options.type === ScrollOptionTypes.Hash) {
|
||||
if (!webviewRef.current) return;
|
||||
webviewRef.current.wrappedInstance.send('scrollToHash', options.value as string);
|
||||
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||
const p = options.value as number;
|
||||
setEditorPercentScroll(p);
|
||||
setViewerPercentScroll(p);
|
||||
} else {
|
||||
throw new Error(`Unsupported scroll options: ${options.type}`);
|
||||
}
|
||||
},
|
||||
supportsCommand: (/* name:string*/) => {
|
||||
// TODO: not implemented, currently only used for "search" command
|
||||
// which is not directly supported by Ace Editor.
|
||||
return false;
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
if (!editor) return false;
|
||||
|
||||
reg.logger().debug('AceEditor: execCommand', cmd);
|
||||
|
||||
let commandProcessed = true;
|
||||
|
||||
if (cmd.name === 'dropItems') {
|
||||
if (cmd.value.type === 'notes') {
|
||||
wrapSelectionWithStrings('', '', '', cmd.value.markdownTags.join('\n'));
|
||||
} else if (cmd.value.type === 'files') {
|
||||
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else if (cmd.name === 'focus') {
|
||||
editor.focus();
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
}
|
||||
|
||||
if (!commandProcessed) {
|
||||
const commands: any = {
|
||||
textBold: () => wrapSelectionWithStrings('**', '**', _('strong text')),
|
||||
textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')),
|
||||
textLink: async () => {
|
||||
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||
if (url) wrapSelectionWithStrings('[', `](${url})`);
|
||||
},
|
||||
textCode: () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const string = props.content.substr(selection.start, selection.end - selection.start);
|
||||
|
||||
// Look for newlines
|
||||
const match = string.match(/\r?\n/);
|
||||
|
||||
if (match && match.length > 0) {
|
||||
if (string.startsWith('```') && string.endsWith('```')) {
|
||||
wrapSelectionWithStrings('', '', '', string.substr(4, selection.end - selection.start - 8));
|
||||
} else {
|
||||
wrapSelectionWithStrings(`\`\`\`${match[0]}`, `${match[0]}\`\`\``);
|
||||
}
|
||||
} else {
|
||||
wrapSelectionWithStrings('`', '`', '');
|
||||
}
|
||||
},
|
||||
insertText: (value: any) => wrapSelectionWithStrings(value),
|
||||
attachFile: async () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const newBody = await commandAttachFileToBody(props.content, null, { position: selection ? selection.start : 0 });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
},
|
||||
textNumberedList: () => {
|
||||
const selection = selectionRange(editor);
|
||||
let bulletNumber = markdownUtils.olLineNumber(selectionRangeCurrentLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(selectionRangePreviousLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = 0;
|
||||
addListItem(`${bulletNumber + 1}. `, '', _('List item'), true);
|
||||
},
|
||||
textBulletedList: () => addListItem('- ', '', _('List item'), true),
|
||||
textCheckbox: () => addListItem('- [ ] ', '', _('List item'), true),
|
||||
textHeading: () => addListItem('## ','','', true),
|
||||
textHorizontalRule: () => addListItem('* * *'),
|
||||
};
|
||||
|
||||
if (commands[cmd.name]) {
|
||||
commands[cmd.name](cmd.value);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported Joplin command: ', cmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}, [editor, props.content, addListItem, wrapSelectionWithStrings, selectionRangeCurrentLine, aceEditor_change, setEditorPercentScroll, setViewerPercentScroll, resetScroll, renderedBody]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
wrapSelectionWithStrings('', '', resourceMds.join('\n'));
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const editorCutText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
if (!text) return;
|
||||
|
||||
clipboard.writeText(text);
|
||||
|
||||
const s = textOffsetSelection(selectionRange(editor), props.content);
|
||||
if (!s || s.start === s.end) return;
|
||||
|
||||
const s1 = props.content.substr(0, s.start);
|
||||
const s2 = props.content.substr(s.end);
|
||||
|
||||
aceEditor_change(s1 + s2);
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(range.start.row, range.start.column);
|
||||
range.setEnd(range.start.row, range.start.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}, [props.content, editor, aceEditor_change]);
|
||||
|
||||
function clipboardText() {
|
||||
return clipboard.readText() ? clipboard.readText() : clipboard.readHTML();
|
||||
}
|
||||
|
||||
const editorCopyText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
clipboard.writeText(text);
|
||||
}, [props.content, editor]);
|
||||
|
||||
const editorPasteText = useCallback(() => {
|
||||
wrapSelectionWithStrings(clipboardText(), '', '', '');
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const onEditorContextMenu = useCallback(() => {
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = !!selectedText(selectionRange(editor), props.content);
|
||||
const currentClipboardText = clipboardText();
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Cut'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCutText();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Copy'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCopyText();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
if (currentClipboardText) {
|
||||
editorPasteText();
|
||||
} else {
|
||||
// To handle pasting images
|
||||
onEditorPaste();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, editor]);
|
||||
|
||||
function aceEditor_load(editor: any) {
|
||||
setEditor(editor);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const cancelledKeys = [];
|
||||
const letters = ['F', 'T', 'P', 'Q', 'L', ',', 'G', 'K'];
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
const l = letters[i];
|
||||
cancelledKeys.push(`Ctrl+${l}`);
|
||||
cancelledKeys.push(`Command+${l}`);
|
||||
}
|
||||
cancelledKeys.push('Alt+E');
|
||||
cancelledKeys.push('Command+Shift+L');
|
||||
cancelledKeys.push('Ctrl+Shift+L');
|
||||
|
||||
for (let i = 0; i < cancelledKeys.length; i++) {
|
||||
const k = cancelledKeys[i];
|
||||
editor.commands.bindKey(k, () => {
|
||||
// HACK: Ace doesn't seem to provide a way to override its shortcuts, but throwing
|
||||
// an exception from this undocumented function seems to cancel it without any
|
||||
// side effect.
|
||||
// https://stackoverflow.com/questions/36075846
|
||||
throw new Error(`HACK: Overriding Ace Editor shortcut: ${k}`);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelector('#note-editor').addEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').addEventListener('contextmenu', onEditorContextMenu);
|
||||
|
||||
// Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash.
|
||||
// https://github.com/ajaxorg/ace/issues/2754
|
||||
// @ts-ignore: Keep the function signature as-is despite unusued arguments
|
||||
editor.getSession().getMode().getNextLineIndent = function(state: any, line: string) {
|
||||
const leftSpaces = lineLeftSpaces(line);
|
||||
const lineNoLeftSpaces = line.trimLeft();
|
||||
|
||||
if (lineNoLeftSpaces.indexOf('- [ ] ') === 0 || lineNoLeftSpaces.indexOf('- [x] ') === 0 || lineNoLeftSpaces.indexOf('- [X] ') === 0) return `${leftSpaces}- [ ] `;
|
||||
if (lineNoLeftSpaces.indexOf('- ') === 0) return `${leftSpaces}- `;
|
||||
if (lineNoLeftSpaces.indexOf('* ') === 0 && line.trim() !== '* * *') return `${leftSpaces}* `;
|
||||
|
||||
const bulletNumber = markdownUtils.olLineNumber(lineNoLeftSpaces);
|
||||
if (bulletNumber) return `${leftSpaces + (bulletNumber + 1)}. `;
|
||||
|
||||
return this.$getIndent(line);
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.querySelector('#note-editor').removeEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').removeEventListener('contextmenu', onEditorContextMenu);
|
||||
};
|
||||
}, [editor, onEditorPaste, onEditorContextMenu]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
}, []);
|
||||
|
||||
const webview_ipcMessage = useCallback((event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, props.content);
|
||||
aceEditor_change(newBody);
|
||||
} else if (msg === 'percentScroll') {
|
||||
setEditorPercentScroll(arg0);
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
}, [props.onMessage, props.content, aceEditor_change, setEditorPercentScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const interval = contentKeyHasChangedRef.current ? 0 : 500;
|
||||
|
||||
const timeoutId = setTimeout(async () => {
|
||||
let bodyToRender = props.content;
|
||||
|
||||
if (!bodyToRender.trim() && props.visiblePanes.indexOf('viewer') >= 0 && props.visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||
}
|
||||
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||
if (cancelled) return;
|
||||
setRenderedBody(result);
|
||||
}, interval);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [props.content, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
if (contentKeyHasChangedRef.current) {
|
||||
// editor.getSession().setMode(new CustomMdMode());
|
||||
const undoManager = editor.getSession().getUndoManager();
|
||||
undoManager.reset();
|
||||
editor.getSession().setUndoManager(undoManager);
|
||||
}
|
||||
}, [props.content, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewReady) return;
|
||||
|
||||
const options: any = {
|
||||
pluginAssets: renderedBody.pluginAssets,
|
||||
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||
};
|
||||
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
}
|
||||
}, [props.searchMarkers, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
if (!props.visiblePanes.includes('editor')) {
|
||||
// Note: Ideally we'd set the display to "none" to take the editor out
|
||||
// of the DOM but if we do that, certain things won't work, in particular
|
||||
// things related to scroll, which are based on the editor.
|
||||
|
||||
// Note that the below hack doesn't work and causes a bug in this case:
|
||||
// - Put Ace Editor in viewer-only mode
|
||||
// - Go to WYSIWYG editor
|
||||
// - Create new to-do - set title only
|
||||
// - Switch to Code View
|
||||
// - Switch layout and type something
|
||||
// => Text editor layout is broken and text is off-screen
|
||||
|
||||
output.display = 'none'; // Seems to work fine since the refactoring
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [styles.cellEditor, props.visiblePanes]);
|
||||
|
||||
const cellViewerStyle = useMemo(() => {
|
||||
const output = { ...styles.cellViewer };
|
||||
if (!props.visiblePanes.includes('viewer')) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
output.width = 1;
|
||||
output.maxWidth = 1;
|
||||
} else if (!props.visiblePanes.includes('editor')) {
|
||||
output.borderLeftStyle = 'none';
|
||||
}
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
|
||||
|
||||
function renderEditor() {
|
||||
// Need to hard-code the editor width, otherwise various bugs pops up
|
||||
let width = 0;
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
width = !props.visiblePanes.includes('viewer') ? rootWidth : Math.floor(rootWidth / 2);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<AceEditorReact
|
||||
value={props.content}
|
||||
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
|
||||
theme={styles.editor.aceEditorTheme}
|
||||
style={styles.editor}
|
||||
width={`${width}px`}
|
||||
fontSize={styles.editor.fontSize}
|
||||
showGutter={false}
|
||||
readOnly={editorReadOnly}
|
||||
name="note-editor"
|
||||
wrapEnabled={true}
|
||||
onScroll={editor_scroll}
|
||||
onChange={aceEditor_change}
|
||||
showPrintMargin={false}
|
||||
onLoad={aceEditor_load}
|
||||
// Enable/Disable the autoclosing braces
|
||||
setOptions={
|
||||
{
|
||||
behavioursEnabled: Setting.value('editor.autoMatchingBraces'),
|
||||
useSoftTabs: false,
|
||||
}
|
||||
}
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
editorProps={{ $blockScrolling: Infinity }}
|
||||
// This is buggy (gets outside the container)
|
||||
highlightActiveLine={false}
|
||||
keyboardHandler={props.keyboardMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderViewer() {
|
||||
return (
|
||||
<div style={cellViewerStyle}>
|
||||
<NoteTextViewer
|
||||
ref={webviewRef}
|
||||
viewerStyle={styles.viewer}
|
||||
onIpcMessage={webview_ipcMessage}
|
||||
onDomReady={webview_domReady}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar
|
||||
theme={props.theme}
|
||||
dispatch={props.dispatch}
|
||||
disabled={editorReadOnly}
|
||||
/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
<div style={styles.rowEditorViewer}>
|
||||
{renderEditor()}
|
||||
{renderViewer()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(AceEditor);
|
||||
|
@@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import CommandService from '../../../../lib/services/CommandService';
|
||||
|
||||
const ToolbarBase = require('../../../Toolbar.min.js');
|
||||
const { buildStyle, themeStyle } = require('lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
theme: number,
|
||||
dispatch: Function,
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
function styles_(props:ToolbarProps) {
|
||||
return buildStyle('AceEditorToolbar', props.theme, (/* theme:any*/) => {
|
||||
const theme = themeStyle(props.theme);
|
||||
return {
|
||||
root: {
|
||||
flex: 1,
|
||||
marginBottom: 0,
|
||||
borderTop: `1px solid ${theme.dividerColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function Toolbar(props:ToolbarProps) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const cmdService = CommandService.instance();
|
||||
|
||||
const toolbarItems = [
|
||||
cmdService.commandToToolbarButton('textBold'),
|
||||
cmdService.commandToToolbarButton('textItalic'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textLink'),
|
||||
cmdService.commandToToolbarButton('textCode'),
|
||||
cmdService.commandToToolbarButton('attachFile'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textNumberedList'),
|
||||
cmdService.commandToToolbarButton('textBulletedList'),
|
||||
cmdService.commandToToolbarButton('textCheckbox'),
|
||||
cmdService.commandToToolbarButton('textHeading'),
|
||||
cmdService.commandToToolbarButton('textHorizontalRule'),
|
||||
cmdService.commandToToolbarButton('insertDateTime'),
|
||||
];
|
||||
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
export default function styles(props: NoteBodyEditorProps) {
|
||||
return buildStyle('AceEditor', props.theme, (theme: any) => {
|
||||
return {
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...props.style,
|
||||
},
|
||||
rowToolbar: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
rowEditorViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
},
|
||||
cellEditor: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
},
|
||||
cellViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.dividerColor,
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
viewer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
},
|
||||
editor: {
|
||||
display: 'flex',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: 0,
|
||||
lineHeight: `${theme.textAreaLineHeight}px`,
|
||||
fontSize: `${theme.editorFontSize}px`,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
aceEditorTheme: theme.aceEditorTheme, // Defined in theme.js
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@@ -1,219 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
if (!body) return 0;
|
||||
|
||||
const noteLines = body.split('\n');
|
||||
|
||||
let pos = 0;
|
||||
for (let i = 0; i < noteLines.length; i++) {
|
||||
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
|
||||
|
||||
if (i === cursorPos.row) {
|
||||
pos += cursorPos.column;
|
||||
break;
|
||||
} else {
|
||||
pos += noteLines[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
export function currentTextOffset(editor: any, body: string) {
|
||||
return cursorPositionToTextOffset(editor.getCursorPosition(), body);
|
||||
}
|
||||
|
||||
export function rangeToTextOffsets(range: any, body: string) {
|
||||
return {
|
||||
start: cursorPositionToTextOffset(range.start, body),
|
||||
end: cursorPositionToTextOffset(range.end, body),
|
||||
};
|
||||
}
|
||||
|
||||
export function textOffsetSelection(selectionRange: any, body: string) {
|
||||
return selectionRange && body ? rangeToTextOffsets(selectionRange, body) : null;
|
||||
}
|
||||
|
||||
export function selectedText(selectionRange: any, body: string) {
|
||||
const selection = textOffsetSelection(selectionRange, body);
|
||||
if (!selection || selection.start === selection.end) return '';
|
||||
|
||||
return body.substr(selection.start, selection.end - selection.start);
|
||||
}
|
||||
|
||||
export function selectionRange(editor:any) {
|
||||
const ranges = editor.getSelection().getAllRanges();
|
||||
return ranges && ranges.length ? ranges[0] : null;
|
||||
}
|
||||
|
||||
export function textOffsetToCursorPosition(offset: number, body: string) {
|
||||
const lines = body.split('\n');
|
||||
let row = 0;
|
||||
let currentOffset = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (currentOffset + line.length >= offset) {
|
||||
return {
|
||||
row: row,
|
||||
column: offset - currentOffset,
|
||||
};
|
||||
}
|
||||
|
||||
row++;
|
||||
currentOffset += line.length + 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function lineAtRow(body: string, row: number) {
|
||||
if (!body) return '';
|
||||
const lines = body.split('\n');
|
||||
if (row < 0 || row >= lines.length) return '';
|
||||
return lines[row];
|
||||
}
|
||||
|
||||
export function selectionRangeCurrentLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row);
|
||||
}
|
||||
|
||||
export function selectionRangePreviousLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row - 1);
|
||||
}
|
||||
|
||||
export function lineLeftSpaces(line: string) {
|
||||
let output = '';
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
if ([' ', '\t'].indexOf(line[i]) >= 0) {
|
||||
output += line[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function usePrevious(value: any): any {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export function useScrollHandler(editor: any, webviewRef: any, onScroll: Function) {
|
||||
const editorMaxScrollTop_ = useRef(0);
|
||||
const restoreScrollTop_ = useRef<any>(null);
|
||||
const ignoreNextEditorScrollEvent_ = useRef(false);
|
||||
const scrollTimeoutId_ = useRef<any>(null);
|
||||
|
||||
// TODO: Below is not needed anymore????
|
||||
//
|
||||
// this.editorMaxScrollTop_ = 0;
|
||||
// // HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// // and then (in the renderer callback) to the value we actually need. The first
|
||||
// // operation helps clear the scroll position cache. See:
|
||||
// //
|
||||
// this.editorSetScrollTop(1);
|
||||
// this.restoreScrollTop_ = 0;
|
||||
|
||||
const editorSetScrollTop = useCallback((v) => {
|
||||
if (!editor) return;
|
||||
editor.getSession().setScrollTop(v);
|
||||
}, [editor]);
|
||||
|
||||
// Complicated but reliable method to get editor content height
|
||||
// https://github.com/ajaxorg/ace/issues/2046
|
||||
const onAfterEditorRender = useCallback(() => {
|
||||
const r = editor.renderer;
|
||||
editorMaxScrollTop_.current = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight);
|
||||
|
||||
if (restoreScrollTop_.current !== null) {
|
||||
editorSetScrollTop(restoreScrollTop_.current);
|
||||
restoreScrollTop_.current = null;
|
||||
}
|
||||
}, [editor, editorSetScrollTop]);
|
||||
|
||||
const scheduleOnScroll = useCallback((event: any) => {
|
||||
if (scrollTimeoutId_.current) {
|
||||
clearTimeout(scrollTimeoutId_.current);
|
||||
scrollTimeoutId_.current = null;
|
||||
}
|
||||
|
||||
scrollTimeoutId_.current = setTimeout(() => {
|
||||
scrollTimeoutId_.current = null;
|
||||
onScroll(event);
|
||||
}, 10);
|
||||
}, [onScroll]);
|
||||
|
||||
const setEditorPercentScroll = useCallback((p: number) => {
|
||||
ignoreNextEditorScrollEvent_.current = true;
|
||||
editorSetScrollTop(p * editorMaxScrollTop_.current);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}, [editorSetScrollTop, scheduleOnScroll]);
|
||||
|
||||
const setViewerPercentScroll = useCallback((p: number) => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.wrappedInstance.send('setPercentScroll', p);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}
|
||||
}, [scheduleOnScroll]);
|
||||
|
||||
const editor_scroll = useCallback(() => {
|
||||
if (ignoreNextEditorScrollEvent_.current) {
|
||||
ignoreNextEditorScrollEvent_.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const m = editorMaxScrollTop_.current;
|
||||
const percent = m ? editor.getSession().getScrollTop() / m : 0;
|
||||
|
||||
setViewerPercentScroll(percent);
|
||||
}, [editor, setViewerPercentScroll]);
|
||||
|
||||
const resetScroll = useCallback(() => {
|
||||
if (!editor) return;
|
||||
|
||||
// Ace Editor caches scroll values, which makes
|
||||
// it hard to reset the scroll position, so we
|
||||
// need to use this hack.
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
editor.session.$scrollTop = -1;
|
||||
editor.session.$scrollLeft = -1;
|
||||
editor.renderer.scrollTop = -1;
|
||||
editor.renderer.scrollLeft = -1;
|
||||
editor.renderer.scrollBarV.scrollTop = -1;
|
||||
editor.renderer.scrollBarH.scrollLeft = -1;
|
||||
editor.session.setScrollTop(0);
|
||||
editor.session.setScrollLeft(0);
|
||||
}, [editorSetScrollTop, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
editor.renderer.on('afterRender', onAfterEditorRender);
|
||||
|
||||
return () => {
|
||||
editor.renderer.off('afterRender', onAfterEditorRender);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
|
||||
export function useRootWidth(dependencies:any) {
|
||||
const { rootRef } = dependencies;
|
||||
|
||||
const [rootWidth, setRootWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
if (rootWidth !== rootRef.current.offsetWidth) setRootWidth(rootRef.current.offsetWidth);
|
||||
});
|
||||
|
||||
return rootWidth;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
export interface RenderedBody {
|
||||
html: string;
|
||||
pluginAssets: any[];
|
||||
}
|
||||
|
||||
export function defaultRenderedBody(): RenderedBody {
|
||||
return {
|
||||
html: '',
|
||||
pluginAssets: [],
|
||||
};
|
||||
}
|
@@ -1,178 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { selectionRange } from './index';
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
|
||||
// The line that contains only `- ` is
|
||||
// recognized as a heading in Ace.
|
||||
function hyphenEmptyListItem(tokens: any[]) {
|
||||
return (
|
||||
tokens.length === 2 &&
|
||||
tokens[0].type === 'markup.heading.2' &&
|
||||
tokens[0].value === '-' &&
|
||||
tokens[1].type === 'text.xml' &&
|
||||
tokens[1].value === ' '
|
||||
);
|
||||
}
|
||||
|
||||
// Returns tokens of the line if it starts with a 'markup.list' token.
|
||||
function listTokens(editor: any, row: number) {
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (
|
||||
!(tokens.length > 0 && tokens[0].type === 'markup.list') &&
|
||||
!hyphenEmptyListItem(tokens)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function countIndent(line: string): number {
|
||||
return line.match(/\t| {4}/g)?.length || 0;
|
||||
}
|
||||
|
||||
// Finds the list item with indent level `prevIndent`.
|
||||
function findPrevListNum(editor: any, row: number, indent: number) {
|
||||
while (row > 0) {
|
||||
row--;
|
||||
const line = editor.session.getLine(row);
|
||||
|
||||
if (countIndent(line) === indent) {
|
||||
const num = markdownUtils.olLineNumber(line.trimLeft());
|
||||
if (num) {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
editor: any;
|
||||
}
|
||||
|
||||
export default function useListIdent(dependencies: HookDependencies) {
|
||||
const { editor } = dependencies;
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
// Markdown list indentation. (https://github.com/laurent22/joplin/pull/2713)
|
||||
// If the current line starts with `markup.list` token,
|
||||
// hitting `Tab` key indents the line instead of inserting tab at cursor.
|
||||
const originalEditorIndent = editor.indent;
|
||||
|
||||
editor.indent = function() {
|
||||
const range = selectionRange(editor);
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
const tokens = listTokens(this, row);
|
||||
|
||||
if (tokens.length > 0) {
|
||||
if (tokens[0].value.search(/\d+\./) != -1) {
|
||||
const line = this.session.getLine(row);
|
||||
const n = findPrevListNum(this, row, countIndent(line) + 1) + 1;
|
||||
this.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
|
||||
this.session.indentRows(row, row, '\t');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalEditorIndent) originalEditorIndent.call(this);
|
||||
};
|
||||
|
||||
// Correct the number of numbered list item when outdenting.
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownOutdent',
|
||||
bindKey: { win: 'Shift+Tab', mac: 'Shift+Tab' },
|
||||
multiSelectAction: 'forEachLine',
|
||||
exec: function(editor: any) {
|
||||
const range = selectionRange(editor);
|
||||
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (tokens.length && tokens[0].type === 'markup.list') {
|
||||
const matches = tokens[0].value.match(/^(\t+)\d+\./);
|
||||
if (matches && matches.length) {
|
||||
const indent = countIndent(matches[1]);
|
||||
const n = findPrevListNum(editor, row, indent - 1) + 1;
|
||||
console.log(n);
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.blockOutdent();
|
||||
},
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
// Delete a list markup (e.g. `- `) from an empty list item on hitting Enter.
|
||||
// (https://github.com/laurent22/joplin/pull/2772)
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownEnter',
|
||||
bindKey: 'Enter',
|
||||
multiSelectAction: 'forEach',
|
||||
exec: function(editor: any) {
|
||||
const range = editor.getSelectionRange();
|
||||
const tokens = listTokens(editor, range.start.row);
|
||||
|
||||
const emptyListItem =
|
||||
tokens.length === 1 || hyphenEmptyListItem(tokens);
|
||||
const emptyCheckboxItem =
|
||||
tokens.length === 3 &&
|
||||
['[ ]', '[x]'].includes(tokens[1].value) &&
|
||||
tokens[2].value === ' ';
|
||||
|
||||
if (!range.isEmpty() || !(emptyListItem || emptyCheckboxItem)) {
|
||||
editor.insert('\n');
|
||||
// Cursor can go out of the view after inserting '\n'.
|
||||
editor.renderer.scrollCursorIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = range.start.row;
|
||||
const line = editor.session.getLine(row);
|
||||
let indent = editor
|
||||
.getSession()
|
||||
.getMode()
|
||||
.getNextLineIndent(null, line);
|
||||
if (indent.startsWith('\t')) {
|
||||
indent = indent.slice(1);
|
||||
} else {
|
||||
indent = '';
|
||||
}
|
||||
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: line.length },
|
||||
},
|
||||
indent
|
||||
);
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
return () => {
|
||||
editor.indent = originalEditorIndent;
|
||||
editor.commands.removeCommand('markdownOutdent');
|
||||
editor.commands.removeCommand('markdownEnter');
|
||||
};
|
||||
}, [editor]);
|
||||
}
|
@@ -113,7 +113,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
},
|
||||
supportsCommand: (/* name:string*/) => {
|
||||
// TODO: not implemented, currently only used for "search" command
|
||||
// which is not directly supported by Ace Editor.
|
||||
// which is not directly supported by this Editor.
|
||||
return false;
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
@@ -308,11 +308,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
async function loadScripts() {
|
||||
const scriptsToLoad:{src: string, id:string, loaded: boolean}[] = [
|
||||
{
|
||||
src: 'node_modules/codemirror/lib/codemirror.css',
|
||||
id: 'codemirrorBaseStyle',
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
src: 'node_modules/codemirror/addon/dialog/dialog.css',
|
||||
id: 'codemirrorDialogStyle',
|
||||
@@ -484,36 +479,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
// SEARCHHACK
|
||||
// TODO: remove this options hack when aceeditor is removed
|
||||
// Currently the webviewRef will send out an ipcMessage to set the results count
|
||||
// Also setting it here will start an infinite loop of repeating the search
|
||||
// Unfortunately we can't remove the function in the webview setMarkers
|
||||
// until the aceeditor is remove.
|
||||
// The below search is more accurate than the webview based one as it searches
|
||||
// the text and not rendered html (rendered html fails if there is a match
|
||||
// in a katex block)
|
||||
// Once AceEditor is removed the options definition below can be removed and
|
||||
// props.searchMarkers.options can be directly passed to as the 3rd argument below
|
||||
// (replacing options)
|
||||
let options = { notFromAce: true };
|
||||
if (props.searchMarkers.options) {
|
||||
options = Object.assign({}, props.searchMarkers.options, options);
|
||||
}
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, options);
|
||||
// SEARCHHACK
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
if (editorRef.current) {
|
||||
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: when aceeditor is removed then this check will be performed in the NoteSearchbar
|
||||
// End the if statement can be removed in favor of simply returning matches
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
} else {
|
||||
props.setLocalSearchResultCount(-1);
|
||||
}
|
||||
// end SEARCHHACK
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
|
||||
|
@@ -139,8 +139,8 @@ function Editor(props: EditorProps, ref: any) {
|
||||
'Right': 'goCharRight',
|
||||
'Up': 'goLineUp',
|
||||
'Down': 'goLineDown',
|
||||
'End': 'goLineEnd',
|
||||
'Home': 'goLineStartSmart',
|
||||
'End': 'goLineRight',
|
||||
'Home': 'goLineLeftSmart',
|
||||
'PageUp': 'goPageUp',
|
||||
'PageDown': 'goPageDown',
|
||||
'Delete': 'delCharAfter',
|
||||
|
@@ -267,7 +267,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
} else if (cmd.value.type === 'files') {
|
||||
insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
reg.logger().warn('TinyMCE: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
@@ -626,6 +626,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
update: function(element:any) {
|
||||
let itemType:ContextMenuItemType = ContextMenuItemType.None;
|
||||
let resourceId = '';
|
||||
let linkToCopy = null;
|
||||
|
||||
if (element.nodeName === 'IMG') {
|
||||
itemType = ContextMenuItemType.Image;
|
||||
@@ -633,6 +634,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
} else if (element.nodeName === 'A') {
|
||||
resourceId = Resource.pathToId(element.href);
|
||||
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
|
||||
linkToCopy = element.getAttribute('href') || '';
|
||||
} else {
|
||||
itemType = ContextMenuItemType.Text;
|
||||
}
|
||||
@@ -640,6 +642,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
contextMenuActionOptions.current = {
|
||||
itemType,
|
||||
resourceId,
|
||||
linkToCopy,
|
||||
textToCopy: null,
|
||||
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
||||
insertContent: (content:string) => {
|
||||
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import AceEditor from './NoteBody/AceEditor/AceEditor';
|
||||
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
@@ -365,7 +364,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId);
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
|
||||
const editorProps:NoteBodyEditorProps = {
|
||||
ref: editorRef,
|
||||
@@ -398,8 +397,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'AceEditor') {
|
||||
editor = <AceEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror') {
|
||||
editor = <CodeMirror {...editorProps}/>;
|
||||
} else {
|
||||
@@ -466,6 +463,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
onNext={localSearch_next}
|
||||
onPrevious={localSearch_previous}
|
||||
onClose={localSearch_close}
|
||||
visiblePanes={props.noteVisiblePanes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -531,6 +529,7 @@ const mapStateToProps = (state: any) => {
|
||||
customCss: state.customCss,
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
watchedResources: state.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -20,6 +20,7 @@ export enum ContextMenuItemType {
|
||||
export interface ContextMenuOptions {
|
||||
itemType: ContextMenuItemType,
|
||||
resourceId: string,
|
||||
linkToCopy: string,
|
||||
textToCopy: string,
|
||||
htmlToCopy: string,
|
||||
insertContent: Function,
|
||||
@@ -114,7 +115,7 @@ export function menuItems():ContextMenuItems {
|
||||
copyLinkUrl: {
|
||||
label: _('Copy Link Address'),
|
||||
onAction: async (options:ContextMenuOptions) => {
|
||||
clipboard.writeText(options.textToCopy);
|
||||
clipboard.writeText(options.linkToCopy !== null ? options.linkToCopy : options.textToCopy);
|
||||
},
|
||||
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Link,
|
||||
},
|
||||
|
@@ -23,6 +23,7 @@ export interface NoteEditorProps {
|
||||
customCss: string,
|
||||
noteVisiblePanes: string[],
|
||||
watchedResources: any,
|
||||
highlightedWords: any[],
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
|
@@ -41,6 +41,7 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
|
||||
itemType: arg0 && arg0.type,
|
||||
resourceId: arg0.resourceId,
|
||||
textToCopy: arg0.textToCopy,
|
||||
linkToCopy: null,
|
||||
htmlToCopy: '',
|
||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||
});
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
|
||||
interface SearchMarkersOptions {
|
||||
searchTimestamp: number,
|
||||
selectedIndex: number,
|
||||
@@ -25,18 +22,14 @@ function defaultSearchMarkers():SearchMarkers {
|
||||
};
|
||||
}
|
||||
|
||||
export default function useSearchMarkers(showLocalSearch:boolean, localSearchMarkerOptions:Function, searches:any[], selectedSearchId:string) {
|
||||
|
||||
export default function useSearchMarkers(showLocalSearch:boolean, localSearchMarkerOptions:Function, searches:any[], selectedSearchId:string, highlightedWords: any[] = []) {
|
||||
return useMemo(():SearchMarkers => {
|
||||
if (showLocalSearch) return localSearchMarkerOptions();
|
||||
|
||||
const output = defaultSearchMarkers();
|
||||
|
||||
const search = BaseModel.byId(searches, selectedSearchId);
|
||||
if (search) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(search.query_pattern);
|
||||
output.keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
output.keywords = highlightedWords;
|
||||
|
||||
return output;
|
||||
}, [showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
|
||||
}, [highlightedWords, showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ const BaseModel = require('lib/BaseModel');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const eventManager = require('lib/eventManager');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const Note = require('lib/models/Note');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const NoteListUtils = require('../utils/NoteListUtils');
|
||||
@@ -229,8 +228,7 @@ class NoteListComponent extends React.Component {
|
||||
if (this.props.notesParentType === 'Search') {
|
||||
const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
if (query) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(query.query_pattern);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
return this.props.highlightedWords;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -460,6 +458,7 @@ const mapStateToProps = state => {
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -148,14 +148,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// Currently searching in the viewer does not support jumping between matches
|
||||
// So we explicitly disable those commands when only the viewer is open (this is
|
||||
// currently signaled by results count being set to -1, but once Ace editor is removed
|
||||
// we can observe the visible panes directly).
|
||||
// SEARCHHACK
|
||||
// TODO: remove the props.resultCount check here and replace it by checking visible panes directly
|
||||
const allowScrolling = this.props.resultCount !== -1;
|
||||
// end SEARCHHACK
|
||||
const allowScrolling = this.props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
const viewerWarning = (
|
||||
<div style={textStyle}>
|
||||
|
@@ -277,25 +277,7 @@
|
||||
let selectedElement = null;
|
||||
let elementIndex = 0;
|
||||
|
||||
const onEachElement = (element) => {
|
||||
// SEARCHHACK
|
||||
// TODO: remove notFromAce hack when removing aceeditor
|
||||
// when removing just remove the 'notFromAce' part and leave the rest alone
|
||||
if (!('selectedIndex' in options) || 'notFromAce' in options) return;
|
||||
// SEARCHHACK
|
||||
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
element.classList.add('mark-selected');
|
||||
selectedElement = element;
|
||||
}
|
||||
|
||||
elementIndex++;
|
||||
}
|
||||
|
||||
const markKeywordOptions = {
|
||||
each: onEachElement,
|
||||
};
|
||||
const markKeywordOptions = {};
|
||||
|
||||
if ('separateWordSearch' in options) markKeywordOptions.separateWordSearch = options.separateWordSearch;
|
||||
|
||||
@@ -307,22 +289,6 @@
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
}, markKeywordOptions);
|
||||
}
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: Remove this block (until the other SEARCHHACK marker) when removing Ace
|
||||
// HACK: Aceeditor uses this view to handle all the searching
|
||||
// The newer editor wont and this needs to be disabled in order to
|
||||
// prevent an infinite loop
|
||||
if (!('notFromAce' in options)) {
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
||||
// We only scroll the element into view if the search just happened. So when the user type the search
|
||||
// or select the next/previous result, we scroll into view. However for other actions that trigger a
|
||||
// re-render, we don't scroll as this is normally not wanted.
|
||||
// This is to go around this issue: https://github.com/laurent22/joplin/issues/1833
|
||||
if (selectedElement && Date.now() - options.searchTimestamp <= 1000) selectedElement.scrollIntoView();
|
||||
}
|
||||
// SEARCHHACK
|
||||
}
|
||||
|
||||
let markLoader_ = { state: 'idle', whenDone: null };
|
||||
|
@@ -25,7 +25,7 @@ class NoteListUtils {
|
||||
|
||||
if (!hasEncrypted) {
|
||||
menu.append(
|
||||
new MenuItem(cmdService.commandToMenuItem('setTags'))
|
||||
new MenuItem(cmdService.commandToMenuItem('setTags', { noteIds }))
|
||||
);
|
||||
|
||||
menu.append(
|
||||
|
@@ -17,6 +17,9 @@ const tasks = {
|
||||
electronRebuild: {
|
||||
fn: require('./tools/electronRebuild.js'),
|
||||
},
|
||||
compileExtensions: {
|
||||
fn: require('../Tools/gulp/tasks/compileExtensions.js'),
|
||||
},
|
||||
copyLib: require('../Tools/gulp/tasks/copyLib'),
|
||||
tsc: require('../Tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
@@ -25,6 +28,7 @@ const tasks = {
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
const buildSeries = [
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
];
|
||||
|
||||
|
@@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
|
||||
|
||||
<style>
|
||||
.smalltalk {
|
||||
|
@@ -29,8 +29,9 @@ Logger.fsDriver_ = new FsDriverNode();
|
||||
|
||||
const env = envFromArgs(process.argv);
|
||||
const profilePath = profileFromArgs(process.argv);
|
||||
const isDebugMode = !!process.argv && process.argv.indexOf('--debug') >= 0;
|
||||
|
||||
const wrapper = new ElectronAppWrapper(electronApp, env, profilePath);
|
||||
const wrapper = new ElectronAppWrapper(electronApp, env, profilePath, isDebugMode);
|
||||
|
||||
initBridge(wrapper);
|
||||
|
||||
|
268
ElectronClient/package-lock.json
generated
268
ElectronClient/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.237",
|
||||
"version": "1.1.1",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -51,6 +51,10 @@
|
||||
{
|
||||
"from": "build-win/Joplin.VisualElementsManifest.xml",
|
||||
"to": "."
|
||||
},
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.dll",
|
||||
"to": "usr/lib/spellfix.dll"
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
@@ -65,7 +69,13 @@
|
||||
"artifactName": "${productName}Portable.${ext}"
|
||||
},
|
||||
"mac": {
|
||||
"icon": "../../Assets/macOs.icns"
|
||||
"icon": "../../Assets/macOs.icns",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.dylib",
|
||||
"to": "usr/lib/spellfix.dylib"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "../Assets/LinuxIcons/256x256.png",
|
||||
@@ -73,7 +83,13 @@
|
||||
"desktop": {
|
||||
"Icon": "joplin"
|
||||
},
|
||||
"target": "AppImage"
|
||||
"target": "AppImage",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.so",
|
||||
"to": "usr/lib/spellfix.so"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
@@ -162,7 +178,6 @@
|
||||
"promise": "^8.0.1",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.9.0",
|
||||
"react-ace": "^6.1.4",
|
||||
"react-datetime": "^2.14.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-redux": "^5.0.7",
|
||||
|
@@ -177,8 +177,8 @@ class Dialog extends React.PureComponent {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
keywords(searchQuery) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(searchQuery);
|
||||
async keywords(searchQuery) {
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ class Dialog extends React.PureComponent {
|
||||
}
|
||||
} else {
|
||||
const limit = 20;
|
||||
const searchKeywords = this.keywords(searchQuery);
|
||||
const searchKeywords = await this.keywords(searchQuery);
|
||||
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] });
|
||||
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
|
||||
|
||||
@@ -283,7 +283,7 @@ class Dialog extends React.PureComponent {
|
||||
this.setState({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: this.keywords(searchQuery),
|
||||
keywords: await this.keywords(searchQuery),
|
||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||
resultsInBody: resultsInBody,
|
||||
});
|
||||
@@ -455,6 +455,7 @@ const mapStateToProps = (state) => {
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -63,13 +63,6 @@ a {
|
||||
transition: 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||
as red boxes, but since those are actually valid characters and common in imported
|
||||
Evernote data, we hide them here. */
|
||||
.ace-chrome .ace_invisible_space {
|
||||
background-color: transparent !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.note-list .list-item-container:hover {
|
||||
background-color: rgba(0,160,255,0.1) !important;
|
||||
|
11
README.md
11
README.md
@@ -20,15 +20,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
|
||||
|
||||
Operating System | Download | Alternative
|
||||
-----------------|--------|-------------------
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.233/Joplin-Setup-1.0.233.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.233/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.233/Joplin-1.0.233.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew (unsupported): `brew cask install joplin`
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.233/Joplin-1.0.233.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package (unsupported) [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, and Mint; the desktop environments supported are GNOME, KDE, Xfce, MATE, LXQT, LXDE, Unity, Cinnamon, Deepin and Pantheon), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh \| bash`
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.241/Joplin-Setup-1.0.241.exe'><img alt='Get it on Windows' width="134px" src='https://joplinapp.org/images/BadgeWindows.png'/></a> | Or get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.241/JoplinPortable.exe'>Portable version</a><br><br>The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.241/Joplin-1.0.241.dmg'><img alt='Get it on macOS' width="134px" src='https://joplinapp.org/images/BadgeMacOS.png'/></a> | You can also use Homebrew (unsupported): `brew cask install joplin`
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.241/Joplin-1.0.241.AppImage'><img alt='Get it on Linux' width="134px" src='https://joplinapp.org/images/BadgeLinux.png'/></a> | An Arch Linux package (unsupported) [is also available](#terminal-application).<br><br>If it works with your distribution (it has been tested on Ubuntu, Fedora, and Mint; the desktop environments supported are GNOME, KDE, Xfce, MATE, LXQT, LXDE, Unity, Cinnamon, Deepin and Pantheon), the recommended way is to use this script as it will handle the desktop icon too:<br><br> `wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/Joplin_install_and_update.sh \| bash`
|
||||
|
||||
## Mobile applications
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.338/joplin-v1.0.338.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.338/joplin-v1.0.338-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.1.1/joplin-v1.1.1.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.1.1/joplin-v1.1.1-32bit.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -64,6 +64,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
| :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/6979755?s=96&v=4"/></br>[Devon Zuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[小西 孝宗](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[Alexander van der Berg](https://github.com/avanderberg)
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[Frank Bloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[Thomas Broussard](https://github.com/thomasbroussard)
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[Brandon Johnson](https://github.com/dbrandonjohnson) | |
|
||||
|
||||
<!-- TOC -->
|
||||
# Table of contents
|
||||
@@ -87,7 +88,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
|
||||
- Development
|
||||
|
||||
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec/e2ee.md)
|
||||
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md)
|
||||
- [Note History spec](https://github.com/laurent22/joplin/blob/master/readme/spec/history.md)
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/master/readme/spec/sync_lock.md)
|
||||
|
||||
|
@@ -125,8 +125,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097574
|
||||
versionName "1.0.338"
|
||||
versionCode 2097577
|
||||
versionName "1.1.1"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
BIN
ReactNativeClient/images/StartUpIcon.png
Normal file
BIN
ReactNativeClient/images/StartUpIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
BIN
ReactNativeClient/images/StartUpIcon@2x.png
Normal file
BIN
ReactNativeClient/images/StartUpIcon@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
ReactNativeClient/images/StartUpIcon@3x.png
Normal file
BIN
ReactNativeClient/images/StartUpIcon@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
@@ -337,7 +337,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
@@ -357,7 +357,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 10.0.52;
|
||||
MARKETING_VERSION = 10.1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -380,7 +380,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 52;
|
||||
CURRENT_PROJECT_VERSION = 53;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -393,7 +393,7 @@
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = "$(inherited)";
|
||||
MARKETING_VERSION = 10.0.52;
|
||||
MARKETING_VERSION = 10.1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@@ -44,6 +44,9 @@ const KvStore = require('lib/services/KvStore');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
const { toSystemSlashes } = require('lib/path-utils.js');
|
||||
|
||||
// const ntpClient = require('lib/vendor/ntp-client');
|
||||
// ntpClient.dgram = require('dgram');
|
||||
|
||||
class BaseApplication {
|
||||
constructor() {
|
||||
this.logger_ = new Logger();
|
||||
@@ -167,6 +170,12 @@ class BaseApplication {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--debug') {
|
||||
// Currently only handled by ElectronAppWrapper (isDebugMode property)
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--update-geolocation-disabled') {
|
||||
Note.updateGeolocationEnabled_ = false;
|
||||
argv.splice(0, 1);
|
||||
@@ -274,6 +283,7 @@ class BaseApplication {
|
||||
});
|
||||
|
||||
let notes = [];
|
||||
let highlightedWords = [];
|
||||
|
||||
if (parentId) {
|
||||
if (parentType === Folder.modelType()) {
|
||||
@@ -282,12 +292,21 @@ class BaseApplication {
|
||||
notes = await Tag.notes(parentId, options);
|
||||
} else if (parentType === BaseModel.TYPE_SEARCH) {
|
||||
const search = BaseModel.byId(state.searches, parentId);
|
||||
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
|
||||
notes = await SearchEngineUtils.notesForQuery(search.query_pattern, { fuzzy: search.fuzzy });
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(search.query_pattern, search.fuzzy);
|
||||
highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
} else if (parentType === BaseModel.TYPE_SMART_FILTER) {
|
||||
notes = await Note.previews(parentId, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightedWords.length) {
|
||||
this.store().dispatch({
|
||||
type: 'SET_HIGHLIGHTED',
|
||||
words: highlightedWords,
|
||||
});
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_UPDATE_ALL',
|
||||
notes: notes,
|
||||
@@ -657,6 +676,7 @@ class BaseApplication {
|
||||
reg.dispatch = () => {};
|
||||
|
||||
BaseService.logger_ = this.logger_;
|
||||
// require('lib/ntpDate').setLogger(reg.logger());
|
||||
|
||||
this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` });
|
||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||
@@ -675,6 +695,23 @@ class BaseApplication {
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
this.database_.setLogger(this.dbLogger_);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
if (shim.isElectron()) {
|
||||
this.database_.extensionToLoad = './lib/sql-extensions/spellfix';
|
||||
}
|
||||
} else {
|
||||
if (shim.isElectron()) {
|
||||
if (shim.isWindows()) {
|
||||
const appDir = process.execPath.substring(0, process.execPath.lastIndexOf('\\'));
|
||||
this.database_.extensionToLoad = `${appDir}/usr/lib/spellfix`;
|
||||
} else {
|
||||
const appDir = process.execPath.substring(0, process.execPath.lastIndexOf('/'));
|
||||
this.database_.extensionToLoad = `${appDir}/usr/lib/spellfix`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.database_.open({ name: `${profileDir}/database.sqlite` });
|
||||
|
||||
// if (Setting.value('env') === 'dev') await this.database_.clearForTesting();
|
||||
@@ -701,6 +738,11 @@ class BaseApplication {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
if (Setting.value('db.fuzzySearchEnabled') === -1) {
|
||||
const fuzzySearchEnabled = await this.database_.fuzzySearchEnabled();
|
||||
Setting.setValue('db.fuzzySearchEnabled', fuzzySearchEnabled ? 1 : 0);
|
||||
}
|
||||
|
||||
if (Setting.value('encryption.shouldReencrypt') < 0) {
|
||||
// We suggest re-encryption if the user has at least one notebook
|
||||
// and if encryption is enabled. This code runs only when shouldReencrypt = -1
|
||||
|
@@ -93,7 +93,7 @@ class ActionButtonComponent extends React.Component {
|
||||
}
|
||||
|
||||
if (!buttonComps.length && !this.props.mainButton) {
|
||||
return <ReactNativeActionButton style={{ display: 'none' }} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainButton = this.props.mainButton ? this.props.mainButton : {};
|
||||
|
@@ -40,7 +40,6 @@ const ImagePicker = require('react-native-image-picker');
|
||||
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
|
||||
const ShareExtension = require('lib/ShareExtension.js').default;
|
||||
const CameraView = require('lib/components/CameraView');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
@@ -975,8 +974,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
|
||||
@@ -1016,8 +1014,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
const onCheckboxChange = newBody => {
|
||||
@@ -1202,6 +1199,7 @@ const NoteScreen = connect(state => {
|
||||
sharedData: state.sharedData,
|
||||
showSideMenu: state.showSideMenu,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
})(NoteScreenComponent);
|
||||
|
||||
|
@@ -227,9 +227,12 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
|
||||
let buttonFolderId = this.props.selectedFolderId != Folder.conflictFolderId() ? this.props.selectedFolderId : null;
|
||||
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
|
||||
|
||||
const addFolderNoteButtons = !!buttonFolderId;
|
||||
const thisComp = this;
|
||||
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>;
|
||||
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={buttonFolderId}></ActionButton>;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
@@ -250,6 +253,7 @@ const NotesScreen = connect(state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
activeFolderId: state.settings.activeFolderId,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedTagId: state.selectedTagId,
|
||||
|
@@ -156,6 +156,12 @@ shared.settingsSections = createSelector(
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
output.push({
|
||||
name: 'keymap',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
@@ -36,6 +36,10 @@ class DatabaseDriverNode {
|
||||
});
|
||||
}
|
||||
|
||||
loadExtension(path) {
|
||||
return this.db_.loadExtension(path);
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@@ -50,6 +50,10 @@ class DatabaseDriverReactNative {
|
||||
});
|
||||
}
|
||||
|
||||
loadExtension(path) {
|
||||
throw new Error(`No extension support for ${path} in react-native-sqlite-storage`);
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(
|
||||
|
@@ -97,6 +97,16 @@ class Database {
|
||||
return this.tryCall('selectOne', sql, params);
|
||||
}
|
||||
|
||||
async loadExtension(path) {
|
||||
let result = null;
|
||||
try {
|
||||
result = await this.driver().loadExtension(path);
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(`Could not load extension ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
async selectAll(sql, params = null) {
|
||||
return this.tryCall('selectAll', sql, params);
|
||||
}
|
||||
|
@@ -44,7 +44,7 @@ class FileApiDriverDropbox {
|
||||
metadataToStat_(md, path) {
|
||||
const output = {
|
||||
path: path,
|
||||
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
||||
updated_time: md.server_modified ? (new Date(md.server_modified)).getTime() : Date.now(),
|
||||
isDir: md['.tag'] === 'folder',
|
||||
};
|
||||
|
||||
|
@@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError');
|
||||
const ArrayUtils = require('lib/ArrayUtils');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
function requestCanBeRepeated(error) {
|
||||
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
||||
@@ -57,6 +58,65 @@ class FileApi {
|
||||
this.tempDirName_ = null;
|
||||
this.driver_.fileApi_ = this;
|
||||
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
|
||||
this.remoteDateOffset_ = 0;
|
||||
this.remoteDateNextCheckTime_ = 0;
|
||||
this.remoteDateMutex_ = new Mutex();
|
||||
}
|
||||
|
||||
|
||||
async fetchRemoteDateOffset_() {
|
||||
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
|
||||
const startTime = Date.now();
|
||||
await this.put(tempFile, 'timeCheck');
|
||||
|
||||
// Normally it should be possible to read the file back immediately but
|
||||
// just in case, read it in a loop.
|
||||
const loopStartTime = Date.now();
|
||||
let stat = null;
|
||||
while (Date.now() - loopStartTime < 5000) {
|
||||
stat = await this.stat(tempFile);
|
||||
if (stat) break;
|
||||
await time.msleep(200);
|
||||
}
|
||||
|
||||
if (!stat) throw new Error('Timed out trying to get sync target clock time');
|
||||
|
||||
this.delete(tempFile); // No need to await for this call
|
||||
|
||||
const endTime = Date.now();
|
||||
const expectedTime = Math.round((endTime + startTime) / 2);
|
||||
return stat.updated_time - expectedTime;
|
||||
}
|
||||
|
||||
// Approximates the current time on the sync target. It caches the time offset to
|
||||
// improve performance.
|
||||
async remoteDate() {
|
||||
const shouldSyncTime = () => {
|
||||
return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_;
|
||||
};
|
||||
|
||||
if (shouldSyncTime()) {
|
||||
const release = await this.remoteDateMutex_.acquire();
|
||||
|
||||
try {
|
||||
// Another call might have refreshed the time while we were waiting for the mutex,
|
||||
// so check again if we need to refresh.
|
||||
if (shouldSyncTime()) {
|
||||
this.remoteDateOffset_ = await this.fetchRemoteDateOffset_();
|
||||
// The sync target clock should rarely change but the device one might,
|
||||
// so we need to refresh relatively frequently.
|
||||
this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger().warn('Could not retrieve remote date - defaulting to device date:', error);
|
||||
this.remoteDateOffset_ = 0;
|
||||
this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
return new Date(Date.now() + this.remoteDateOffset_);
|
||||
}
|
||||
|
||||
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
|
||||
|
@@ -3,6 +3,7 @@ const { Database } = require('lib/database.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const Resource = require('lib/models/Resource');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const structureSql = `
|
||||
CREATE TABLE folders (
|
||||
@@ -125,6 +126,12 @@ class JoplinDatabase extends Database {
|
||||
this.tableFields_ = null;
|
||||
this.version_ = null;
|
||||
this.tableFieldNames_ = {};
|
||||
this.extensionToLoad = './build/lib/sql-extensions/spellfix';
|
||||
this.eventEmitter_ = new EventEmitter();
|
||||
}
|
||||
|
||||
eventEmitter() {
|
||||
return this.eventEmitter_;
|
||||
}
|
||||
|
||||
initialized() {
|
||||
@@ -277,6 +284,8 @@ class JoplinDatabase extends Database {
|
||||
if (tableName == 'table_fields') continue;
|
||||
if (tableName == 'sqlite_sequence') continue;
|
||||
if (tableName.indexOf('notes_fts') === 0) continue;
|
||||
if (tableName == 'notes_spellfix') continue;
|
||||
if (tableName == 'search_aux') continue;
|
||||
chain.push(() => {
|
||||
return this.selectAll(`PRAGMA table_info("${tableName}")`).then(pragmas => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
@@ -326,7 +335,7 @@ class JoplinDatabase extends Database {
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
// Note: v16 and v17 don't do anything. They were used to debug an issue.
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
|
||||
@@ -350,6 +359,8 @@ class JoplinDatabase extends Database {
|
||||
|
||||
let queries = [];
|
||||
|
||||
this.eventEmitter_.emit('startMigration', { version: targetVersion });
|
||||
|
||||
if (targetVersion == 1) {
|
||||
queries = this.wrapQueries(this.sqlStringToLines(structureSql));
|
||||
}
|
||||
@@ -840,13 +851,20 @@ class JoplinDatabase extends Database {
|
||||
queries.push(this.addMigrationFile(33));
|
||||
}
|
||||
|
||||
if (targetVersion == 34) {
|
||||
queries.push('CREATE VIRTUAL TABLE search_aux USING fts4aux(notes_fts)');
|
||||
queries.push('CREATE VIRTUAL TABLE notes_spellfix USING spellfix1');
|
||||
}
|
||||
|
||||
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
|
||||
|
||||
try {
|
||||
await this.transactionExecBatch(queries);
|
||||
} catch (error) {
|
||||
if (targetVersion === 15 || targetVersion === 18 || targetVersion === 33) {
|
||||
this.logger().warn('Could not upgrade to database v15 or v18 or v33- FTS feature will not be used', error);
|
||||
this.logger().warn('Could not upgrade to database v15 or v18 or v33 - FTS feature will not be used', error);
|
||||
} else if (targetVersion === 34) {
|
||||
this.logger().warn('Could not upgrade to database v34 - fuzzy search will not be used', error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -873,6 +891,17 @@ class JoplinDatabase extends Database {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fuzzySearchEnabled() {
|
||||
try {
|
||||
await this.selectOne('SELECT count(*) FROM notes_spellfix');
|
||||
} catch (error) {
|
||||
this.logger().warn('Fuzzy search check failed', error);
|
||||
return false;
|
||||
}
|
||||
this.logger().info('Fuzzy search check succeeded');
|
||||
return true;
|
||||
}
|
||||
|
||||
version() {
|
||||
return this.version_;
|
||||
}
|
||||
@@ -880,6 +909,12 @@ class JoplinDatabase extends Database {
|
||||
async initialize() {
|
||||
this.logger().info('Checking for database schema update...');
|
||||
|
||||
try {
|
||||
await this.loadExtension(this.extensionToLoad);
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
}
|
||||
|
||||
let versionRow = null;
|
||||
try {
|
||||
// Will throw if the database has not been created yet, but this is handled below
|
||||
|
@@ -18,7 +18,7 @@ function mermaidInit() {
|
||||
// Resetting elements size - see mermaid.ts
|
||||
const elements = document.getElementsByClassName('mermaid');
|
||||
for (const element of elements) {
|
||||
element.style.width = 'fit-content';
|
||||
element.style.width = '100%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user