1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-13 22:12:50 +02:00

Merge pull request #5 from laurent22/master

Update fork from original repository
This commit is contained in:
rtmkrlv
2018-03-15 10:41:44 +02:00
committed by GitHub
119 changed files with 3055 additions and 1299 deletions

View File

@@ -43,6 +43,17 @@ That will create the executable file in the `dist` directory.
From `/ElectronClient` you can also run `run.sh` to run the app for testing. From `/ElectronClient` you can also run `run.sh` to run the app for testing.
## Building Electron application on Windows
```
cd Tools
npm install
cd ..\ElectronClient\app
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
npm install
yarn dist
```
# Building the Mobile application # Building the Mobile application
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab. First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab.

View File

@@ -1,6 +1,6 @@
# Reporting a bug # Reporting a bug
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/README_debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it. Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is. If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
@@ -16,5 +16,4 @@ Building the apps is relatively easy - please [see the build instructions](https
# Coding style # Coding style
- Only use tabs for indentation, not spaces. See the [prettier config](https://github.com/laurent22/joplin/blob/master/prettier.config.js).
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.

View File

@@ -496,7 +496,7 @@ class AppGui {
cmd = cmd.trim(); cmd = cmd.trim();
if (!cmd.length) return; if (!cmd.length) return;
this.logger().info('Got command: ' + cmd); // this.logger().debug('Got command: ' + cmd);
try { try {
let note = this.widget('noteList').currentItem; let note = this.widget('noteList').currentItem;
@@ -757,7 +757,7 @@ class AppGui {
if (statusBar.promptActive) processShortcutKeys = false; if (statusBar.promptActive) processShortcutKeys = false;
if (processShortcutKeys) { if (processShortcutKeys) {
this.logger().info('Shortcut:', shortcutKey, keymapItem); this.logger().debug('Shortcut:', shortcutKey, keymapItem);
this.currentShortcutKeys_ = []; this.currentShortcutKeys_ = [];

View File

@@ -292,7 +292,7 @@ class Application extends BaseApplication {
async execCommand(argv) { async execCommand(argv) {
if (!argv.length) return this.execCommand(['help']); if (!argv.length) return this.execCommand(['help']);
reg.logger().info('execCommand()', argv); // reg.logger().debug('execCommand()', argv);
const commandName = argv[0]; const commandName = argv[0];
this.activeCommand_ = this.findCommandByName(commandName); this.activeCommand_ = this.findCommandByName(commandName);

View File

@@ -53,9 +53,8 @@ function renderCommandHelp(cmd, width = null) {
desc.push(label); desc.push(label);
} }
if (md.description) { const description = Setting.keyDescription(md.key, 'cli');
desc.push(md.description()); if (description) desc.push(description);
}
desc.push(_('Type: %s.', md.isEnum ? _('Enum') : Setting.typeToString(md.type))); desc.push(_('Type: %s.', md.isEnum ? _('Enum') : Setting.typeToString(md.type)));
if (md.isEnum) desc.push(_('Possible values: %s.', Setting.enumOptionsDoc(md.key, '%s (%s)'))); if (md.isEnum) desc.push(_('Possible values: %s.', Setting.enumOptionsDoc(md.key, '%s (%s)')));

View File

@@ -87,6 +87,13 @@ process.stdout.on('error', function( err ) {
application.start(process.argv).catch((error) => { application.start(process.argv).catch((error) => {
console.error(_('Fatal error:')); if (error.code == 'flagError') {
console.error(error); console.error(error.message);
console.error(_('Type `joplin help` for usage information.'));
} else {
console.error(_('Fatal error:'));
console.error(error);
}
process.exit(1);
}); });

View File

@@ -18,7 +18,8 @@ msgstr ""
msgid "To delete a tag, untag the associated notes." msgid "To delete a tag, untag the associated notes."
msgstr "" msgstr ""
"Hebe die Markierungen zugehöriger Notizen auf, um eine Markierung zu löschen." "Um eine Markierung zu löschen, entferne diese bei allen damit verbundenen "
"Notizen."
msgid "Please select the note or notebook to be deleted first." msgid "Please select the note or notebook to be deleted first."
msgstr "" msgstr ""
@@ -26,19 +27,18 @@ msgstr ""
"soll." "soll."
msgid "Press Ctrl+D or type \"exit\" to exit the application" msgid "Press Ctrl+D or type \"exit\" to exit the application"
msgstr "Drücke Strg+D oder tippe \"exit\", um das Programm zu verlassen" msgstr "Drücke Strg+D oder tippe \"exit\" um das Programm zu verlassen"
#, javascript-format #, javascript-format
msgid "More than one item match \"%s\". Please narrow down your query." msgid "More than one item match \"%s\". Please narrow down your query."
msgstr "" msgstr ""
"Mehr als eine Notiz stimmt mit \"%s\" überein. Bitte schränke deine Suche " "Mehr als eine Notiz stimmt mit \"%s\" überein. Bitte die Suche einschränken."
"ein."
msgid "No notebook selected." msgid "No notebook selected."
msgstr "Kein Notizbuch ausgewählt." msgstr "Kein Notizbuch ausgewählt."
msgid "No notebook has been specified." msgid "No notebook has been specified."
msgstr "Kein Notizbuch wurde angegeben." msgstr "Kein Notizbuch wurde ausgewählt."
msgid "Y" msgid "Y"
msgstr "J" msgstr "J"
@@ -68,7 +68,7 @@ msgstr "Kann verschlüsseltes Objekt nicht ändern"
#, javascript-format #, javascript-format
msgid "Missing required argument: %s" msgid "Missing required argument: %s"
msgstr "Fehlendes benötigtes Argument: %s" msgstr "Fehlendes erforderliches Argument: %s"
#, javascript-format #, javascript-format
msgid "%s: %s" msgid "%s: %s"
@@ -104,8 +104,7 @@ msgstr ""
"gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt." "gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt."
msgid "Also displays unset and hidden config variables." msgid "Also displays unset and hidden config variables."
msgstr "" msgstr "Zeigt auch nicht gesetzte und versteckte Konfigurationsvariablen an."
"Zeigt auch nicht angegebene oder versteckte Konfigurationsvariablen an."
#, javascript-format #, javascript-format
msgid "%s = %s (%s)" msgid "%s = %s (%s)"
@@ -119,12 +118,12 @@ msgid ""
"Duplicates the notes matching <note> to [notebook]. If no notebook is " "Duplicates the notes matching <note> to [notebook]. If no notebook is "
"specified the note is duplicated in the current notebook." "specified the note is duplicated in the current notebook."
msgstr "" msgstr ""
"Dupliziert die Notizen die mit <note> übereinstimmen zu [Notizbuch]. Wenn " "Dupliziert die Notizen die mit <note> übereinstimmen in [Notizbuch]. Wenn "
"kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch " "kein Notizbuch angegeben ist, wird die Notiz in das aktuelle Notizbuch "
"kopiert." "kopiert."
msgid "Marks a to-do as done." msgid "Marks a to-do as done."
msgstr "Markiert ein To-Do als abgeschlossen." msgstr "Markiert ein To-Do als erledigt."
#, javascript-format #, javascript-format
msgid "Note is not a to-do: \"%s\"" msgid "Note is not a to-do: \"%s\""
@@ -134,7 +133,7 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, " "Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`." "`status` and `target-status`."
msgstr "" msgstr ""
"Verwaltet die E2EE-Konfiguration. Die Befehle sind `enable`, `disable`, " "Verwaltet die E2EE-Konfiguration. Die Befehle lauten `enable`, `disable`, "
"`decrypt`, `status` und `target-status`." "`decrypt`, `status` und `target-status`."
msgid "Enter master password:" msgid "Enter master password:"
@@ -147,8 +146,8 @@ msgid ""
"Starting decryption... Please wait as it may take several minutes depending " "Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt." "on how much there is to decrypt."
msgstr "" msgstr ""
"Entschlüsselung starten.... Warte bitte, da es einige Minuten dauern kann, " "Starte Entschlüsselung.... Bitte warten, da dies je nach Anzahl der "
"je nachdem, wie viel es zu entschlüsseln gibt." "betreffenden Objekte einige Minuten dauern kann."
msgid "Completed decryption." msgid "Completed decryption."
msgstr "Entschlüsselung abgeschlossen." msgstr "Entschlüsselung abgeschlossen."
@@ -161,7 +160,7 @@ msgstr "Deaktiviert"
#, javascript-format #, javascript-format
msgid "Encryption is: %s" msgid "Encryption is: %s"
msgstr "Die Verschlüsselung ist: %s" msgstr "Verschlüsselung ist: %s"
msgid "Edit note." msgid "Edit note."
msgstr "Notiz bearbeiten." msgstr "Notiz bearbeiten."
@@ -169,8 +168,8 @@ msgstr "Notiz bearbeiten."
msgid "" msgid ""
"No text editor is defined. Please set it using `config editor <editor-path>`" "No text editor is defined. Please set it using `config editor <editor-path>`"
msgstr "" msgstr ""
"Kein Textverarbeitungsprogramm angegeben. Bitte lege eines mit `config " "Kein Texteditor definiert. Bitte lege einen mit `config editor <Pfad-Zum-"
"editor <Pfad-Zum-Textverarbeitungsprogramm>` fest" "Texteditor>` fest"
msgid "No active notebook." msgid "No active notebook."
msgstr "Kein aktives Notizbuch." msgstr "Kein aktives Notizbuch."
@@ -192,20 +191,19 @@ msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert." msgstr "Die Notiz wurde gespeichert."
msgid "Exits the application." msgid "Exits the application."
msgstr "Schließt das Programm." msgstr "Beendet das Programm."
#, fuzzy
msgid "" msgid ""
"Exports Joplin data to the given path. By default, it will export the " "Exports Joplin data to the given path. By default, it will export the "
"complete database including notebooks, notes, tags and resources." "complete database including notebooks, notes, tags and resources."
msgstr "" msgstr ""
"Exportiert Joplins Dateien zu dem angegebenen Pfad. Standardmäßig wird die " "Exportiert Joplin Dateien in den angegebenen Pfad. Standardmäßig wird die "
"komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen und " "komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen und "
"Anhängen exportiert." "Anhängen exportiert."
#, fuzzy, javascript-format #, javascript-format
msgid "Destination format: %s" msgid "Destination format: %s"
msgstr "Datumsformat" msgstr "Zielformat: %s"
msgid "Exports only the given note." msgid "Exports only the given note."
msgstr "Exportiert nur die angegebene Notiz." msgstr "Exportiert nur die angegebene Notiz."
@@ -222,6 +220,8 @@ msgstr "Zeigt die Nutzungsstatistik an."
#, javascript-format #, javascript-format
msgid "For information on how to customise the shortcuts please visit %s" msgid "For information on how to customise the shortcuts please visit %s"
msgstr "" msgstr ""
"Für weitere Informationen über die Anpassung von Tastenkürzel besuche bitte "
"%s"
msgid "Shortcuts are not available in CLI mode." msgid "Shortcuts are not available in CLI mode."
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar." msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
@@ -230,11 +230,12 @@ msgid ""
"Type `help [command]` for more information about a command; or type `help " "Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information." "all` for the complete usage information."
msgstr "" msgstr ""
"Tippe `help [Befehl]` für weitere Informationen über einen Befehl; oder " "Tippe `help [Befehl]` um weitere Informationen über einen Befehl zu erhalten "
"tippe `help all` für die vollständigen Informationen zur Befehlsverwendung." "oder tippe `help all` für die vollständigen Informationen zur "
"Befehlsverwendung."
msgid "The possible commands are:" msgid "The possible commands are:"
msgstr "Mögliche Befehle sind:" msgstr "Mögliche Befehle lauten:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be refered to by title or ID, or "
@@ -243,13 +244,13 @@ msgid ""
msgstr "" msgstr ""
"In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre " "In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre "
"ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder " "ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder "
"das momentan ausgewählte Notizbuch oder die momentan ausgewählte Notiz zu " "das momentan ausgewählte Notizbuch oder die momentan ausgewählte Notiz "
"wählen. `$c` kann benutzt werden, um auf die momentane Auswahl zu verweisen." "auszuwählen. `$c` kann benutzt werden, um auf die momentane Auswahl zu "
"verweisen."
msgid "To move from one pane to another, press Tab or Shift+Tab." msgid "To move from one pane to another, press Tab or Shift+Tab."
msgstr "" msgstr ""
"Um ein von einem Fenster zu einem anderen zu wechseln, drücke Tab oder Shift" "Um von einem Fenster zu einem anderen zu wechseln, drücke Tab oder Shift+Tab."
"+Tab."
msgid "" msgid ""
"Use the arrows and page up/down to scroll the lists and text areas " "Use the arrows and page up/down to scroll the lists and text areas "
@@ -267,19 +268,18 @@ msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\""
msgid "To exit command line mode, press ESCAPE" msgid "To exit command line mode, press ESCAPE"
msgstr "Um den Kommandozeilen Modus zu beenden, drücke ESCAPE" msgstr "Um den Kommandozeilen Modus zu beenden, drücke ESCAPE"
#, fuzzy
msgid "" msgid ""
"For the list of keyboard shortcuts and config options, type `help keymap`" "For the list of keyboard shortcuts and config options, type `help keymap`"
msgstr "" msgstr ""
"Um die komplette Liste von verfügbaren Tastenkürzeln anzuzeigen, tippe `help " "Um die komplette Liste aller verfügbaren Tastenkürzeln anzuzeigen, tippe "
"shortcuts` ein" "`help keymap` ein"
msgid "Imports data into Joplin." msgid "Imports data into Joplin."
msgstr "" msgstr "Importiert Daten in Joplin."
#, fuzzy, javascript-format #, javascript-format
msgid "Source format: %s" msgid "Source format: %s"
msgstr "Ungültiger Befehl: %s" msgstr "Quellformat: %s"
msgid "Do not ask for confirmation." msgid "Do not ask for confirmation."
msgstr "Nicht nach einer Bestätigung fragen." msgstr "Nicht nach einer Bestätigung fragen."
@@ -458,7 +458,7 @@ msgid "Starting synchronisation..."
msgstr "Starte Synchronisation..." msgstr "Starte Synchronisation..."
msgid "Cancelling... Please wait." msgid "Cancelling... Please wait."
msgstr "Abbrechen... Bitte warten." msgstr "Abbrechen Bitte warten."
msgid "" msgid ""
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove " "<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
@@ -520,6 +520,10 @@ msgstr "Standard: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Mögliche Werte:" msgstr "Mögliche Werte:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Zeigt die Nutzungsstatistik an."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Schwerwiegender Fehler:" msgstr "Schwerwiegender Fehler:"
@@ -576,17 +580,18 @@ msgstr ""
#, javascript-format #, javascript-format
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr "Exportiere „%s“ ins „%s“ Format. Bitte warten..."
msgid "File"
msgstr "Datei"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr "Importiere „%s“ ins „%s“ Format. Bitte warten…"
#, fuzzy
msgid "PDF File"
msgstr "Datei"
msgid "File"
msgstr "Datei"
msgid "New note" msgid "New note"
msgstr "Neue Notiz" msgstr "Neue Notiz"
@@ -600,13 +605,15 @@ msgstr "Neues Notizbuch"
msgid "Import" msgid "Import"
msgstr "Importieren" msgstr "Importieren"
#, fuzzy
msgid "Export" msgid "Export"
msgstr "Importieren" msgstr "Exportieren"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr "%s ausblenden"
msgid "Quit" msgid "Quit"
msgstr "Verlassen" msgstr "Verlassen"
@@ -627,10 +634,10 @@ msgid "Search in all the notes"
msgstr "Alle Notizen durchsuchen" msgstr "Alle Notizen durchsuchen"
msgid "View" msgid "View"
msgstr "" msgstr "Ansicht"
msgid "Toggle editor layout" msgid "Toggle editor layout"
msgstr "" msgstr "Editor Layout umschalten"
msgid "Tools" msgid "Tools"
msgstr "Werkzeuge" msgstr "Werkzeuge"
@@ -639,7 +646,7 @@ msgid "Synchronisation status"
msgstr "Status der Synchronisation" msgstr "Status der Synchronisation"
msgid "Encryption options" msgid "Encryption options"
msgstr "Verschlüsselungsoptionen" msgstr "Verschlüsselung"
msgid "General Options" msgid "General Options"
msgstr "Allgemeine Einstellungen" msgstr "Allgemeine Einstellungen"
@@ -650,8 +657,12 @@ msgstr "Hilfe"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Webseite und Dokumentation" msgstr "Webseite und Dokumentation"
#, fuzzy
msgid "Make a donation"
msgstr "Webseite und Dokumentation"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr "Überprüfe auf Updates..."
msgid "About Joplin" msgid "About Joplin"
msgstr "Über Joplin" msgstr "Über Joplin"
@@ -660,12 +671,12 @@ msgstr "Über Joplin"
msgid "%s %s (%s, %s)" msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)" msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format #, javascript-format
msgid "Open %s" msgid "Open %s"
msgstr "Auf %s: %s" msgstr "Öffne %s"
msgid "Exit" msgid "Exit"
msgstr "" msgstr "Verlassen"
msgid "OK" msgid "OK"
msgstr "OK" msgstr "OK"
@@ -673,33 +684,24 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Abbrechen" msgstr "Abbrechen"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr "Die aktuelle Version ist up-to-date."
"Release notes:\n"
"\n"
"%s"
msgstr "Notizen löschen?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr "Es ist ein Update verfügbar! Soll dies jetzt heruntergeladen werden?"
msgid "Yes" msgid "Yes"
msgstr "" msgstr "Ja"
#, fuzzy
msgid "No" msgid "No"
msgstr "N" msgstr "Nein"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Synchronisation abbrechen" msgstr "Überprüfen der Synchronisationseinstellungen"
#, javascript-format #, javascript-format
msgid "Notes and settings are stored in: %s" msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen gespeichert in: %s" msgstr "Notizen und Einstellungen werden gespeichert in: %s"
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
@@ -765,15 +767,14 @@ msgstr ""
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder " "verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
"Notizbücher ursprünglich verschlüsselt wurden." "Notizbücher ursprünglich verschlüsselt wurden."
#, fuzzy
msgid "Missing Master Keys" msgid "Missing Master Keys"
msgstr "Hauptschlüssel" msgstr "Fehlender Master-Key"
msgid "" msgid ""
"The master keys with these IDs are used to encrypt some of your items, " "The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely " "however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation." "they will eventually be downloaded via synchronisation."
msgstr "" msgstr "Die Master-Keas dieser IDs werden für die Verschlüsselung einiger ..."
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -788,7 +789,7 @@ msgstr "Zurück"
msgid "" msgid ""
"New notebook \"%s\" will be created and file \"%s\" will be imported into it" "New notebook \"%s\" will be created and file \"%s\" will be imported into it"
msgstr "" msgstr ""
"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein " "Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird dort hinein "
"importiert" "importiert"
msgid "Please create a notebook first." msgid "Please create a notebook first."
@@ -807,7 +808,7 @@ msgid "Separate each tag by a comma."
msgstr "Trenne jede Markierung mit einem Komma." msgstr "Trenne jede Markierung mit einem Komma."
msgid "Rename notebook:" msgid "Rename notebook:"
msgstr "Benne Notizbuch um:" msgstr "Notizbuch umbenennen:"
msgid "Set alarm:" msgid "Set alarm:"
msgstr "Alarm erstellen:" msgstr "Alarm erstellen:"
@@ -854,11 +855,10 @@ msgstr ""
"(+) Knopf drückst." "(+) Knopf drückst."
msgid "Open..." msgid "Open..."
msgstr "" msgstr "Öffne..."
#, fuzzy
msgid "Save as..." msgid "Save as..."
msgstr "Änderungen speichern" msgstr "Sichern unter..."
#, javascript-format #, javascript-format
msgid "Unsupported link or message: %s" msgid "Unsupported link or message: %s"
@@ -878,18 +878,18 @@ msgid ""
"This note has no content. Click on \"%s\" to toggle the editor and edit the " "This note has no content. Click on \"%s\" to toggle the editor and edit the "
"note." "note."
msgstr "" msgstr ""
"Diese Notiz hat keinen Inhalt. Klicke auf „%s“ um den Editor zu aktivieren "
"und die Notiz zu bearbeiten."
#, fuzzy
msgid "to-do" msgid "to-do"
msgstr "Neues To-Do" msgstr "To-Do"
#, fuzzy
msgid "note" msgid "note"
msgstr "Neue Notiz" msgstr "Notiz"
#, fuzzy, javascript-format #, javascript-format
msgid "Creating new %s..." msgid "Creating new %s..."
msgstr "Importiere Notizen..." msgstr "Erstelle neue %s..."
msgid "Refresh" msgid "Refresh"
msgstr "Aktualisieren" msgstr "Aktualisieren"
@@ -942,9 +942,8 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system" msgid "File system"
msgstr "Dateisystem" msgstr "Dateisystem"
#, fuzzy
msgid "Nextcloud" msgid "Nextcloud"
msgstr "Nextcloud (Beta)" msgstr "Nextcloud"
msgid "OneDrive" msgid "OneDrive"
msgstr "OneDrive" msgstr "OneDrive"
@@ -952,9 +951,8 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)" msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Nur für Tests)" msgstr "OneDrive Dev (Nur für Tests)"
#, fuzzy
msgid "WebDAV" msgid "WebDAV"
msgstr "Nextcloud WebDAV URL" msgstr "WebDAV"
#, javascript-format #, javascript-format
msgid "Unknown log level: %s" msgid "Unknown log level: %s"
@@ -1029,6 +1027,16 @@ msgstr "Abbrechen..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Abgeschlossen: %s" msgstr "Abgeschlossen: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Schwerwiegender Fehler:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s" msgstr "Synchronisation ist bereits im Gange. Status: %s"
@@ -1037,7 +1045,7 @@ msgid "Encrypted"
msgstr "Verschlüsselt" msgstr "Verschlüsselt"
msgid "Encrypted items cannot be modified" msgid "Encrypted items cannot be modified"
msgstr "Verschlüsselte Objekte können nicht verändert werden." msgstr "Verschlüsselte Objekte können nicht verändert werden"
msgid "Conflicts" msgid "Conflicts"
msgstr "Konflikte" msgstr "Konflikte"
@@ -1094,40 +1102,45 @@ msgstr "Hell"
msgid "Dark" msgid "Dark"
msgstr "Dunkel" msgstr "Dunkel"
#, fuzzy
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Zeige unvollständige To-Dos oben in der Liste" msgstr "Zeige unvollständige To-Dos an oberster Stelle"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr "Sortiere Notizen nach"
#, fuzzy
msgid "Reverse sort order" msgid "Reverse sort order"
msgstr "Dreht die Sortierreihenfolge um." msgstr "Sortierreihenfolge umdrehen"
msgid "Save geo-location with notes" msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern" msgstr "Momentanen Standort zusammen mit Notizen speichern"
#, fuzzy
msgid "When creating a new to-do:" msgid "When creating a new to-do:"
msgstr "Erstellt ein neues To-Do." msgstr "Wenn eine neue To-Do erstellt wird:"
#, fuzzy
msgid "Focus title" msgid "Focus title"
msgstr "Notiz Titel:" msgstr "Fokussiere Titel"
msgid "Focus body" msgid "Focus body"
msgstr "" msgstr "Fokussiere Inhalt"
#, fuzzy
msgid "When creating a new note:" msgid "When creating a new note:"
msgstr "Erstellt eine neue Notiz." msgstr "Wenn eine neue Notiz erstellt wird:"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr "Zeige Tray Icon"
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr "Einstellen des Anwendungszooms" msgstr "Zoomstufe der Benutzeroberfläche"
msgid "Editor font family"
msgstr "Editor Schriftenfamilie"
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr ""
"Der Name der Schrift wird nicht überprüft. Ist dieser inkorrekt oder leer "
"wird eine generische Monospace Schrift verwendet."
msgid "Automatically update the application" msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren" msgstr "Die Applikation automatisch aktualisieren"
@@ -1180,57 +1193,56 @@ msgstr "Nextcloud Benutzername"
msgid "Nextcloud password" msgid "Nextcloud password"
msgstr "Nextcloud Passwort" msgstr "Nextcloud Passwort"
#, fuzzy
msgid "WebDAV URL" msgid "WebDAV URL"
msgstr "Nextcloud WebDAV URL" msgstr "WebDAV URL"
#, fuzzy
msgid "WebDAV username" msgid "WebDAV username"
msgstr "Nextcloud Benutzername" msgstr "WebDAV Benutzername"
#, fuzzy
msgid "WebDAV password" msgid "WebDAV password"
msgstr "Setze ein Passwort" msgstr "WebDAV Passwort"
#, javascript-format #, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s." msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s." msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
#, fuzzy
msgid "Joplin Export File" msgid "Joplin Export File"
msgstr "Evernote Export Dateien" msgstr "Joplin Export Datei"
msgid "Markdown" msgid "Markdown"
msgstr "" msgstr "Markdown"
msgid "Joplin Export Directory" msgid "Joplin Export Directory"
msgstr "" msgstr "Joplin Export Verzeichnis"
#, fuzzy
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Evernote Export Dateien" msgstr "Evernote Export Datei"
msgid "Directory"
msgstr "Verzeichnis"
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr "Das Modul „%s“ für das Format „%s“ kann nicht geladen werden"
#, javascript-format #, javascript-format
msgid "Please specify import format for %s" msgid "Please specify import format for %s"
msgstr "" msgstr "Bitte das Exportformat für %s angeben"
#, javascript-format #, javascript-format
msgid "" msgid ""
"This item is currently encrypted: %s \"%s\". Please wait for all items to be " "This item is currently encrypted: %s \"%s\". Please wait for all items to be "
"decrypted and try again." "decrypted and try again."
msgstr "" msgstr ""
"Dieses Objekt ist zur Zeit verschlüsselt: %s „%s“. Bitte warten bis alle "
"Objekte entschlüsselt wurden und versuche es dann erneut."
msgid "There is no data to export." msgid "There is no data to export."
msgstr "" msgstr "Keine Daten für den Export vorhanden."
#, fuzzy
msgid "Please specify the notebook where the notes should be imported to." msgid "Please specify the notebook where the notes should be imported to."
msgstr "" msgstr ""
"Bitte wähle aus, wohin der Synchronisations Status exportiert werden soll" "Bitte wähle aus, wohin der Synchronisations-Status exportiert werden soll."
msgid "Items that cannot be synchronised" msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden" msgstr "Objekte können nicht synchronisiert werden"
@@ -1320,6 +1332,9 @@ msgstr "Bestätigen"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Synchronisation abbrechen" msgstr "Synchronisation abbrechen"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Hauptschlüssel %s" msgstr "Hauptschlüssel %s"
@@ -1345,10 +1360,10 @@ msgid "Edit notebook"
msgstr "Notizbuch bearbeiten" msgstr "Notizbuch bearbeiten"
msgid "Show all" msgid "Show all"
msgstr "" msgstr "Zeige Alles"
msgid "Errors only" msgid "Errors only"
msgstr "" msgstr "Nur Fehler"
msgid "This note has been modified:" msgid "This note has been modified:"
msgstr "Diese Notiz wurde verändert:" msgstr "Diese Notiz wurde verändert:"
@@ -1406,6 +1421,15 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Willkommen" msgstr "Willkommen"
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr ""
#~ "Versionshinweise:\n"
#~ "\n"
#~ "%s"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importiert eine Evernote Notizbuch-Datei (.enex Datei)." #~ msgstr "Importiert eine Evernote Notizbuch-Datei (.enex Datei)."

View File

@@ -449,6 +449,9 @@ msgstr ""
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "" msgstr ""
msgid "Type `joplin help` for usage information."
msgstr ""
msgid "Fatal error:" msgid "Fatal error:"
msgstr "" msgstr ""
@@ -490,16 +493,16 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr ""
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "PDF File"
msgstr ""
msgid "File"
msgstr ""
msgid "New note" msgid "New note"
msgstr "" msgstr ""
@@ -515,6 +518,9 @@ msgstr ""
msgid "Export" msgid "Export"
msgstr "" msgstr ""
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -561,6 +567,9 @@ msgstr ""
msgid "Website and documentation" msgid "Website and documentation"
msgstr "" msgstr ""
msgid "Make a donation"
msgstr ""
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -584,11 +593,7 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#, javascript-format msgid "Current version is up-to-date."
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "" msgstr ""
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
@@ -600,9 +605,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "" msgstr ""
@@ -905,6 +907,16 @@ msgstr ""
msgid "Completed: %s" msgid "Completed: %s"
msgstr "" msgstr ""
#, javascript-format
msgid "Last error: %s"
msgstr ""
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "" msgstr ""
@@ -993,7 +1005,15 @@ msgstr ""
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1067,6 +1087,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "" msgstr ""
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1170,6 +1193,9 @@ msgstr ""
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "" msgstr ""
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""

View File

@@ -188,7 +188,6 @@ msgstr "La nota ha sido guardada."
msgid "Exits the application." msgid "Exits the application."
msgstr "Sale de la aplicación." msgstr "Sale de la aplicación."
#, fuzzy
msgid "" msgid ""
"Exports Joplin data to the given path. By default, it will export the " "Exports Joplin data to the given path. By default, it will export the "
"complete database including notebooks, notes, tags and resources." "complete database including notebooks, notes, tags and resources."
@@ -196,9 +195,9 @@ msgstr ""
"Exporta datos de Joplin al directorio indicado. Por defecto, se exportará la " "Exporta datos de Joplin al directorio indicado. Por defecto, se exportará la "
"base de datos completa incluyendo libretas, notas, etiquetas y recursos." "base de datos completa incluyendo libretas, notas, etiquetas y recursos."
#, fuzzy, javascript-format #, javascript-format
msgid "Destination format: %s" msgid "Destination format: %s"
msgstr "Formato de fecha" msgstr "Formato de destino: %s"
msgid "Exports only the given note." msgid "Exports only the given note."
msgstr "Exporta únicamente la nota indicada." msgstr "Exporta únicamente la nota indicada."
@@ -266,11 +265,11 @@ msgstr ""
"Para una lista de los atajos de teclado disponibles, escriba `help keymap`" "Para una lista de los atajos de teclado disponibles, escriba `help keymap`"
msgid "Imports data into Joplin." msgid "Imports data into Joplin."
msgstr "" msgstr "Importa los datos en Joplin."
#, fuzzy, javascript-format #, javascript-format
msgid "Source format: %s" msgid "Source format: %s"
msgstr "El comando no existe: %s" msgstr "Formato de origen: %s"
msgid "Do not ask for confirmation." msgid "Do not ask for confirmation."
msgstr "No requiere confirmación." msgstr "No requiere confirmación."
@@ -506,6 +505,9 @@ msgstr "Por defecto: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Claves/valores posbiles:" msgstr "Claves/valores posbiles:"
msgid "Type `joplin help` for usage information."
msgstr "Escriba `joplin help` para mostrar información de uso."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Error fatal:" msgstr "Error fatal:"
@@ -563,17 +565,18 @@ msgstr ""
#, javascript-format #, javascript-format
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr "Exportando el formato de \"%s\" a \"%s\". Por favor espere..."
msgid "File"
msgstr "Archivo"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr "Importando el formato de \"%s\" a \"%s\". Por favor espere..."
#, fuzzy
msgid "PDF File"
msgstr "Archivo"
msgid "File"
msgstr "Archivo"
msgid "New note" msgid "New note"
msgstr "Nueva nota" msgstr "Nueva nota"
@@ -587,9 +590,11 @@ msgstr "Nueva libreta"
msgid "Import" msgid "Import"
msgstr "Importar" msgstr "Importar"
#, fuzzy
msgid "Export" msgid "Export"
msgstr "Importar" msgstr "Exportar"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
@@ -637,6 +642,9 @@ msgstr "Ayuda"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Sitio web y documentación" msgstr "Sitio web y documentación"
msgid "Make a donation"
msgstr "Hacer una donación"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "Comprobar actualizaciones..." msgstr "Comprobar actualizaciones..."
@@ -660,15 +668,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr "La versión actual está actualizada."
"Release notes:\n"
"\n"
"%s"
msgstr ""
"Notas de la versión:\n"
"\n"
"%s"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "Hay disponible una actualización. ¿Quiere descargarla ahora?" msgstr "Hay disponible una actualización. ¿Quiere descargarla ahora?"
@@ -679,9 +680,6 @@ msgstr "Sí"
msgid "No" msgid "No"
msgstr "No" msgstr "No"
msgid "Current version is up-to-date."
msgstr "La versión actual está actualizada."
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Comprobar sincronización" msgstr "Comprobar sincronización"
@@ -859,6 +857,8 @@ msgid ""
"This note has no content. Click on \"%s\" to toggle the editor and edit the " "This note has no content. Click on \"%s\" to toggle the editor and edit the "
"note." "note."
msgstr "" msgstr ""
"Esta nota no tiene contenido. Pulse en \"%s\" para cambiar al editor y "
"editar la nota."
msgid "to-do" msgid "to-do"
msgstr "lista de tareas" msgstr "lista de tareas"
@@ -1005,6 +1005,16 @@ msgstr "Cancelando..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Completado: %s" msgstr "Completado: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Error fatal:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronización ya está en progreso. Estado: %s" msgstr "La sincronización ya está en progreso. Estado: %s"
@@ -1069,16 +1079,14 @@ msgstr "Claro"
msgid "Dark" msgid "Dark"
msgstr "Oscuro" msgstr "Oscuro"
#, fuzzy
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Mostrar tareas incompletas al inicio de las listas" msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr "Ordenar notas por"
#, fuzzy
msgid "Reverse sort order" msgid "Reverse sort order"
msgstr "Invierte el orden." msgstr "Invierte el orden"
msgid "Save geo-location with notes" msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas" msgstr "Guardar geolocalización en las notas"
@@ -1098,9 +1106,19 @@ msgstr "Cuando se crear una nota nueva:"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "Mostrar icono en la bandeja" msgstr "Mostrar icono en la bandeja"
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr "Establecer el porcentaje de aumento de la aplicación" msgstr "Establecer el porcentaje de aumento de la aplicación"
msgid "Editor font family"
msgstr "Fuente del editor"
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr ""
"El nombre de la fuente no se comprobado. Si es incorrecto o está vacío, se "
"utilizará una fuente genérica monoespaciada."
msgid "Automatically update the application" msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente" msgstr "Actualizar la aplicación automáticamente"
@@ -1165,40 +1183,42 @@ msgstr "Contraseña de WebDAV"
msgid "Invalid option value: \"%s\". Possible values are: %s." msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s." msgstr "Opción inválida: «%s». Los valores posibles son: %s."
#, fuzzy
msgid "Joplin Export File" msgid "Joplin Export File"
msgstr "Archivos exportados de Evernote" msgstr "Archivo de exportación de Joplin"
msgid "Markdown" msgid "Markdown"
msgstr "" msgstr "Markdown"
msgid "Joplin Export Directory" msgid "Joplin Export Directory"
msgstr "" msgstr "Directorio para exportar de Joplin"
#, fuzzy
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Archivos exportados de Evernote" msgstr "Archivo exportado de Evernote"
msgid "Directory"
msgstr "Directorio"
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr "No se puede cargar el módulo \"%s\" para el formato \"%s\""
#, javascript-format #, javascript-format
msgid "Please specify import format for %s" msgid "Please specify import format for %s"
msgstr "" msgstr "Por favor especifique el formato para importar de %s"
#, javascript-format #, javascript-format
msgid "" msgid ""
"This item is currently encrypted: %s \"%s\". Please wait for all items to be " "This item is currently encrypted: %s \"%s\". Please wait for all items to be "
"decrypted and try again." "decrypted and try again."
msgstr "" msgstr ""
"El elemento se encuentra cifrado: %s \"%s\". Por favor espere a que todos "
"los elementos estén descifrados y pruebe de nuevo."
msgid "There is no data to export." msgid "There is no data to export."
msgstr "" msgstr "No hay datos para exportar."
#, fuzzy
msgid "Please specify the notebook where the notes should be imported to." msgid "Please specify the notebook where the notes should be imported to."
msgstr "Seleccione a dónde se debería exportar el estado de sincronización" msgstr "Por favor especifique la libreta donde las notas deben ser importadas."
msgid "Items that cannot be synchronised" msgid "Items that cannot be synchronised"
msgstr "Elementos que no se pueden sincronizar" msgstr "Elementos que no se pueden sincronizar"
@@ -1286,6 +1306,9 @@ msgstr "Confirmar"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Cancelar sincronización" msgstr "Cancelar sincronización"
msgid "Joplin website"
msgstr "Sitio web de Joplin"
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Clave maestra %s" msgstr "Clave maestra %s"
@@ -1370,6 +1393,15 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Bienvenido" msgstr "Bienvenido"
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr ""
#~ "Notas de la versión:\n"
#~ "\n"
#~ "%s"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importar una libreta de Evernote (archivo .enex)." #~ msgstr "Importar una libreta de Evernote (archivo .enex)."

View File

@@ -503,6 +503,10 @@ msgstr "Lehenetsia: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Litezkeen balioak:" msgstr "Litezkeen balioak:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Erakutsi erabilera datuak."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Aio! Agur! :_( " msgstr "Aio! Agur! :_( "
@@ -560,16 +564,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "Fitxategia"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "Fitxategia"
msgid "File"
msgstr "Fitxategia"
msgid "New note" msgid "New note"
msgstr "Ohar berria" msgstr "Ohar berria"
@@ -586,6 +591,9 @@ msgstr "Inportatu"
msgid "Export" msgid "Export"
msgstr "Inportatu" msgstr "Inportatu"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -632,6 +640,10 @@ msgstr "Laguntza"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Web orria eta dokumentazioa (en)" msgstr "Web orria eta dokumentazioa (en)"
#, fuzzy
msgid "Make a donation"
msgstr "Web orria eta dokumentazioa (en)"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -655,12 +667,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Utzi" msgstr "Utzi"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "Oharrak ezabatu?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -672,9 +680,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "E" msgstr "E"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Sinkronizazioa utzi" msgstr "Sinkronizazioa utzi"
@@ -1006,6 +1011,16 @@ msgstr "Bertan behera uzten..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Osatuta: %s" msgstr "Osatuta: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Aio! Agur! :_( "
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinkronizazioa hasita dago. Egoera: %s" msgstr "Sinkronizazioa hasita dago. Egoera: %s"
@@ -1101,9 +1116,18 @@ msgstr "Ohar berria sortzen du."
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" #, fuzzy
msgid "Global zoom percentage"
msgstr "Ezarri aplikazioaren zoomaren ehunekoa" msgstr "Ezarri aplikazioaren zoomaren ehunekoa"
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
msgstr "Automatikoki eguneratu aplikazioa" msgstr "Automatikoki eguneratu aplikazioa"
@@ -1184,6 +1208,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Evernotetik esportatutako fitxategiak" msgstr "Evernotetik esportatutako fitxategiak"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1290,6 +1317,9 @@ msgstr "Baieztatu"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Sinkronizazioa utzi" msgstr "Sinkronizazioa utzi"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Pasahitz Nagusia %s" msgstr "Pasahitz Nagusia %s"
@@ -1373,6 +1403,13 @@ msgstr "Oraindik ez duzu koadernorik. Sortu bat (+) botoian sakatuta."
msgid "Welcome" msgid "Welcome"
msgstr "Ongi etorri!" msgstr "Ongi etorri!"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Oharrak ezabatu?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Inportatu Evernote koaderno fitxategia (.enex fitxategia)." #~ msgstr "Inportatu Evernote koaderno fitxategia (.enex fitxategia)."

View File

@@ -504,6 +504,9 @@ msgstr "Défaut : %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Clefs/Valeurs possibles :" msgstr "Clefs/Valeurs possibles :"
msgid "Type `joplin help` for usage information."
msgstr "Tapez `Joplin help` pour afficher l'aide."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Erreur fatale :" msgstr "Erreur fatale :"
@@ -561,16 +564,16 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "Exporter vers \"%s\" au format \"%s\". Veuillez patienter..." msgstr "Exporter vers \"%s\" au format \"%s\". Veuillez patienter..."
msgid "File"
msgstr "Fichier"
msgid "Directory"
msgstr "Dossier"
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "Importer depuis \"%s\" au format \"%s\". Veuillez patienter..." msgstr "Importer depuis \"%s\" au format \"%s\". Veuillez patienter..."
msgid "PDF File"
msgstr "Fichier PDF"
msgid "File"
msgstr "Fichier"
msgid "New note" msgid "New note"
msgstr "Nouvelle note" msgstr "Nouvelle note"
@@ -586,6 +589,9 @@ msgstr "Importer"
msgid "Export" msgid "Export"
msgstr "Exporter" msgstr "Exporter"
msgid "Print"
msgstr "Imprimer"
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "Cacher %s" msgstr "Cacher %s"
@@ -632,6 +638,9 @@ msgstr "Aide"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Documentation en ligne" msgstr "Documentation en ligne"
msgid "Make a donation"
msgstr "Faire un don"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "Vérifier les mises à jour..." msgstr "Vérifier les mises à jour..."
@@ -655,15 +664,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
#, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr "La version actuelle est à jour."
"Release notes:\n"
"\n"
"%s"
msgstr ""
"Notes de version :\n"
"\n"
"%s"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -675,9 +677,6 @@ msgstr "Oui"
msgid "No" msgid "No"
msgstr "Non" msgstr "Non"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Vérifier config synchronisation" msgstr "Vérifier config synchronisation"
@@ -1011,6 +1010,16 @@ msgstr "Annulation..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Terminé : %s" msgstr "Terminé : %s"
#, javascript-format
msgid "Last error: %s"
msgstr "Dernière erreur : %s"
msgid "Idle"
msgstr "Arrêté"
msgid "In progress"
msgstr "En cours"
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "La synchronisation est déjà en cours. État : %s" msgstr "La synchronisation est déjà en cours. État : %s"
@@ -1081,7 +1090,7 @@ msgid "Sort notes by"
msgstr "Trier les notes par" msgstr "Trier les notes par"
msgid "Reverse sort order" msgid "Reverse sort order"
msgstr "Inverser l'ordre." msgstr "Inverser l'ordre"
msgid "Save geo-location with notes" msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes" msgstr "Enregistrer l'emplacement avec les notes"
@@ -1101,9 +1110,19 @@ msgstr "Lors de la création d'une note :"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "Afficher icône dans la zone de notifications" msgstr "Afficher icône dans la zone de notifications"
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr "Niveau de zoom" msgstr "Niveau de zoom"
msgid "Editor font family"
msgstr "Police de l'éditeur"
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr ""
"Le nom de la police ne sera pas vérifié. Si incorrect ou vide une police "
"monospace sera utilisée par défaut."
msgid "Automatically update the application" msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement" msgstr "Mettre à jour le logiciel automatiquement"
@@ -1180,6 +1199,9 @@ msgstr "Dossier d'export Joplin"
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Fichiers d'export Evernote" msgstr "Fichiers d'export Evernote"
msgid "Directory"
msgstr "Dossier"
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "Impossible de charger module \"%s\" pour le format \"%s\"" msgstr "Impossible de charger module \"%s\" pour le format \"%s\""
@@ -1290,6 +1312,9 @@ msgstr "Confirmer"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Annuler synchronisation" msgstr "Annuler synchronisation"
msgid "Joplin website"
msgstr "Site web de Joplin"
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Clef maître %s" msgstr "Clef maître %s"
@@ -1375,6 +1400,15 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Bienvenue" msgstr "Bienvenue"
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr ""
#~ "Notes de version :\n"
#~ "\n"
#~ "%s"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importer un carnet Evernote (fichier .enex)." #~ msgstr "Importer un carnet Evernote (fichier .enex)."

View File

@@ -510,6 +510,10 @@ msgstr "Default: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Mogući ključevi/vrijednosti:" msgstr "Mogući ključevi/vrijednosti:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Prikazuje informacije o korištenju."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Fatalna greška:" msgstr "Fatalna greška:"
@@ -562,16 +566,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "Datoteka"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "Datoteka"
msgid "File"
msgstr "Datoteka"
msgid "New note" msgid "New note"
msgstr "Nova bilješka" msgstr "Nova bilješka"
@@ -588,6 +593,9 @@ msgstr "Uvoz"
msgid "Export" msgid "Export"
msgstr "Uvoz" msgstr "Uvoz"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -635,6 +643,10 @@ msgstr "Pomoć"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Website i dokumentacija" msgstr "Website i dokumentacija"
#, fuzzy
msgid "Make a donation"
msgstr "Website i dokumentacija"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -658,12 +670,8 @@ msgstr "U redu"
msgid "Cancel" msgid "Cancel"
msgstr "Odustani" msgstr "Odustani"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "Obriši bilješke?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -675,9 +683,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "N" msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Prekini sinkronizaciju" msgstr "Prekini sinkronizaciju"
@@ -993,6 +998,16 @@ msgstr "Prekidam..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Dovršeno: %s" msgstr "Dovršeno: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Fatalna greška:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Sinkronizacija je već u toku. Stanje: %s" msgstr "Sinkronizacija je već u toku. Stanje: %s"
@@ -1089,7 +1104,15 @@ msgstr "Stvara novu bilješku."
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1167,6 +1190,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Evernote izvozne datoteke" msgstr "Evernote izvozne datoteke"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1271,6 +1297,9 @@ msgstr "Potvrdi"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Prekini sinkronizaciju" msgstr "Prekini sinkronizaciju"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""
@@ -1355,6 +1384,13 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
msgid "Welcome" msgid "Welcome"
msgstr "Dobro došli" msgstr "Dobro došli"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Obriši bilješke?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Uvozi Evernote bilježnicu (.enex datoteku)." #~ msgstr "Uvozi Evernote bilježnicu (.enex datoteku)."

View File

@@ -496,6 +496,10 @@ msgstr "Predefinito: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Chiave/valore possibili:" msgstr "Chiave/valore possibili:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Mostra le informazioni di utilizzo."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Errore fatale:" msgstr "Errore fatale:"
@@ -544,16 +548,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "File"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "File"
msgid "File"
msgstr "File"
msgid "New note" msgid "New note"
msgstr "Nuova nota" msgstr "Nuova nota"
@@ -570,6 +575,9 @@ msgstr "Importa"
msgid "Export" msgid "Export"
msgstr "Importa" msgstr "Importa"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -617,6 +625,10 @@ msgstr "Aiuto"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Sito web e documentazione" msgstr "Sito web e documentazione"
#, fuzzy
msgid "Make a donation"
msgstr "Sito web e documentazione"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -640,12 +652,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Cancella" msgstr "Cancella"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "Eliminare le note?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -657,9 +665,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "N" msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Cancella la sincronizzazione" msgstr "Cancella la sincronizzazione"
@@ -979,6 +984,16 @@ msgstr "Cancellazione..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Completata: %s" msgstr "Completata: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Errore fatale:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "La sincronizzazione è già in corso. Stato: %s" msgstr "La sincronizzazione è già in corso. Stato: %s"
@@ -1075,7 +1090,15 @@ msgstr "Crea una nuova nota."
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1153,6 +1176,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Esposta i files di Evernote" msgstr "Esposta i files di Evernote"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1257,6 +1283,9 @@ msgstr "Conferma"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Cancella la sincronizzazione" msgstr "Cancella la sincronizzazione"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""
@@ -1343,6 +1372,13 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Benvenuto" msgstr "Benvenuto"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Eliminare le note?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importa un file blocco note di Evernote (.enex file)." #~ msgstr "Importa un file blocco note di Evernote (.enex file)."

View File

@@ -492,6 +492,10 @@ msgstr "規定値: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "取り得るキーバリュー: " msgstr "取り得るキーバリュー: "
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "使い方を表示する。"
msgid "Fatal error:" msgid "Fatal error:"
msgstr "致命的なエラー: " msgstr "致命的なエラー: "
@@ -545,16 +549,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "ファイル"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "ファイル"
msgid "File"
msgstr "ファイル"
msgid "New note" msgid "New note"
msgstr "新しいノート" msgstr "新しいノート"
@@ -571,6 +576,9 @@ msgstr "インポート"
msgid "Export" msgid "Export"
msgstr "インポート" msgstr "インポート"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -618,6 +626,10 @@ msgstr "ヘルプ"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Webサイトとドキュメント" msgstr "Webサイトとドキュメント"
#, fuzzy
msgid "Make a donation"
msgstr "Webサイトとドキュメント"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -641,12 +653,8 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "ノートを削除しますか?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -657,9 +665,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "同期の中止" msgstr "同期の中止"
@@ -981,6 +986,16 @@ msgstr "中止中..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "完了: %s" msgstr "完了: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "致命的なエラー: "
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "同期作業はすでに実行中です。状態: %s" msgstr "同期作業はすでに実行中です。状態: %s"
@@ -1079,7 +1094,15 @@ msgstr "あたらしいノートを作成します。"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1157,6 +1180,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Evernote Exportファイル" msgstr "Evernote Exportファイル"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1261,6 +1287,9 @@ msgstr "確認"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "同期の中止" msgstr "同期の中止"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""
@@ -1347,6 +1376,13 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "ようこそ" msgstr "ようこそ"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "ノートを削除しますか?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Evernoteノートブックファイル(.enex)のインポート" #~ msgstr "Evernoteノートブックファイル(.enex)のインポート"

View File

@@ -449,6 +449,9 @@ msgstr ""
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "" msgstr ""
msgid "Type `joplin help` for usage information."
msgstr ""
msgid "Fatal error:" msgid "Fatal error:"
msgstr "" msgstr ""
@@ -490,16 +493,16 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr ""
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "PDF File"
msgstr ""
msgid "File"
msgstr ""
msgid "New note" msgid "New note"
msgstr "" msgstr ""
@@ -515,6 +518,9 @@ msgstr ""
msgid "Export" msgid "Export"
msgstr "" msgstr ""
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -561,6 +567,9 @@ msgstr ""
msgid "Website and documentation" msgid "Website and documentation"
msgstr "" msgstr ""
msgid "Make a donation"
msgstr ""
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -584,11 +593,7 @@ msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
#, javascript-format msgid "Current version is up-to-date."
msgid ""
"Release notes:\n"
"\n"
"%s"
msgstr "" msgstr ""
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
@@ -600,9 +605,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "" msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "" msgstr ""
@@ -905,6 +907,16 @@ msgstr ""
msgid "Completed: %s" msgid "Completed: %s"
msgstr "" msgstr ""
#, javascript-format
msgid "Last error: %s"
msgstr ""
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "" msgstr ""
@@ -993,7 +1005,15 @@ msgstr ""
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1067,6 +1087,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "" msgstr ""
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1170,6 +1193,9 @@ msgstr ""
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "" msgstr ""
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""

View File

@@ -505,6 +505,10 @@ msgstr "Standaard: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Mogelijke sleutels/waarden:" msgstr "Mogelijke sleutels/waarden:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Toont gebruiksinformatie."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Fatale fout:" msgstr "Fatale fout:"
@@ -562,16 +566,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "Bestand"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "Bestand"
msgid "File"
msgstr "Bestand"
msgid "New note" msgid "New note"
msgstr "Nieuwe notitie" msgstr "Nieuwe notitie"
@@ -588,6 +593,9 @@ msgstr "Importeer"
msgid "Export" msgid "Export"
msgstr "Importeer" msgstr "Importeer"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -634,6 +642,10 @@ msgstr "Help"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Website en documentatie" msgstr "Website en documentatie"
#, fuzzy
msgid "Make a donation"
msgstr "Website en documentatie"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -657,12 +669,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Annuleer" msgstr "Annuleer"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "Notities verwijderen?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -674,9 +682,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "N" msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Annuleer synchronisatie" msgstr "Annuleer synchronisatie"
@@ -1008,6 +1013,16 @@ msgstr "Annuleren..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Voltooid: %s" msgstr "Voltooid: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Fatale fout:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisatie is reeds bezig. Status: %s" msgstr "Synchronisatie is reeds bezig. Status: %s"
@@ -1105,7 +1120,15 @@ msgstr "Maakt een nieuwe notitie aan."
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1185,6 +1208,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Exporteer Evernote bestanden" msgstr "Exporteer Evernote bestanden"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1293,6 +1319,9 @@ msgstr "Bevestig"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Annuleer synchronisatie" msgstr "Annuleer synchronisatie"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Hoofdsleutel: %s" msgstr "Hoofdsleutel: %s"
@@ -1378,6 +1407,13 @@ msgstr ""
msgid "Welcome" msgid "Welcome"
msgstr "Welkom" msgstr "Welkom"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Notities verwijderen?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importeer een Evernote notitieboek (.enex bestand)." #~ msgstr "Importeer een Evernote notitieboek (.enex bestand)."

View File

@@ -493,6 +493,10 @@ msgstr "Padrão: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Possíveis chaves/valores:" msgstr "Possíveis chaves/valores:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Exibe informações de uso."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Erro fatal:" msgstr "Erro fatal:"
@@ -540,16 +544,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "Arquivo"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "Arquivo"
msgid "File"
msgstr "Arquivo"
msgid "New note" msgid "New note"
msgstr "Nova nota" msgstr "Nova nota"
@@ -566,6 +571,9 @@ msgstr "Importar"
msgid "Export" msgid "Export"
msgstr "Importar" msgstr "Importar"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -614,6 +622,10 @@ msgstr "Ajuda"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Website e documentação" msgstr "Website e documentação"
#, fuzzy
msgid "Make a donation"
msgstr "Website e documentação"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -637,12 +649,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Cancelar" msgstr "Cancelar"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "Excluir notas?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -654,9 +662,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "N" msgstr "N"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Cancelar sincronização" msgstr "Cancelar sincronização"
@@ -979,6 +984,16 @@ msgstr "Cancelando..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Completado: %s" msgstr "Completado: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Erro fatal:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Sincronização já em andamento. Estado: %s" msgstr "Sincronização já em andamento. Estado: %s"
@@ -1076,7 +1091,15 @@ msgstr "Cria uma nova nota."
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1154,6 +1177,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Arquivos de Exportação do Evernote" msgstr "Arquivos de Exportação do Evernote"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1258,6 +1284,9 @@ msgstr "Confirmar"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Cancelar sincronização" msgstr "Cancelar sincronização"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""
@@ -1342,6 +1371,13 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome" msgid "Welcome"
msgstr "Bem-vindo" msgstr "Bem-vindo"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Excluir notas?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Importa um arquivo de caderno do Evernote (arquivo .enex)." #~ msgstr "Importa um arquivo de caderno do Evernote (arquivo .enex)."

View File

@@ -507,6 +507,10 @@ msgstr "По умолчанию: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Возможные ключи/значения:" msgstr "Возможные ключи/значения:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Выводит информацию об использовании."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Фатальная ошибка:" msgstr "Фатальная ошибка:"
@@ -564,16 +568,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "Файл"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "Файл"
msgid "File"
msgstr "Файл"
msgid "New note" msgid "New note"
msgstr "Новая заметка" msgstr "Новая заметка"
@@ -590,6 +595,9 @@ msgstr "Импорт"
msgid "Export" msgid "Export"
msgstr "Импорт" msgstr "Импорт"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -636,6 +644,10 @@ msgstr "Помощь"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "Сайт и документация" msgstr "Сайт и документация"
#, fuzzy
msgid "Make a donation"
msgstr "Сайт и документация"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "Проверить обновления..." msgstr "Проверить обновления..."
@@ -659,12 +671,8 @@ msgstr "OK"
msgid "Cancel" msgid "Cancel"
msgstr "Отмена" msgstr "Отмена"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr "Вы используете самую свежую версию."
"Release notes:\n"
"\n"
"%s"
msgstr "Удалить заметки?"
#, fuzzy #, fuzzy
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
@@ -677,9 +685,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "N" msgstr "N"
msgid "Current version is up-to-date."
msgstr "Вы используете самую свежую версию."
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "Отменить синхронизацию" msgstr "Отменить синхронизацию"
@@ -1009,6 +1014,16 @@ msgstr "Отмена..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "Завершено: %s" msgstr "Завершено: %s"
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "Фатальная ошибка:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s" msgstr "Синхронизация уже выполняется. Статус: %s"
@@ -1101,9 +1116,18 @@ msgstr "При создании новой заметки:"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" #, fuzzy
msgid "Global zoom percentage"
msgstr "Масштаб приложения в процентах" msgstr "Масштаб приложения в процентах"
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение" msgstr "Автоматически обновлять приложение"
@@ -1184,6 +1208,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Файлы экспорта Evernote" msgstr "Файлы экспорта Evernote"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1291,6 +1318,9 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию" msgstr "Отменить синхронизацию"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "Мастер-ключ %s" msgstr "Мастер-ключ %s"
@@ -1375,6 +1405,13 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome" msgid "Welcome"
msgstr "Добро пожаловать" msgstr "Добро пожаловать"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "Удалить заметки?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "Импортирует файл блокнотов Evernote (.enex-файл)." #~ msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."

View File

@@ -471,6 +471,10 @@ msgstr "默认值: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "可用键/值:" msgstr "可用键/值:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "显示使用信息。"
msgid "Fatal error:" msgid "Fatal error:"
msgstr "严重错误:" msgstr "严重错误:"
@@ -515,16 +519,17 @@ msgstr ""
msgid "Exporting to \"%s\" as \"%s\" format. Please wait..." msgid "Exporting to \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
msgid "File"
msgstr "文件"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Importing from \"%s\" as \"%s\" format. Please wait..." msgid "Importing from \"%s\" as \"%s\" format. Please wait..."
msgstr "" msgstr ""
#, fuzzy
msgid "PDF File"
msgstr "文件"
msgid "File"
msgstr "文件"
msgid "New note" msgid "New note"
msgstr "新笔记" msgstr "新笔记"
@@ -541,6 +546,9 @@ msgstr "导入"
msgid "Export" msgid "Export"
msgstr "导入" msgstr "导入"
msgid "Print"
msgstr ""
#, javascript-format #, javascript-format
msgid "Hide %s" msgid "Hide %s"
msgstr "" msgstr ""
@@ -588,6 +596,10 @@ msgstr "帮助"
msgid "Website and documentation" msgid "Website and documentation"
msgstr "网站与文档" msgstr "网站与文档"
#, fuzzy
msgid "Make a donation"
msgstr "网站与文档"
msgid "Check for updates..." msgid "Check for updates..."
msgstr "" msgstr ""
@@ -611,12 +623,8 @@ msgstr "确认"
msgid "Cancel" msgid "Cancel"
msgstr "取消" msgstr "取消"
#, fuzzy, javascript-format msgid "Current version is up-to-date."
msgid "" msgstr ""
"Release notes:\n"
"\n"
"%s"
msgstr "是否删除笔记?"
msgid "An update is available, do you want to download it now?" msgid "An update is available, do you want to download it now?"
msgstr "" msgstr ""
@@ -628,9 +636,6 @@ msgstr ""
msgid "No" msgid "No"
msgstr "否" msgstr "否"
msgid "Current version is up-to-date."
msgstr ""
#, fuzzy #, fuzzy
msgid "Check synchronisation configuration" msgid "Check synchronisation configuration"
msgstr "取消同步" msgstr "取消同步"
@@ -947,6 +952,16 @@ msgstr "正在取消..."
msgid "Completed: %s" msgid "Completed: %s"
msgstr "已完成:\"%s\"" msgstr "已完成:\"%s\""
#, fuzzy, javascript-format
msgid "Last error: %s"
msgstr "严重错误:"
msgid "Idle"
msgstr ""
msgid "In progress"
msgstr ""
#, javascript-format #, javascript-format
msgid "Synchronisation is already in progress. State: %s" msgid "Synchronisation is already in progress. State: %s"
msgstr "同步正在进行中。状态:%s" msgstr "同步正在进行中。状态:%s"
@@ -1041,7 +1056,15 @@ msgstr "创建新笔记。"
msgid "Show tray icon" msgid "Show tray icon"
msgstr "" msgstr ""
msgid "Set application zoom percentage" msgid "Global zoom percentage"
msgstr ""
msgid "Editor font family"
msgstr ""
msgid ""
"The font name will not be checked. If incorrect or empty, it will default to "
"a generic monospace font."
msgstr "" msgstr ""
msgid "Automatically update the application" msgid "Automatically update the application"
@@ -1117,6 +1140,9 @@ msgstr ""
msgid "Evernote Export File" msgid "Evernote Export File"
msgstr "Evernote导出文件" msgstr "Evernote导出文件"
msgid "Directory"
msgstr ""
#, javascript-format #, javascript-format
msgid "Cannot load \"%s\" module for format \"%s\"" msgid "Cannot load \"%s\" module for format \"%s\""
msgstr "" msgstr ""
@@ -1221,6 +1247,9 @@ msgstr "确认"
msgid "Cancel synchronisation" msgid "Cancel synchronisation"
msgstr "取消同步" msgstr "取消同步"
msgid "Joplin website"
msgstr ""
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
msgstr "" msgstr ""
@@ -1303,6 +1332,13 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
msgid "Welcome" msgid "Welcome"
msgstr "欢迎" msgstr "欢迎"
#, fuzzy
#~ msgid ""
#~ "Release notes:\n"
#~ "\n"
#~ "%s"
#~ msgstr "是否删除笔记?"
#~ msgid "Imports an Evernote notebook file (.enex file)." #~ msgid "Imports an Evernote notebook file (.enex file)."
#~ msgstr "导入Evernote笔记本文件(.enex文件)。" #~ msgstr "导入Evernote笔记本文件(.enex文件)。"

View File

@@ -209,4 +209,44 @@ describe('services_InteropService', function() {
expect(fileContentEqual(resourcePath1, resourcePath2)).toBe(true); expect(fileContentEqual(resourcePath1, resourcePath2)).toBe(true);
})); }));
it('should export and import single notes', asyncTest(async () => {
const service = new InteropService();
const filePath = exportDir() + '/test.jex';
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await service.export({ path: filePath, sourceNoteIds: [note1.id] });
await Note.delete(note1.id);
await Folder.delete(folder1.id);
await service.import({ path: filePath });
expect(await Note.count()).toBe(1);
expect(await Folder.count()).toBe(1);
let folder2 = (await Folder.all())[0];
expect(folder2.title).toBe('test');
}));
it('should export and import single folders', asyncTest(async () => {
const service = new InteropService();
const filePath = exportDir() + '/test.jex';
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await service.export({ path: filePath, sourceFolderIds: [folder1.id] });
await Note.delete(note1.id);
await Folder.delete(folder1.id);
await service.import({ path: filePath });
expect(await Note.count()).toBe(1);
expect(await Folder.count()).toBe(1);
let folder2 = (await Folder.all())[0];
expect(folder2.title).toBe('folder1');
}));
}); });

View File

@@ -119,7 +119,7 @@ describe('Synchronizer', function() {
await localItemsSameAsRemote(all, expect); await localItemsSameAsRemote(all, expect);
})); }));
it('should update remote item', asyncTest(async () => { it('should update remote items', asyncTest(async () => {
let folder = await Folder.save({ title: "folder1" }); let folder = await Folder.save({ title: "folder1" });
let note = await Note.save({ title: "un", parent_id: folder.id }); let note = await Note.save({ title: "un", parent_id: folder.id });
await synchronizer().start(); await synchronizer().start();
@@ -1006,4 +1006,29 @@ describe('Synchronizer', function() {
await localItemsSameAsRemote(all, expect); await localItemsSameAsRemote(all, expect);
})); }));
it("should update remote items but not pull remote changes", asyncTest(async () => {
let folder = await Folder.save({ title: "folder1" });
let note = await Note.save({ title: "un", parent_id: folder.id });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await Note.save({ title: "deux", parent_id: folder.id });
await synchronizer().start();
await switchClient(1);
await Note.save({ title: "un UPDATE", id: note.id });
await synchronizer().start({ syncSteps: ["update_remote"] });
let all = await allItems();
expect(all.length).toBe(2);
await switchClient(2);
await synchronizer().start();
let note2 = await Note.load(note.id);
expect(note2.title).toBe("un UPDATE");
}));
}); });

