You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-06 09:19:22 +02:00
Merge branch 'dev' into release-1.1
This commit is contained in:
@@ -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/mermaid.js
|
||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||||
ReactNativeClient/lib/JoplinServerApi.js
|
ReactNativeClient/lib/JoplinServerApi.js
|
||||||
|
ReactNativeClient/lib/ntpDate.js
|
||||||
ReactNativeClient/lib/services/CommandService.js
|
ReactNativeClient/lib/services/CommandService.js
|
||||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -145,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/mermaid.js
|
||||||
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
|
||||||
ReactNativeClient/lib/JoplinServerApi.js
|
ReactNativeClient/lib/JoplinServerApi.js
|
||||||
|
ReactNativeClient/lib/ntpDate.js
|
||||||
ReactNativeClient/lib/services/CommandService.js
|
ReactNativeClient/lib/services/CommandService.js
|
||||||
ReactNativeClient/lib/services/keychain/KeychainService.js
|
ReactNativeClient/lib/services/keychain/KeychainService.js
|
||||||
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: Poedit 2.2.4\n"
|
"X-Generator: Poedit 2.2.4\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"POT-Creation-Date: \n"
|
||||||
|
"PO-Revision-Date: \n"
|
||||||
|
|
||||||
#: CliClient/app/command-cp.js:13
|
#: CliClient/app/command-cp.js:13
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -106,9 +108,9 @@ msgid "Do not ask for confirmation."
|
|||||||
msgstr "Χωρίς να ζητείται επιβεβαίωση."
|
msgstr "Χωρίς να ζητείται επιβεβαίωση."
|
||||||
|
|
||||||
#: CliClient/app/command-import.js:27
|
#: CliClient/app/command-import.js:27
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Output format: %s"
|
msgid "Output format: %s"
|
||||||
msgstr "Μορφή προέλευσης: %s"
|
msgstr "Μορφή εξόδου: %s"
|
||||||
|
|
||||||
#: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69
|
#: CliClient/app/command-import.js:47 ElectronClient/gui/ImportScreen.min.js:69
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@@ -222,14 +224,16 @@ msgstr ""
|
|||||||
"αρκετά λεπτά ανάλογα με το μέγεθος αυτών που πρέπει να αποκρυπτογραφηθούν."
|
"αρκετά λεπτά ανάλογα με το μέγεθος αυτών που πρέπει να αποκρυπτογραφηθούν."
|
||||||
|
|
||||||
#: CliClient/app/command-e2ee.js:53
|
#: CliClient/app/command-e2ee.js:53
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Decrypted items: %d"
|
msgid "Decrypted items: %d"
|
||||||
msgstr "Αποκρυπτογραφημένα στοιχεία: %s / %s"
|
msgstr "Αποκρυπτογραφημένα στοιχεία: %d"
|
||||||
|
|
||||||
#: CliClient/app/command-e2ee.js:54
|
#: CliClient/app/command-e2ee.js:54
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Skipped items: %d (use --retry-failed-items to retry decrypting them)"
|
msgid "Skipped items: %d (use --retry-failed-items to retry decrypting them)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Στοιχεία που έχουν παραλειφθεί: %d (χρήσιμοποίησε --retry-failed-items για "
|
||||||
|
"επανάληψη της αποκρυπτογράφησης τους)"
|
||||||
|
|
||||||
#: CliClient/app/command-e2ee.js:68
|
#: CliClient/app/command-e2ee.js:68
|
||||||
msgid "Completed decryption."
|
msgid "Completed decryption."
|
||||||
@@ -373,7 +377,6 @@ msgid "Synchronisation target: %s (%s)"
|
|||||||
msgstr "Στόχος συγχρονισμού: %s (%s)"
|
msgstr "Στόχος συγχρονισμού: %s (%s)"
|
||||||
|
|
||||||
#: CliClient/app/command-sync.js:177
|
#: CliClient/app/command-sync.js:177
|
||||||
#, fuzzy
|
|
||||||
msgid "Cannot initialise synchroniser."
|
msgid "Cannot initialise synchroniser."
|
||||||
msgstr "Δεν είναι δυνατή η προετοιμασία του συγχρονιστή."
|
msgstr "Δεν είναι δυνατή η προετοιμασία του συγχρονιστή."
|
||||||
|
|
||||||
@@ -839,14 +842,14 @@ msgid "Goto Anything..."
|
|||||||
msgstr "Γρήγορη Μετακίνηση..."
|
msgstr "Γρήγορη Μετακίνηση..."
|
||||||
|
|
||||||
#: ElectronClient/InteropServiceHelper.js:147
|
#: ElectronClient/InteropServiceHelper.js:147
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
|
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
|
||||||
msgstr "Εισαγωγή από \"%s\" σε μορφή \"%s\". Παρακαλώ περιμένετε..."
|
msgstr "Γίνεται εξαγωγή στο \"%s\" με μορφή \"%s\". Παρακαλώ περιμένετε..."
|
||||||
|
|
||||||
#: ElectronClient/InteropServiceHelper.js:164
|
#: ElectronClient/InteropServiceHelper.js:164
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Could not export notes: %s"
|
msgid "Could not export notes: %s"
|
||||||
msgstr "Δεν ήταν δυνατή η αναβάθμιση του master κλειδιού: %s"
|
msgstr "Δεν ήταν δυνατή η εξαγωγή σημειώσεων: %s"
|
||||||
|
|
||||||
#: ElectronClient/checkForUpdates.js:138
|
#: ElectronClient/checkForUpdates.js:138
|
||||||
msgid "Current version is up-to-date."
|
msgid "Current version is up-to-date."
|
||||||
@@ -959,9 +962,10 @@ msgid "Delete"
|
|||||||
msgstr "Διαγραφή"
|
msgstr "Διαγραφή"
|
||||||
|
|
||||||
#: ElectronClient/gui/SideBar/SideBar.min.js:276
|
#: ElectronClient/gui/SideBar/SideBar.min.js:276
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Remove tag \"%s\" and its descendant tags from all notes?"
|
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/SideBar.min.js:278
|
||||||
#: ElectronClient/gui/SideBar.min.js:292
|
#: ElectronClient/gui/SideBar.min.js:292
|
||||||
@@ -1031,9 +1035,8 @@ msgstr "έντονη γραφή"
|
|||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:145
|
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:145
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:261
|
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:261
|
||||||
#, fuzzy
|
|
||||||
msgid "emphasised text"
|
msgid "emphasised text"
|
||||||
msgstr "πλάγια γραφή"
|
msgstr "κείμενο με έμφαση"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147
|
#: ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js:147
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:263
|
#: ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js:263
|
||||||
@@ -1091,9 +1094,8 @@ msgstr ""
|
|||||||
"επεξεργαστείτε τη σημείωση στον editor."
|
"επεξεργαστείτε τη σημείωση στον editor."
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/plugins/lists.js:2151
|
||||||
#, fuzzy
|
|
||||||
msgid "Checkbox list"
|
msgid "Checkbox list"
|
||||||
msgstr "Πλαίσιο ελέγχου"
|
msgstr "Λίστα πλαισίων ελέγχου"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:470
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:470
|
||||||
#: ElectronClient/gui/ConfigScreen.min.js:642
|
#: ElectronClient/gui/ConfigScreen.min.js:642
|
||||||
@@ -1110,11 +1112,11 @@ msgstr "Επισύναψη αρχείου"
|
|||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:535
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:535
|
||||||
msgid "Code Block"
|
msgid "Code Block"
|
||||||
msgstr ""
|
msgstr "Μπλοκ Κώδικα"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:544
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:544
|
||||||
msgid "Inline Code"
|
msgid "Inline Code"
|
||||||
msgstr ""
|
msgstr "Ενσωματωμένος κώδικας"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:559
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:559
|
||||||
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:80
|
#: ElectronClient/gui/NoteEditor/commands/editorCommandDeclarations.js:80
|
||||||
@@ -1124,13 +1126,12 @@ msgstr "Εισαγωγή Ημερομηνίας Ώρας"
|
|||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||||
msgid "Drop notes or files here"
|
msgid "Drop notes or files here"
|
||||||
msgstr ""
|
msgstr "Απόθεση σημειώσεων ή αρχείων εδώ"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||||
#: ElectronClient/gui/MainScreen/MainScreen.min.js:401
|
#: ElectronClient/gui/MainScreen/MainScreen.min.js:401
|
||||||
#, fuzzy
|
|
||||||
msgid "Code View"
|
msgid "Code View"
|
||||||
msgstr "Κώδικας"
|
msgstr "Προβολή κώδικα"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
#: ElectronClient/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js:903
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@@ -1138,6 +1139,8 @@ msgid ""
|
|||||||
"Please wait for all attachments to be downloaded and decrypted. You may also "
|
"Please wait for all attachments to be downloaded and decrypted. You may also "
|
||||||
"switch to %s to edit the note."
|
"switch to %s to edit the note."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Παρακαλώ περιμένετε να γίνει λήψη και αποκρυπτογράφηση όλων των συνημμένων. "
|
||||||
|
"Μπορείτε επίσης να μεταβείτε στο %s για να επεξεργαστείτε τη σημείωση."
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/utils/useMessageHandler.js:70
|
#: ElectronClient/gui/NoteEditor/utils/useMessageHandler.js:70
|
||||||
#: ElectronClient/gui/NoteText.min.js:833
|
#: ElectronClient/gui/NoteText.min.js:833
|
||||||
@@ -1162,7 +1165,7 @@ msgstr "Αποθήκευση ως..."
|
|||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:65
|
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:65
|
||||||
msgid "Reveal file in folder"
|
msgid "Reveal file in folder"
|
||||||
msgstr ""
|
msgstr "Αποκάλυψη αρχείου στο φάκελο"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:73
|
#: ElectronClient/gui/NoteEditor/utils/contextMenu.js:73
|
||||||
#: ElectronClient/gui/NoteText.min.js:797
|
#: ElectronClient/gui/NoteText.min.js:797
|
||||||
@@ -1342,7 +1345,7 @@ msgstr "Αναζήτηση..."
|
|||||||
|
|
||||||
#: ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js:16
|
#: ElectronClient/gui/MainScreen/commands/showNoteContentProperties.js:16
|
||||||
msgid "Statistics..."
|
msgid "Statistics..."
|
||||||
msgstr ""
|
msgstr "Στατιστικά..."
|
||||||
|
|
||||||
#: ElectronClient/gui/MainScreen/commands/renameFolder.js:17
|
#: ElectronClient/gui/MainScreen/commands/renameFolder.js:17
|
||||||
#: ElectronClient/gui/MainScreen/commands/renameTag.js:17
|
#: ElectronClient/gui/MainScreen/commands/renameTag.js:17
|
||||||
@@ -1605,13 +1608,12 @@ msgstr "Yποβολή"
|
|||||||
#: ElectronClient/gui/NoteList.min.js:147
|
#: ElectronClient/gui/NoteList.min.js:147
|
||||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
||||||
msgid "Custom order"
|
msgid "Custom order"
|
||||||
msgstr ""
|
msgstr "Προσαρμοσμένη σειρά"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteList.min.js:147
|
#: ElectronClient/gui/NoteList.min.js:147
|
||||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
||||||
#, fuzzy
|
|
||||||
msgid "View"
|
msgid "View"
|
||||||
msgstr "&Εμφάνιση"
|
msgstr "Προβολή"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteList.min.js:147
|
#: ElectronClient/gui/NoteList.min.js:147
|
||||||
#: ElectronClient/gui/NoteList/NoteList.min.js:152
|
#: 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 "
|
"To manually sort the notes, the sort order must be changed to \"%s\" in the "
|
||||||
"menu \"%s\" > \"%s\""
|
"menu \"%s\" > \"%s\""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Για να ταξινομήσετε τις σημειώσεις χειροκίνητα, η σειρά ταξινόμησης πρέπει "
|
||||||
|
"να αλλάξει σε \"%s\" στο μενού \"%s\" - \"%s\""
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteList.min.js:148
|
#: ElectronClient/gui/NoteList.min.js:148
|
||||||
#: ElectronClient/gui/NoteList/NoteList.min.js:153
|
#: ElectronClient/gui/NoteList/NoteList.min.js:153
|
||||||
#, fuzzy
|
|
||||||
msgid "Do it now"
|
msgid "Do it now"
|
||||||
msgstr "Λήψη τώρα:"
|
msgstr "Κάνε το τώρα"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteList.min.js:452
|
#: ElectronClient/gui/NoteList.min.js:452
|
||||||
#: ElectronClient/gui/NoteList/NoteList.min.js:425
|
#: ElectronClient/gui/NoteList/NoteList.min.js:425
|
||||||
@@ -1870,14 +1873,13 @@ msgid "Viewer"
|
|||||||
msgstr "Εμφάνιση"
|
msgstr "Εμφάνιση"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:107
|
#: ElectronClient/gui/NoteContentPropertiesDialog.js:107
|
||||||
#, fuzzy
|
|
||||||
msgid "Statistics"
|
msgid "Statistics"
|
||||||
msgstr "Κατάσταση"
|
msgstr "Στατιστικά"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:111
|
#: ElectronClient/gui/NoteContentPropertiesDialog.js:111
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Read time: %s min"
|
msgid "Read time: %s min"
|
||||||
msgstr ""
|
msgstr "Χρόνος ανάγνωσης: %s min"
|
||||||
|
|
||||||
#: ElectronClient/gui/NoteContentPropertiesDialog.js:112
|
#: ElectronClient/gui/NoteContentPropertiesDialog.js:112
|
||||||
#: ElectronClient/gui/ShareNoteDialog.js:175
|
#: ElectronClient/gui/ShareNoteDialog.js:175
|
||||||
@@ -2102,7 +2104,7 @@ msgstr "Έκδοση προφίλ: %s"
|
|||||||
#: ElectronClient/app.js:608
|
#: ElectronClient/app.js:608
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Keychain Supported: %s"
|
msgid "Keychain Supported: %s"
|
||||||
msgstr ""
|
msgstr "Υποστηριζόμενη κλειδοθήκη: %s"
|
||||||
|
|
||||||
#: ElectronClient/app.js:630 ElectronClient/app.js:706
|
#: ElectronClient/app.js:630 ElectronClient/app.js:706
|
||||||
msgid "&File"
|
msgid "&File"
|
||||||
@@ -2166,9 +2168,8 @@ msgid "Zoom Out"
|
|||||||
msgstr "Σμίκρινση"
|
msgstr "Σμίκρινση"
|
||||||
|
|
||||||
#: ElectronClient/app.js:860
|
#: ElectronClient/app.js:860
|
||||||
#, fuzzy
|
|
||||||
msgid "&Note"
|
msgid "&Note"
|
||||||
msgstr "Σημείωση"
|
msgstr "&Σημείωση"
|
||||||
|
|
||||||
#: ElectronClient/app.js:870
|
#: ElectronClient/app.js:870
|
||||||
msgid "&Tools"
|
msgid "&Tools"
|
||||||
@@ -2230,7 +2231,6 @@ msgstr ""
|
|||||||
"δεδομένα. Δεν πρόκειται να κοινοποιηθούν δεδομένα σε τρίτους."
|
"δεδομένα. Δεν πρόκειται να κοινοποιηθούν δεδομένα σε τρίτους."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/registry.js:156
|
#: ReactNativeClient/lib/registry.js:156
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Could not synchronise with OneDrive.\n"
|
"Could not synchronise with OneDrive.\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -2241,10 +2241,10 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Δεν ήταν δυνατός ο συγχρονισμός με το OneDrive.\n"
|
"Δεν ήταν δυνατός ο συγχρονισμός με το OneDrive.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive for Business, "
|
"Αυτό το σφάλμα συμβαίνει συχνά όταν χρησιμοποιείτε το OneDrive για "
|
||||||
"το οποίο δυστυχώς δεν υποστηρίζεται.\n"
|
"επιχειρήσεις, το οποίο δυστυχώς δεν μπορεί να υποστηριχθεί.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Παρακαλώ σκεφτείτε να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
|
"Εξετάστε το ενδεχόμενο να χρησιμοποιήσετε έναν κανονικό λογαριασμό OneDrive."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/logger.js:178
|
#: ReactNativeClient/lib/logger.js:178
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@@ -2258,7 +2258,7 @@ msgstr "Άγνωστο level ID: %s"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
|
#: ReactNativeClient/lib/SyncTargetAmazonS3.js:28
|
||||||
msgid "AWS S3"
|
msgid "AWS S3"
|
||||||
msgstr ""
|
msgstr "AWS S3"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
|
#: ReactNativeClient/lib/SyncTargetDropbox.js:25
|
||||||
msgid "Dropbox"
|
msgid "Dropbox"
|
||||||
@@ -2404,15 +2404,15 @@ msgstr "WebDAV password"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:189
|
#: ReactNativeClient/lib/models/Setting.js:189
|
||||||
msgid "AWS S3 bucket"
|
msgid "AWS S3 bucket"
|
||||||
msgstr ""
|
msgstr "AWS S3 bucket"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:200
|
#: ReactNativeClient/lib/models/Setting.js:200
|
||||||
msgid "AWS key"
|
msgid "AWS key"
|
||||||
msgstr ""
|
msgstr "AWS key"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:210
|
#: ReactNativeClient/lib/models/Setting.js:210
|
||||||
msgid "AWS secret"
|
msgid "AWS secret"
|
||||||
msgstr ""
|
msgstr "AWS secret"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:224
|
#: ReactNativeClient/lib/models/Setting.js:224
|
||||||
msgid "Attachment download behaviour"
|
msgid "Attachment download behaviour"
|
||||||
@@ -2463,15 +2463,15 @@ msgstr "Θέμα"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:315
|
#: ReactNativeClient/lib/models/Setting.js:315
|
||||||
msgid "Automatically switch theme to match system theme"
|
msgid "Automatically switch theme to match system theme"
|
||||||
msgstr ""
|
msgstr "Αυτόματη εναλλαγή θέματος ώστε να ταιριάζει με το θέμα συστήματος"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:327
|
#: ReactNativeClient/lib/models/Setting.js:327
|
||||||
msgid "Preferred light theme"
|
msgid "Preferred light theme"
|
||||||
msgstr ""
|
msgstr "Προτιμώμενο φωτεινό θέμα"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:341
|
#: ReactNativeClient/lib/models/Setting.js:341
|
||||||
msgid "Preferred dark theme"
|
msgid "Preferred dark theme"
|
||||||
msgstr ""
|
msgstr "Προτιμώμενο σκοτεινό θέμα"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:346
|
#: ReactNativeClient/lib/models/Setting.js:346
|
||||||
msgid "Show note counts"
|
msgid "Show note counts"
|
||||||
@@ -2510,6 +2510,7 @@ msgstr "Αυτόματη-σύζευξη αγκίστρων, παρενθέσεω
|
|||||||
#: ReactNativeClient/lib/models/Setting.js:395
|
#: ReactNativeClient/lib/models/Setting.js:395
|
||||||
msgid "Use CodeMirror as the code editor (WARNING: BETA)."
|
msgid "Use CodeMirror as the code editor (WARNING: BETA)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Χρησιμοποιείστε το CodeMirror ως τον επεξεργαστή κώδικα (ΠΡΟΣΟΧΗ: BETA)"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:397
|
#: ReactNativeClient/lib/models/Setting.js:397
|
||||||
#: ReactNativeClient/lib/models/Setting.js:415
|
#: ReactNativeClient/lib/models/Setting.js:415
|
||||||
@@ -2637,15 +2638,14 @@ msgid "Editor font family"
|
|||||||
msgstr "Οικογένεια γραμματοσειράς editor"
|
msgstr "Οικογένεια γραμματοσειράς editor"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:550
|
#: ReactNativeClient/lib/models/Setting.js:550
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"This should be a *monospace* font or some elements will render incorrectly. "
|
"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 "
|
"If the font is incorrect or empty, it will default to a generic monospace "
|
||||||
"font."
|
"font."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς δεν θα λειτουργήσει "
|
"H γραμματοσειρά πρέπει να είναι *monospace* αλλιώς κάποια στοιχεία δεν θα "
|
||||||
"σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα προεπιλεγεί μια γενική "
|
"εμφανιστούν σωστά. Εάν η γραμματοσειρά είναι εσφαλμένη ή κενή, θα "
|
||||||
"γραμματοσειρά *monospace*."
|
"προεπιλεγεί μια γενική γραμματοσειρά *monospace*."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:589
|
#: ReactNativeClient/lib/models/Setting.js:589
|
||||||
msgid "Custom stylesheet for Joplin-wide app styles"
|
msgid "Custom stylesheet for Joplin-wide app styles"
|
||||||
@@ -2816,7 +2816,7 @@ msgstr "Διατήρηση ιστορικού σημειώσεων για"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:741
|
#: ReactNativeClient/lib/models/Setting.js:741
|
||||||
msgid "Notebook list growth factor"
|
msgid "Notebook list growth factor"
|
||||||
msgstr ""
|
msgstr "Συντελεστής ανάπτυξης λίστας σημειωματάριων"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:743
|
#: ReactNativeClient/lib/models/Setting.js:743
|
||||||
#: ReactNativeClient/lib/models/Setting.js:756
|
#: 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 "
|
"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."
|
"factor of 1.Restart app to see changes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Ο συντελεστής ανάπτυξης καθορίζει το πόσο θα αναπτυχθεί ή θα συρρίκνωθεί το "
|
||||||
|
"αντικειμένο ώστε να χωρά στον διαθέσιμο χώρο σε σχέση με τα άλλα "
|
||||||
|
"αντικείμενα. Έτσι, ένα στοιχείο με συντελεστή 2 θα πάρει διπλάσιο χώρο από "
|
||||||
|
"ένα στοιχείο με συντελεστή 1. Επανεκκινήστε την εφαρμογή για να δείτε "
|
||||||
|
"αλλαγές."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:754
|
#: ReactNativeClient/lib/models/Setting.js:754
|
||||||
msgid "Note list growth factor"
|
msgid "Note list growth factor"
|
||||||
msgstr ""
|
msgstr "Συντελεστής ανάπτυξης λίστας σημειώσεων"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:767
|
#: ReactNativeClient/lib/models/Setting.js:767
|
||||||
msgid "Note area growth factor"
|
msgid "Note area growth factor"
|
||||||
msgstr ""
|
msgstr "Συντελεστής ανάπτυξης περιοχής σημειώσεων"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Setting.js:919
|
#: ReactNativeClient/lib/models/Setting.js:919
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@@ -2910,30 +2915,32 @@ msgid "Downloaded"
|
|||||||
msgstr "Έχουν ληφθεί"
|
msgstr "Έχουν ληφθεί"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Resource.js:371
|
#: ReactNativeClient/lib/models/Resource.js:371
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Attachment conflict: \"%s\""
|
msgid "Attachment conflict: \"%s\""
|
||||||
msgstr "Επισυναπτόμενα"
|
msgstr "Διένεξη συνημμένου: \"%s\""
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Resource.js:372
|
#: ReactNativeClient/lib/models/Resource.js:372
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"There was a [conflict](%s) on the attachment below.\n"
|
"There was a [conflict](%s) on the attachment below.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"%s"
|
"%s"
|
||||||
msgstr "Παρουσιάστηκε σφάλμα κατά τη λήψη αυτού του συνημμένου:"
|
msgstr ""
|
||||||
|
"Υπήρξε μια [διένεξη](%s) στο παρακάτω συνημμένο.\n"
|
||||||
|
"\n"
|
||||||
|
"%s"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Tag.js:384
|
#: ReactNativeClient/lib/models/Tag.js:384
|
||||||
#, fuzzy
|
|
||||||
msgid "Cannot move tag to this location."
|
msgid "Cannot move tag to this location."
|
||||||
msgstr "Δεν είναι δυνατή η μετακίνηση του σημειωματάριου σε αυτήν τη θέση"
|
msgstr "Δεν είναι δυνατή η μετακίνηση της ετικέτας σε αυτήν τη θέση."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Tag.js:429
|
#: ReactNativeClient/lib/models/Tag.js:429
|
||||||
msgid "Tag name cannot start or end with a `/`."
|
msgid "Tag name cannot start or end with a `/`."
|
||||||
msgstr ""
|
msgstr "Το όνομα της ετικέτας δεν μπορεί να ξεκινάει ή να τελειώνει με `/`."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Tag.js:431
|
#: ReactNativeClient/lib/models/Tag.js:431
|
||||||
msgid "Tag name cannot contain `//`."
|
msgid "Tag name cannot contain `//`."
|
||||||
msgstr ""
|
msgstr "Το όνομα της ετικέτας δεν μπορεί να περιέχει `//`."
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Tag.js:482
|
#: ReactNativeClient/lib/models/Tag.js:482
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@@ -2946,7 +2953,7 @@ msgstr "ημερομηνία δημιουργίας"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/models/Note.js:28
|
#: ReactNativeClient/lib/models/Note.js:28
|
||||||
msgid "custom order"
|
msgid "custom order"
|
||||||
msgstr ""
|
msgstr "προσαρμοσμένη σειρά"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/models/Note.js:92
|
#: ReactNativeClient/lib/models/Note.js:92
|
||||||
msgid "This note does not have geolocation information."
|
msgid "This note does not have geolocation information."
|
||||||
@@ -3564,12 +3571,11 @@ msgstr "File system"
|
|||||||
|
|
||||||
#: ReactNativeClient/lib/commands/historyForward.js:16
|
#: ReactNativeClient/lib/commands/historyForward.js:16
|
||||||
msgid "Forward"
|
msgid "Forward"
|
||||||
msgstr ""
|
msgstr "Μπροστά"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/commands/synchronize.js:17
|
#: ReactNativeClient/lib/commands/synchronize.js:17
|
||||||
#, fuzzy
|
|
||||||
msgid "Synchronize"
|
msgid "Synchronize"
|
||||||
msgstr "Συγχρονισμός"
|
msgstr "Συγχρόνισε"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/services/InteropService_Exporter_Jex.js:29
|
#: ReactNativeClient/lib/services/InteropService_Exporter_Jex.js:29
|
||||||
msgid "There is no data to export."
|
msgid "There is no data to export."
|
||||||
@@ -3737,9 +3743,10 @@ msgid "Directory"
|
|||||||
msgstr "Φάκελος"
|
msgstr "Φάκελος"
|
||||||
|
|
||||||
#: ReactNativeClient/lib/services/InteropService.js:174
|
#: ReactNativeClient/lib/services/InteropService.js:174
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Cannot load \"%s\" module for format \"%s\" and output \"%s\""
|
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
|
#: ReactNativeClient/lib/services/InteropService.js:232
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,10 @@ require('app-module-path').addPath(__dirname);
|
|||||||
const filterParser = require('lib/services/searchengine/filterParser.js').default;
|
const filterParser = require('lib/services/searchengine/filterParser.js').default;
|
||||||
// import filterParser from 'lib/services/searchengine/filterParser.js';
|
// import filterParser from 'lib/services/searchengine/filterParser.js';
|
||||||
|
|
||||||
const makeTerm = (name, value, negated, quoted = false) => {
|
const makeTerm = (name, value, negated, quoted = false, wildcard = false) => {
|
||||||
if (name !== 'text') { return { name, value, negated }; } else { return { name, value, negated, quoted }; }
|
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', () => {
|
describe('filterParser should be correct filter for keyword', () => {
|
||||||
@@ -108,6 +110,9 @@ describe('filterParser should be correct filter for keyword', () => {
|
|||||||
|
|
||||||
searchString = 'tag:bl*sphemy';
|
searchString = 'tag:bl*sphemy';
|
||||||
expect(filterParser(searchString)).toContain(makeTerm('tag', 'bl%sphemy', false));
|
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', () => {
|
it('wildcard notebooks', () => {
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ describe('services_SearchFilter', function() {
|
|||||||
|
|
||||||
await Tag.setNoteTagsByTitles(n1.id, ['tag1', 'tag2']);
|
await Tag.setNoteTagsByTitles(n1.id, ['tag1', 'tag2']);
|
||||||
await Tag.setNoteTagsByTitles(n2.id, ['tag2', 'tag3']);
|
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();
|
await engine.syncTables();
|
||||||
|
|
||||||
@@ -304,6 +304,10 @@ describe('services_SearchFilter', function() {
|
|||||||
expect(rows.length).toBe(2);
|
expect(rows.length).toBe(2);
|
||||||
expect(ids(rows)).toContain(n1.id);
|
expect(ids(rows)).toContain(n1.id);
|
||||||
expect(ids(rows)).toContain(n3.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 () => {
|
it('should support filtering by notebook', asyncTest(async () => {
|
||||||
|
|||||||
@@ -141,5 +141,23 @@ describe('services_SearchFuzzy', function() {
|
|||||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should leave wild card searches alone', asyncTest(async () => {
|
||||||
|
let rows;
|
||||||
|
const n1 = await Note.save({ title: 'abc def' });
|
||||||
|
const n2 = await Note.save({ title: 'abcc ghi' });
|
||||||
|
const n3 = await Note.save({ title: 'abccc ghi' });
|
||||||
|
const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||||
|
const n5 = await Note.save({ title: 'wxy zzz' });
|
||||||
|
|
||||||
|
await engine.syncTables();
|
||||||
|
|
||||||
|
rows = await engine.search('abc*', { fuzzy: true });
|
||||||
|
|
||||||
|
expect(rows.length).toBe(4);
|
||||||
|
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||||
|
expect(rows.map(r=>r.id)).toContain(n2.id);
|
||||||
|
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||||
|
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import LockHandler from 'lib/services/synchronizer/LockHandler';
|
import LockHandler from 'lib/services/synchronizer/LockHandler';
|
||||||
import MigrationHandler from 'lib/services/synchronizer/MigrationHandler';
|
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:
|
// To create a sync target snapshot for the current syncVersion:
|
||||||
// - In test-utils, set syncTargetName_ to "filesystem"
|
// - In test-utils, set syncTargetName_ to "filesystem"
|
||||||
@@ -70,6 +71,14 @@ describe('synchronizer_MigrationHandler', function() {
|
|||||||
done();
|
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 () => {
|
it('should not allow syncing if the sync target is out-dated', asyncTest(async () => {
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);
|
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriv
|
|||||||
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
|
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const S3 = require('aws-sdk/clients/s3');
|
const S3 = require('aws-sdk/clients/s3');
|
||||||
|
const { Dirnames } = require('lib/services/synchronizer/utils/types');
|
||||||
|
|
||||||
const databases_ = [];
|
const databases_ = [];
|
||||||
let synchronizers_ = [];
|
let synchronizers_ = [];
|
||||||
@@ -438,6 +439,7 @@ async function initFileApi() {
|
|||||||
|
|
||||||
fileApi.setLogger(logger);
|
fileApi.setLogger(logger);
|
||||||
fileApi.setSyncTargetId(syncTargetId_);
|
fileApi.setSyncTargetId(syncTargetId_);
|
||||||
|
fileApi.setTempDirName(Dirnames.Temp);
|
||||||
fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0;
|
fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0;
|
||||||
|
|
||||||
fileApis_[syncTargetId_] = fileApi;
|
fileApis_[syncTargetId_] = fileApi;
|
||||||
|
|||||||
@@ -105,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
|
// 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
|
// 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.
|
// 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) => {
|
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)
|
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||||
|
|||||||
2
ElectronClient/package-lock.json
generated
2
ElectronClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"version": "1.1.244",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"version": "1.1.244",
|
"version": "1.1.0",
|
||||||
"description": "Joplin for Desktop",
|
"description": "Joplin for Desktop",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -177,8 +177,9 @@ class Dialog extends React.PureComponent {
|
|||||||
return output.join(' ');
|
return output.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
keywords() {
|
async keywords(searchQuery) {
|
||||||
return this.props.highlightedWords;
|
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
|
||||||
|
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
markupToHtml() {
|
markupToHtml() {
|
||||||
@@ -226,7 +227,7 @@ class Dialog extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
const searchKeywords = this.keywords();
|
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 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), {});
|
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
|
||||||
|
|
||||||
@@ -282,7 +283,7 @@ class Dialog extends React.PureComponent {
|
|||||||
this.setState({
|
this.setState({
|
||||||
listType: listType,
|
listType: listType,
|
||||||
results: results,
|
results: results,
|
||||||
keywords: this.keywords(),
|
keywords: await this.keywords(searchQuery),
|
||||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||||
resultsInBody: resultsInBody,
|
resultsInBody: resultsInBody,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ const KvStore = require('lib/services/KvStore');
|
|||||||
const MigrationService = require('lib/services/MigrationService');
|
const MigrationService = require('lib/services/MigrationService');
|
||||||
const { toSystemSlashes } = require('lib/path-utils.js');
|
const { toSystemSlashes } = require('lib/path-utils.js');
|
||||||
|
|
||||||
|
// const ntpClient = require('lib/vendor/ntp-client');
|
||||||
|
// ntpClient.dgram = require('dgram');
|
||||||
|
|
||||||
class BaseApplication {
|
class BaseApplication {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.logger_ = new Logger();
|
this.logger_ = new Logger();
|
||||||
@@ -673,6 +676,7 @@ class BaseApplication {
|
|||||||
reg.dispatch = () => {};
|
reg.dispatch = () => {};
|
||||||
|
|
||||||
BaseService.logger_ = this.logger_;
|
BaseService.logger_ = this.logger_;
|
||||||
|
// require('lib/ntpDate').setLogger(reg.logger());
|
||||||
|
|
||||||
this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` });
|
this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` });
|
||||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class FileApiDriverDropbox {
|
|||||||
metadataToStat_(md, path) {
|
metadataToStat_(md, path) {
|
||||||
const output = {
|
const output = {
|
||||||
path: path,
|
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',
|
isDir: md['.tag'] === 'folder',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError');
|
|||||||
const ArrayUtils = require('lib/ArrayUtils');
|
const ArrayUtils = require('lib/ArrayUtils');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
function requestCanBeRepeated(error) {
|
function requestCanBeRepeated(error) {
|
||||||
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
||||||
@@ -57,6 +58,65 @@ class FileApi {
|
|||||||
this.tempDirName_ = null;
|
this.tempDirName_ = null;
|
||||||
this.driver_.fileApi_ = this;
|
this.driver_.fileApi_ = this;
|
||||||
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
|
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
|
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ class Setting extends BaseModel {
|
|||||||
value: false,
|
value: false,
|
||||||
type: Setting.TYPE_BOOL,
|
type: Setting.TYPE_BOOL,
|
||||||
section: 'note',
|
section: 'note',
|
||||||
public: true,
|
public: mobilePlatform === 'ios',
|
||||||
appTypes: ['mobile'],
|
appTypes: ['mobile'],
|
||||||
label: () => 'Opt-in to the editor beta',
|
label: () => 'Opt-in to the editor beta',
|
||||||
description: () => 'This beta adds list continuation, Markdown preview, and Markdown shortcuts. If you find bugs, please report them in the Discourse forum.',
|
description: () => 'This beta adds list continuation, Markdown preview, and Markdown shortcuts. If you find bugs, please report them in the Discourse forum.',
|
||||||
|
|||||||
59
ReactNativeClient/lib/ntpDate.ts
Normal file
59
ReactNativeClient/lib/ntpDate.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
const ntpClient = require('lib/vendor/ntp-client');
|
||||||
|
const { Logger } = require('lib/logger.js');
|
||||||
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
|
let nextSyncTime = 0;
|
||||||
|
let timeOffset = 0;
|
||||||
|
let logger = new Logger();
|
||||||
|
|
||||||
|
const fetchingTimeMutex = new Mutex();
|
||||||
|
|
||||||
|
const server = {
|
||||||
|
domain: 'pool.ntp.org',
|
||||||
|
port: 123,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function networkTime():Promise<Date> {
|
||||||
|
return new Promise(function(resolve:Function, reject:Function) {
|
||||||
|
ntpClient.getNetworkTime(server.domain, server.port, function(error:any, date:Date) {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldSyncTime() {
|
||||||
|
return !nextSyncTime || Date.now() > nextSyncTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLogger(v:any) {
|
||||||
|
logger = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function():Promise<Date> {
|
||||||
|
if (shouldSyncTime()) {
|
||||||
|
const release = await fetchingTimeMutex.acquire();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (shouldSyncTime()) {
|
||||||
|
const date = await networkTime();
|
||||||
|
nextSyncTime = Date.now() + 60 * 1000;
|
||||||
|
timeOffset = date.getTime() - Date.now();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Could not get NTP time - falling back to device time:', error);
|
||||||
|
// Fallback to device time since
|
||||||
|
// most of the time it's actually correct
|
||||||
|
nextSyncTime = Date.now() + 20 * 1000;
|
||||||
|
timeOffset = 0;
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(Date.now() + timeOffset);
|
||||||
|
}
|
||||||
@@ -434,7 +434,6 @@ class SearchEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async parseQuery(query, fuzzy = false) {
|
async parseQuery(query, fuzzy = false) {
|
||||||
// fuzzy = false;
|
|
||||||
const trimQuotes = (str) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
|
const trimQuotes = (str) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
|
||||||
|
|
||||||
let allTerms = [];
|
let allTerms = [];
|
||||||
@@ -453,18 +452,22 @@ class SearchEngine {
|
|||||||
const fuzzyScore = [];
|
const fuzzyScore = [];
|
||||||
let numFuzzyMatches = [];
|
let numFuzzyMatches = [];
|
||||||
let terms = null;
|
let terms = null;
|
||||||
if (fuzzy) {
|
|
||||||
const fuzzyText = await this.fuzzifier(textTerms.filter(x => !x.quoted).map(x => trimQuotes(x.value)));
|
|
||||||
const fuzzyTitle = await this.fuzzifier(titleTerms.map(x => trimQuotes(x.value)));
|
|
||||||
const fuzzyBody = await this.fuzzifier(bodyTerms.map(x => trimQuotes(x.value)));
|
|
||||||
const phraseSearches = textTerms.filter(x => x.quoted).map(x => x.value);
|
|
||||||
|
|
||||||
// Save number of matches we got for each word
|
if (fuzzy) {
|
||||||
// fuzzifier() is currently set to return at most 3 matches)
|
const fuzzyText = await this.fuzzifier(textTerms.filter(x => !(x.quoted || x.wildcard)).map(x => trimQuotes(x.value)));
|
||||||
|
const fuzzyTitle = await this.fuzzifier(titleTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
|
||||||
|
const fuzzyBody = await this.fuzzifier(bodyTerms.filter(x => !x.wildcard).map(x => trimQuotes(x.value)));
|
||||||
|
|
||||||
|
const phraseTextSearch = textTerms.filter(x => x.quoted);
|
||||||
|
const wildCardSearch = textTerms.concat(titleTerms).concat(bodyTerms).filter(x => x.wildcard);
|
||||||
|
|
||||||
|
// Save number of fuzzy matches we got for each word
|
||||||
|
// fuzzifier() is currently set to return at most 3 matches
|
||||||
// We need to know which fuzzy words go together so that we can filter out notes that don't contain a required word.
|
// We need to know which fuzzy words go together so that we can filter out notes that don't contain a required word.
|
||||||
numFuzzyMatches = fuzzyText.concat(fuzzyTitle).concat(fuzzyBody).map(x => x.length);
|
numFuzzyMatches = fuzzyText.concat(fuzzyTitle).concat(fuzzyBody).map(x => x.length);
|
||||||
for (let i = 0; i < phraseSearches.length; i++) {
|
for (let i = 0; i < phraseTextSearch.length + wildCardSearch.length; i++) {
|
||||||
numFuzzyMatches.push(1); // Phrase searches are preserved without fuzzification
|
// Phrase searches and wildcard searches are preserved without fuzzification (A single match)
|
||||||
|
numFuzzyMatches.push(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedFuzzyText = [].concat.apply([], fuzzyText);
|
const mergedFuzzyText = [].concat.apply([], fuzzyText);
|
||||||
@@ -474,18 +477,33 @@ class SearchEngine {
|
|||||||
const fuzzyTextTerms = mergedFuzzyText.map(x => { return { name: 'text', value: x.word, negated: false, score: x.score }; });
|
const fuzzyTextTerms = mergedFuzzyText.map(x => { return { name: 'text', value: x.word, negated: false, score: x.score }; });
|
||||||
const fuzzyTitleTerms = mergedFuzzyTitle.map(x => { return { name: 'title', value: x.word, negated: false, score: x.score }; });
|
const fuzzyTitleTerms = mergedFuzzyTitle.map(x => { return { name: 'title', value: x.word, negated: false, score: x.score }; });
|
||||||
const fuzzyBodyTerms = mergedFuzzyBody.map(x => { return { name: 'body', value: x.word, negated: false, score: x.score }; });
|
const fuzzyBodyTerms = mergedFuzzyBody.map(x => { return { name: 'body', value: x.word, negated: false, score: x.score }; });
|
||||||
const phraseTextTerms = phraseSearches.map(x => { return { name: 'text', value: x, negated: false, score: 0 }; });
|
|
||||||
|
|
||||||
|
// Remove previous text, title and body and replace with fuzzy versions
|
||||||
allTerms = allTerms.filter(x => (x.name !== 'text' && x.name !== 'title' && x.name !== 'body'));
|
allTerms = allTerms.filter(x => (x.name !== 'text' && x.name !== 'title' && x.name !== 'body'));
|
||||||
|
|
||||||
allFuzzyTerms = allTerms.concat(fuzzyTextTerms).concat(fuzzyTitleTerms).concat(fuzzyBodyTerms).concat(phraseTextTerms);
|
// The order matters here!
|
||||||
|
// The text goes first, then title, then body, then phrase and finally wildcard
|
||||||
|
// This is because it needs to match with numFuzzyMathches.
|
||||||
|
allFuzzyTerms = allTerms.concat(fuzzyTextTerms).concat(fuzzyTitleTerms).concat(fuzzyBodyTerms).concat(phraseTextSearch).concat(wildCardSearch);
|
||||||
|
|
||||||
const allTextTerms = allFuzzyTerms.filter(x => x.name === 'title' || x.name === 'body' || x.name === 'text');
|
const allTextTerms = allFuzzyTerms.filter(x => x.name === 'title' || x.name === 'body' || x.name === 'text');
|
||||||
for (let i = 0; i < allTextTerms.length; i++) {
|
for (let i = 0; i < allTextTerms.length; i++) {
|
||||||
|
// Phrase searches and wildcard searches will get a fuzziness score of zero.
|
||||||
|
// This means that they will go first in the sort order (Even if there are other words with matches in the title)
|
||||||
|
// Undesirable?
|
||||||
fuzzyScore.push(allFuzzyTerms[i].score ? allFuzzyTerms[i].score : 0);
|
fuzzyScore.push(allFuzzyTerms[i].score ? allFuzzyTerms[i].score : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
terms = { _: fuzzyTextTerms.concat(phraseTextTerms).map(x =>trimQuotes(x.value)), 'title': fuzzyTitleTerms.map(x =>trimQuotes(x.value)), 'body': fuzzyBodyTerms.map(x =>trimQuotes(x.value)) };
|
const wildCardTextTerms = wildCardSearch.filter(x => x.name === 'text').map(x =>trimQuotes(x.value));
|
||||||
|
const wildCardTitleTerms = wildCardSearch.filter(x => x.name === 'title').map(x =>trimQuotes(x.value));
|
||||||
|
const wildCardBodyTerms = wildCardSearch.filter(x => x.name === 'body').map(x =>trimQuotes(x.value));
|
||||||
|
const phraseTextTerms = phraseTextSearch.map(x => trimQuotes(x.value));
|
||||||
|
|
||||||
|
terms = {
|
||||||
|
_: fuzzyTextTerms.map(x => trimQuotes(x.value)).concat(phraseTextTerms).concat(wildCardTextTerms),
|
||||||
|
title: fuzzyTitleTerms.map(x => trimQuotes(x.value)).concat(wildCardTitleTerms),
|
||||||
|
body: fuzzyBodyTerms.map(x => trimQuotes(x.value)).concat(wildCardBodyTerms),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const nonNegatedTextTerms = textTerms.length + titleTerms.length + bodyTerms.length;
|
const nonNegatedTextTerms = textTerms.length + titleTerms.length + bodyTerms.length;
|
||||||
for (let i = 0; i < nonNegatedTextTerms; i++) {
|
for (let i = 0; i < nonNegatedTextTerms; i++) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface Term {
|
|||||||
value: string
|
value: string
|
||||||
negated: boolean
|
negated: boolean
|
||||||
quoted?: boolean
|
quoted?: boolean
|
||||||
|
wildcard?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeTerm = (name: string, value: string): Term => {
|
const makeTerm = (name: string, value: string): Term => {
|
||||||
@@ -82,13 +83,13 @@ const parseQuery = (query: string): Term[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'tag' || name === 'notebook' || name === 'resource' || name === 'sourceurl') {
|
if (name === 'tag' || name === 'notebook' || name === 'resource' || name === 'sourceurl') {
|
||||||
result.push({ name, value: value.replace(/[*]/g, '%'), negated }); // for wildcard search
|
result.push({ name, value: trimQuotes(value.replace(/[*]/g, '%')), negated }); // for wildcard search
|
||||||
} else if (name === 'title' || name === 'body') {
|
} else if (name === 'title' || name === 'body') {
|
||||||
// Trim quotes since we don't support phrase query here
|
// Trim quotes since we don't support phrase query here
|
||||||
// eg. Split title:"hello world" to title:hello title:world
|
// eg. Split title:"hello world" to title:hello title:world
|
||||||
const values = trimQuotes(value).split(/[\s-_]+/);
|
const values = trimQuotes(value).split(/[\s-_]+/);
|
||||||
values.forEach(value => {
|
values.forEach(value => {
|
||||||
result.push({ name, value, negated });
|
result.push({ name, value, negated, wildcard: value.indexOf('*') >= 0 });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
result.push({ name, value, negated });
|
result.push({ name, value, negated });
|
||||||
@@ -97,9 +98,21 @@ const parseQuery = (query: string): Term[] => {
|
|||||||
// Every word is quoted if not already.
|
// Every word is quoted if not already.
|
||||||
// By quoting the word, FTS match query will take care of removing dashes and other word seperators.
|
// By quoting the word, FTS match query will take care of removing dashes and other word seperators.
|
||||||
if (value.startsWith('-')) {
|
if (value.startsWith('-')) {
|
||||||
result.push({ name: 'text', value: quote(value.slice(1)) , negated: true, quoted: quoted(value) });
|
result.push({
|
||||||
|
name: 'text',
|
||||||
|
value: quote(value.slice(1)),
|
||||||
|
negated: true,
|
||||||
|
quoted: quoted(value),
|
||||||
|
wildcard: value.indexOf('*') >= 0,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
result.push({ name: 'text', value: quote(value), negated: false, quoted: quoted(value) });
|
result.push({
|
||||||
|
name: 'text',
|
||||||
|
value: quote(value),
|
||||||
|
negated: false,
|
||||||
|
quoted: quoted(value),
|
||||||
|
wildcard: value.indexOf('*') >= 0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Dirnames } from './utils/types';
|
import { Dirnames } from './utils/types';
|
||||||
|
|
||||||
const JoplinError = require('lib/JoplinError');
|
const JoplinError = require('lib/JoplinError');
|
||||||
const { time } = require('lib/time-utils');
|
const { time } = require('lib/time-utils');
|
||||||
const { fileExtension, filename } = require('lib/path-utils.js');
|
const { fileExtension, filename } = require('lib/path-utils.js');
|
||||||
@@ -98,8 +99,8 @@ export default class LockHandler {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
private lockIsActive(lock:Lock):boolean {
|
private lockIsActive(lock:Lock, currentDate:Date):boolean {
|
||||||
return Date.now() - lock.updatedTime < this.lockTtl;
|
return currentDate.getTime() - lock.updatedTime < this.lockTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
||||||
@@ -112,11 +113,12 @@ export default class LockHandler {
|
|||||||
// of that type instead.
|
// of that type instead.
|
||||||
async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
||||||
const locks = await this.locks(lockType);
|
const locks = await this.locks(lockType);
|
||||||
|
const currentDate = await this.api_.remoteDate();
|
||||||
|
|
||||||
if (lockType === LockType.Exclusive) {
|
if (lockType === LockType.Exclusive) {
|
||||||
const activeLocks = locks
|
const activeLocks = locks
|
||||||
.slice()
|
.slice()
|
||||||
.filter((lock:Lock) => this.lockIsActive(lock))
|
.filter((lock:Lock) => this.lockIsActive(lock, currentDate))
|
||||||
.sort((a:Lock, b:Lock) => {
|
.sort((a:Lock, b:Lock) => {
|
||||||
if (a.updatedTime === b.updatedTime) {
|
if (a.updatedTime === b.updatedTime) {
|
||||||
return a.clientId < b.clientId ? -1 : +1;
|
return a.clientId < b.clientId ? -1 : +1;
|
||||||
@@ -134,7 +136,7 @@ export default class LockHandler {
|
|||||||
for (const lock of locks) {
|
for (const lock of locks) {
|
||||||
if (clientType && lock.clientType !== clientType) continue;
|
if (clientType && lock.clientType !== clientType) continue;
|
||||||
if (clientId && lock.clientId !== clientId) continue;
|
if (clientId && lock.clientId !== clientId) continue;
|
||||||
if (this.lockIsActive(lock)) return lock;
|
if (this.lockIsActive(lock, currentDate)) return lock;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -290,6 +292,12 @@ export default class LockHandler {
|
|||||||
if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared
|
if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared
|
||||||
|
|
||||||
if (!hasActiveLock) {
|
if (!hasActiveLock) {
|
||||||
|
// If the previous lock has expired, we shouldn't try to acquire a new one. This is because other clients might have performed
|
||||||
|
// in the meantime operations that invalidates the current operation. For example, another client might have upgraded the
|
||||||
|
// sync target in the meantime, so any active operation should be cancelled here. Or if the current client was upgraded
|
||||||
|
// the sync target, another client might have synced since then, making any cached data invalid.
|
||||||
|
// In some cases it should be safe to re-acquire a lock but adding support for this would make the algorithm more complex
|
||||||
|
// without much benefits.
|
||||||
error = new JoplinError('Lock has expired', 'lockExpired');
|
error = new JoplinError('Lock has expired', 'lockExpired');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -90,9 +90,11 @@ export default class MigrationHandler extends BaseService {
|
|||||||
// it the lock handler will break. So we create the directory now.
|
// it the lock handler will break. So we create the directory now.
|
||||||
// Also if the sync target version is 0, it means it's a new one so we need the
|
// Also if the sync target version is 0, it means it's a new one so we need the
|
||||||
// lock folder first before doing anything else.
|
// lock folder first before doing anything else.
|
||||||
|
// Temp folder is needed too to get remoteDate() call to work.
|
||||||
if (syncTargetInfo.version === 0 || syncTargetInfo.version === 1) {
|
if (syncTargetInfo.version === 0 || syncTargetInfo.version === 1) {
|
||||||
this.logger().info('MigrationHandler: Sync target version is 0 or 1 - creating "locks" directory:', syncTargetInfo);
|
this.logger().info('MigrationHandler: Sync target version is 0 or 1 - creating "locks" and "temp" directory:', syncTargetInfo);
|
||||||
await this.api_.mkdir(Dirnames.Locks);
|
await this.api_.mkdir(Dirnames.Locks);
|
||||||
|
await this.api_.mkdir(Dirnames.Temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger().info('MigrationHandler: Acquiring exclusive lock');
|
this.logger().info('MigrationHandler: Acquiring exclusive lock');
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ class Synchronizer {
|
|||||||
let syncLock = null;
|
let syncLock = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.api().setTempDirName(Dirnames.Temp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const syncTargetInfo = await this.migrationHandler().checkCanSync();
|
const syncTargetInfo = await this.migrationHandler().checkCanSync();
|
||||||
|
|
||||||
@@ -321,8 +323,6 @@ class Synchronizer {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.api().setTempDirName(Dirnames.Temp);
|
|
||||||
|
|
||||||
syncLock = await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
|
syncLock = await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
|
||||||
|
|
||||||
this.lockHandler().startAutoLockRefresh(syncLock, (error) => {
|
this.lockHandler().startAutoLockRefresh(syncLock, (error) => {
|
||||||
|
|||||||
155
ReactNativeClient/lib/vendor/ntp-client.js
vendored
Normal file
155
ReactNativeClient/lib/vendor/ntp-client.js
vendored
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/*
|
||||||
|
* ntp-client
|
||||||
|
* https://github.com/moonpyk/node-ntp-client
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013 Clément Bourgeois
|
||||||
|
* Licensed under the MIT license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
// 2020-08-09: We vendor the package because although it works
|
||||||
|
// it has several bugs and is currently unmaintained
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const Buffer = require('buffer').Buffer;
|
||||||
|
|
||||||
|
(function (exports) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
exports.defaultNtpPort = 123;
|
||||||
|
exports.defaultNtpServer = "pool.ntp.org";
|
||||||
|
|
||||||
|
exports.dgram = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of acceptable time to await for a response from the remote server.
|
||||||
|
* Configured default to 10 seconds.
|
||||||
|
*/
|
||||||
|
exports.ntpReplyTimeout = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the current NTP Time from the given server and port.
|
||||||
|
* @param {string} server IP/Hostname of the remote NTP Server
|
||||||
|
* @param {number} port Remote NTP Server port number
|
||||||
|
* @param {function(Object, Date)} callback(err, date) Async callback for
|
||||||
|
* the result date or eventually error.
|
||||||
|
*/
|
||||||
|
exports.getNetworkTime = function (server, port, callback) {
|
||||||
|
if (callback === null || typeof callback !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server = server || exports.defaultNtpServer;
|
||||||
|
port = port || exports.defaultNtpPort;
|
||||||
|
|
||||||
|
if (!exports.dgram) throw new Error('dgram package has not been set!!');
|
||||||
|
|
||||||
|
var client = exports.dgram.createSocket("udp4"),
|
||||||
|
ntpData = new Buffer(48);
|
||||||
|
|
||||||
|
// RFC 2030 -> LI = 0 (no warning, 2 bits), VN = 3 (IPv4 only, 3 bits), Mode = 3 (Client Mode, 3 bits) -> 1 byte
|
||||||
|
// -> rtol(LI, 6) ^ rotl(VN, 3) ^ rotl(Mode, 0)
|
||||||
|
// -> = 0x00 ^ 0x18 ^ 0x03
|
||||||
|
ntpData[0] = 0x1B;
|
||||||
|
|
||||||
|
for (var i = 1; i < 48; i++) {
|
||||||
|
ntpData[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some errors can happen before/after send() or cause send() to was impossible.
|
||||||
|
// Some errors will also be given to the send() callback.
|
||||||
|
// We keep a flag, therefore, to prevent multiple callbacks.
|
||||||
|
// NOTE : the error callback is not generalised, as the client has to lose the connection also, apparently.
|
||||||
|
var errorFired = false;
|
||||||
|
|
||||||
|
function closeClient(client) {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (error) {
|
||||||
|
// Doesn't mater if it could not be closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout = setTimeout(function () {
|
||||||
|
closeClient(client);
|
||||||
|
|
||||||
|
if (errorFired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(new Error("Timeout waiting for NTP response."), null);
|
||||||
|
errorFired = true;
|
||||||
|
}, exports.ntpReplyTimeout);
|
||||||
|
|
||||||
|
client.on('error', function (err) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
|
||||||
|
if (errorFired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(err, null);
|
||||||
|
errorFired = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: To make it work in React Native (Android), a port need to be bound
|
||||||
|
// before calling client.send()
|
||||||
|
|
||||||
|
// client.bind(5555, '0.0.0.0', function() {
|
||||||
|
client.send(ntpData, 0, ntpData.length, port, server, function (err) {
|
||||||
|
if (err) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (errorFired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(err, null);
|
||||||
|
errorFired = true;
|
||||||
|
closeClient(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.once('message', function (msg) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
closeClient(client);
|
||||||
|
|
||||||
|
// Offset to get to the "Transmit Timestamp" field (time at which the reply
|
||||||
|
// departed the server for the client, in 64-bit timestamp format."
|
||||||
|
var offsetTransmitTime = 40,
|
||||||
|
intpart = 0,
|
||||||
|
fractpart = 0;
|
||||||
|
|
||||||
|
// Get the seconds part
|
||||||
|
for (var i = 0; i <= 3; i++) {
|
||||||
|
intpart = 256 * intpart + msg[offsetTransmitTime + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the seconds fraction
|
||||||
|
for (i = 4; i <= 7; i++) {
|
||||||
|
fractpart = 256 * fractpart + msg[offsetTransmitTime + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var milliseconds = (intpart * 1000 + (fractpart * 1000) / 0x100000000);
|
||||||
|
|
||||||
|
// **UTC** time
|
||||||
|
var date = new Date("Jan 01 1900 GMT");
|
||||||
|
date.setUTCMilliseconds(date.getUTCMilliseconds() + milliseconds);
|
||||||
|
|
||||||
|
callback(null, date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.demo = function (argv) {
|
||||||
|
exports.getNetworkTime(
|
||||||
|
exports.defaultNtpServer,
|
||||||
|
exports.defaultNtpPort,
|
||||||
|
function (err, date) {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(date);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}(exports));
|
||||||
@@ -401,6 +401,7 @@ async function initialize(dispatch, messageHandler) {
|
|||||||
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
|
reg.setShowErrorMessageBoxHandler((message) => { alert(message); });
|
||||||
|
|
||||||
BaseService.logger_ = mainLogger;
|
BaseService.logger_ = mainLogger;
|
||||||
|
// require('lib/ntpDate').setLogger(reg.logger());
|
||||||
|
|
||||||
reg.logger().info('====================================');
|
reg.logger().info('====================================');
|
||||||
reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`);
|
reg.logger().info(`Starting application ${Setting.value('appId')} (${Setting.value('env')})`);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ For example, if a client is currently syncing, it must stop doing so if it could
|
|||||||
|
|
||||||
For example, if a client is upgrading a target, it must stop doing so if it couldn't refresh the lock within Y seconds.
|
For example, if a client is upgrading a target, it must stop doing so if it couldn't refresh the lock within Y seconds.
|
||||||
|
|
||||||
|
If the previous lock has expired, we shouldn't try to acquire a new one. This is because other clients, seeing no active lock, might have performed in the meantime operations that invalidates the current operation. For example, another client might have upgraded the sync target, so any active sync with an expired lock should be cancelled. Or if the current client was upgrading the sync target, another client might have synced since then, making any cached data invalid.
|
||||||
|
|
||||||
|
In some cases it should be safe to re-acquire a lock but adding support for this would make the algorithm more complex without much benefits.
|
||||||
|
|
||||||
# Acquiring a SYNC lock
|
# Acquiring a SYNC lock
|
||||||
|
|
||||||
- The client check if there is a valid EXCLUSIVE lock on the target
|
- The client check if there is a valid EXCLUSIVE lock on the target
|
||||||
|
|||||||
Reference in New Issue
Block a user