mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Merge branch 'dev' into release-1.1
This commit is contained in:
commit
b7523e1b21
@ -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
|
||||
|
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/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,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
@ -4,8 +4,10 @@ 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, quoted = false) => {
|
||||
if (name !== 'text') { return { name, value, negated }; } else { return { name, value, negated, quoted }; }
|
||||
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', () => {
|
||||
@ -108,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', () => {
|
||||
|
@ -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 () => {
|
||||
|
@ -141,5 +141,23 @@ describe('services_SearchFuzzy', function() {
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
}));
|
||||
|
||||
it('should leave wild card searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'abcc ghi' });
|
||||
const n3 = await Note.save({ title: 'abccc ghi' });
|
||||
const n4 = await Note.save({ title: 'abcccc ghi' });
|
||||
const n5 = await Note.save({ title: 'wxy zzz' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc*', { fuzzy: true });
|
||||
|
||||
expect(rows.length).toBe(4);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n2.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import LockHandler from 'lib/services/synchronizer/LockHandler';
|
||||
import MigrationHandler from 'lib/services/synchronizer/MigrationHandler';
|
||||
import { Dirnames } from 'lib/services/synchronizer/utils/types';
|
||||
|
||||
// To create a sync target snapshot for the current syncVersion:
|
||||
// - In test-utils, set syncTargetName_ to "filesystem"
|
||||
@ -70,6 +71,14 @@ describe('synchronizer_MigrationHandler', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should init a new sync target', asyncTest(async () => {
|
||||
// Check that basic folders "locks" and "temp" are created for new sync targets.
|
||||
await migrationHandler().upgrade(1);
|
||||
const result = await fileApi().list();
|
||||
expect(result.items.filter((i:any) => i.path === Dirnames.Locks).length).toBe(1);
|
||||
expect(result.items.filter((i:any) => i.path === Dirnames.Temp).length).toBe(1);
|
||||
}), specTimeout);
|
||||
|
||||
it('should not allow syncing if the sync target is out-dated', asyncTest(async () => {
|
||||
await synchronizer().start();
|
||||
await fileApi().put('info.json', `{"version":${Setting.value('syncVersion') - 1}}`);
|
||||
|
@ -50,6 +50,7 @@ const KeychainServiceDriver = require('lib/services/keychain/KeychainServiceDriv
|
||||
const KeychainServiceDriverDummy = require('lib/services/keychain/KeychainServiceDriver.dummy').default;
|
||||
const md5 = require('md5');
|
||||
const S3 = require('aws-sdk/clients/s3');
|
||||
const { Dirnames } = require('lib/services/synchronizer/utils/types');
|
||||
|
||||
const databases_ = [];
|
||||
let synchronizers_ = [];
|
||||
@ -438,6 +439,7 @@ async function initFileApi() {
|
||||
|
||||
fileApi.setLogger(logger);
|
||||
fileApi.setSyncTargetId(syncTargetId_);
|
||||
fileApi.setTempDirName(Dirnames.Temp);
|
||||
fileApi.requestRepeatCount_ = isNetworkSyncTarget_ ? 1 : 0;
|
||||
|
||||
fileApis_[syncTargetId_] = fileApi;
|
||||
|
@ -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
|
||||
// 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)
|
||||
|
2
ElectronClient/package-lock.json
generated
2
ElectronClient/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.1.244",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.1.244",
|
||||
"version": "1.1.0",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
@ -177,8 +177,9 @@ class Dialog extends React.PureComponent {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
keywords() {
|
||||
return this.props.highlightedWords;
|
||||
async keywords(searchQuery) {
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(searchQuery, false);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
markupToHtml() {
|
||||
@ -226,7 +227,7 @@ class Dialog extends React.PureComponent {
|
||||
}
|
||||
} else {
|
||||
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 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({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: this.keywords(),
|
||||
keywords: await this.keywords(searchQuery),
|
||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||
resultsInBody: resultsInBody,
|
||||
});
|
||||
|
@ -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();
|
||||
@ -673,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);
|
||||
|
@ -44,7 +44,7 @@ class FileApiDriverDropbox {
|
||||
metadataToStat_(md, path) {
|
||||
const output = {
|
||||
path: path,
|
||||
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
||||
updated_time: md.server_modified ? (new Date(md.server_modified)).getTime() : Date.now(),
|
||||
isDir: md['.tag'] === 'folder',
|
||||
};
|
||||
|
||||
|
@ -6,6 +6,7 @@ const JoplinError = require('lib/JoplinError');
|
||||
const ArrayUtils = require('lib/ArrayUtils');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
function requestCanBeRepeated(error) {
|
||||
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
||||
@ -57,6 +58,65 @@ class FileApi {
|
||||
this.tempDirName_ = null;
|
||||
this.driver_.fileApi_ = this;
|
||||
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
|
||||
this.remoteDateOffset_ = 0;
|
||||
this.remoteDateNextCheckTime_ = 0;
|
||||
this.remoteDateMutex_ = new Mutex();
|
||||
}
|
||||
|
||||
|
||||
async fetchRemoteDateOffset_() {
|
||||
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
|
||||
const startTime = Date.now();
|
||||
await this.put(tempFile, 'timeCheck');
|
||||
|
||||
// Normally it should be possible to read the file back immediately but
|
||||
// just in case, read it in a loop.
|
||||
const loopStartTime = Date.now();
|
||||
let stat = null;
|
||||
while (Date.now() - loopStartTime < 5000) {
|
||||
stat = await this.stat(tempFile);
|
||||
if (stat) break;
|
||||
await time.msleep(200);
|
||||
}
|
||||
|
||||
if (!stat) throw new Error('Timed out trying to get sync target clock time');
|
||||
|
||||
this.delete(tempFile); // No need to await for this call
|
||||
|
||||
const endTime = Date.now();
|
||||
const expectedTime = Math.round((endTime + startTime) / 2);
|
||||
return stat.updated_time - expectedTime;
|
||||
}
|
||||
|
||||
// Approximates the current time on the sync target. It caches the time offset to
|
||||
// improve performance.
|
||||
async remoteDate() {
|
||||
const shouldSyncTime = () => {
|
||||
return !this.remoteDateNextCheckTime_ || Date.now() > this.remoteDateNextCheckTime_;
|
||||
};
|
||||
|
||||
if (shouldSyncTime()) {
|
||||
const release = await this.remoteDateMutex_.acquire();
|
||||
|
||||
try {
|
||||
// Another call might have refreshed the time while we were waiting for the mutex,
|
||||
// so check again if we need to refresh.
|
||||
if (shouldSyncTime()) {
|
||||
this.remoteDateOffset_ = await this.fetchRemoteDateOffset_();
|
||||
// The sync target clock should rarely change but the device one might,
|
||||
// so we need to refresh relatively frequently.
|
||||
this.remoteDateNextCheckTime_ = Date.now() + 10 * 60 * 1000;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger().warn('Could not retrieve remote date - defaulting to device date:', error);
|
||||
this.remoteDateOffset_ = 0;
|
||||
this.remoteDateNextCheckTime_ = Date.now() + 60 * 1000;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
return new Date(Date.now() + this.remoteDateOffset_);
|
||||
}
|
||||
|
||||
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
|
||||
|
@ -422,7 +422,7 @@ class Setting extends BaseModel {
|
||||
value: false,
|
||||
type: Setting.TYPE_BOOL,
|
||||
section: 'note',
|
||||
public: true,
|
||||
public: mobilePlatform === 'ios',
|
||||
appTypes: ['mobile'],
|
||||
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.',
|
||||
|
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) {
|
||||
// fuzzy = false;
|
||||
const trimQuotes = (str) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
|
||||
|
||||
let allTerms = [];
|
||||
@ -453,18 +452,22 @@ class SearchEngine {
|
||||
const fuzzyScore = [];
|
||||
let numFuzzyMatches = [];
|
||||
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
|
||||
// fuzzifier() is currently set to return at most 3 matches)
|
||||
if (fuzzy) {
|
||||
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.
|
||||
numFuzzyMatches = fuzzyText.concat(fuzzyTitle).concat(fuzzyBody).map(x => x.length);
|
||||
for (let i = 0; i < phraseSearches.length; i++) {
|
||||
numFuzzyMatches.push(1); // Phrase searches are preserved without fuzzification
|
||||
for (let i = 0; i < phraseTextSearch.length + wildCardSearch.length; i++) {
|
||||
// Phrase searches and wildcard searches are preserved without fuzzification (A single match)
|
||||
numFuzzyMatches.push(1);
|
||||
}
|
||||
|
||||
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 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 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'));
|
||||
|
||||
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');
|
||||
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);
|
||||
}
|
||||
|
||||
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 {
|
||||
const nonNegatedTextTerms = textTerms.length + titleTerms.length + bodyTerms.length;
|
||||
for (let i = 0; i < nonNegatedTextTerms; i++) {
|
||||
|
@ -4,6 +4,7 @@ interface Term {
|
||||
value: string
|
||||
negated: boolean
|
||||
quoted?: boolean
|
||||
wildcard?: boolean
|
||||
}
|
||||
|
||||
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') {
|
||||
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') {
|
||||
// Trim quotes since we don't support phrase query here
|
||||
// eg. Split title:"hello world" to title:hello title:world
|
||||
const values = trimQuotes(value).split(/[\s-_]+/);
|
||||
values.forEach(value => {
|
||||
result.push({ name, value, negated });
|
||||
result.push({ name, value, negated, wildcard: value.indexOf('*') >= 0 });
|
||||
});
|
||||
} else {
|
||||
result.push({ name, value, negated });
|
||||
@ -97,9 +98,21 @@ const parseQuery = (query: string): Term[] => {
|
||||
// Every word is quoted if not already.
|
||||
// By quoting the word, FTS match query will take care of removing dashes and other word seperators.
|
||||
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 {
|
||||
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';
|
||||
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const { time } = require('lib/time-utils');
|
||||
const { fileExtension, filename } = require('lib/path-utils.js');
|
||||
@ -98,8 +99,8 @@ export default class LockHandler {
|
||||
return output;
|
||||
}
|
||||
|
||||
private lockIsActive(lock:Lock):boolean {
|
||||
return Date.now() - lock.updatedTime < this.lockTtl;
|
||||
private lockIsActive(lock:Lock, currentDate:Date):boolean {
|
||||
return currentDate.getTime() - lock.updatedTime < this.lockTtl;
|
||||
}
|
||||
|
||||
async hasActiveLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
||||
@ -112,11 +113,12 @@ export default class LockHandler {
|
||||
// of that type instead.
|
||||
async activeLock(lockType:LockType, clientType:string = null, clientId:string = null) {
|
||||
const locks = await this.locks(lockType);
|
||||
const currentDate = await this.api_.remoteDate();
|
||||
|
||||
if (lockType === LockType.Exclusive) {
|
||||
const activeLocks = locks
|
||||
.slice()
|
||||
.filter((lock:Lock) => this.lockIsActive(lock))
|
||||
.filter((lock:Lock) => this.lockIsActive(lock, currentDate))
|
||||
.sort((a:Lock, b:Lock) => {
|
||||
if (a.updatedTime === b.updatedTime) {
|
||||
return a.clientId < b.clientId ? -1 : +1;
|
||||
@ -134,7 +136,7 @@ export default class LockHandler {
|
||||
for (const lock of locks) {
|
||||
if (clientType && lock.clientType !== clientType) continue;
|
||||
if (clientId && lock.clientId !== clientId) continue;
|
||||
if (this.lockIsActive(lock)) return lock;
|
||||
if (this.lockIsActive(lock, currentDate)) return lock;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -290,6 +292,12 @@ export default class LockHandler {
|
||||
if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared
|
||||
|
||||
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');
|
||||
} else {
|
||||
try {
|
||||
|
@ -90,9 +90,11 @@ export default class MigrationHandler extends BaseService {
|
||||
// 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
|
||||
// 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) {
|
||||
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.Temp);
|
||||
}
|
||||
|
||||
this.logger().info('MigrationHandler: Acquiring exclusive lock');
|
||||
|
@ -305,6 +305,8 @@ class Synchronizer {
|
||||
let syncLock = null;
|
||||
|
||||
try {
|
||||
this.api().setTempDirName(Dirnames.Temp);
|
||||
|
||||
try {
|
||||
const syncTargetInfo = await this.migrationHandler().checkCanSync();
|
||||
|
||||
@ -321,8 +323,6 @@ class Synchronizer {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.api().setTempDirName(Dirnames.Temp);
|
||||
|
||||
syncLock = await this.lockHandler().acquireLock('sync', this.appType_, this.clientId_);
|
||||
|
||||
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); });
|
||||
|
||||
BaseService.logger_ = mainLogger;
|
||||
// require('lib/ntpDate').setLogger(reg.logger());
|
||||
|
||||
reg.logger().info('====================================');
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
- The client check if there is a valid EXCLUSIVE lock on the target
|
||||
|
Loading…
Reference in New Issue
Block a user