View File

@@ -51,8 +51,8 @@ SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive); SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud); SyncTargetRegistry.addClass(SyncTargetNextcloud);
const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud'); // const syncTargetId_ = SyncTargetRegistry.nameToId("nextcloud");
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory'); const syncTargetId_ = SyncTargetRegistry.nameToId("memory");
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem'); //const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync'; const syncDir = __dirname + '/../tests/sync';

View File

@@ -110,10 +110,14 @@ class ElectronAppWrapper {
}); });
} }
async exit() { async quit() {
this.electronApp_.quit(); this.electronApp_.quit();
} }
exit(errorCode = 0) {
this.electronApp_.exit(errorCode);
}
trayShown() { trayShown() {
return !!this.tray_; return !!this.tray_;
} }
@@ -172,6 +176,8 @@ class ElectronAppWrapper {
} }
ensureSingleInstance() { ensureSingleInstance() {
if (this.env_ === 'dev') return false;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const alreadyRunning = this.electronApp_.makeSingleInstance((commandLine, workingDirectory) => { const alreadyRunning = this.electronApp_.makeSingleInstance((commandLine, workingDirectory) => {
const win = this.window(); const win = this.window();

View File

@@ -0,0 +1,51 @@
const { _ } = require('lib/locale');
const { bridge } = require('electron').remote.require('./bridge');
const InteropService = require('lib/services/InteropService');
class InteropServiceHelper {
static async export(dispatch, module, options = null) {
if (!options) options = {};
let path = null;
if (module.target === 'file') {
path = bridge().showSaveDialog({
filters: [{ name: module.description, extensions: [module.fileExtension]}]
});
} else {
path = bridge().showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
});
}
if (!path || (Array.isArray(path) && !path.length)) return;
if (Array.isArray(path)) path = path[0];
dispatch({
type: 'WINDOW_COMMAND',
name: 'showModalMessage',
message: _('Exporting to "%s" as "%s" format. Please wait...', path, module.format),
});
const exportOptions = {};
exportOptions.path = path;
exportOptions.format = module.format;
if (options.sourceFolderIds) exportOptions.sourceFolderIds = options.sourceFolderIds;
if (options.sourceNoteIds) exportOptions.sourceNoteIds = options.sourceNoteIds;
const service = new InteropService();
const result = await service.export(exportOptions);
console.info('Export result: ', result);
dispatch({
type: 'WINDOW_COMMAND',
name: 'hideModalMessage',
});
}
}
module.exports = InteropServiceHelper;

View File

@@ -21,6 +21,7 @@ const AlarmService = require('lib/services/AlarmService.js');
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode'); const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
const DecryptionWorker = require('lib/services/DecryptionWorker'); const DecryptionWorker = require('lib/services/DecryptionWorker');
const InteropService = require('lib/services/InteropService'); const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('./InteropServiceHelper.js');
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu; const Menu = bridge().Menu;
@@ -145,8 +146,12 @@ class Application extends BaseApplication {
this.updateTray(); this.updateTray();
} }
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) { if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'style.editor.fontFamily' || action.type == 'SETTING_UPDATE_ALL') {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(); this.updateEditorFont();
}
if (["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(5, { syncSteps: ["update_remote", "delete_remote"] });
} }
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) { if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
@@ -200,55 +205,17 @@ class Application extends BaseApplication {
const module = ioModules[i]; const module = ioModules[i];
if (module.type === 'exporter') { if (module.type === 'exporter') {
exportItems.push({ exportItems.push({
label: module.format + ' - ' + module.description, label: module.fullLabel(),
screens: ['Main'], screens: ['Main'],
click: async () => { click: async () => {
let path = null; await InteropServiceHelper.export(this.dispatch.bind(this), module);
if (module.target === 'file') {
path = bridge().showSaveDialog({
filters: [{ name: module.description, extensions: [module.fileExtension]}]
});
} else {
path = bridge().showOpenDialog({
properties: ['openDirectory', 'createDirectory'],
});
}
if (!path || (Array.isArray(path) && !path.length)) return;
if (Array.isArray(path)) path = path[0];
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'showModalMessage',
message: _('Exporting to "%s" as "%s" format. Please wait...', path, module.format),
});
const exportOptions = {};
exportOptions.path = path;
exportOptions.format = module.format;
const service = new InteropService();
const result = await service.export(exportOptions);
console.info('Export result: ', result);
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'hideModalMessage',
});
} }
}); });
} else { } else {
for (let j = 0; j < module.sources.length; j++) { for (let j = 0; j < module.sources.length; j++) {
const moduleSource = module.sources[j]; const moduleSource = module.sources[j];
let label = [module.format + ' - ' + module.description];
if (module.sources.length > 1) {
label.push('(' + (moduleSource === 'file' ? _('File') : _('Directory')) + ')');
}
importItems.push({ importItems.push({
label: label.join(' '), label: module.fullLabel(moduleSource),
screens: ['Main'], screens: ['Main'],
click: async () => { click: async () => {
let path = null; let path = null;
@@ -298,6 +265,17 @@ class Application extends BaseApplication {
} }
} }
exportItems.push({
label: 'PDF - ' + _('PDF File'),
screens: ['Main'],
click: async () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'exportPdf',
});
}
});
const template = [ const template = [
{ {
label: _('File'), label: _('File'),
@@ -333,31 +311,24 @@ class Application extends BaseApplication {
} }
}, { }, {
type: 'separator', type: 'separator',
// }, {
// label: _('Import Evernote notes'),
// click: () => {
// const filePaths = bridge().showOpenDialog({
// properties: ['openFile', 'createDirectory'],
// filters: [
// { name: _('Evernote Export Files'), extensions: ['enex'] },
// ]
// });
// if (!filePaths || !filePaths.length) return;
// this.dispatch({
// type: 'NAV_GO',
// routeName: 'Import',
// props: {
// filePath: filePaths[0],
// },
// });
// }
}, { }, {
label: _('Import'), label: _('Import'),
submenu: importItems, submenu: importItems,
}, { }, {
label: _('Export'), label: _('Export'),
submenu: exportItems, submenu: exportItems,
}, {
type: 'separator',
}, {
label: _('Print'),
accelerator: 'CommandOrControl+P',
screens: ['Main'],
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'print',
});
}
}, { }, {
type: 'separator', type: 'separator',
platforms: ['darwin'], platforms: ['darwin'],
@@ -371,7 +342,7 @@ class Application extends BaseApplication {
}, { }, {
label: _('Quit'), label: _('Quit'),
accelerator: 'CommandOrControl+Q', accelerator: 'CommandOrControl+Q',
click: () => { bridge().electronApp().exit() } click: () => { bridge().electronApp().quit() }
}] }]
}, { }, {
label: _('Edit'), label: _('Edit'),
@@ -477,11 +448,17 @@ class Application extends BaseApplication {
label: _('Website and documentation'), label: _('Website and documentation'),
accelerator: 'F1', accelerator: 'F1',
click () { bridge().openExternal('http://joplin.cozic.net') } click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('Make a donation'),
click () { bridge().openExternal('http://joplin.cozic.net/donate') }
}, { }, {
label: _('Check for updates...'), label: _('Check for updates...'),
click: () => { click: () => {
bridge().checkForUpdates(false, bridge().window(), this.checkForUpdateLoggerPath()); bridge().checkForUpdates(false, bridge().window(), this.checkForUpdateLoggerPath());
} }
}, {
type: 'separator',
screens: ['Main'],
}, { }, {
label: _('About Joplin'), label: _('About Joplin'),
click: () => { click: () => {
@@ -492,8 +469,8 @@ class Application extends BaseApplication {
'Copyright © 2016-2018 Laurent Cozic', 'Copyright © 2016-2018 Laurent Cozic',
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform), _('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
]; ];
bridge().showMessageBox({ bridge().showInfoMessageBox(message.join('\n'), {
message: message.join('\n'), icon: bridge().electronApp().buildDir() + '/icons/32x32.png',
}); });
} }
}] }]
@@ -547,12 +524,27 @@ class Application extends BaseApplication {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } }, { label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
{ type: 'separator' }, { type: 'separator' },
{ label: _('Exit'), click: () => { app.exit() } }, { label: _('Exit'), click: () => { app.quit() } },
]) ])
app.createTray(contextMenu); app.createTray(contextMenu);
} }
} }
updateEditorFont() {
const fontFamilies = [];
if (Setting.value('style.editor.fontFamily')) fontFamilies.push('"' + Setting.value('style.editor.fontFamily') + '"');
fontFamilies.push('monospace');
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155
const css = '.ace_editor * { font-family: ' + fontFamilies.join(', ') + ' !important; }';
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.appendChild(document.createTextNode(css));
document.head.appendChild(styleTag);
}
async start(argv) { async start(argv) {
argv = await super.start(argv); argv = await super.start(argv);

View File

@@ -26,7 +26,7 @@ class Bridge {
if (!this.window()) return { width: 0, height: 0 }; if (!this.window()) return { width: 0, height: 0 };
const s = this.window().getContentSize(); const s = this.window().getContentSize();
return { width: s[0], height: s[1] }; return { width: s[0], height: s[1] };
} }
windowSize() { windowSize() {
if (!this.window()) return { width: 0, height: 0 }; if (!this.window()) return { width: 0, height: 0 };
@@ -62,20 +62,23 @@ class Bridge {
return filePaths; return filePaths;
} }
showMessageBox(window, options) { // Don't use this directly - call one of the showXxxxxxxMessageBox() instead
showMessageBox_(window, options) {
const {dialog} = require('electron'); const {dialog} = require('electron');
const nativeImage = require('electron').nativeImage
if (!window) window = this.window();
return dialog.showMessageBox(window, options); return dialog.showMessageBox(window, options);
} }
showErrorMessageBox(message) { showErrorMessageBox(message) {
return this.showMessageBox(this.window(), { return this.showMessageBox_(this.window(), {
type: 'error', type: 'error',
message: message, message: message,
}); });
} }
showConfirmMessageBox(message) { showConfirmMessageBox(message) {
const result = this.showMessageBox(this.window(), { const result = this.showMessageBox_(this.window(), {
type: 'question', type: 'question',
message: message, message: message,
buttons: [_('OK'), _('Cancel')], buttons: [_('OK'), _('Cancel')],
@@ -83,12 +86,12 @@ class Bridge {
return result === 0; return result === 0;
} }
showInfoMessageBox(message) { showInfoMessageBox(message, options = {}) {
const result = this.showMessageBox(this.window(), { const result = this.showMessageBox_(this.window(), Object.assign({}, {
type: 'info', type: 'info',
message: message, message: message,
buttons: [_('OK')], buttons: [_('OK')],
}); }, options));
return result === 0; return result === 0;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,31 +1,15 @@
const { dialog } = require('electron') const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
const { Logger } = require('lib/logger.js'); const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const fetch = require('node-fetch');
const packageInfo = require('./packageInfo.js');
const compareVersions = require('compare-versions');
let autoUpdateLogger_ = new Logger(); let autoUpdateLogger_ = new Logger();
let checkInBackground_ = false; let checkInBackground_ = false;
let isCheckingForUpdate_ = false; let isCheckingForUpdate_ = false;
let parentWindow_ = null; let parentWindow_ = null;
// Note: Electron Builder's autoUpdater is incredibly buggy so currently it's only used
// to detect if a new version is present. If it is, the download link is simply opened
// in a new browser window.
autoUpdater.autoDownload = false;
function htmlToText_(html) {
let output = html.replace(/\n/g, '');
output = output.replace(/<li>/g, '- ');
output = output.replace(/<p>/g, '');
output = output.replace(/<\/p>/g, '\n');
output = output.replace(/<\/li>/g, '\n');
output = output.replace(/<ul>/g, '');
output = output.replace(/<\/ul>/g, '');
output = output.replace(/<.*?>/g, '');
output = output.replace(/<\/.*?>/g, '');
return output;
}
function showErrorMessageBox(message) { function showErrorMessageBox(message) {
return dialog.showMessageBox(parentWindow_, { return dialog.showMessageBox(parentWindow_, {
type: 'error', type: 'error',
@@ -43,81 +27,45 @@ function onCheckEnded() {
isCheckingForUpdate_ = false; isCheckingForUpdate_ = false;
} }
autoUpdater.on('error', (error) => { async function fetchLatestRelease() {
autoUpdateLogger_.error(error); const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases/latest');
if (checkInBackground_) return onCheckEnded();
let msg = error == null ? "unknown" : (error.stack || error).toString(); if (!response.ok) {
// Error messages can be very long even without stack trace so shorten const responseText = await response.text();
// then so that the dialog box doesn't take the whole screen. throw new Error('Cannot get latest release info: ' + responseText.substr(0,500));
msg = msg.substr(0,512).replace(/\\n/g, '\n');
showErrorMessageBox(msg)
onCheckEnded();
})
function findDownloadFilename_(info) {
// { version: '1.0.64',
// files:
// [ { url: 'Joplin-1.0.64-mac.zip',
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==' },
// { url: 'Joplin-1.0.64.dmg',
// sha512: 'jAewQQoJ3nCaOj8hWDgf0sc3LBbAWQtiKqfTflK8Hc3Dh7fAy9jRHfFAZKFUZ9ll95Bun0DVsLq8wLSUrdsMXw==',
// size: 77104485 } ],
// path: 'Joplin-1.0.64-mac.zip',
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==',
// releaseDate: '2018-02-16T00:13:01.634Z',
// releaseName: 'v1.0.64',
// releaseNotes: '<p>Still more fixes and im...' }
if (!info) return null;
if (!info.files) {
// info.path seems to contain a default, though not a good one,
// so the loop below if preferable to find the right file.
return info.path;
} }
for (let i = 0; i < info.files.length; i++) { const json = await response.json();
const f = info.files[i].url; // Annoyingly this is called "url" but it's obviously not a url, so hopefully it won't change later on and become one.
if (f.indexOf('.exe') >= 0) return f; const version = json.tag_name.substr(1);
if (f.indexOf('.dmg') >= 0) return f; let downloadUrl = null;
const platform = process.platform;
for (let i = 0; i < json.assets.length; i++) {
const asset = json.assets[i];
let found = false;
if (platform === 'win32' && asset.name.indexOf('.exe') >= 0) {
found = true;
} else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) {
found = true;
} else if (platform === 'linux' && asset.name.indexOf('.AppImage') >= 0) {
found = true;
}
if (found) {
downloadUrl = asset.browser_download_url;
break;
}
} }
return info.path; if (!downloadUrl) throw new Error('Cannot find download Url: ' + JSON.stringify(json).substr(0,500));
return {
version: version,
downloadUrl: downloadUrl,
notes: json.body,
};
} }
autoUpdater.on('update-available', (info) => {
const filename = findDownloadFilename_(info);
if (!info.version || !filename) {
if (checkInBackground_) return onCheckEnded();
showErrorMessageBox(('Could not get version info: ' + JSON.stringify(info)));
return onCheckEnded();
}
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/v' + info.version + '/' + filename;
let releaseNotes = info.releaseNotes + '';
if (releaseNotes) releaseNotes = '\n\n' + _('Release notes:\n\n%s', htmlToText_(releaseNotes));
const buttonIndex = dialog.showMessageBox(parentWindow_, {
type: 'info',
message: _('An update is available, do you want to download it now?' + releaseNotes),
buttons: [_('Yes'), _('No')]
});
onCheckEnded();
if (buttonIndex === 0) require('electron').shell.openExternal(downloadUrl);
})
autoUpdater.on('update-not-available', () => {
if (checkInBackground_) return onCheckEnded();
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
onCheckEnded();
})
function checkForUpdates(inBackground, window, logFilePath) { function checkForUpdates(inBackground, window, logFilePath) {
if (isCheckingForUpdate_) { if (isCheckingForUpdate_) {
autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running'); autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running');
@@ -133,18 +81,30 @@ function checkForUpdates(inBackground, window, logFilePath) {
autoUpdateLogger_.addTarget('file', { path: logFilePath }); autoUpdateLogger_.addTarget('file', { path: logFilePath });
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG); autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
autoUpdateLogger_.info('checkForUpdates: Initializing...'); autoUpdateLogger_.info('checkForUpdates: Initializing...');
autoUpdater.logger = autoUpdateLogger_;
} }
checkInBackground_ = inBackground; checkInBackground_ = inBackground;
try { fetchLatestRelease().then(release => {
autoUpdater.checkForUpdates() if (compareVersions(release.version, packageInfo.version) <= 0) {
} catch (error) { if (!checkInBackground_) dialog.showMessageBox({ message: _('Current version is up-to-date.') })
} else {
const releaseNotes = release.notes.trim() ? "\n\n" + release.notes.trim() : '';
const buttonIndex = dialog.showMessageBox(parentWindow_, {
type: 'info',
message: _('An update is available, do you want to download it now?' + releaseNotes),
buttons: [_('Yes'), _('No')]
});
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl);
}
}).catch(error => {
autoUpdateLogger_.error(error); autoUpdateLogger_.error(error);
if (!checkInBackground_) showErrorMessageBox(error.message); if (!checkInBackground_) showErrorMessageBox(error.message);
}).then(() => {
onCheckEnded(); onCheckEnded();
} });
} }
module.exports.checkForUpdates = checkForUpdates module.exports.checkForUpdates = checkForUpdates

View File

@@ -59,6 +59,12 @@ class ConfigScreenComponent extends React.Component {
display: 'inline-block', display: 'inline-block',
}; };
const descriptionStyle = Object.assign({}, theme.textStyle, {
color: theme.colorFaded,
marginTop: 5,
fontStyle: 'italic',
});
const updateSettingValue = (key, value) => { const updateSettingValue = (key, value) => {
return shared.updateSettingValue(this, key, value); return shared.updateSettingValue(this, key, value);
} }
@@ -67,6 +73,13 @@ class ConfigScreenComponent extends React.Component {
const md = Setting.settingMetadata(key); const md = Setting.settingMetadata(key);
const descriptionText = Setting.keyDescription(key, 'desktop');
const descriptionComp = descriptionText ? (
<div style={descriptionStyle}>
{descriptionText}
</div>
) : null;
if (md.isEnum) { if (md.isEnum) {
let items = []; let items = [];
const settingOptions = md.options(); const settingOptions = md.options();
@@ -82,6 +95,7 @@ class ConfigScreenComponent extends React.Component {
<select value={value} style={controlStyle} onChange={(event) => { updateSettingValue(key, event.target.value) }}> <select value={value} style={controlStyle} onChange={(event) => { updateSettingValue(key, event.target.value) }}>
{items} {items}
</select> </select>
{ descriptionComp }
</div> </div>
); );
} else if (md.type === Setting.TYPE_BOOL) { } else if (md.type === Setting.TYPE_BOOL) {
@@ -96,6 +110,7 @@ class ConfigScreenComponent extends React.Component {
<div key={key+value.toString()} style={rowStyle}> <div key={key+value.toString()} style={rowStyle}>
<div style={controlStyle}> <div style={controlStyle}>
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label> <input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
{ descriptionComp }
</div> </div>
</div> </div>
); );
@@ -111,6 +126,7 @@ class ConfigScreenComponent extends React.Component {
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div> <div style={labelStyle}><label>{md.label()}</label></div>
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} /> <input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
{ descriptionComp }
</div> </div>
); );
} else if (md.type === Setting.TYPE_INT) { } else if (md.type === Setting.TYPE_INT) {
@@ -122,6 +138,7 @@ class ConfigScreenComponent extends React.Component {
<div key={key} style={rowStyle}> <div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div> <div style={labelStyle}><label>{md.label()}</label></div>
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/> <input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
{ descriptionComp }
</div> </div>
); );
} else { } else {

View File

@@ -9,6 +9,8 @@ const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu; const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem; const MenuItem = bridge().MenuItem;
const eventManager = require('../eventManager'); const eventManager = require('../eventManager');
const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('../InteropServiceHelper.js');
class NoteListComponent extends React.Component { class NoteListComponent extends React.Component {
@@ -91,6 +93,23 @@ class NoteListComponent extends React.Component {
eventManager.emit('noteTypeToggle', { noteId: note.id }); eventManager.emit('noteTypeToggle', { noteId: note.id });
} }
}})); }}));
const exportMenu = new Menu();
const ioService = new InteropService();
const ioModules = ioService.modules();
for (let i = 0; i < ioModules.length; i++) {
const module = ioModules[i];
if (module.type !== 'exporter') continue;
exportMenu.append(new MenuItem({ label: module.fullLabel() , click: async () => {
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceNoteIds: noteIds });
}}));
}
const exportMenuItem = new MenuItem({label: _('Export'), submenu: exportMenu});
menu.append(exportMenuItem);
} }
menu.append(new MenuItem({label: _('Delete'), click: async () => { menu.append(new MenuItem({label: _('Delete'), click: async () => {

View File

@@ -240,6 +240,10 @@ class NoteTextComponent extends React.Component {
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) { if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
await this.reloadNote(nextProps, { noReloadIfLocalChanges: true }); await this.reloadNote(nextProps, { noReloadIfLocalChanges: true });
} }
if (nextProps.windowCommand) {
this.doCommand(nextProps.windowCommand);
}
} }
isModified() { isModified() {
@@ -292,7 +296,7 @@ class NoteTextComponent extends React.Component {
const menu = new Menu() const menu = new Menu()
if (itemType === 'image') { if (itemType === "image" || itemType === "link") {
const resource = await Resource.load(arg0.resourceId); const resource = await Resource.load(arg0.resourceId);
const resourcePath = Resource.fullPath(resource); const resourcePath = Resource.fullPath(resource);
@@ -320,10 +324,7 @@ class NoteTextComponent extends React.Component {
bridge().openItem(filePath); bridge().openItem(filePath);
}); });
} else { } else {
bridge().showMessageBox({ bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
type: 'error',
message: _('Unsupported link or message: %s', msg),
});
} }
} }
@@ -365,7 +366,7 @@ class NoteTextComponent extends React.Component {
webviewReady: true, webviewReady: true,
}); });
// if (Setting.value('env') === 'dev') this.webview_.openDevTools(); if (Setting.value('env') === 'dev') this.webview_.openDevTools();
} }
webview_ref(element) { webview_ref(element) {
@@ -429,6 +430,38 @@ class NoteTextComponent extends React.Component {
this.scheduleSave(); this.scheduleSave();
} }
async doCommand(command) {
if (!command) return;
let commandProcessed = true;
if (command.name === 'exportPdf' && this.webview_) {
const path = bridge().showSaveDialog({
filters: [{ name: _('PDF File'), extensions: ['pdf']}]
});
if (path) {
this.webview_.printToPDF({}, (error, data) => {
if (error) {
bridge().showErrorMessageBox(error.message);
} else {
shim.fsDriver().writeFile(path, data, 'buffer');
}
});
}
} else if (command.name === 'print' && this.webview_) {
this.webview_.print();
} else {
commandProcessed = false;
}
if (commandProcessed) {
this.props.dispatch({
type: 'WINDOW_COMMAND',
name: null,
});
}
}
async commandAttachFile() { async commandAttachFile() {
const filePaths = bridge().showOpenDialog({ const filePaths = bridge().showOpenDialog({
@@ -713,6 +746,7 @@ const mapStateToProps = (state) => {
showAdvancedOptions: state.settings.showAdvancedOptions, showAdvancedOptions: state.settings.showAdvancedOptions,
syncStarted: state.syncStarted, syncStarted: state.syncStarted,
newNote: state.newNote, newNote: state.newNote,
windowCommand: state.windowCommand,
}; };
}; };

View File

@@ -54,10 +54,7 @@ class OneDriveLoginScreenComponent extends React.Component {
this.props.dispatch({ type: 'NAV_BACK' }); this.props.dispatch({ type: 'NAV_BACK' });
reg.scheduleSync(0); reg.scheduleSync(0);
} catch (error) { } catch (error) {
bridge().showMessageBox({ bridge().showErrorMessageBox('Could not login to OneDrive. Please try again.\n\n' + error.message + "\n\n" + url.match(/.{1,64}/g).join('\n'));
type: 'error',
message: 'Could not login to OneDrive. Please try again.\n\n' + error.message + "\n\n" + url.match(/.{1,64}/g).join('\n'),
});
} }
this.authCode_ = null; this.authCode_ = null;

View File

@@ -1,19 +1,19 @@
const React = require('react'); const React = require("react");
const { connect } = require('react-redux'); const { connect } = require("react-redux");
const shared = require('lib/components/shared/side-menu-shared.js'); const shared = require("lib/components/shared/side-menu-shared.js");
const { Synchronizer } = require('lib/synchronizer.js'); const { Synchronizer } = require("lib/synchronizer.js");
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require("lib/BaseModel.js");
const Folder = require('lib/models/Folder.js'); const Folder = require("lib/models/Folder.js");
const Note = require('lib/models/Note.js'); const Note = require("lib/models/Note.js");
const Tag = require('lib/models/Tag.js'); const Tag = require("lib/models/Tag.js");
const { _ } = require('lib/locale.js'); const { _ } = require("lib/locale.js");
const { themeStyle } = require('../theme.js'); const { themeStyle } = require("../theme.js");
const { bridge } = require('electron').remote.require('./bridge'); const { bridge } = require("electron").remote.require("./bridge");
const Menu = bridge().Menu; const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem; const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require("../InteropServiceHelper.js");
class SideBarComponent extends React.Component { class SideBarComponent extends React.Component {
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
@@ -27,63 +27,65 @@ class SideBarComponent extends React.Component {
height: itemHeight, height: itemHeight,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize, fontSize: theme.fontSize,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
paddingLeft: 14, paddingLeft: 14,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
cursor: 'default', cursor: "default",
opacity: 0.8, opacity: 0.8,
whiteSpace: 'nowrap', whiteSpace: "nowrap",
}, },
listItemSelected: { listItemSelected: {
backgroundColor: theme.selectedColor2, backgroundColor: theme.selectedColor2,
}, },
conflictFolder: { conflictFolder: {
color: theme.colorError2, color: theme.colorError2,
fontWeight: 'bold', fontWeight: "bold",
}, },
header: { header: {
height: itemHeight * 1.8, height: itemHeight * 1.8,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize * 1.3, fontSize: theme.fontSize * 1.3,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
paddingLeft: 8, paddingLeft: 8,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
}, },
button: { button: {
padding: 6, padding: 6,
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize, fontSize: theme.fontSize,
textDecoration: 'none', textDecoration: "none",
boxSizing: 'border-box', boxSizing: "border-box",
color: theme.color2, color: theme.color2,
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
border: "1px solid rgba(255,255,255,0.2)", border: "1px solid rgba(255,255,255,0.2)",
marginTop: 10, marginTop: 10,
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
cursor: 'default', cursor: "default",
}, },
syncReport: { syncReport: {
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: Math.round(theme.fontSize * .9), fontSize: Math.round(theme.fontSize * 0.9),
color: theme.color2, color: theme.color2,
opacity: .5, opacity: 0.5,
display: 'flex', display: "flex",
alignItems: 'left', alignItems: "left",
justifyContent: 'top', justifyContent: "top",
flexDirection: 'column', flexDirection: "column",
marginTop: 10, marginTop: 10,
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
minHeight: 70, minHeight: 70,
wordWrap: "break-word",
width: "100%",
}, },
}; };
@@ -91,19 +93,19 @@ class SideBarComponent extends React.Component {
} }
itemContextMenu(event) { itemContextMenu(event) {
const itemId = event.target.getAttribute('data-id'); const itemId = event.target.getAttribute("data-id");
if (itemId === Folder.conflictFolderId()) return; if (itemId === Folder.conflictFolderId()) return;
const itemType = Number(event.target.getAttribute('data-type')); const itemType = Number(event.target.getAttribute("data-type"));
if (!itemId || !itemType) throw new Error('No data on element'); if (!itemId || !itemType) throw new Error("No data on element");
let deleteMessage = ''; let deleteMessage = "";
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
deleteMessage = _('Delete notebook? All notes within this notebook will also be deleted.'); deleteMessage = _("Delete notebook? All notes within this notebook will also be deleted.");
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
deleteMessage = _('Remove this tag from all the notes?'); deleteMessage = _("Remove this tag from all the notes?");
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
deleteMessage = _('Remove this search from the sidebar?'); deleteMessage = _("Remove this search from the sidebar?");
} }
const menu = new Menu(); const menu = new Menu();
@@ -113,30 +115,55 @@ class SideBarComponent extends React.Component {
item = BaseModel.byId(this.props.folders, itemId); item = BaseModel.byId(this.props.folders, itemId);
} }
menu.append(new MenuItem({label: _('Delete'), click: async () => { menu.append(
const ok = bridge().showConfirmMessageBox(deleteMessage); new MenuItem({
if (!ok) return; label: _("Delete"),
click: async () => {
const ok = bridge().showConfirmMessageBox(deleteMessage);
if (!ok) return;
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
await Folder.delete(itemId); await Folder.delete(itemId);
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
await Tag.untagAll(itemId); await Tag.untagAll(itemId);
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
this.props.dispatch({ this.props.dispatch({
type: 'SEARCH_DELETE', type: "SEARCH_DELETE",
id: itemId, id: itemId,
}); });
} }
}})) },
})
);
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
menu.append(new MenuItem({label: _('Rename'), click: async () => { menu.append(
this.props.dispatch({ new MenuItem({
type: 'WINDOW_COMMAND', label: _("Rename"),
name: 'renameFolder', click: async () => {
id: itemId, this.props.dispatch({
}); type: "WINDOW_COMMAND",
}})) name: "renameFolder",
id: itemId,
});
},
})
);
menu.append(new MenuItem({ type: "separator" }));
const InteropService = require("lib/services/InteropService.js");
menu.append(
new MenuItem({
label: _("Export"),
click: async () => {
const ioService = new InteropService();
const module = ioService.moduleByFormat_("exporter", "jex");
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] });
},
})
);
} }
menu.popup(bridge().window()); menu.popup(bridge().window());
@@ -144,21 +171,21 @@ class SideBarComponent extends React.Component {
folderItem_click(folder) { folderItem_click(folder) {
this.props.dispatch({ this.props.dispatch({
type: 'FOLDER_SELECT', type: "FOLDER_SELECT",
id: folder ? folder.id : null, id: folder ? folder.id : null,
}); });
} }
tagItem_click(tag) { tagItem_click(tag) {
this.props.dispatch({ this.props.dispatch({
type: 'TAG_SELECT', type: "TAG_SELECT",
id: tag ? tag.id : null, id: tag ? tag.id : null,
}); });
} }
searchItem_click(search) { searchItem_click(search) {
this.props.dispatch({ this.props.dispatch({
type: 'SEARCH_SELECT', type: "SEARCH_SELECT",
id: search ? search.id : null, id: search ? search.id : null,
}); });
} }
@@ -173,105 +200,180 @@ class SideBarComponent extends React.Component {
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder); if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
const onDragOver = (event, folder) => { const onDragOver = (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault(); if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") >= 0) event.preventDefault();
} };
const onDrop = async (event, folder) => { const onDrop = async (event, folder) => {
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') < 0) return; if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") < 0) return;
event.preventDefault(); event.preventDefault();
const noteIds = JSON.parse(event.dataTransfer.getData('text/x-jop-note-ids')); const noteIds = JSON.parse(event.dataTransfer.getData("text/x-jop-note-ids"));
for (let i = 0; i < noteIds.length; i++) { for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folder.id); await Note.moveToFolder(noteIds[i], folder.id);
} }
} };
const itemTitle = Folder.displayTitle(folder); const itemTitle = Folder.displayTitle(folder);
return <a return (
className="list-item" <a
onDragOver={(event) => { onDragOver(event, folder) } } className="list-item"
onDrop={(event) => { onDrop(event, folder) } } onDragOver={event => {
href="#" onDragOver(event, folder);
data-id={folder.id} }}
data-type={BaseModel.TYPE_FOLDER} onDrop={event => {
onContextMenu={(event) => this.itemContextMenu(event)} onDrop(event, folder);
key={folder.id} }}
style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle} href="#"
</a> data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
key={folder.id}
style={style}
onClick={() => {
this.folderItem_click(folder);
}}
>
{itemTitle}
</a>
);
} }
tagItem(tag, selected) { tagItem(tag, selected) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected); if (selected) style = Object.assign(style, this.style().listItemSelected);
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{Tag.displayTitle(tag)}</a> return (
<a
className="list-item"
href="#"
data-id={tag.id}
data-type={BaseModel.TYPE_TAG}
onContextMenu={event => this.itemContextMenu(event)}
key={tag.id}
style={style}
onClick={() => {
this.tagItem_click(tag);
}}
>
{Tag.displayTitle(tag)}
</a>
);
} }
searchItem(search, selected) { searchItem(search, selected) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected); if (selected) style = Object.assign(style, this.style().listItemSelected);
return <a className="list-item" href="#" data-id={search.id} data-type={BaseModel.TYPE_SEARCH} onContextMenu={(event) => this.itemContextMenu(event)} key={search.id} style={style} onClick={() => {this.searchItem_click(search)}}>{search.title}</a> return (
<a
className="list-item"
href="#"
data-id={search.id}
data-type={BaseModel.TYPE_SEARCH}
onContextMenu={event => this.itemContextMenu(event)}
key={search.id}
style={style}
onClick={() => {
this.searchItem_click(search);
}}
>
{search.title}
</a>
);
} }
makeDivider(key) { makeDivider(key) {
return <div style={{height:2, backgroundColor:'blue' }} key={key}></div> return <div style={{ height: 2, backgroundColor: "blue" }} key={key} />;
} }
makeHeader(key, label, iconName) { makeHeader(key, label, iconName) {
const style = this.style().header; const style = this.style().header;
const icon = <i style={{fontSize: style.fontSize * 1.2, marginRight: 5}} className={"fa " + iconName}></i> const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />;
return <div style={style} key={key}>{icon}{label}</div> return (
<div style={style} key={key}>
{icon}
{label}
</div>
);
} }
synchronizeButton(type) { synchronizeButton(type) {
const style = this.style().button; const style = this.style().button;
const iconName = type === 'sync' ? 'fa-refresh' : 'fa-times'; const iconName = type === "sync" ? "fa-refresh" : "fa-times";
const label = type === 'sync' ? _('Synchronise') : _('Cancel'); const label = type === "sync" ? _("Synchronise") : _("Cancel");
const icon = <i style={{fontSize: style.fontSize, marginRight: 5}} className={"fa " + iconName}></i> const icon = <i style={{ fontSize: style.fontSize, marginRight: 5 }} className={"fa " + iconName} />;
return <a className="synchronize-button" style={style} href="#" key="sync_button" onClick={() => {this.sync_click()}}>{icon}{label}</a> return (
<a
className="synchronize-button"
style={style}
href="#"
key="sync_button"
onClick={() => {
this.sync_click();
}}
>
{icon}
{label}
</a>
);
} }
render() { render() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
const style = Object.assign({}, this.style().root, this.props.style, { const style = Object.assign({}, this.style().root, this.props.style, {
overflowX: 'hidden', overflowX: "hidden",
overflowY: 'auto', overflowY: "auto",
}); });
let items = []; let items = [];
items.push(this.makeHeader('folderHeader', _('Notebooks'), 'fa-folder-o')); items.push(this.makeHeader("folderHeader", _("Notebooks"), "fa-folder-o"));
if (this.props.folders.length) { if (this.props.folders.length) {
const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this)); const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this));
items = items.concat(folderItems); items = items.concat(folderItems);
} }
items.push(this.makeHeader('tagHeader', _('Tags'), 'fa-tags')); items.push(this.makeHeader("tagHeader", _("Tags"), "fa-tags"));
if (this.props.tags.length) { if (this.props.tags.length) {
const tagItems = shared.renderTags(this.props, this.tagItem.bind(this)); const tagItems = shared.renderTags(this.props, this.tagItem.bind(this));
items.push(<div className="tags" key="tag_items">{tagItems}</div>); items.push(
<div className="tags" key="tag_items">
{tagItems}
</div>
);
} }
if (this.props.searches.length) { if (this.props.searches.length) {
items.push(this.makeHeader('searchHeader', _('Searches'), 'fa-search')); items.push(this.makeHeader("searchHeader", _("Searches"), "fa-search"));
const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this)); const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this));
items.push(<div className="searches" key="search_items">{searchItems}</div>); items.push(
<div className="searches" key="search_items">
{searchItems}
</div>
);
} }
let lines = Synchronizer.reportToLines(this.props.syncReport); let lines = Synchronizer.reportToLines(this.props.syncReport);
const syncReportText = []; const syncReportText = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
syncReportText.push(<div key={i}>{lines[i]}</div>); syncReportText.push(
<div key={i} style={{ wordWrap: "break-word", width: "100%" }}>
{lines[i]}
</div>
);
} }
items.push(this.synchronizeButton(this.props.syncStarted ? 'cancel' : 'sync')); items.push(this.synchronizeButton(this.props.syncStarted ? "cancel" : "sync"));
items.push(<div style={this.style().syncReport} key='sync_report'>{syncReportText}</div>); items.push(
<div style={this.style().syncReport} key="sync_report">
{syncReportText}
</div>
);
return ( return (
<div className="side-bar" style={style}> <div className="side-bar" style={style}>
@@ -279,10 +381,9 @@ class SideBarComponent extends React.Component {
</div> </div>
); );
} }
} }
const mapStateToProps = (state) => { const mapStateToProps = state => {
return { return {
folders: state.folders, folders: state.folders,
tags: state.tags, tags: state.tags,

View File

@@ -181,7 +181,11 @@
}); });
document.addEventListener('contextmenu', function(event) { document.addEventListener('contextmenu', function(event) {
const element = event.target; let element = event.target;
// To handle right clicks on resource icons
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
if (element && element.getAttribute('data-resource-id')) { if (element && element.getAttribute('data-resource-id')) {
ipcRenderer.sendToHost('contextMenu', { ipcRenderer.sendToHost('contextMenu', {
type: element.getAttribute('src') ? 'image' : 'link', type: element.getAttribute('src') ? 'image' : 'link',

View File

@@ -17,11 +17,6 @@
.smalltalk .page { .smalltalk .page {
max-width: 30em; max-width: 30em;
} }
.ace_editor * {
/* Necessary to make sure Russian text is displayed properly */
/* https://github.com/laurent22/joplin/issues/155 */
font-family: monospace !important;
}
</style> </style>
</head> </head>
<body> <body>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -64,11 +64,17 @@ document.addEventListener('click', (event) => event.preventDefault());
app().start(bridge().processArgv()).then(() => { app().start(bridge().processArgv()).then(() => {
require('./gui/Root.min.js'); require('./gui/Root.min.js');
}).catch((error) => { }).catch((error) => {
// If something goes wrong at this stage we don't have a console or a log file if (error.code == 'flagError') {
// so display the error in a message box. bridge().showErrorMessageBox(error.message);
let msg = ['Fatal error:', error.message]; } else {
if (error.fileName) msg.push(error.fileName); // If something goes wrong at this stage we don't have a console or a log file
if (error.lineNumber) msg.push(error.lineNumber); // so display the error in a message box.
if (error.stack) msg.push(error.stack); let msg = ['Fatal error:', error.message];
bridge().showErrorMessageBox(msg.join('\n')); if (error.fileName) msg.push(error.fileName);
if (error.lineNumber) msg.push(error.lineNumber);
if (error.stack) msg.push(error.stack);
bridge().showErrorMessageBox(msg.join('\n'));
}
bridge().electronApp().exit(1);
}); });

View File

@@ -614,12 +614,14 @@
"bluebird": { "bluebird": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true
}, },
"bluebird-lst": { "bluebird-lst": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz",
"integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==",
"dev": true,
"requires": { "requires": {
"bluebird": "3.5.1" "bluebird": "3.5.1"
} }
@@ -848,6 +850,7 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.0.5.tgz", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.0.5.tgz",
"integrity": "sha512-NT8AxWH6miZQHnZzaTVjVp1uc6C/mWlxi6GQXKpd4CwyTQd3rT7+poOGrcOhtIiHYCL9VEbRsVfxUAPPsgqJdg==", "integrity": "sha512-NT8AxWH6miZQHnZzaTVjVp1uc6C/mWlxi6GQXKpd4CwyTQd3rT7+poOGrcOhtIiHYCL9VEbRsVfxUAPPsgqJdg==",
"dev": true,
"requires": { "requires": {
"bluebird-lst": "1.0.5", "bluebird-lst": "1.0.5",
"debug": "3.1.0", "debug": "3.1.0",
@@ -859,6 +862,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": { "requires": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -867,6 +871,7 @@
"version": "4.5.2", "version": "4.5.2",
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz", "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==", "integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
"dev": true,
"requires": { "requires": {
"bluebird-lst": "1.0.5", "bluebird-lst": "1.0.5",
"fs-extra": "5.0.0" "fs-extra": "5.0.0"
@@ -1084,6 +1089,11 @@
"integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=", "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=",
"dev": true "dev": true
}, },
"compare-versions": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.1.0.tgz",
"integrity": "sha512-4hAxDSBypT/yp2ySFD346So6Ragw5xmBn/e/agIGl3bZr6DLUqnoRZPusxKrXdYRZpgexO9daejmIenlq/wrIQ=="
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1642,11 +1652,6 @@
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.1.2.tgz", "resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.1.2.tgz",
"integrity": "sha1-ihBD4ys6HaHD9VPc4oznZCRhZ+M=" "integrity": "sha1-ihBD4ys6HaHD9VPc4oznZCRhZ+M="
}, },
"electron-log": {
"version": "2.2.11",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.11.tgz",
"integrity": "sha512-e+DHJkLJoaawDxyy9i2SMIViG/GV12DUE3T9a6yMdFp695c2u/kWYUZbUy31RzgX9uC/JuewXrO54tXdANgRRQ=="
},
"electron-osx-sign": { "electron-osx-sign": {
"version": "0.4.8", "version": "0.4.8",
"resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.8.tgz", "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.8.tgz",
@@ -1737,47 +1742,6 @@
} }
} }
}, },
"electron-updater": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-2.20.1.tgz",
"integrity": "sha512-7LJBcHY0T8So5Q/NZqYlU43DPXfyVssGq9ObamXbSL01mRt/gdpcJlXPU1w3vA/L6tC1IMVI9cAulAjI2oSYAw==",
"requires": {
"bluebird-lst": "1.0.5",
"builder-util-runtime": "4.0.5",
"electron-is-dev": "0.3.0",
"fs-extra-p": "4.5.2",
"js-yaml": "3.10.0",
"lazy-val": "1.0.3",
"lodash.isequal": "4.5.0",
"semver": "5.5.0",
"source-map-support": "0.5.3"
},
"dependencies": {
"electron-is-dev": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.3.0.tgz",
"integrity": "sha1-FOb9pcaOnk7L7/nM8DfL18BcWv4="
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-support": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz",
"integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==",
"requires": {
"source-map": "0.6.1"
}
}
}
},
"electron-window-state": { "electron-window-state": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-4.1.1.tgz", "resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-4.1.1.tgz",
@@ -1848,7 +1812,8 @@
"esprima": { "esprima": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==",
"dev": true
}, },
"esutils": { "esutils": {
"version": "2.0.2", "version": "2.0.2",
@@ -2091,15 +2056,6 @@
"universalify": "0.1.1" "universalify": "0.1.1"
} }
}, },
"fs-extra-p": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.5.2.tgz",
"integrity": "sha512-ZYqFpBdy9w7PsK+vB30j+TnHOyWHm/CJbUq1qqoE8tb71m6qgk5Wa7gp3MYQdlGFxb9vfznF+yD4jcl8l+y91A==",
"requires": {
"bluebird-lst": "1.0.5",
"fs-extra": "5.0.0"
}
},
"fs-minipass": { "fs-minipass": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz",
@@ -2559,8 +2515,7 @@
"jsbn": { "jsbn": {
"version": "0.1.1", "version": "0.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"json-schema": { "json-schema": {
"version": "0.2.3", "version": "0.2.3",
@@ -3593,6 +3548,7 @@
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
"integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
"dev": true,
"requires": { "requires": {
"argparse": "1.0.9", "argparse": "1.0.9",
"esprima": "4.0.0" "esprima": "4.0.0"
@@ -3695,11 +3651,6 @@
"package-json": "4.0.1" "package-json": "4.0.1"
} }
}, },
"lazy-val": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz",
"integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg=="
},
"lcid": { "lcid": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "Joplin", "name": "Joplin",
"version": "1.0.70", "version": "1.0.72",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@@ -59,9 +59,8 @@
"app-module-path": "^2.2.0", "app-module-path": "^2.2.0",
"async-mutex": "^0.1.3", "async-mutex": "^0.1.3",
"base-64": "^0.1.0", "base-64": "^0.1.0",
"compare-versions": "^3.1.0",
"electron-context-menu": "^0.9.1", "electron-context-menu": "^0.9.1",
"electron-log": "^2.2.11",
"electron-updater": "^2.18.2",
"electron-window-state": "^4.1.1", "electron-window-state": "^4.1.1",
"follow-redirects": "^1.2.5", "follow-redirects": "^1.2.5",
"form-data": "^2.3.1", "form-data": "^2.3.1",

View File

@@ -1,9 +0,0 @@
License MIT
Copyright (c) 2016-2018 Laurent Cozic
L'autorisation est accordée, gracieusement, à toute personne acquérant une copie de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de commercialiser le Logiciel sans restriction, notamment les droits d'utiliser, de copier, de modifier, de fusionner, de publier, de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel, ainsi que d'autoriser les personnes auxquelles le Logiciel est fourni à le faire, sous réserve des conditions suivantes :
La déclaration de copyright ci-dessus et la présente autorisation doivent être incluses dans toutes copies ou parties substantielles du Logiciel.
LE LOGICIEL EST FOURNI "TEL QUEL", SANS GARANTIE D'AUCUNE SORTE, EXPLICITE OU IMPLICITE, NOTAMMENT SANS GARANTIE DE QUALITÉ MARCHANDE, D’ADÉQUATION À UN USAGE PARTICULIER ET D'ABSENCE DE CONTREFAÇON. EN AUCUN CAS, LES AUTEURS OU TITULAIRES DU DROIT D'AUTEUR NE SERONT RESPONSABLES DE TOUT DOMMAGE, RÉCLAMATION OU AUTRE RESPONSABILITÉ, QUE CE SOIT DANS LE CADRE D'UN CONTRAT, D'UN DÉLIT OU AUTRE, EN PROVENANCE DE, CONSÉCUTIF À OU EN RELATION AVEC LE LOGICIEL OU SON UTILISATION, OU AVEC D'AUTRES ÉLÉMENTS DU LOGICIEL.

View File

@@ -1,6 +1,6 @@
# Joplin # Joplin
Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](https://daringfireball.net/projects/markdown/basics). Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](#markdown).
Notes exported from Evernote via .enex files [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported. Notes exported from Evernote via .enex files [can be imported](#importing) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.). Plain Markdown files can also be imported.
@@ -20,13 +20,13 @@ Operating System | Download
-----------------|-------- -----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-Setup-1.0.70.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a> Windows | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-Setup-1.0.70.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-1.0.70.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a> macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-1.0.70.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-1.0.70-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a> Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.70/Joplin-1.0.70-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## Mobile applications ## Mobile applications
Operating System | Download | Alt. Download Operating System | Download | Alt. Download
-----------------|----------|---------------- -----------------|----------|----------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.102/joplin-v1.0.102.apk) Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.106/joplin-v1.0.106.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | - iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
## Terminal application ## Terminal application
@@ -194,12 +194,18 @@ Here is an example with the Markdown and rendered result side by side:
Checkboxes can be added like so: Checkboxes can be added like so:
-[ ] Milk - [ ] Milk
-[ ] Rice - [ ] Rice
-[ ] Eggs - [ ] Eggs
The checkboxes can then be ticked in the mobile and desktop applications. The checkboxes can then be ticked in the mobile and desktop applications.
# Donations
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
Please see the [donation page](http://joplin.cozic.net/donate/) for information on how to support the development of Joplin.
# Contributing # Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
@@ -222,18 +228,18 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED --> <!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done &nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|--- ---|---|---|---|---
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 82% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 80%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić <trbuhom@net.hr> | 66% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić <trbuhom@net.hr> | 65%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Tobias Strobel <git@strobeltobias.de> | 84% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Tobias Strobel <git@strobeltobias.de> | 97%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín <f@mrtn.es> | 94% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín <f@mrtn.es> | 98%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 68% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 66%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 82% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 80%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | | 66% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | | 65%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 86% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 83%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | RCJacH <RCJacH@outlook.com> | 68% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | RCJacH <RCJacH@outlook.com> | 66%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 66% ![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 65%
<!-- LOCALE-TABLE-AUTO-GENERATED --> <!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs # Known bugs
@@ -243,6 +249,8 @@ Current translations:
# License # License
MIT License
Copyright (c) 2016-2018 Laurent Cozic Copyright (c) 2016-2018 Laurent Cozic
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -1,4 +0,0 @@
# When I open a note in vim, the cursor is not visible
It seems to be due to the setting `set term=ansi` in .vimrc. Removing it should fix the issue. See https://github.com/laurent22/joplin/issues/147 for more information.

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin" applicationId "net.cozic.joplin"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 22 targetSdkVersion 22
versionCode 2097280 versionCode 2097284
versionName "1.0.102" versionName "1.0.106"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }

View File

@@ -1,51 +1,3 @@
const { main } = require('./main.js'); const { main } = require('./main.js');
main(); main();
// const React = require('react'); const Component = React.Component;
// import {
// AppRegistry,
// StyleSheet,
// Text,
// View
// } from 'react-native';
// module.exports = default class Joplin extends Component {;
// render() {
// return (
// <View style={styles.container}>
// <Text style={styles.welcome}>
// Welcome to React Native!
// </Text>
// <Text style={styles.instructions}>
// To get started, edit index.ios.js
// </Text>
// <Text style={styles.instructions}>
// Press Cmd+R to reload,{'\n'}
// Cmd+D or shake for dev menu
// </Text>
// </View>
// );
// }
// }
// const styles = StyleSheet.create({
// container: {
// flex: 1,
// justifyContent: 'center',
// alignItems: 'center',
// backgroundColor: '#F5FCFF',
// },
// welcome: {
// fontSize: 20,
// textAlign: 'center',
// margin: 10,
// },
// instructions: {
// textAlign: 'center',
// color: '#333333',
// marginBottom: 5,
// },
// });
// AppRegistry.registerComponent('Joplin', () => Joplin);

View File

@@ -21,6 +21,7 @@ const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const os = require('os'); const os = require('os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const JoplinError = require('lib/JoplinError');
const EventEmitter = require('events'); const EventEmitter = require('events');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js'); const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js'); const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
@@ -95,14 +96,14 @@ class BaseApplication {
let nextArg = argv.length >= 2 ? argv[1] : null; let nextArg = argv.length >= 2 ? argv[1] : null;
if (arg == '--profile') { if (arg == '--profile') {
if (!nextArg) throw new Error(_('Usage: %s', '--profile <dir-path>')); if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError');
matched.profileDir = nextArg; matched.profileDir = nextArg;
argv.splice(0, 2); argv.splice(0, 2);
continue; continue;
} }
if (arg == '--env') { if (arg == '--env') {
if (!nextArg) throw new Error(_('Usage: %s', '--env <dev|prod>')); if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError');
matched.env = nextArg; matched.env = nextArg;
argv.splice(0, 2); argv.splice(0, 2);
continue; continue;
@@ -133,14 +134,14 @@ class BaseApplication {
} }
if (arg == '--log-level') { if (arg == '--log-level') {
if (!nextArg) throw new Error(_('Usage: %s', '--log-level <none|error|warn|info|debug>')); if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError');
matched.logLevel = Logger.levelStringToId(nextArg); matched.logLevel = Logger.levelStringToId(nextArg);
argv.splice(0, 2); argv.splice(0, 2);
continue; continue;
} }
if (arg.length && arg[0] == '-') { if (arg.length && arg[0] == '-') {
throw new Error(_('Unknown flag: %s', arg)); throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else { } else {
break; break;
} }

View File

@@ -1,4 +1,3 @@
const { Log } = require('lib/log.js');
const { Database } = require('lib/database.js'); const { Database } = require('lib/database.js');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
@@ -423,7 +422,7 @@ class BaseModel {
output = this.filter(o); output = this.filter(o);
} catch (error) { } catch (error) {
Log.error('Cannot save model', error); this.logger().error('Cannot save model', error);
} }
this.releaseSaveMutex(o, mutexRelease); this.releaseSaveMutex(o, mutexRelease);

View File

@@ -135,13 +135,17 @@ class MdToHtml {
// Ideally they should be opened in the user's browser. // Ideally they should be opened in the user's browser.
return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']'; return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']';
} else { } else {
let resourceIdAttr = "";
let icon = "";
if (isResourceUrl) { if (isResourceUrl) {
const resourceId = Resource.pathToId(href); const resourceId = Resource.pathToId(href);
href = 'joplin://' + resourceId; href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} }
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;"; const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>"; let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + icon;
return output; return output;
} }
} }
@@ -192,7 +196,7 @@ class MdToHtml {
let openTag = null; let openTag = null;
let closeTag = null; let closeTag = null;
let attrs = t.attrs ? t.attrs : []; let attrs = t.attrs ? t.attrs : [];
let tokenContent = t.content ? t.content : null; let tokenContent = t.content ? t.content : '';
const isCodeBlock = tag === 'code' && t.block; const isCodeBlock = tag === 'code' && t.block;
const isInlineCode = t.type === 'code_inline'; const isInlineCode = t.type === 'code_inline';
const codeBlockLanguage = t && t.info ? t.info : null; const codeBlockLanguage = t && t.info ? t.info : null;
@@ -394,7 +398,6 @@ class MdToHtml {
let loopCount = 0; let loopCount = 0;
while (renderedBody.indexOf('mJOPm') >= 0) { while (renderedBody.indexOf('mJOPm') >= 0) {
renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) { renderedBody = renderedBody.replace(/mJOPmCHECKBOXm([A-Z]+)m(\d+)m/, function(v, type, index) {
const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.classList.contains('tick') ? this.classList.remove('tick') : this.classList.add('tick'); return false;"; const js = options.postMessageSyntax + "('checkboxclick:" + type + ':' + index + "'); this.classList.contains('tick') ? this.classList.remove('tick') : this.classList.add('tick'); return false;";
return '<a href="#" onclick="' + js + '" class="checkbox ' + (type == 'NOTICK' ? '' : 'tick') + '"><span>' + '' + '</span></a>'; return '<a href="#" onclick="' + js + '" class="checkbox ' + (type == 'NOTICK' ? '' : 'tick') + '"><span>' + '' + '</span></a>';
}); });
@@ -443,6 +446,18 @@ class MdToHtml {
ul { ul {
padding-left: 1.3em; padding-left: 1.3em;
} }
.resource-icon {
display: inline-block;
position: relative;
top: .5em;
text-decoration: none;
width: 1.15em;
height: 1.5em;
margin-right: 0.4em;
background-color: ` + style.htmlColor + `;
/* Awesome Font file */
-webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 384 512'><path d='M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48z'/></svg>");
}
a.checkbox { a.checkbox {
display: inline-block; display: inline-block;
position: relative; position: relative;
@@ -480,13 +495,34 @@ class MdToHtml {
max-width: 100%; max-width: 100%;
} }
.katex .mfrac .frac-line:before { @media print {
/* top: 50%; */ body {
/* padding-bottom: .7em; */ height: auto !important;
}
a.checkbox {
border: 1pt solid ` + style.htmlColor + `;
border-radius: 2pt;
width: 1em;
height: 1em;
line-height: 1em;
text-align: center;
top: .4em;
}
a.checkbox.tick:after {
content: "X";
}
a.checkbox.tick {
top: 0;
left: -0.02em;
color: ` + style.htmlColor + `;
}
} }
`; `;
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; //+ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css">'; const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
const output = styleHtml + renderedBody; const output = styleHtml + renderedBody;

View File

@@ -4,7 +4,6 @@ const Icon = require('react-native-vector-icons/Ionicons').default;
const ReactNativeActionButton = require('react-native-action-button').default; const ReactNativeActionButton = require('react-native-action-button').default;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { globalStyle } = require('lib/components/global-style.js'); const { globalStyle } = require('lib/components/global-style.js');
const { Log } = require('lib/log.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@@ -104,7 +104,7 @@ class NoteBodyViewer extends Component {
style={webViewStyle} style={webViewStyle}
source={source} source={source}
onLoadEnd={() => this.onLoadEnd()} onLoadEnd={() => this.onLoadEnd()}
onError={(e) => reg.logger().error('WebView error', e) } onError={() => reg.logger().error('WebView error') }
onMessage={(event) => { onMessage={(event) => {
let msg = event.nativeEvent.data; let msg = event.nativeEvent.data;

View File

@@ -1,7 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { ListView, Text, TouchableOpacity , View, StyleSheet } = require('react-native'); const { ListView, Text, TouchableOpacity , View, StyleSheet } = require('react-native');
const { Log } = require('lib/log.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { Checkbox } = require('lib/components/checkbox.js'); const { Checkbox } = require('lib/components/checkbox.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');

View File

@@ -1,7 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } = require('react-native'); const { ListView, Text, TouchableHighlight, Switch, View, StyleSheet } = require('react-native');
const { Log } = require('lib/log.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const { Checkbox } = require('lib/components/checkbox.js'); const { Checkbox } = require('lib/components/checkbox.js');
const { NoteItem } = require('lib/components/note-item.js'); const { NoteItem } = require('lib/components/note-item.js');

View File

@@ -2,7 +2,6 @@ const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native'); const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
const Icon = require('react-native-vector-icons/Ionicons').default; const Icon = require('react-native-vector-icons/Ionicons').default;
const { Log } = require('lib/log.js');
const { BackButtonService } = require('lib/services/back-button.js'); const { BackButtonService } = require('lib/services/back-button.js');
const NavService = require('lib/services/NavService.js'); const NavService = require('lib/services/NavService.js');
const { ReportService } = require('lib/services/report.js'); const { ReportService } = require('lib/services/report.js');

View File

@@ -201,10 +201,18 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>); </View>);
} }
settingComps.push(
<View key="donate_link" style={this.styles().settingContainer}>
<TouchableOpacity onPress={() => { Linking.openURL('http://joplin.cozic.net/donate/') }}>
<Text key="label" style={this.styles().linkText}>{_('Make a donation')}</Text>
</TouchableOpacity>
</View>
);
settingComps.push( settingComps.push(
<View key="website_link" style={this.styles().settingContainer}> <View key="website_link" style={this.styles().settingContainer}>
<TouchableOpacity onPress={() => { Linking.openURL('http://joplin.cozic.net/') }}> <TouchableOpacity onPress={() => { Linking.openURL('http://joplin.cozic.net/') }}>
<Text key="label" style={this.styles().linkText}>Joplin Website</Text> <Text key="label" style={this.styles().linkText}>{_('Joplin website')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );

View File

@@ -1,7 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { View, Button, TextInput, StyleSheet } = require('react-native'); const { View, Button, TextInput, StyleSheet } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const { ActionButton } = require('lib/components/action-button.js'); const { ActionButton } = require('lib/components/action-button.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');

View File

@@ -1,7 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { ListView, View, Text, Button, StyleSheet, Platform } = require('react-native'); const { ListView, View, Text, Button, StyleSheet, Platform } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { ScreenHeader } = require('lib/components/screen-header.js'); const { ScreenHeader } = require('lib/components/screen-header.js');
const { time } = require('lib/time-utils'); const { time } = require('lib/time-utils');

View File

@@ -2,7 +2,6 @@ const React = require('react'); const Component = React.Component;
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native'); const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
const { Log } = require('lib/log.js');
const RNFS = require('react-native-fs'); const RNFS = require('react-native-fs');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');

View File

@@ -3,7 +3,6 @@ const { View, Button, Text } = require('react-native');
const { stateUtils } = require('lib/reducer.js'); const { stateUtils } = require('lib/reducer.js');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { Log } = require('lib/log.js');
const { NoteList } = require('lib/components/note-list.js'); const { NoteList } = require('lib/components/note-list.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');

View File

@@ -2,7 +2,6 @@ const React = require('react'); const Component = React.Component;
const { View } = require('react-native'); const { View } = require('react-native');
const { WebView, Button, Text } = require('react-native'); const { WebView, Button, Text } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { ScreenHeader } = require('lib/components/screen-header.js'); const { ScreenHeader } = require('lib/components/screen-header.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
@@ -44,8 +43,6 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
const parsedUrl = parseUri(url); const parsedUrl = parseUri(url);
if (!this.authCode_ && parsedUrl && parsedUrl.queryKey && parsedUrl.queryKey.code) { if (!this.authCode_ && parsedUrl && parsedUrl.queryKey && parsedUrl.queryKey.code) {
Log.info('URL: ', url, parsedUrl.queryKey);
this.authCode_ = parsedUrl.queryKey.code this.authCode_ = parsedUrl.queryKey.code
try { try {
@@ -60,8 +57,8 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
} }
} }
async webview_error(error) { async webview_error() {
Log.error(error); alert('Could not load page. Please check your connection and try again.');
} }
retryButton_click() { retryButton_click() {
@@ -93,7 +90,7 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent {
<WebView <WebView
source={source} source={source}
onNavigationStateChange={(o) => { this.webview_load(o); }} onNavigationStateChange={(o) => { this.webview_load(o); }}
onError={(error) => { this.webview_error(error); }} onError={() => { this.webview_error(); }}
/> />
<Button title={_("Refresh")} onPress={() => { this.retryButton_click(); }}></Button> <Button title={_("Refresh")} onPress={() => { this.retryButton_click(); }}></Button>
</View> </View>

View File

@@ -2,7 +2,6 @@ const React = require('react'); const Component = React.Component;
const { ListView, StyleSheet, View, Text, Button, FlatList } = require('react-native'); const { ListView, StyleSheet, View, Text, Button, FlatList } = require('react-native');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { ScreenHeader } = require('lib/components/screen-header.js'); const { ScreenHeader } = require('lib/components/screen-header.js');
const { time } = require('lib/time-utils'); const { time } = require('lib/time-utils');

View File

@@ -1,7 +1,6 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { View, Text, StyleSheet } = require('react-native'); const { View, Text, StyleSheet } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const { ScreenHeader } = require('lib/components/screen-header.js'); const { ScreenHeader } = require('lib/components/screen-header.js');
const { ActionButton } = require('lib/components/action-button.js'); const { ActionButton } = require('lib/components/action-button.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js');

View File

@@ -63,7 +63,9 @@ shared.checkPasswords = async function(comp) {
shared.decryptedStatText = function(comp) { shared.decryptedStatText = function(comp) {
const stats = comp.state.stats; const stats = comp.state.stats;
return _('Decrypted items: %s / %s', stats.encrypted !== null ? (stats.total - stats.encrypted) : '-', stats.total !== null ? stats.total : '-'); const doneCount = stats.encrypted !== null ? (stats.total - stats.encrypted) : '-';
const totalCount = stats.total !== null ? stats.total : '-';
return _('Decrypted items: %s / %s', doneCount, totalCount);
} }
shared.onSavePasswordClick = function(comp, mk) { shared.onSavePasswordClick = function(comp, mk) {

View File

@@ -2,7 +2,6 @@ const React = require('react'); const Component = React.Component;
const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View } = require('react-native'); const { TouchableOpacity , Button, Text, Image, StyleSheet, ScrollView, View } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const Icon = require('react-native-vector-icons/Ionicons').default; const Icon = require('react-native-vector-icons/Ionicons').default;
const { Log } = require('lib/log.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');

View File

@@ -1,6 +1,5 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { Log } = require('lib/log.js');
const SideMenu_ = require('react-native-side-menu').default; const SideMenu_ = require('react-native-side-menu').default;
class SideMenuComponent extends SideMenu_ {}; class SideMenuComponent extends SideMenu_ {};

View File

@@ -35,7 +35,11 @@ class FsDriverNode extends FsDriverBase {
async writeFile(path, string, encoding = 'base64') { async writeFile(path, string, encoding = 'base64') {
try { try {
return await fs.writeFile(path, string, { encoding: encoding }); if (encoding === 'buffer') {
return await fs.writeFile(path, string);
} else {
return await fs.writeFile(path, string, { encoding: encoding });
}
} catch (error) { } catch (error) {
throw this.fsErrorToJsError_(error, path); throw this.fsErrorToJsError_(error, path);
} }

View File

@@ -1,40 +0,0 @@
// Custom wrapper for `console` to allow for custom logging (to file, etc.) if needed.
class Log {
static setLevel(v) {
this.level_ = v;
}
static level() {
return this.level_ === undefined ? Log.LEVEL_DEBUG : this.level_;
}
static debug(...o) {
if (Log.level() > Log.LEVEL_DEBUG) return;
console.info(...o);
}
static info(...o) {
if (Log.level() > Log.LEVEL_INFO) return;
console.info(...o);
}
static warn(...o) {
if (Log.level() > Log.LEVEL_WARN) return;
console.info(...o);
}
static error(...o) {
if (Log.level() > Log.LEVEL_ERROR) return;
console.info(...o);
}
}
Log.LEVEL_DEBUG = 0;
Log.LEVEL_INFO = 10;
Log.LEVEL_WARN = 20;
Log.LEVEL_ERROR = 30;
module.exports = { Log };

View File

@@ -1,5 +1,4 @@
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
const { Log } = require('lib/log.js');
const { promiseChain } = require('lib/promise-utils.js'); const { promiseChain } = require('lib/promise-utils.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');

View File

@@ -1,5 +1,4 @@
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
const { Log } = require('lib/log.js');
const { sprintf } = require('sprintf-js'); const { sprintf } = require('sprintf-js');
const BaseItem = require('lib/models/BaseItem.js'); const BaseItem = require('lib/models/BaseItem.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
@@ -81,7 +80,17 @@ class Note extends BaseItem {
static defaultTitle(note) { static defaultTitle(note) {
if (note.body && note.body.length) { if (note.body && note.body.length) {
const lines = note.body.trim().split("\n"); const lines = note.body.trim().split("\n");
return lines[0].trim().substr(0, 80).trim(); let output = lines[0].trim();
// Remove the first #, *, etc.
while (output.length) {
const c = output[0];
if (['#', ' ', "\n", "\t", '*', '`', '-'].indexOf(c) >= 0) {
output = output.substr(1);
} else {
break;
}
}
return output.substr(0, 80).trim();
} }
return _('Untitled'); return _('Untitled');
@@ -161,7 +170,7 @@ class Note extends BaseItem {
} }
static previewFields() { static previewFields() {
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time', 'encryption_applied']; return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time', 'user_created_time', 'encryption_applied'];
} }
static previewFieldsSql() { static previewFieldsSql() {

View File

@@ -83,7 +83,8 @@ class Setting extends BaseModel {
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false }, 'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false }, 'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false }, 'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false },
'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Set application zoom percentage'), minimum: "50", maximum: "500", step: "10"}, 'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Global zoom percentage'), minimum: "50", maximum: "500", step: "10"},
'style.editor.fontFamily': {value: "", type: Setting.TYPE_STRING, public: true, appTypes: ['desktop'], label: () => _('Editor font family'), description: () => _('The font name will not be checked. If incorrect or empty, it will default to a generic monospace font.')},
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') }, 'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => { 'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return { return {
@@ -98,7 +99,7 @@ class Setting extends BaseModel {
}}, }},
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] }, 'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') }, 'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).'), options: () => { 'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => {
return SyncTargetRegistry.idAndLabelPlainObject(); return SyncTargetRegistry.idAndLabelPlainObject();
}}, }},
@@ -108,7 +109,7 @@ class Setting extends BaseModel {
} catch (error) { } catch (error) {
return false; return false;
} }
}, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') }, }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: (appType) => { return appType !== 'cli' ? null : _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.'); } },
'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud WebDAV URL') }, 'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud WebDAV URL') },
'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud username') }, 'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud username') },
@@ -143,6 +144,12 @@ class Setting extends BaseModel {
return key in this.metadata(); return key in this.metadata();
} }
static keyDescription(key, appType = null) {
const md = this.settingMetadata(key);
if (!md.description) return null;
return md.description(appType);
}
static keys(publicOnly = false, appType = null) { static keys(publicOnly = false, appType = null) {
if (!this.keys_) { if (!this.keys_) {
const metadata = this.metadata(); const metadata = this.metadata();

View File

@@ -43,8 +43,9 @@ reg.syncTarget = (syncTargetId = null) => {
return target; return target;
} }
reg.scheduleSync = async (delay = null) => { reg.scheduleSync = async (delay = null, syncOptions = null) => {
if (delay === null) delay = 1000 * 10; if (delay === null) delay = 1000 * 10;
if (syncOptions === null) syncOptions = {};
let promiseResolve = null; let promiseResolve = null;
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
@@ -58,10 +59,10 @@ reg.scheduleSync = async (delay = null) => {
reg.logger().info('Scheduling sync operation...'); reg.logger().info('Scheduling sync operation...');
if (Setting.value('env') === 'dev' && delay !== 0) { // if (Setting.value("env") === "dev" && delay !== 0) {
reg.logger().info('Schedule sync DISABLED!!!'); // reg.logger().info("Schedule sync DISABLED!!!");
return; // return;
} // }
const timeoutCallback = async () => { const timeoutCallback = async () => {
reg.scheduleSyncId_ = null; reg.scheduleSyncId_ = null;
@@ -92,8 +93,9 @@ reg.scheduleSync = async (delay = null) => {
} }
try { try {
reg.logger().info('Starting scheduled sync'); reg.logger().info("Starting scheduled sync");
let newContext = await sync.start({ context: context }); const options = Object.assign({}, syncOptions, { context: context });
const newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext)); Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) { } catch (error) {
if (error.code == 'alreadyStarted') { if (error.code == 'alreadyStarted') {

View File

@@ -80,7 +80,9 @@ class DecryptionWorker {
// } // }
const ItemClass = BaseItem.itemClass(item); const ItemClass = BaseItem.itemClass(item);
this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
// Don't log in production as it results in many messages when importing many items
// this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
try { try {
await ItemClass.decrypt(item); await ItemClass.decrypt(item);
} catch (error) { } catch (error) {

View File

@@ -81,6 +81,17 @@ class InteropService {
this.modules_ = importModules.concat(exportModules); this.modules_ = importModules.concat(exportModules);
this.modules_ = this.modules_.map((a) => {
a.fullLabel = function(moduleSource = null) {
const label = [this.format.toUpperCase() + ' - ' + this.description];
if (moduleSource && this.sources.length > 1) {
label.push('(' + (moduleSource === 'file' ? _('File') : _('Directory')) + ')');
}
return label.join(' ');
};
return a;
});
return this.modules_; return this.modules_;
} }

View File

@@ -36,6 +36,8 @@ class InteropService_Importer_Jex extends InteropService_Importer_Base {
throw e; throw e;
} }
if (!('defaultFolderTitle' in this.options_)) this.options_.defaultFolderTitle = filename(this.sourcePath_);
const importer = new InteropService_Importer_Raw(); const importer = new InteropService_Importer_Raw();
await importer.init(tempDir, this.options_); await importer.init(tempDir, this.options_);
result = await importer.exec(result); result = await importer.exec(result);

View File

@@ -14,7 +14,6 @@ const { shim } = require('lib/shim');
const { _ } = require('lib/locale'); const { _ } = require('lib/locale');
const { fileExtension } = require('lib/path-utils'); const { fileExtension } = require('lib/path-utils');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
const { importEnex } = require('lib/import-enex');
class InteropService_Importer_Raw extends InteropService_Importer_Base { class InteropService_Importer_Raw extends InteropService_Importer_Base {
@@ -41,6 +40,25 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
} }
const stats = await shim.fsDriver().readDirStats(this.sourcePath_); const stats = await shim.fsDriver().readDirStats(this.sourcePath_);
const folderExists = function(stats, folderId) {
folderId = folderId.toLowerCase();
for (let i = 0; i < stats.length; i++) {
const stat = stats[i];
const statId = BaseItem.pathToId(stat.path);
if (statId.toLowerCase() === folderId) return true;
}
return false;
}
let defaultFolder_ = null;
const defaultFolder = async () => {
if (defaultFolder_) return defaultFolder_;
const folderTitle = await Folder.findUniqueFolderTitle(this.options_.defaultFolderTitle ? this.options_.defaultFolderTitle : 'Imported');
defaultFolder_ = await Folder.save({ title: folderTitle });
return defaultFolder_;
}
for (let i = 0; i < stats.length; i++) { for (let i = 0; i < stats.length; i++) {
const stat = stats[i]; const stat = stats[i];
if (stat.isDirectory()) continue; if (stat.isDirectory()) continue;
@@ -54,7 +72,22 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
delete item.type_; delete item.type_;
if (itemType === BaseModel.TYPE_NOTE) { if (itemType === BaseModel.TYPE_NOTE) {
if (!folderIdMap[item.parent_id]) folderIdMap[item.parent_id] = destinationFolderId ? destinationFolderId : uuid.create();
// Logic is a bit complex here:
// - If a destination folder was specified, move the note to it.
// - Otherwise, if the associated folder exists, use this.
// - If it doesn't exist, use the default folder. This is the case for example when importing JEX archives that contain only one or more notes, but no folder.
if (!folderIdMap[item.parent_id]) {
if (destinationFolderId) {
folderIdMap[item.parent_id] = destinationFolderId;
} else if (!folderExists(stats, item.parent_id)) {
const parentFolder = await defaultFolder();
folderIdMap[item.parent_id] = parentFolder.id;
} else {
folderIdMap[item.parent_id] = uuid.create();
}
}
const noteId = uuid.create(); const noteId = uuid.create();
noteIdMap[item.id] = noteId; noteIdMap[item.id] = noteId;
item.id = noteId; item.id = noteId;

View File

@@ -53,7 +53,7 @@ shim.isElectron = () => {
// Node requests can go wrong is so many different ways and with so // Node requests can go wrong is so many different ways and with so
// many different error messages... This handler inspects the error // many different error messages... This handler inspects the error
// and decides whether the request can safely be repeated or not. // and decides whether the request can safely be repeated or not.
function fetchRequestCanBeRetried(error) { shim.fetchRequestCanBeRetried = function(error) {
if (!error) return false; if (!error) return false;
// Unfortunately the error 'Network request failed' doesn't have a type // Unfortunately the error 'Network request failed' doesn't have a type
@@ -86,7 +86,7 @@ function fetchRequestCanBeRetried(error) {
if (error.code === 'ETIMEDOUT') return true; if (error.code === 'ETIMEDOUT') return true;
return false; return false;
} };
shim.fetchWithRetry = async function(fetchFn, options = null) { shim.fetchWithRetry = async function(fetchFn, options = null) {
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
@@ -101,7 +101,7 @@ shim.fetchWithRetry = async function(fetchFn, options = null) {
const response = await fetchFn(); const response = await fetchFn();
return response; return response;
} catch (error) { } catch (error) {
if (fetchRequestCanBeRetried(error)) { if (shim.fetchRequestCanBeRetried(error)) {
retryCount++; retryCount++;
if (retryCount > options.maxRetry) throw error; if (retryCount > options.maxRetry) throw error;
await time.sleep(retryCount * 3); await time.sleep(retryCount * 3);

View File

@@ -65,16 +65,17 @@ class Synchronizer {
static reportToLines(report) { static reportToLines(report) {
let lines = []; let lines = [];
if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal)); if (report.createLocal) lines.push(_("Created local items: %d.", report.createLocal));
if (report.updateLocal) lines.push(_('Updated local items: %d.', report.updateLocal)); if (report.updateLocal) lines.push(_("Updated local items: %d.", report.updateLocal));
if (report.createRemote) lines.push(_('Created remote items: %d.', report.createRemote)); if (report.createRemote) lines.push(_("Created remote items: %d.", report.createRemote));
if (report.updateRemote) lines.push(_('Updated remote items: %d.', report.updateRemote)); if (report.updateRemote) lines.push(_("Updated remote items: %d.", report.updateRemote));
if (report.deleteLocal) lines.push(_('Deleted local items: %d.', report.deleteLocal)); if (report.deleteLocal) lines.push(_("Deleted local items: %d.", report.deleteLocal));
if (report.deleteRemote) lines.push(_('Deleted remote items: %d.', report.deleteRemote)); if (report.deleteRemote) lines.push(_("Deleted remote items: %d.", report.deleteRemote));
if (report.fetchingTotal && report.fetchingProcessed) lines.push(_('Fetched items: %d/%d.', report.fetchingProcessed, report.fetchingTotal)); if (report.fetchingTotal && report.fetchingProcessed) lines.push(_("Fetched items: %d/%d.", report.fetchingProcessed, report.fetchingTotal));
if (!report.completedTime && report.state) lines.push(_('State: "%s".', report.state)); if (!report.completedTime && report.state) lines.push(_('State: "%s".', Synchronizer.stateToLabel(report.state)));
if (report.cancelling && !report.completedTime) lines.push(_('Cancelling...')); if (report.cancelling && !report.completedTime) lines.push(_("Cancelling..."));
if (report.completedTime) lines.push(_('Completed: %s', time.unixMsToLocalDateTime(report.completedTime))); if (report.completedTime) lines.push(_("Completed: %s", time.unixMsToLocalDateTime(report.completedTime)));
if (report.errors && report.errors.length) lines.push(_("Last error: %s", report.errors[report.errors.length - 1].toString().substr(0, 500)));
return lines; return lines;
} }
@@ -158,6 +159,17 @@ class Synchronizer {
return this.cancelling_; return this.cancelling_;
} }
static stateToLabel(state) {
if (state === "idle") return _("Idle");
if (state === "in_progress") return _("In progress");
return state;
}
// Synchronisation is done in three major steps:
//
// 1. UPLOAD: Send to the sync target the items that have changed since the last sync.
// 2. DELETE_REMOTE: Delete on the sync target, the items that have been deleted locally.
// 3. DELTA: Find on the sync target the items that have been modified or deleted and apply the changes locally.
async start(options = null) { async start(options = null) {
if (!options) options = {}; if (!options) options = {};
@@ -174,6 +186,8 @@ class Synchronizer {
const lastContext = options.context ? options.context : {}; const lastContext = options.context ? options.context : {};
const syncSteps = options.syncSteps ? options.syncSteps : ["update_remote", "delete_remote", "delta"];
const syncTargetId = this.api().syncTargetId(); const syncTargetId = this.api().syncTargetId();
this.cancelling_ = false; this.cancelling_ = false;
@@ -181,11 +195,6 @@ class Synchronizer {
const masterKeysBefore = await MasterKey.count(); const masterKeysBefore = await MasterKey.count();
let hasAutoEnabledEncryption = false; let hasAutoEnabledEncryption = false;
// ------------------------------------------------------------------------
// First, find all the items that have been changed since the
// last sync and apply the changes to remote.
// ------------------------------------------------------------------------
let synchronizationId = time.unixMs().toString(); let synchronizationId = time.unixMs().toString();
let outputContext = Object.assign({}, lastContext); let outputContext = Object.assign({}, lastContext);
@@ -199,397 +208,412 @@ class Synchronizer {
this.api().setTempDirName(this.syncDirName_); this.api().setTempDirName(this.syncDirName_);
await this.api().mkdir(this.resourceDirName_); await this.api().mkdir(this.resourceDirName_);
let donePaths = []; // ========================================================================
while (true) { // 1. UPLOAD
if (this.cancelling()) break; // ------------------------------------------------------------------------
// First, find all the items that have been changed since the
// last sync and apply the changes to remote.
// ========================================================================
let result = await BaseItem.itemsThatNeedSync(syncTargetId); if (syncSteps.indexOf("update_remote") >= 0) {
let locals = result.items; let donePaths = [];
while (true) {
for (let i = 0; i < locals.length; i++) {
if (this.cancelling()) break; if (this.cancelling()) break;
let local = locals[i]; let result = await BaseItem.itemsThatNeedSync(syncTargetId);
let ItemClass = BaseItem.itemClass(local); let locals = result.items;
let path = BaseItem.systemPath(local);
// Safety check to avoid infinite loops. for (let i = 0; i < locals.length; i++) {
// In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in if (this.cancelling()) break;
// progress. In that case exit anyway to be sure we aren't in a loop and the item will be re-synced next time.
if (donePaths.indexOf(path) > 0) throw new Error(sprintf('Processing a path that has already been done: %s. sync_time was not updated?', path));
let remote = await this.api().stat(path); let local = locals[i];
let action = null; let ItemClass = BaseItem.itemClass(local);
let updateSyncTimeOnly = true; let path = BaseItem.systemPath(local);
let reason = '';
let remoteContent = null;
if (!remote) { // Safety check to avoid infinite loops.
if (!local.sync_time) { // In fact this error is possible if the item is marked for sync (via sync_time or force_sync) while synchronisation is in
action = 'createRemote'; // progress. In that case exit anyway to be sure we aren't in a loop and the item will be re-synced next time.
reason = 'remote does not exist, and local is new and has never been synced'; if (donePaths.indexOf(path) > 0) throw new Error(sprintf("Processing a path that has already been done: %s. sync_time was not updated?", path));
} else {
// Note or item was modified after having been deleted remotely
// "itemConflict" if for all the items except the notes, which are dealt with in a special way
action = local.type_ == BaseModel.TYPE_NOTE ? 'noteConflict' : 'itemConflict';
reason = 'remote has been deleted, but local has changes';
}
} else {
// Note: in order to know the real updated_time value, we need to load the content. In theory we could
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
// updated_time). updated_time is set and managed by clients so it's always accurate.
// Same situation below for updateLocal.
//
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
// content, but for now that will do since being reliable is the priority.
//
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
// a few seconds ahead of what it was set with setTimestamp()
remoteContent = await this.api().get(path);
if (!remoteContent) throw new Error('Got metadata for path but could not fetch content: ' + path);
remoteContent = await BaseItem.unserialize(remoteContent);
if (remoteContent.updated_time > local.sync_time) { let remote = await this.api().stat(path);
// Since, in this loop, we are only dealing with items that require sync, if the let action = null;
// remote has been modified after the sync time, it means both items have been let updateSyncTimeOnly = true;
// modified and so there's a conflict. let reason = "";
action = local.type_ == BaseModel.TYPE_NOTE ? 'noteConflict' : 'itemConflict'; let remoteContent = null;
reason = 'both remote and local have changes';
} else {
action = 'updateRemote';
reason = 'local has changes';
}
}
this.logSyncOperation(action, local, remote, reason); if (!remote) {
if (!local.sync_time) {
const handleCannotSyncItem = async (syncTargetId, item, cannotSyncReason) => { action = "createRemote";
await ItemClass.saveSyncDisabled(syncTargetId, item, cannotSyncReason); reason = "remote does not exist, and local is new and has never been synced";
this.dispatch({ type: 'SYNC_HAS_DISABLED_SYNC_ITEMS' });
}
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || action === 'updateRemote' || (action == 'itemConflict' && remote))) {
try {
const remoteContentPath = this.resourceDirName_ + '/' + local.id;
const result = await Resource.fullPathForSyncUpload(local);
local = result.resource;
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
} catch (error) {
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
await handleCannotSyncItem(syncTargetId, local, error.message);
action = null;
} else { } else {
throw error; // Note or item was modified after having been deleted remotely
// "itemConflict" if for all the items except the notes, which are dealt with in a special way
action = local.type_ == BaseModel.TYPE_NOTE ? "noteConflict" : "itemConflict";
reason = "remote has been deleted, but local has changes";
} }
} } else {
} // Note: in order to know the real updated_time value, we need to load the content. In theory we could
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
// updated_time). updated_time is set and managed by clients so it's always accurate.
// Same situation below for updateLocal.
//
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
// content, but for now that will do since being reliable is the priority.
//
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
// a few seconds ahead of what it was set with setTimestamp()
remoteContent = await this.api().get(path);
if (!remoteContent) throw new Error("Got metadata for path but could not fetch content: " + path);
remoteContent = await BaseItem.unserialize(remoteContent);
if (action == 'createRemote' || action == 'updateRemote') { if (remoteContent.updated_time > local.sync_time) {
let canSync = true; // Since, in this loop, we are only dealing with items that require sync, if the
try { // remote has been modified after the sync time, it means both items have been
if (this.testingHooks_.indexOf('rejectedByTarget') >= 0) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget'); // modified and so there's a conflict.
const content = await ItemClass.serializeForSync(local); action = local.type_ == BaseModel.TYPE_NOTE ? "noteConflict" : "itemConflict";
await this.api().put(path, content); reason = "both remote and local have changes";
} catch (error) {
if (error && error.code === 'rejectedByTarget') {
await handleCannotSyncItem(syncTargetId, local, error.message);
canSync = false;
} else { } else {
throw error; action = "updateRemote";
reason = "local has changes";
} }
} }
// Note: Currently, we set sync_time to update_time, which should work fine given that the resolution is the millisecond. this.logSyncOperation(action, local, remote, reason);
// In theory though, this could happen:
//
// 1. t0: Editor: Note is modified
// 2. t0: Sync: Found that note was modified so start uploading it
// 3. t0: Editor: Note is modified again
// 4. t1: Sync: Note has finished uploading, set sync_time to t0
//
// Later any attempt to sync will not detect that note was modified in (3) (within the same millisecond as it was being uploaded)
// because sync_time will be t0 too.
//
// The solution would be to use something like an etag (a simple counter incremented on every change) to make sure each
// change is uniquely identified. Leaving it like this for now.
if (canSync) { const handleCannotSyncItem = async (syncTargetId, item, cannotSyncReason) => {
// 2018-01-21: Setting timestamp is not needed because the delta() logic doesn't rely await ItemClass.saveSyncDisabled(syncTargetId, item, cannotSyncReason);
// on it (instead it uses a more reliable `context` object) and the itemsThatNeedSync loop this.dispatch({ type: "SYNC_HAS_DISABLED_SYNC_ITEMS" });
// above also doesn't use it because it fetches the whole remote object and read the };
// more reliable 'updated_time' property. Basically remote.updated_time is deprecated.
// await this.api().setTimestamp(path, local.updated_time); if (local.type_ == BaseModel.TYPE_RESOURCE && (action == "createRemote" || action === "updateRemote" || (action == "itemConflict" && remote))) {
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time); try {
const remoteContentPath = this.resourceDirName_ + "/" + local.id;
const result = await Resource.fullPathForSyncUpload(local);
local = result.resource;
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: "file" });
} catch (error) {
if (error && ["rejectedByTarget", "fileNotFound"].indexOf(error.code) >= 0) {
await handleCannotSyncItem(syncTargetId, local, error.message);
action = null;
} else {
throw error;
}
}
} }
} else if (action == 'itemConflict') { if (action == "createRemote" || action == "updateRemote") {
let canSync = true;
try {
if (this.testingHooks_.indexOf("rejectedByTarget") >= 0) throw new JoplinError("Testing rejectedByTarget", "rejectedByTarget");
const content = await ItemClass.serializeForSync(local);
await this.api().put(path, content);
} catch (error) {
if (error && error.code === "rejectedByTarget") {
await handleCannotSyncItem(syncTargetId, local, error.message);
canSync = false;
} else {
throw error;
}
}
// ------------------------------------------------------------------------------ // Note: Currently, we set sync_time to update_time, which should work fine given that the resolution is the millisecond.
// For non-note conflicts, we take the remote version (i.e. the version that was // In theory though, this could happen:
// synced first) and overwrite the local content. //
// ------------------------------------------------------------------------------ // 1. t0: Editor: Note is modified
// 2. t0: Sync: Found that note was modified so start uploading it
// 3. t0: Editor: Note is modified again
// 4. t1: Sync: Note has finished uploading, set sync_time to t0
//
// Later any attempt to sync will not detect that note was modified in (3) (within the same millisecond as it was being uploaded)
// because sync_time will be t0 too.
//
// The solution would be to use something like an etag (a simple counter incremented on every change) to make sure each
// change is uniquely identified. Leaving it like this for now.
if (remote) { if (canSync) {
local = remoteContent; // 2018-01-21: Setting timestamp is not needed because the delta() logic doesn't rely
// on it (instead it uses a more reliable `context` object) and the itemsThatNeedSync loop
// above also doesn't use it because it fetches the whole remote object and read the
// more reliable 'updated_time' property. Basically remote.updated_time is deprecated.
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs()); // await this.api().setTimestamp(path, local.updated_time);
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries }); await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
} else { }
await ItemClass.delete(local.id); } else if (action == "itemConflict") {
} // ------------------------------------------------------------------------------
// For non-note conflicts, we take the remote version (i.e. the version that was
} else if (action == 'noteConflict') { // synced first) and overwrite the local content.
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------
// First find out if the conflict matters. For example, if the conflict is on the title or body if (remote) {
// we want to preserve all the changes. If it's on todo_completed it doesn't really matter local = remoteContent;
// so in this case we just take the remote content.
// ------------------------------------------------------------------------------ const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
let mustHandleConflict = true; } else {
if (remoteContent) { await ItemClass.delete(local.id);
mustHandleConflict = Note.mustHandleConflict(local, remoteContent); }
} } else if (action == "noteConflict") {
// ------------------------------------------------------------------------------
// ------------------------------------------------------------------------------ // First find out if the conflict matters. For example, if the conflict is on the title or body
// Create a duplicate of local note into Conflicts folder // we want to preserve all the changes. If it's on todo_completed it doesn't really matter
// (to preserve the user's changes) // so in this case we just take the remote content.
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
if (mustHandleConflict) { let mustHandleConflict = true;
let conflictedNote = Object.assign({}, local); if (remoteContent) {
delete conflictedNote.id; mustHandleConflict = Note.mustHandleConflict(local, remoteContent);
conflictedNote.is_conflict = 1; }
await Note.save(conflictedNote, { autoTimestamp: false });
} // ------------------------------------------------------------------------------
// Create a duplicate of local note into Conflicts folder
// ------------------------------------------------------------------------------ // (to preserve the user's changes)
// Either copy the remote content to local or, if the remote content has // ------------------------------------------------------------------------------
// been deleted, delete the local content.
// ------------------------------------------------------------------------------ if (mustHandleConflict) {
let conflictedNote = Object.assign({}, local);
if (remote) { delete conflictedNote.id;
local = remoteContent; conflictedNote.is_conflict = 1;
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs()); await Note.save(conflictedNote, { autoTimestamp: false });
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries }); }
if (!!local.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' }); // ------------------------------------------------------------------------------
} else { // Either copy the remote content to local or, if the remote content has
// Remote no longer exists (note deleted) so delete local one too // been deleted, delete the local content.
await ItemClass.delete(local.id); // ------------------------------------------------------------------------------
if (remote) {
local = remoteContent;
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
if (!!local.encryption_applied) this.dispatch({ type: "SYNC_GOT_ENCRYPTED_ITEM" });
} else {
// Remote no longer exists (note deleted) so delete local one too
await ItemClass.delete(local.id);
}
} }
donePaths.push(path);
} }
donePaths.push(path); if (!result.hasMore) break;
} }
} // UPLOAD STEP
if (!result.hasMore) break; // ========================================================================
} // 2. DELETE_REMOTE
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Delete the remote items that have been deleted locally. // Delete the remote items that have been deleted locally.
// ========================================================================
if (syncSteps.indexOf("delete_remote") >= 0) {
let deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
let item = deletedItems[i];
let path = BaseItem.systemPath(item.item_id);
this.logSyncOperation("deleteRemote", null, { id: item.item_id }, "local has been deleted");
await this.api().delete(path);
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
} // DELETE_REMOTE STEP
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 3. DELTA
let deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
let item = deletedItems[i];
let path = BaseItem.systemPath(item.item_id)
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.api().delete(path);
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// Loop through all the remote items, find those that // Loop through all the remote items, find those that
// have been updated, and apply the changes to local. // have been updated, and apply the changes to local.
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// At this point all the local items that have changed have been pushed to remote if (syncSteps.indexOf("delta") >= 0) {
// or handled as conflicts, so no conflict is possible after this. // At this point all the local items that have changed have been pushed to remote
// or handled as conflicts, so no conflict is possible after this.
let context = null; let context = null;
let newDeltaContext = null; let newDeltaContext = null;
let localFoldersToDelete = []; let localFoldersToDelete = [];
let hasCancelled = false; let hasCancelled = false;
if (lastContext.delta) context = lastContext.delta; if (lastContext.delta) context = lastContext.delta;
while (true) { while (true) {
if (this.cancelling() || hasCancelled) break; if (this.cancelling() || hasCancelled) break;
let listResult = await this.api().delta('', { let listResult = await this.api().delta("", {
context: context, context: context,
// allItemIdsHandler() provides a way for drivers that don't have a delta API to // allItemIdsHandler() provides a way for drivers that don't have a delta API to
// still provide delta functionality by comparing the items they have to the items // still provide delta functionality by comparing the items they have to the items
// the client has. Very inefficient but that's the only possible workaround. // the client has. Very inefficient but that's the only possible workaround.
// It's a function so that it is only called if the driver needs these IDs. For // It's a function so that it is only called if the driver needs these IDs. For
// drivers with a delta functionality it's a noop. // drivers with a delta functionality it's a noop.
allItemIdsHandler: async () => { return BaseItem.syncedItemIds(syncTargetId); } allItemIdsHandler: async () => {
}); return BaseItem.syncedItemIds(syncTargetId);
},
});
let remotes = listResult.items; let remotes = listResult.items;
this.logSyncOperation('fetchingTotal', null, null, 'Fetching delta items from sync target', remotes.length); this.logSyncOperation("fetchingTotal", null, null, "Fetching delta items from sync target", remotes.length);
for (let i = 0; i < remotes.length; i++) { for (let i = 0; i < remotes.length; i++) {
if (this.cancelling() || this.testingHooks_.indexOf('cancelDeltaLoop2') >= 0) { if (this.cancelling() || this.testingHooks_.indexOf("cancelDeltaLoop2") >= 0) {
hasCancelled = true; hasCancelled = true;
break; break;
}
this.logSyncOperation('fetchingProcessed', null, null, 'Processing fetched item');
let remote = remotes[i];
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
const loadContent = async () => {
let content = await this.api().get(path);
if (!content) return null;
return await BaseItem.unserialize(content);
}
let path = remote.path;
let action = null;
let reason = '';
let local = await BaseItem.loadItemByPath(path);
let ItemClass = null;
let content = null;
if (!local) {
if (remote.isDeleted !== true) {
action = 'createLocal';
reason = 'remote exists but local does not';
content = await loadContent();
ItemClass = content ? BaseItem.itemClass(content) : null;
} }
} else {
ItemClass = BaseItem.itemClass(local); this.logSyncOperation("fetchingProcessed", null, null, "Processing fetched item");
local = ItemClass.filter(local);
if (remote.isDeleted) { let remote = remotes[i];
action = 'deleteLocal'; if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
reason = 'remote has been deleted';
const loadContent = async () => {
let content = await this.api().get(path);
if (!content) return null;
return await BaseItem.unserialize(content);
};
let path = remote.path;
let action = null;
let reason = "";
let local = await BaseItem.loadItemByPath(path);
let ItemClass = null;
let content = null;
if (!local) {
if (remote.isDeleted !== true) {
action = "createLocal";
reason = "remote exists but local does not";
content = await loadContent();
ItemClass = content ? BaseItem.itemClass(content) : null;
}
} else { } else {
content = await loadContent(); ItemClass = BaseItem.itemClass(local);
if (content && content.updated_time > local.updated_time) { local = ItemClass.filter(local);
action = 'updateLocal'; if (remote.isDeleted) {
reason = 'remote is more recent than local'; action = "deleteLocal";
reason = "remote has been deleted";
} else {
content = await loadContent();
if (content && content.updated_time > local.updated_time) {
action = "updateLocal";
reason = "remote is more recent than local";
}
} }
} }
if (!action) continue;
this.logSyncOperation(action, local, remote, reason);
if (action == "createLocal" || action == "updateLocal") {
if (content === null) {
this.logger().warn("Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: " + path);
continue;
}
content = ItemClass.filter(content);
// 2017-12-03: This was added because the new user_updated_time and user_created_time properties were added
// to the items. However changing the database is not enough since remote items that haven't been synced yet
// will not have these properties and, since they are required, it would cause a problem. So this check
// if they are present and, if not, set them to a reasonable default.
// Let's leave these two lines for 6 months, by which time all the clients should have been synced.
if (!content.user_updated_time) content.user_updated_time = content.updated_time;
if (!content.user_created_time) content.user_created_time = content.created_time;
let options = {
autoTimestamp: false,
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
};
if (action == "createLocal") options.isNew = true;
if (action == "updateLocal") options.oldItem = local;
if (content.type_ == BaseModel.TYPE_RESOURCE && action == "createLocal") {
let localResourceContentPath = Resource.fullPath(content);
let remoteResourceContentPath = this.resourceDirName_ + "/" + content.id;
await this.api().get(remoteResourceContentPath, { path: localResourceContentPath, target: "file" });
}
await ItemClass.save(content, options);
if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) {
hasAutoEnabledEncryption = true;
this.logger().info("One master key was downloaded and none was previously available: automatically enabling encryption");
this.logger().info("Using master key: ", content);
await this.encryptionService().enableEncryption(content);
await this.encryptionService().loadMasterKeysFromSettings();
this.logger().info(
"Encryption has been enabled with downloaded master key as active key. However, note that no password was initially supplied. It will need to be provided by user."
);
}
if (!!content.encryption_applied) this.dispatch({ type: "SYNC_GOT_ENCRYPTED_ITEM" });
} else if (action == "deleteLocal") {
if (local.type_ == BaseModel.TYPE_FOLDER) {
localFoldersToDelete.push(local);
continue;
}
let ItemClass = BaseItem.itemClass(local.type_);
await ItemClass.delete(local.id, { trackDeleted: false });
}
} }
if (!action) continue; // If user has cancelled, don't record the new context (2) so that synchronisation
// can start again from the previous context (1) next time. It is ok if some items
this.logSyncOperation(action, local, remote, reason); // have been synced between (1) and (2) because the loop above will handle the same
// items being synced twice as an update. If the local and remote items are indentical
if (action == 'createLocal' || action == 'updateLocal') { // the update will simply be skipped.
if (!hasCancelled) {
if (content === null) { if (!listResult.hasMore) {
this.logger().warn('Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: ' + path); newDeltaContext = listResult.context;
continue; break;
} }
content = ItemClass.filter(content); context = listResult.context;
// 2017-12-03: This was added because the new user_updated_time and user_created_time properties were added
// to the items. However changing the database is not enough since remote items that haven't been synced yet
// will not have these properties and, since they are required, it would cause a problem. So this check
// if they are present and, if not, set them to a reasonable default.
// Let's leave these two lines for 6 months, by which time all the clients should have been synced.
if (!content.user_updated_time) content.user_updated_time = content.updated_time;
if (!content.user_created_time) content.user_created_time = content.created_time;
let options = {
autoTimestamp: false,
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
};
if (action == 'createLocal') options.isNew = true;
if (action == 'updateLocal') options.oldItem = local;
if (content.type_ == BaseModel.TYPE_RESOURCE && action == 'createLocal') {
let localResourceContentPath = Resource.fullPath(content);
let remoteResourceContentPath = this.resourceDirName_ + '/' + content.id;
await this.api().get(remoteResourceContentPath, { path: localResourceContentPath, target: 'file' });
}
await ItemClass.save(content, options);
if (!hasAutoEnabledEncryption && content.type_ === BaseModel.TYPE_MASTER_KEY && !masterKeysBefore) {
hasAutoEnabledEncryption = true;
this.logger().info('One master key was downloaded and none was previously available: automatically enabling encryption');
this.logger().info('Using master key: ', content);
await this.encryptionService().enableEncryption(content);
await this.encryptionService().loadMasterKeysFromSettings();
this.logger().info('Encryption has been enabled with downloaded master key as active key. However, note that no password was initially supplied. It will need to be provided by user.');
}
if (!!content.encryption_applied) this.dispatch({ type: 'SYNC_GOT_ENCRYPTED_ITEM' });
} else if (action == 'deleteLocal') {
if (local.type_ == BaseModel.TYPE_FOLDER) {
localFoldersToDelete.push(local);
continue;
}
let ItemClass = BaseItem.itemClass(local.type_);
await ItemClass.delete(local.id, { trackDeleted: false });
} }
} }
// If user has cancelled, don't record the new context (2) so that synchronisation outputContext.delta = newDeltaContext ? newDeltaContext : lastContext.delta;
// can start again from the previous context (1) next time. It is ok if some items
// have been synced between (1) and (2) because the loop above will handle the same // ------------------------------------------------------------------------
// items being synced twice as an update. If the local and remote items are indentical // Delete the folders that have been collected in the loop above.
// the update will simply be skipped. // Folders are always deleted last, and only if they are empty.
if (!hasCancelled) { // If they are not empty it's considered a conflict since whatever deleted
if (!listResult.hasMore) { // them should have deleted their content too. In that case, all its notes
newDeltaContext = listResult.context; // are marked as "is_conflict".
break; // ------------------------------------------------------------------------
if (!this.cancelling()) {
for (let i = 0; i < localFoldersToDelete.length; i++) {
const item = localFoldersToDelete[i];
const noteIds = await Folder.noteIds(item.id);
if (noteIds.length) {
// CONFLICT
await Folder.markNotesAsConflict(item.id);
}
await Folder.delete(item.id, { deleteChildren: false, trackDeleted: false });
} }
context = listResult.context;
} }
}
outputContext.delta = newDeltaContext ? newDeltaContext : lastContext.delta; if (!this.cancelling()) {
await BaseItem.deleteOrphanSyncItems();
// ------------------------------------------------------------------------
// Delete the folders that have been collected in the loop above.
// Folders are always deleted last, and only if they are empty.
// If they are not empty it's considered a conflict since whatever deleted
// them should have deleted their content too. In that case, all its notes
// are marked as "is_conflict".
// ------------------------------------------------------------------------
if (!this.cancelling()) {
for (let i = 0; i < localFoldersToDelete.length; i++) {
const item = localFoldersToDelete[i];
const noteIds = await Folder.noteIds(item.id);
if (noteIds.length) { // CONFLICT
await Folder.markNotesAsConflict(item.id);
}
await Folder.delete(item.id, { deleteChildren: false, trackDeleted: false });
} }
} } // DELTA STEP
if (!this.cancelling()) {
await BaseItem.deleteOrphanSyncItems();
}
} catch (error) { } catch (error) {
if (error && ['cannotEncryptEncrypted', 'noActiveMasterKey'].indexOf(error.code) >= 0) { if (error && ["cannotEncryptEncrypted", "noActiveMasterKey"].indexOf(error.code) >= 0) {
// Only log an info statement for this since this is a common condition that is reported // Only log an info statement for this since this is a common condition that is reported
// in the application, and needs to be resolved by the user // in the application, and needs to be resolved by the user
this.logger().info(error.message); this.logger().info(error.message);
} else { } else {
this.logger().error(error); this.logger().error(error);
this.progressReport_.errors.push(error);
// Don't save to the report errors that are due to things like temporary network errors or timeout.
if (!shim.fetchRequestCanBeRetried(error)) this.progressReport_.errors.push(error);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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