1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-30 20:39:46 +02:00

Compare commits

...

71 Commits

Author SHA1 Message Date
Laurent Cozic
314686bede Android release v1.1.1 2020-09-12 00:18:03 +01:00
Laurent Cozic
af8845f209 Tools: Fixed Android version handling 2020-09-12 00:10:18 +01:00
Laurent Cozic
c95d7f9d37 Electron release v1.1.1 2020-09-12 00:06:59 +01:00
Laurent Cozic
2510c659e6 Clear build number 2020-09-12 00:06:25 +01:00
Laurent Cozic
b7523e1b21 Merge branch 'dev' into release-1.1 2020-09-12 00:03:29 +01:00
Laurent Cozic
e4e9e801a2 Android: Fix build 2020-09-11 23:57:06 +01:00
Laurent Cozic
524ec12d8a Android: Remove option for new editor as it crashes the app 2020-09-11 23:56:45 +01:00
Laurent Cozic
1108e8c28a Merge branch 'release-1.0' into dev 2020-09-11 23:34:44 +01:00
Laurent Cozic
0f1156ab9c Desktop: Fixed clock sync logic when creating new sync target 2020-09-11 23:33:34 +01:00
Laurent Cozic
2a08cc332a Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-11 23:02:53 +01:00
Naveen M V
5d2baa872e Desktop: Fix wildcard search (#3713) 2020-09-11 22:52:32 +01:00
Naveen M V
4b377589aa Desktop: Fix bug with quotes when searching (#3735) 2020-09-11 11:30:57 +01:00
Laurent Cozic
cf78204c85 Doc: Added more info about sync lock refresh mechanism 2020-09-11 11:26:07 +01:00
Laurent Cozic
c513cdd4eb Desktop: Sync immediately on startup 2020-09-09 14:45:26 +01:00
Laurent Cozic
5f410e80e6 Merge branch 'release-1.0' into dev 2020-09-09 13:59:36 +01:00
Laurent Cozic
2aa7eaa192 Electron release v1.0.245 2020-09-09 12:39:06 +01:00
Laurent Cozic
b24d060281 All: Got clock sync to work on mobile 2020-09-09 12:25:31 +01:00
Laurent Cozic
a014b9347e Merge branch 'release-1.0' of github.com:laurent22/joplin into release-1.0 2020-09-09 11:39:57 +01:00
Laurent Cozic
582ab4ac13 All: Implemented more reliable way to sync device and server clocks that would work with filesystem sync too 2020-09-09 11:39:13 +01:00
Laurent Cozic
c9adccad4a Get NTP time working on Android 2020-09-09 10:56:17 +01:00
Laurent Cozic
f41ba67e15 Improved ntp time 2020-09-09 00:34:27 +01:00
Laurent Cozic
1f70a76c7e Desktop: Fixes #3729: Fix lock issue when device does not have the right time 2020-09-08 23:57:48 +01:00
Cristi
5fe3732a38 All: Translation: Update ro.po (#3728) 2020-09-08 18:29:07 -04:00
Harris Arvanitis
445533cfcc All: Translation: Update el_GR.po (#3718) 2020-09-08 18:28:42 -04:00
Laurent Cozic
a8e29249d6 Electron release v1.1.244 2020-09-08 01:03:29 +01:00
Laurent Cozic
e4a3cbd2ff Increase minor version 2020-09-08 01:03:12 +01:00
Laurent Cozic
96b7ce9d50 Electron release v1.0.243 2020-09-08 00:57:35 +01:00
Laurent Cozic
2bbc1e7ecd Desktop: Fixes #3710: Fix applying tags to multiple notes 2020-09-08 00:49:58 +01:00
Laurent Cozic
83619b279d Desktop: Fixes #3697: Fixed copying link in Rich Text editor 2020-09-08 00:29:31 +01:00
Laurent Cozic
8b5a99d494 Desktop: Fixes #3553: Fixed viewer font size, in particular for inline code 2020-09-08 00:00:43 +01:00
Laurent Cozic
67d4123608 Desktop: Add log statement to try to fix issue #3536 2020-09-07 23:55:37 +01:00
Laurent Cozic
a424e3c899 Desktop, Cli: Fixes #3689: Fixed note export when there are folders with non-existing parents. Also fixed long path issue on Windows. 2020-09-07 22:12:51 +01:00
Laurent
08d4b5a714 Mobile: Fixes #3022: Fix issue with action button disappearing in some contexts (#3702) 2020-09-07 17:42:16 +01:00
Laurent Cozic
68aefd5e4c Desktop: Rename menu item from "Export" to "Export all" to clarify what it does 2020-09-07 17:33:51 +01:00
Laurent Cozic
57d750bc9a All: Security: Disallow EMBED tags to prevent XSS vulnerability 2020-09-06 19:29:42 +01:00
Caleb John
fbe966903b Desktop: Resolves #3560: Make codemirror the default code editor (#3703) 2020-09-06 16:28:23 +01:00
Jose Esteve
652748f969 Cli: Resolves #3711: Fix keytar library being loaded up in FreeBSD. (#3712)
Issue https://github.com/laurent22/joplin/issues/3711

This patch replaces the 'isLinux' check by a more restrictive version
which fixes the false positive in BSD systems. This was causing Joplin
not to load due to the lack of X11 in headless mode.
2020-09-06 15:20:38 +01:00
Naveen M V
e108fdb1d8 Desktop: Fuzzy search (#3632) 2020-09-06 13:07:00 +01:00
Anjula Karunarathne
a8296e2e37 Desktop: Add keyboard shortcut editor (#3525) 2020-09-06 13:00:25 +01:00
Laurent Cozic
0998fc0ad7 Doc: Update dontate page 2020-09-05 22:49:40 +01:00
Laurent Cozic
d5f3e860b9 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-05 22:48:42 +01:00
Laurent Cozic
4e624f7db5 Doc: Added new sponsor 2020-09-05 22:28:28 +01:00
Anton Deriabin
5b697b7e16 Doc: Fixed wrong e2ee spec link (#3655) 2020-09-05 00:14:49 +01:00
Caleb John
30e0d69a74 Desktop: Load Codemirror css in index.html (#3673) 2020-09-05 00:02:20 +01:00
Caleb John
652816fd26 Desktop: Change codemirror default home and end to be visual line based (#3672) 2020-09-05 00:01:06 +01:00
Laurent Cozic
3a33e5f416 Electron release v1.0.242 2020-09-04 22:46:26 +01:00
Laurent Cozic
277dac5512 Desktop: Fixes sync target upgrade issue when custom TLS settings are used 2020-09-04 22:46:09 +01:00
Laurent Cozic
81d97d9f9d Update website 2020-09-04 19:34:43 +01:00
Laurent Cozic
a4873cd40d Android release v1.0.340 2020-09-04 19:22:28 +01:00
Laurent Cozic
20cb2daf43 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-04 19:15:04 +01:00
Laurent Cozic
e5b5250a91 Tools: Make it easier to create Android pre-releases 2020-09-04 19:11:46 +01:00
Laurent Cozic
db7d617e2b Update website 2020-09-04 19:06:36 +01:00
Laurent Cozic
a627884876 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-04 18:31:01 +01:00
Laurent Cozic
ed30d09e07 ios-v10.0.53 2020-09-04 18:30:43 +01:00
Laurent Cozic
179e3f9aee Electron release v1.0.241 2020-09-04 18:25:04 +01:00
Laurent Cozic
a67aedba35 Tools: Fixed changelog script 2020-09-04 18:24:53 +01:00
Laurent Cozic
199c411a7d CLI v1.0.167 2020-09-04 18:17:24 +01:00
Laurent Cozic
5cd7bb5bdb Electron release v1.0.240 2020-09-04 18:01:38 +01:00
Laurent Cozic
30b8f5e2aa Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-04 18:01:05 +01:00
Laurent Cozic
44f2842820 Android release v1.0.339-3 2020-09-04 17:28:18 +01:00
Laurent Cozic
569355a318 Mobile: Add startup screen to show progress of db migration 2020-09-04 17:07:57 +01:00
Laurent Cozic
8464e16d5d Doc: Added note on how to update Katex or Mermaid package 2020-09-02 22:54:58 +01:00
Laurent Cozic
874c1e3e82 Fixing mermaid script 2020-09-02 22:51:16 +01:00
Laurent Cozic
2530ecfc86 All: Fixes #3664: Fixed Katex font rendering 2020-09-02 22:48:24 +01:00
Laurent Cozic
6b49f1dfcc Desktop: Fixes #3618: Fix links within Mermaid documents 2020-09-02 22:44:24 +01:00
Laurent Cozic
b1af25ea18 Electron release v1.0.239 2020-09-01 22:26:52 +01:00
Laurent Cozic
3086007a9c Electron release v1.0.238 2020-09-01 22:25:42 +01:00
Laurent Cozic
bdfb6b97f5 Desktop: Fixes #3645: Improved handling of startup errors, and prevent window from being invisible when upgrading sync target 2020-09-01 22:25:23 +01:00
Laurent Cozic
c01219e6be Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-09-01 20:55:12 +01:00
Laurent Cozic
c903947704 Merge branch 'dev' of github.com:laurent22/joplin into dev 2020-08-19 00:12:33 +01:00
Laurent Cozic
e190d90832 Desktop: Fixed restarting portable app 2020-08-18 23:51:23 +01:00
150 changed files with 3290 additions and 2917 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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.

View File

@@ -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',
]));

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "1.0.166",
"version": "1.0.167",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -28,7 +28,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.166",
"version": "1.1.167",
"bin": {
"joplin": "./main.js"
},

View File

@@ -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', () => {

View File

@@ -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++) {

View File

@@ -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);
}));
});

View File

@@ -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();
}
});
});

View File

@@ -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;

View File

@@ -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 () => {

View 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);
}));
});

View File

@@ -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}}`);

View File

@@ -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;

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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}`);
}

View File

@@ -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

View 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>
);
};

View File

@@ -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=

View 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>
);
};

View File

@@ -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==

View 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,
},
};
});
}

View File

@@ -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

View 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;

View File

@@ -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==

View 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;

View File

@@ -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=

View 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;

View File

@@ -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}>

View File

@@ -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,
},
});

View File

@@ -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);

View File

@@ -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} />;
}

View File

@@ -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
},
};
});
}

View File

@@ -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;
}

View File

@@ -1,11 +0,0 @@
export interface RenderedBody {
html: string;
pluginAssets: any[];
}
export function defaultRenderedBody(): RenderedBody {
return {
html: '',
pluginAssets: [],
};
}

View File

@@ -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]);
}

View File

@@ -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]);

View File

@@ -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',

View File

@@ -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) => {

View File

@@ -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,
};
};

View File

@@ -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,
},

View File

@@ -23,6 +23,7 @@ export interface NoteEditorProps {
customCss: string,
noteVisiblePanes: string[],
watchedResources: any,
highlightedWords: any[],
}
export interface NoteBodyEditorProps {

View File

@@ -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'); },
});

View File

@@ -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]);
}

View File

@@ -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,
};
};

View File

@@ -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}>

View File

@@ -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 };

View File

@@ -25,7 +25,7 @@ class NoteListUtils {
if (!hasEncrypted) {
menu.append(
new MenuItem(cmdService.commandToMenuItem('setTags'))
new MenuItem(cmdService.commandToMenuItem('setTags', { noteIds }))
);
menu.append(

View File

@@ -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',
];

View File

@@ -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 {

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -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)

View File

@@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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",

View File

@@ -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

View File

@@ -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 : {};

View File

@@ -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);

View File

@@ -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,

View File

@@ -156,6 +156,12 @@ shared.settingsSections = createSelector(
isScreen: true,
});
output.push({
name: 'keymap',
metadatas: [],
isScreen: true,
});
return output;
}
);

View File

@@ -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) => {

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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',
};

View File

@@ -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

View File

@@ -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

View File

@@ -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%';
}
}
}

Some files were not shown because too many files have changed in this diff Show More