You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-13 22:12:50 +02:00
Merge pull request #3 from laurent22/master
Update fork from original repository
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -37,4 +37,7 @@ _mydocs
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
||||
_releases
|
||||
ReactNativeClient/lib/csstojs/
|
||||
ElectronClient/app/gui/note-viewer/fonts/
|
||||
Tools/commit_hook.txt
|
@@ -46,6 +46,8 @@ before_install:
|
||||
|
||||
script:
|
||||
- |
|
||||
cd ElectronClient/app
|
||||
cd Tools
|
||||
npm install
|
||||
cd ../ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && yarn dist
|
||||
|
9
BUILD.md
9
BUILD.md
@@ -17,6 +17,15 @@ If you get a node-gyp related error you might need to manually install it: `npm
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
|
||||
Before building any of the applications, you need to build the tools:
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
```
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
|
@@ -691,6 +691,13 @@ msgstr "Über Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Auf %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -700,22 +707,29 @@ msgstr "Abbrechen"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Notizen löschen?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Synchronisation abbrechen"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -785,6 +799,16 @@ msgstr ""
|
||||
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
|
||||
"Notizbücher ursprünglich verschlüsselt wurden."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Hauptschlüssel"
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
@@ -860,6 +884,13 @@ msgstr ""
|
||||
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den "
|
||||
"(+) Knopf drückst."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Änderungen speichern"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Nicht unterstützter Link oder Nachricht: %s"
|
||||
@@ -867,9 +898,24 @@ msgstr "Nicht unterstützter Link oder Nachricht: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Datei anhängen"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Markierungen"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Alarm erstellen"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Neues To-Do"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Neue Notiz"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importiere Notizen..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Aktualisieren"
|
||||
|
||||
@@ -906,9 +952,6 @@ msgstr "Synchronisieren"
|
||||
msgid "Notebooks"
|
||||
msgstr "Notizbücher"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Markierungen"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Suchen"
|
||||
|
||||
@@ -936,6 +979,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (Nur für Tests)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Unbekanntes Log Level: %s"
|
||||
@@ -1095,6 +1141,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Erstellt eine neue Notiz."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr "Einstellen des Anwendungszooms"
|
||||
|
||||
@@ -1149,6 +1198,18 @@ msgstr "Nexcloud Benutzername"
|
||||
msgid "Nexcloud password"
|
||||
msgstr "Nexcloud Passwort"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV URL"
|
||||
msgstr "Nexcloud WebDAV URL"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV username"
|
||||
msgstr "Nexcloud Benutzername"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV password"
|
||||
msgstr "Setze ein Passwort"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
|
||||
@@ -1265,6 +1326,12 @@ msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Notizbuch bearbeiten"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Diese Notiz wurde verändert:"
|
||||
|
||||
|
@@ -599,6 +599,13 @@ msgstr ""
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
@@ -608,21 +615,26 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
@@ -681,6 +693,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
@@ -750,6 +771,12 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -757,9 +784,22 @@ msgstr ""
|
||||
msgid "Attach file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr ""
|
||||
|
||||
msgid "to-do"
|
||||
msgstr ""
|
||||
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -796,9 +836,6 @@ msgstr ""
|
||||
msgid "Notebooks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
@@ -825,6 +862,9 @@ msgstr ""
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr ""
|
||||
@@ -969,6 +1009,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1018,6 +1061,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
@@ -1129,6 +1181,12 @@ msgstr ""
|
||||
msgid "Edit notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -657,6 +657,13 @@ msgstr "Acerca de Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "En %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "Ok"
|
||||
|
||||
@@ -666,22 +673,29 @@ msgstr "Cancelar"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Eliminar notas?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Sincronizacion cancelada"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -741,6 +755,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Estatus"
|
||||
|
||||
@@ -817,6 +840,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"Actualmente no hay notas. Crea una nueva nota dando client en el boton (+)."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Guardar cambios"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Enlace o mensaje sin soporte: %s"
|
||||
@@ -824,9 +854,24 @@ msgstr "Enlace o mensaje sin soporte: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Adjuntar archivo"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Ajustar alarma"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nueva lista de tareas"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nueva nota"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importando notas..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Refrescar"
|
||||
|
||||
@@ -864,9 +909,6 @@ msgstr "Sincronizar"
|
||||
msgid "Notebooks"
|
||||
msgstr "Libretas"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Busquedas"
|
||||
|
||||
@@ -895,6 +937,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev(Solo para pruebas)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nivel de log desconocido: %s"
|
||||
@@ -1058,6 +1103,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Crea una nueva nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1109,6 +1157,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Valor inválido de opción: \"%s\". Los válores inválidos son: %s."
|
||||
@@ -1226,6 +1283,12 @@ msgstr "Esta libreta no pudo ser guardada: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Editar libreta"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Esta nota ha sido modificada:"
|
||||
|
||||
|
@@ -671,6 +671,13 @@ msgstr "Acerca de Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "En %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -680,22 +687,29 @@ msgstr "Cancelar"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "¿Desea eliminar notas?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Cancelar sincronización"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -753,6 +767,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Estado"
|
||||
|
||||
@@ -823,6 +846,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "No hay ninguna libreta. Cree una pulsando en «Libreta nueva»."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Guardar cambios"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Enlace o mensaje no soportado: %s"
|
||||
@@ -830,9 +860,24 @@ msgstr "Enlace o mensaje no soportado: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Adjuntar archivo"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Fijar alarma"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Lista de tareas nueva"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nota nueva"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importando notas..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Refrescar"
|
||||
|
||||
@@ -869,9 +914,6 @@ msgstr "Sincronizar"
|
||||
msgid "Notebooks"
|
||||
msgstr "Libretas"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Búsquedas"
|
||||
|
||||
@@ -898,6 +940,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (Solo para pruebas)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nivel de log desconocido: %s"
|
||||
@@ -1057,6 +1102,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Crea una nueva nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1108,6 +1156,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
|
||||
@@ -1220,6 +1277,12 @@ msgstr "No se ha podido guardar esta libreta: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Editar libreta"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Esta nota ha sido modificada:"
|
||||
|
||||
|
1356
CliClient/locales/eu.po
Normal file
1356
CliClient/locales/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Last-Translator: Laurent Cozic\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -665,7 +665,7 @@ msgid "Website and documentation"
|
||||
msgstr "Documentation en ligne"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
msgstr "Vérifier les mises à jour..."
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "A propos de Joplin"
|
||||
@@ -674,6 +674,13 @@ msgstr "A propos de Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Ouvrir %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr "Quitter"
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -681,24 +688,33 @@ msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
msgstr ""
|
||||
msgstr "Erreur"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Notes de version :\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Non"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
msgstr "La version actuelle est à jour."
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Vérifier config synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -768,6 +784,18 @@ msgstr ""
|
||||
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
|
||||
"à l'origine."
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Clefs maître manquantes"
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
|
||||
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
|
||||
"probable qu'elle vont être prochainement disponible via la synchronisation."
|
||||
|
||||
msgid "Status"
|
||||
msgstr "État"
|
||||
|
||||
@@ -842,6 +870,12 @@ msgstr ""
|
||||
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
|
||||
"carnet\"."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr "Ouvrir..."
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr "Enregistrer sous..."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Lien ou message non géré : %s"
|
||||
@@ -849,9 +883,22 @@ msgstr "Lien ou message non géré : %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Attacher un fichier"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Régler alarme"
|
||||
|
||||
msgid "to-do"
|
||||
msgstr "tâche"
|
||||
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Création de %s..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Rafraîchir"
|
||||
|
||||
@@ -888,9 +935,6 @@ msgstr "Synchroniser"
|
||||
msgid "Notebooks"
|
||||
msgstr "Carnets"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Recherches"
|
||||
|
||||
@@ -918,6 +962,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dév (Pour tester uniquement)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr "WebDAV (Bêta)"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Paramètre inconnu : %s"
|
||||
@@ -1060,20 +1107,20 @@ msgstr "Tâches non-terminées en haut des listes"
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Enregistrer l'emplacement avec les notes"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Créer une nouvelle tâche."
|
||||
msgstr "Lors de la création d'une tâche :"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Focus title"
|
||||
msgstr "Titre de la note :"
|
||||
msgstr "Curseur sur le titre"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
msgstr "Curseur sur corps du message"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Créer une note."
|
||||
msgstr "Lors de la création d'une note :"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr "Afficher icône dans la zone de notifications"
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr "Niveau de zoom"
|
||||
@@ -1129,6 +1176,15 @@ msgstr "Nextcloud : Nom utilisateur"
|
||||
msgid "Nexcloud password"
|
||||
msgstr "Nextcloud : Mot de passe"
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr "WebDAV : URL"
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr "WebDAV : Nom utilisateur"
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr "WebDAV : Mot de passe"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
@@ -1203,7 +1259,7 @@ msgid "Configuration"
|
||||
msgstr "Configuration"
|
||||
|
||||
msgid "Move to notebook..."
|
||||
msgstr "Déplacer la note vers carnet..."
|
||||
msgstr "Déplacer vers..."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
@@ -1245,6 +1301,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Éditer le carnet"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr "Afficher tous"
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr "Erreurs seulement"
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Cette note a été modifiée :"
|
||||
|
||||
@@ -1300,6 +1362,17 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenue"
|
||||
|
||||
#~ msgid "Could not download the update: %s"
|
||||
#~ msgstr "Impossible de télécharger la mise à jour : %s"
|
||||
|
||||
#~ msgid "New version downloaded - application will quit now and update..."
|
||||
#~ msgstr ""
|
||||
#~ "La nouvelle version a été téléchargée - le programme va se fermer et se "
|
||||
#~ "mettre à jour..."
|
||||
|
||||
#~ msgid "Could not install the update: %s"
|
||||
#~ msgstr "Impossible d'installer la mise à jour : %s"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
@@ -1371,9 +1444,6 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Todo filter"
|
||||
#~ msgstr "Filtre des tâches"
|
||||
|
||||
#~ msgid "Show all"
|
||||
#~ msgstr "Afficher tous"
|
||||
|
||||
#~ msgid "Non-completed and recently completed ones"
|
||||
#~ msgstr "Tâches non-complétées et récentes"
|
||||
|
||||
|
@@ -679,6 +679,13 @@ msgstr "O Joplinu"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "On %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "U redu"
|
||||
|
||||
@@ -688,22 +695,29 @@ msgstr "Odustani"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Obriši bilješke?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Prekini sinkronizaciju"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -761,6 +775,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
@@ -833,6 +856,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Ovdje nema bilježnica. Stvori novu pritiskom na \"Nova bilježnica\"."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Spremi promjene"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Nepodržana poveznica ili poruka: %s"
|
||||
@@ -840,9 +870,24 @@ msgstr "Nepodržana poveznica ili poruka: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Priloži datoteku"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Oznake"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Postavi upozorenje"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Novi zadatak"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nova bilješka"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Uvozim bilješke..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Osvježi"
|
||||
|
||||
@@ -879,9 +924,6 @@ msgstr "Sinkroniziraj"
|
||||
msgid "Notebooks"
|
||||
msgstr "Bilježnice"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Oznake"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Pretraživanja"
|
||||
|
||||
@@ -908,6 +950,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (Samo za testiranje)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nepoznata razina logiranja: %s"
|
||||
@@ -1064,6 +1109,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Stvara novu bilješku."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1115,6 +1163,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
|
||||
@@ -1227,6 +1284,12 @@ msgstr "Bilježnicu nije moguće snimiti: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Uredi bilježnicu"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Bilješka je promijenjena:"
|
||||
|
||||
|
@@ -657,6 +657,13 @@ msgstr "Informazione si Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Su %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -666,22 +673,29 @@ msgstr "Cancella"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Eliminare le note?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Cancella la sincronizzazione"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -741,6 +755,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Stato"
|
||||
|
||||
@@ -812,6 +835,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Al momento non ci sono note. Creane una cliccando sul bottone (+)."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Salva i cambiamenti"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Collegamento o messaggio non supportato: %s"
|
||||
@@ -819,9 +849,24 @@ msgstr "Collegamento o messaggio non supportato: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Allega file"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etichette"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Imposta allarme"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nuova attività"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nuova nota"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importazione delle note..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Aggiorna"
|
||||
|
||||
@@ -858,9 +903,6 @@ msgstr "Sincronizza"
|
||||
msgid "Notebooks"
|
||||
msgstr "Blocchi note"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etichette"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Ricerche"
|
||||
|
||||
@@ -888,6 +930,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (solo per test)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Livello di log sconosciuto: %s"
|
||||
@@ -1046,6 +1091,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Crea una nuova nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1097,6 +1145,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
|
||||
@@ -1209,6 +1266,12 @@ msgstr "Il blocco note non può essere salvato: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Modifica blocco note"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Questa note è stata modificata:"
|
||||
|
||||
|
@@ -656,6 +656,13 @@ msgstr "Joplinについて"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
@@ -665,22 +672,28 @@ msgstr "キャンセル"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "ノートを削除しますか?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "同期の中止"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -742,6 +755,15 @@ msgstr ""
|
||||
"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に"
|
||||
"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。"
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状態"
|
||||
|
||||
@@ -814,6 +836,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "ノートブックがありません。新しいノートブックを作成してください。"
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "変更を保存"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -821,9 +850,24 @@ msgstr ""
|
||||
msgid "Attach file"
|
||||
msgstr "ファイルを添付"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "タグ"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "アラームをセット"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "新しいToDo"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "新しいノート"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "ノートのインポート…"
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "更新"
|
||||
|
||||
@@ -860,9 +904,6 @@ msgstr "同期"
|
||||
msgid "Notebooks"
|
||||
msgstr "ノートブック"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "タグ"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "検索"
|
||||
|
||||
@@ -889,6 +930,9 @@ msgstr ""
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr ""
|
||||
@@ -1049,6 +1093,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "あたらしいノートを作成します。"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1100,6 +1147,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
|
||||
@@ -1212,6 +1268,12 @@ msgstr "ノートブックは保存できませんでした:%s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "ノートブックの編集"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "ノートは変更されています:"
|
||||
|
||||
|
@@ -599,6 +599,13 @@ msgstr ""
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
@@ -608,21 +615,26 @@ msgstr ""
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
@@ -681,6 +693,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
@@ -750,6 +771,12 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -757,9 +784,22 @@ msgstr ""
|
||||
msgid "Attach file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr ""
|
||||
|
||||
msgid "to-do"
|
||||
msgstr ""
|
||||
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -796,9 +836,6 @@ msgstr ""
|
||||
msgid "Notebooks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
@@ -825,6 +862,9 @@ msgstr ""
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr ""
|
||||
@@ -969,6 +1009,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1018,6 +1061,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
@@ -1129,6 +1181,12 @@ msgstr ""
|
||||
msgid "Edit notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -674,6 +674,13 @@ msgstr "Over Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Op %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -683,22 +690,29 @@ msgstr "Annuleer"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Notities verwijderen?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Annuleer synchronisatie"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -767,6 +781,16 @@ msgstr ""
|
||||
"(aangeduid met \"active\"). Alle sleutels kunnen gebruikt worden voor "
|
||||
"decodering, afhankelijk van hoe de notitieboeken initieel versleuteld zijn."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Hoofdsleutels"
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
@@ -840,6 +864,13 @@ msgstr ""
|
||||
"U heeft momenteel geen notitieboek. Maak een notitieboek door op \"Nieuw "
|
||||
"notitieboek\" te klikken."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Sla wijzigingen op"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Link of bericht \"%s\" wordt niet ondersteund"
|
||||
@@ -847,9 +878,24 @@ msgstr "Link of bericht \"%s\" wordt niet ondersteund"
|
||||
msgid "Attach file"
|
||||
msgstr "Voeg bestand toe"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Zet melding"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nieuwe to-do"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nieuwe notitie"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Notities importeren..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Vernieuwen"
|
||||
|
||||
@@ -886,9 +932,6 @@ msgstr "Synchroniseer"
|
||||
msgid "Notebooks"
|
||||
msgstr "Notitieboeken"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Zoekopdrachten"
|
||||
|
||||
@@ -915,6 +958,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (Alleen voor testen)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Onbekend log level: %s"
|
||||
@@ -1074,6 +1120,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Maakt een nieuwe notitie aan."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1126,6 +1175,16 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr "Stel wachtwoord in"
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV password"
|
||||
msgstr "Stel wachtwoord in"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
|
||||
@@ -1241,6 +1300,12 @@ msgstr "Het notitieboek kon niet opgeslaan worden: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Bewerk notitieboek"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Deze notitie werd aangepast:"
|
||||
|
||||
|
@@ -652,6 +652,13 @@ msgstr "Sobre o Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Em %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -661,22 +668,29 @@ msgstr "Cancelar"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Excluir notas?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Cancelar sincronização"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -736,6 +750,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
@@ -809,6 +832,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Atualmente, não há notas. Crie uma, clicando no botão (+)."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Gravar alterações"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Link ou mensagem não suportada: %s"
|
||||
@@ -816,9 +846,24 @@ msgstr "Link ou mensagem não suportada: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Anexar arquivo"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Definir alarme"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nova tarefa"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nova nota"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importando notas ..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Atualizar"
|
||||
|
||||
@@ -856,9 +901,6 @@ msgstr "Sincronizar"
|
||||
msgid "Notebooks"
|
||||
msgstr "Cadernos"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Pesquisas"
|
||||
|
||||
@@ -886,6 +928,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (apenas para testes)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nível de log desconhecido: %s"
|
||||
@@ -1045,6 +1090,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Cria uma nova nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1096,6 +1144,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
|
||||
@@ -1208,6 +1265,12 @@ msgstr "O caderno não pôde ser salvo: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Editar caderno"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Esta nota foi modificada:"
|
||||
|
||||
|
@@ -7,13 +7,13 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: rtmkrlv <artyom.karlov@gmail.com>\n"
|
||||
"Last-Translator: Artyom Karlov <artyom.karlov@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: ru_RU\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.5\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
@@ -112,7 +112,7 @@ msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "Команда «%s» доступна только в режиме GUI"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
msgstr "Не удалось изменить зашифрованный элемент"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
@@ -123,7 +123,7 @@ msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
msgid "Your choice: "
|
||||
msgstr "Ваш выбор:"
|
||||
msgstr "Ваш выбор: "
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid answer: %s"
|
||||
@@ -180,22 +180,24 @@ msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
|
||||
"`status` и `target-status`."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enter master password:"
|
||||
msgstr "Установить пароль"
|
||||
msgstr "Введите мастер-пароль:"
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
msgstr "Операция отменена"
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
"Запуск расшифровки... Пожалуйста, ожидайте. Время расшифровки зависит от "
|
||||
"объёма расшифровываемых данных."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Completed decryption."
|
||||
msgstr "Включить шифрование"
|
||||
msgstr "Расшифровка завершена."
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
@@ -203,9 +205,9 @@ msgstr "Включено"
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr "Шифрование:"
|
||||
msgstr "Шифрование: %s"
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Редактировать заметку."
|
||||
@@ -230,7 +232,7 @@ msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
msgstr "Ошибка при открытии заметки в редакторе: %s"
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "Заметка сохранена."
|
||||
@@ -281,7 +283,7 @@ msgstr ""
|
||||
"элемент."
|
||||
|
||||
msgid "To move from one pane to another, press Tab or Shift+Tab."
|
||||
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab"
|
||||
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab."
|
||||
|
||||
msgid ""
|
||||
"Use the arrows and page up/down to scroll the lists and text areas "
|
||||
@@ -303,7 +305,7 @@ msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
msgstr ""
|
||||
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
|
||||
"shortcuts`."
|
||||
"shortcuts`"
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
|
||||
@@ -466,6 +468,7 @@ msgstr "Аутентификация не была завершена (не по
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
"Не аутентифицировано с %s. Пожалуйста, предоставьте все недостающие данные."
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "Синхронизация уже выполняется."
|
||||
@@ -601,6 +604,10 @@ msgid ""
|
||||
"supplied the password, the encrypted items are being decrypted in the "
|
||||
"background and will be available soon."
|
||||
msgstr ""
|
||||
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
|
||||
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
|
||||
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
|
||||
"расшифруются в фоновом режиме и вскоре станут доступны."
|
||||
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
@@ -644,13 +651,11 @@ msgstr "Инструменты"
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Статус синхронизации"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encryption options"
|
||||
msgstr "Настройки шифрования"
|
||||
|
||||
#, fuzzy
|
||||
msgid "General Options"
|
||||
msgstr "Настройки"
|
||||
msgstr "Основные настройки"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Помощь"
|
||||
@@ -659,7 +664,7 @@ msgid "Website and documentation"
|
||||
msgstr "Сайт и документация"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
msgstr "Проверить обновления..."
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "О Joplin"
|
||||
@@ -668,6 +673,13 @@ msgstr "О Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "В %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
@@ -675,24 +687,32 @@ msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Ошибка"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Удалить заметки?"
|
||||
|
||||
#, fuzzy
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr "Доступно обновление. Обновить сейчас?"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
msgstr "Вы используете самую свежую версию."
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Отменить синхронизацию"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -762,6 +782,16 @@ msgstr ""
|
||||
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
|
||||
"блокноты."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Мастер-ключи"
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
@@ -831,6 +861,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Сейчас здесь нет блокнотов. Создайте новый нажав «Новый блокнот»."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Сохранить изменения"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Неподдерживаемая ссыка или сообщение: %s"
|
||||
@@ -838,9 +875,24 @@ msgstr "Неподдерживаемая ссыка или сообщение: %
|
||||
msgid "Attach file"
|
||||
msgstr "Прикрепить файл"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Теги"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Установить напоминание"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Новая задача"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Новая заметка"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Импорт заметок..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Обновить"
|
||||
|
||||
@@ -877,9 +929,6 @@ msgstr "Синхронизировать"
|
||||
msgid "Notebooks"
|
||||
msgstr "Блокноты"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Теги"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Запросы"
|
||||
|
||||
@@ -898,7 +947,7 @@ msgid "File system"
|
||||
msgstr "Файловая система"
|
||||
|
||||
msgid "Nextcloud (Beta)"
|
||||
msgstr ""
|
||||
msgstr "Nextcloud (Beta)"
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
@@ -906,6 +955,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (только для тестирования)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Неизвестный уровень лога: %s"
|
||||
@@ -964,9 +1016,9 @@ msgstr "Удалено локальных элементов: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Удалено удалённых элементов: %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Создано локальных элементов: %d."
|
||||
msgstr "Получено элементов: %d/%d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
@@ -983,13 +1035,11 @@ msgstr "Завершено: %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "Синхронизация уже выполняется. Статус: %s"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encrypted"
|
||||
msgstr "Шифрование:"
|
||||
msgstr "Зашифровано"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Некоторые элементы не могут быть синхронизированы."
|
||||
msgstr "Зашифрованные элементы не могут быть изменены"
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Конфликты"
|
||||
@@ -1050,23 +1100,23 @@ msgstr "Показывать незавершённые задачи вверх
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Сохранять информацию о геолокации в заметках"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Создаёт новую задачу."
|
||||
msgstr "При создании новой задачи:"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Focus title"
|
||||
msgstr "Название заметки:"
|
||||
msgstr "Фокус на названии"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
msgstr "Фокус на содержимом"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Создаёт новую заметку."
|
||||
msgstr "При создании новой заметки:"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
msgstr "Масштаб приложения в процентах"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Автоматически обновлять приложение"
|
||||
@@ -1096,6 +1146,8 @@ msgid ""
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
|
||||
"параметры, именованные как «sync.NUM.NAME» (все описаны ниже)."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr "Каталог синхронизации (абсолютный путь)"
|
||||
@@ -1108,13 +1160,24 @@ msgstr ""
|
||||
"`sync.target`."
|
||||
|
||||
msgid "Nexcloud WebDAV URL"
|
||||
msgstr ""
|
||||
msgstr "Nexcloud WebDAV URL"
|
||||
|
||||
msgid "Nexcloud username"
|
||||
msgstr ""
|
||||
msgstr "Имя пользователя Nexcloud"
|
||||
|
||||
msgid "Nexcloud password"
|
||||
msgstr "Пароль Nexcloud"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Nexcloud password"
|
||||
msgid "WebDAV URL"
|
||||
msgstr "Nexcloud WebDAV URL"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV username"
|
||||
msgstr "Имя пользователя Nexcloud"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV password"
|
||||
msgstr "Установить пароль"
|
||||
|
||||
#, javascript-format
|
||||
@@ -1124,15 +1187,18 @@ msgstr "Неверное значение параметра: «%s». Досту
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr "Элементы, которые не могут быть синхронизированы"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s %s (%s)"
|
||||
msgstr "%s (%s): %s"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
"Эти элементы будут оставаться на устройстве, но не будут загружены в целевой "
|
||||
"объект синхронизации. Чтобы найти эти элементы, воспользуйтесь поиском по "
|
||||
"названию или ID (который указывается в скобках выше)."
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
|
||||
@@ -1179,9 +1245,8 @@ msgstr "Лог"
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Экспортировать отладочный отчёт"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encryption Config"
|
||||
msgstr "Шифрование:"
|
||||
msgstr "Конфигурация шифрования"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Конфигурация"
|
||||
@@ -1194,7 +1259,7 @@ msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Переместить %d заметок в блокнот «%s»?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
msgstr "Нажмите, чтобы установить пароль для расшифровки."
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Выбрать дату"
|
||||
@@ -1205,22 +1270,20 @@ msgstr "Подтвердить"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Отменить синхронизацию"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr "Мастер-ключи"
|
||||
msgstr "Мастер-ключ %s"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Создано: %d."
|
||||
msgstr "Создано: %s"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Password:"
|
||||
msgstr "Пароль"
|
||||
msgstr "Пароль:"
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
msgstr "Пароль не может быть пустым"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enable"
|
||||
msgstr "Включено"
|
||||
|
||||
@@ -1231,6 +1294,13 @@ msgstr "Не удалось сохранить блокнот: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Редактировать блокнот"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Errors only"
|
||||
msgstr "Ошибка"
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Эта заметка была изменена:"
|
||||
|
||||
@@ -1284,6 +1354,16 @@ msgstr "У вас сейчас нет блокнота. Создайте его
|
||||
msgid "Welcome"
|
||||
msgstr "Добро пожаловать"
|
||||
|
||||
#~ msgid "Could not download the update: %s"
|
||||
#~ msgstr "Не удалось загрузить обновление: %s"
|
||||
|
||||
#~ msgid "New version downloaded - application will quit now and update..."
|
||||
#~ msgstr ""
|
||||
#~ "Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
|
||||
|
||||
#~ msgid "Could not install the update: %s"
|
||||
#~ msgstr "Не удалось установить обновление: %s"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
|
@@ -623,6 +623,13 @@ msgstr "关于Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "%s:%s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "确认"
|
||||
|
||||
@@ -632,22 +639,29 @@ msgstr "取消"
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to update now?"
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "是否删除笔记?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not download the update: %s"
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "否"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "New version downloaded - application will quit now and update..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Could not install the update: %s"
|
||||
msgstr ""
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "取消同步"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
@@ -707,6 +721,15 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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 "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
@@ -778,6 +801,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "当前无笔记。点击(+)创建新笔记。"
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "保存更改"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "不支持的链接或信息:%s"
|
||||
@@ -785,9 +815,24 @@ msgstr "不支持的链接或信息:%s"
|
||||
msgid "Attach file"
|
||||
msgstr "附加文件"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "标签"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "设置提醒"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "新待办事项"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "新笔记"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "正在导入笔记..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "刷新"
|
||||
|
||||
@@ -824,9 +869,6 @@ msgstr "同步"
|
||||
msgid "Notebooks"
|
||||
msgstr "笔记本"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "标签"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "搜索历史"
|
||||
|
||||
@@ -854,6 +896,9 @@ msgstr "OneDrive"
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive开发员(仅测试用)"
|
||||
|
||||
msgid "WebDAV (Beta)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "未知日志level:%s"
|
||||
@@ -1007,6 +1052,9 @@ msgstr ""
|
||||
msgid "When creating a new note:"
|
||||
msgstr "创建新笔记。"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
@@ -1056,6 +1104,15 @@ msgstr ""
|
||||
msgid "Nexcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
|
||||
@@ -1168,6 +1225,12 @@ msgstr "此笔记本无法保存:%s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "编辑笔记本"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "此笔记已被修改:"
|
||||
|
||||
|
7
CliClient/package-lock.json
generated
7
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.92",
|
||||
"version": "1.0.95",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -64,6 +64,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
|
||||
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
|
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.92",
|
||||
"version": "1.0.95",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"async-mutex": "^0.1.3",
|
||||
"base-64": "^0.1.0",
|
||||
"compare-version": "^0.1.2",
|
||||
"follow-redirects": "^1.2.4",
|
||||
|
@@ -9,9 +9,7 @@ rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||
mkdir -p "$BUILD_DIR/data"
|
||||
|
||||
if [[ $TEST_FILE == "" ]]; then
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js)
|
||||
else
|
||||
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
|
||||
fi
|
32
CliClient/tests/models_Setting.js
Normal file
32
CliClient/tests/models_Setting.js
Normal file
@@ -0,0 +1,32 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('models_Setting', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should return only sub-values', asyncTest(async () => {
|
||||
const settings = {
|
||||
'sync.5.path': 'http://example.com',
|
||||
'sync.5.username': 'testing',
|
||||
}
|
||||
|
||||
let output = Setting.subValues('sync.5', settings);
|
||||
expect(output['path']).toBe('http://example.com');
|
||||
expect(output['username']).toBe('testing');
|
||||
|
||||
output = Setting.subValues('sync.4', settings);
|
||||
expect('path' in output).toBe(false);
|
||||
expect('username' in output).toBe(false);
|
||||
}));
|
||||
|
||||
});
|
@@ -19,7 +19,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; // The first test is slow because the database needs to be built
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000; // The first test is slow because the database needs to be built
|
||||
|
||||
async function allItems() {
|
||||
let folders = await Folder.all();
|
||||
@@ -60,6 +60,7 @@ async function allSyncTargetItemsEncrypted() {
|
||||
}
|
||||
|
||||
async function localItemsSameAsRemote(locals, expect) {
|
||||
let error = null;
|
||||
try {
|
||||
let files = await fileApi().list();
|
||||
files = files.items;
|
||||
@@ -81,12 +82,15 @@ async function localItemsSameAsRemote(locals, expect) {
|
||||
// }
|
||||
|
||||
let remoteContent = await fileApi().get(path);
|
||||
|
||||
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
|
||||
expect(remoteContent.title).toBe(dbItem.title);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBe(null);
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
@@ -985,4 +989,15 @@ describe('Synchronizer', function() {
|
||||
expect(resource1.encryption_blob_encrypted).toBe(0);
|
||||
}));
|
||||
|
||||
it('should create remote items with UTF-8 content', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "Fahrräder" });
|
||||
await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id });
|
||||
|
||||
let all = await allItems();
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
});
|
@@ -1,9 +1,11 @@
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, Menu, Tray } = require('electron');
|
||||
const { shim } = require('lib/shim');
|
||||
const url = require('url')
|
||||
const path = require('path')
|
||||
const urlUtils = require('lib/urlUtils.js');
|
||||
const { dirname, basename } = require('lib/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class ElectronAppWrapper {
|
||||
|
||||
@@ -12,6 +14,8 @@ class ElectronAppWrapper {
|
||||
this.env_ = env;
|
||||
this.win_ = null;
|
||||
this.willQuitApp_ = false;
|
||||
this.tray_ = null;
|
||||
this.buildDir_ = null;
|
||||
}
|
||||
|
||||
electronApp() {
|
||||
@@ -62,11 +66,27 @@ class ElectronAppWrapper {
|
||||
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
|
||||
|
||||
this.win_.on('close', (event) => {
|
||||
if (this.willQuitApp_ || process.platform !== 'darwin') {
|
||||
this.win_ = null;
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
|
||||
// user clicks on the icon in the task bar).
|
||||
|
||||
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
|
||||
// case the app must be explicitely closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (this.willQuitApp_) {
|
||||
this.win_ = null;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
}
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
if (this.trayShown() && !this.willQuitApp_) {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
} else {
|
||||
this.win_ = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,6 +113,43 @@ class ElectronAppWrapper {
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
|
||||
trayShown() {
|
||||
return !!this.tray_;
|
||||
}
|
||||
|
||||
buildDir() {
|
||||
if (this.buildDir_) return this.buildDir_;
|
||||
let dir = __dirname + '/build';
|
||||
if (!fs.pathExistsSync(dir)) {
|
||||
dir = dirname(__dirname) + '/build';
|
||||
if (!fs.pathExistsSync(dir)) throw new Error('Cannot find build dir');
|
||||
}
|
||||
|
||||
this.buildDir_ = dir;
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Note: this must be called only after the "ready" event of the app has been dispatched
|
||||
createTray(contextMenu) {
|
||||
try {
|
||||
this.tray_ = new Tray(this.buildDir() + '/icons/16x16.png')
|
||||
this.tray_.setToolTip(this.electronApp_.getName())
|
||||
this.tray_.setContextMenu(contextMenu)
|
||||
|
||||
this.tray_.on('click', () => {
|
||||
this.window().show();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Cannot create tray", error);
|
||||
}
|
||||
}
|
||||
|
||||
destroyTray() {
|
||||
if (!this.tray_) return;
|
||||
this.tray_.destroy();
|
||||
this.tray_ = null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Since we are doing other async things before creating the window, we might miss
|
||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||
|
@@ -140,6 +140,10 @@ class Application extends BaseApplication {
|
||||
this.refreshMenu();
|
||||
}
|
||||
|
||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
this.updateTray();
|
||||
}
|
||||
|
||||
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
|
||||
}
|
||||
@@ -349,6 +353,28 @@ class Application extends BaseApplication {
|
||||
this.lastMenuScreen_ = screen;
|
||||
}
|
||||
|
||||
updateTray() {
|
||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||
// Might be fixed in Electron 18.x but no non-beta release yet.
|
||||
if (!shim.isWindows() && !shim.isMac()) return;
|
||||
|
||||
const app = bridge().electronApp();
|
||||
|
||||
if (app.trayShown() === Setting.value('showTrayIcon')) return;
|
||||
|
||||
if (!Setting.value('showTrayIcon')) {
|
||||
app.destroyTray();
|
||||
} else {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: _('Exit'), click: () => { app.exit() } },
|
||||
])
|
||||
app.createTray(contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
async start(argv) {
|
||||
argv = await super.start(argv);
|
||||
|
||||
@@ -405,6 +431,8 @@ class Application extends BaseApplication {
|
||||
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
this.updateTray();
|
||||
|
||||
setTimeout(() => {
|
||||
AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
|
@@ -6,6 +6,9 @@ const { _ } = require('lib/locale.js');
|
||||
let autoUpdateLogger_ = new Logger();
|
||||
let checkInBackground_ = false;
|
||||
|
||||
// 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;
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
@@ -14,19 +17,36 @@ autoUpdater.on('error', (error) => {
|
||||
dialog.showErrorBox(_('Error'), error == null ? "unknown" : (error.stack || error).toString())
|
||||
})
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
function htmlToText_(html) {
|
||||
let output = html.replace(/\n/g, '');
|
||||
output = output.replace(/<li>/g, '- ');
|
||||
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;
|
||||
}
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
if (!info.version || !info.path) {
|
||||
if (checkInBackground_) return;
|
||||
dialog.showErrorBox(_('Error'), ('Could not get version info: ' + JSON.stringify(info)));
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/v' + info.version + '/' + info.path;
|
||||
|
||||
let releaseNotes = info.releaseNotes + '';
|
||||
if (releaseNotes) releaseNotes = '\n\n' + _('Release notes:\n\n%s', htmlToText_(releaseNotes));
|
||||
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
message: _('An update is available, do you want to update now?'),
|
||||
buttons: ['Sure', 'No']
|
||||
message: _('An update is available, do you want to download it now?' + releaseNotes),
|
||||
buttons: [_('Yes'), _('No')]
|
||||
}, (buttonIndex) => {
|
||||
if (buttonIndex === 0) {
|
||||
try {
|
||||
autoUpdater.downloadUpdate()
|
||||
} catch (error) {
|
||||
autoUpdateLogger_.error(error);
|
||||
dialog.showErrorBox(_('Error'), _('Could not download the update: %s', error.message));
|
||||
}
|
||||
require('electron').shell.openExternal(downloadUrl);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -37,18 +57,18 @@ autoUpdater.on('update-not-available', () => {
|
||||
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
|
||||
})
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
dialog.showMessageBox({ message: _('New version downloaded - application will quit now and update...') }, () => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
autoUpdater.quitAndInstall();
|
||||
} catch (error) {
|
||||
autoUpdateLogger_.error(error);
|
||||
dialog.showErrorBox(_('Error'), _('Could not install the update: %s', error.message));
|
||||
}
|
||||
}, 100);
|
||||
})
|
||||
})
|
||||
// autoUpdater.on('update-downloaded', () => {
|
||||
// dialog.showMessageBox({ message: _('New version downloaded - application will quit now and update...') }, () => {
|
||||
// setTimeout(() => {
|
||||
// try {
|
||||
// autoUpdater.quitAndInstall();
|
||||
// } catch (error) {
|
||||
// autoUpdateLogger_.error(error);
|
||||
// dialog.showErrorBox(_('Error'), _('Could not install the update: %s', error.message));
|
||||
// }
|
||||
// }, 100);
|
||||
// })
|
||||
// })
|
||||
|
||||
function checkForUpdates(inBackground, logFilePath) {
|
||||
if (logFilePath && !autoUpdateLogger_.targets().length) {
|
||||
@@ -61,7 +81,12 @@ function checkForUpdates(inBackground, logFilePath) {
|
||||
|
||||
checkInBackground_ = inBackground;
|
||||
|
||||
autoUpdater.checkForUpdates()
|
||||
try {
|
||||
autoUpdater.checkForUpdates()
|
||||
} catch (error) {
|
||||
autoUpdateLogger_.error(error);
|
||||
if (!checkInBackground_) dialog.showErrorBox(_('Error'), error.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.checkForUpdates = checkForUpdates
|
@@ -7,14 +7,22 @@ const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
settings: {},
|
||||
shared.init(this);
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
this.rowStyle_ = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +52,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
let output = null;
|
||||
|
||||
const rowStyle = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
const rowStyle = this.rowStyle_;
|
||||
|
||||
const labelStyle = Object.assign({}, theme.textStyle, {
|
||||
display: 'inline-block',
|
||||
@@ -58,9 +64,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = value;
|
||||
this.setState({ settings: settings });
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
}
|
||||
|
||||
// Component key needs to be key+value otherwise it doesn't update when the settings change.
|
||||
@@ -104,12 +108,13 @@ class ConfigScreenComponent extends React.Component {
|
||||
updateSettingValue(key, event.target.value);
|
||||
}
|
||||
|
||||
const inputStyle = Object.assign({}, controlStyle, { width: '50%', minWidth: '20em' });
|
||||
const inputType = md.secure === true ? 'password' : 'text';
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<input type={inputType} style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
@@ -131,10 +136,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
onSaveClick() {
|
||||
for (let n in this.state.settings) {
|
||||
if (!this.state.settings.hasOwnProperty(n)) continue;
|
||||
Setting.setValue(n, this.state.settings[n]);
|
||||
}
|
||||
shared.saveSettings(this);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
@@ -144,7 +146,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const style = Object.assign({}, this.props.style, { overflow: 'auto' });
|
||||
const settings = this.state.settings;
|
||||
|
||||
const headerStyle = {
|
||||
@@ -156,23 +158,28 @@ class ConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
display: this.state.settings === this.props.settings ? 'none' : 'inline-block',
|
||||
display: this.state.changedSettingKeys.length ? 'inline-block' : 'none',
|
||||
marginRight: 10,
|
||||
}
|
||||
|
||||
let settingComps = [];
|
||||
let keys = Setting.keys(true, 'desktop');
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in settings)) {
|
||||
console.warn('Missing setting: ' + key);
|
||||
continue;
|
||||
}
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
const comp = this.settingToComponent(key, settings[key]);
|
||||
if (!comp) continue;
|
||||
settingComps.push(comp);
|
||||
const settingComps = shared.settingsToComponents(this, 'desktop', settings);
|
||||
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
{messages[0]}
|
||||
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
|
||||
</div>);
|
||||
|
||||
settingComps.push(
|
||||
<div key="check_sync_config_button" style={this.rowStyle_}>
|
||||
<button disabled={this.state.checkSyncConfigResult === 'checking'} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
|
||||
{ statusComp }
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -92,10 +92,14 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const mkComps = [];
|
||||
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
@@ -149,11 +153,36 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(<tr key={id}><td style={theme.textStyle}>{id}</td></tr>);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('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 they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle} />
|
||||
<div style={containerStyle}>
|
||||
<div style={{backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
{/*<div style={{backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||
<p style={theme.textStyle}>
|
||||
Important: This is a <b>beta</b> feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain.
|
||||
</p>
|
||||
@@ -163,12 +192,13 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
<p style={theme.textStyle}>
|
||||
For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation: <a onClick={() => {bridge().openExternal('http://joplin.cozic.net/help/e2ee.html')}} href="#">http://joplin.cozic.net/help/e2ee.html</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>*/}
|
||||
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{masterKeySection}
|
||||
{nonExistingMasterKeySection}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -183,6 +213,7 @@ const mapStateToProps = (state) => {
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: state.settings['encryption.enabled'],
|
||||
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -25,9 +25,7 @@ class ImportScreenComponent extends React.Component {
|
||||
doImport: true,
|
||||
filePath: newProps.filePath,
|
||||
messages: [],
|
||||
});
|
||||
|
||||
this.doImport();
|
||||
}, () => { this.doImport() });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -274,20 +274,22 @@ class MainScreenComponent extends React.Component {
|
||||
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const selectedFolderId = this.props.selectedFolderId;
|
||||
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
|
||||
|
||||
const headerButtons = [];
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New note'),
|
||||
iconName: 'fa-file-o',
|
||||
enabled: !!folders.length,
|
||||
enabled: !!folders.length && !onConflictFolder,
|
||||
onClick: () => { this.doCommand({ name: 'newNote' }) },
|
||||
});
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New to-do'),
|
||||
iconName: 'fa-check-square-o',
|
||||
enabled: !!folders.length,
|
||||
enabled: !!folders.length && !onConflictFolder,
|
||||
onClick: () => { this.doCommand({ name: 'newTodo' }) },
|
||||
});
|
||||
|
||||
@@ -384,6 +386,7 @@ const mapStateToProps = (state) => {
|
||||
notes: state.notes,
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -16,6 +16,7 @@ const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const { shim } = require('lib/shim.js');
|
||||
const eventManager = require('../eventManager');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
@@ -115,10 +116,14 @@ class NoteTextComponent extends React.Component {
|
||||
eventManager.removeListener('todoToggle', this.onTodoToggle_);
|
||||
}
|
||||
|
||||
async saveIfNeeded() {
|
||||
async saveIfNeeded(saveIfNewNote = false) {
|
||||
const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id);
|
||||
|
||||
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
if (!shared.isModified(this)) return;
|
||||
if (!forceSave) {
|
||||
if (!shared.isModified(this)) return;
|
||||
}
|
||||
await shared.saveNoteButton_press(this);
|
||||
}
|
||||
|
||||
@@ -260,7 +265,7 @@ class NoteTextComponent extends React.Component {
|
||||
shared.showMetadata_onPress(this);
|
||||
}
|
||||
|
||||
webview_ipcMessage(event) {
|
||||
async webview_ipcMessage(event) {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
@@ -282,6 +287,32 @@ class NoteTextComponent extends React.Component {
|
||||
} else if (msg === 'percentScroll') {
|
||||
this.ignoreNextEditorScroll_ = true;
|
||||
this.setEditorPercentScroll(arg0);
|
||||
} else if (msg === 'contextMenu') {
|
||||
const itemType = arg0 && arg0.type;
|
||||
|
||||
const menu = new Menu()
|
||||
|
||||
if (itemType === 'image') {
|
||||
const resource = await Resource.load(arg0.resourceId);
|
||||
const resourcePath = Resource.fullPath(resource);
|
||||
|
||||
menu.append(new MenuItem({label: _('Open...'), click: async () => {
|
||||
bridge().openExternal(resourcePath);
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Save as...'), click: async () => {
|
||||
const filePath = bridge().showSaveDialog({
|
||||
defaultPath: resource.filename ? resource.filename : resource.title,
|
||||
});
|
||||
if (!filePath) return;
|
||||
await fs.copy(resourcePath, filePath);
|
||||
}}));
|
||||
} else {
|
||||
reg.logger().error('Unhandled item type: ' + itemType);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.popup(bridge().window());
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceId = msg.substr('joplin://'.length);
|
||||
Resource.load(resourceId).then((resource) => {
|
||||
@@ -400,16 +431,13 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
|
||||
async commandAttachFile() {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
|
||||
const filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||
});
|
||||
if (!filePaths || !filePaths.length) return;
|
||||
|
||||
await this.saveIfNeeded();
|
||||
let note = await Note.load(noteId);
|
||||
await this.saveIfNeeded(true);
|
||||
let note = await Note.load(this.state.note.id);
|
||||
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
@@ -427,20 +455,29 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
commandSetAlarm() {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
async commandSetAlarm() {
|
||||
await this.saveIfNeeded(true);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'editAlarm',
|
||||
noteId: noteId,
|
||||
noteId: this.state.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
async commandSetTags() {
|
||||
await this.saveIfNeeded(true);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: this.state.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
const note = this.state.note;
|
||||
if (!note) return;
|
||||
|
||||
const menu = new Menu()
|
||||
|
||||
@@ -448,10 +485,16 @@ class NoteTextComponent extends React.Component {
|
||||
return this.commandAttachFile();
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
|
||||
return this.commandSetAlarm();
|
||||
menu.append(new MenuItem({label: _('Tags'), click: async () => {
|
||||
return this.commandSetTags();
|
||||
}}));
|
||||
|
||||
if (!!note.is_todo) {
|
||||
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
|
||||
return this.commandSetAlarm();
|
||||
}}));
|
||||
}
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
@@ -461,6 +504,7 @@ class NoteTextComponent extends React.Component {
|
||||
const body = note && note.body ? note.body : '';
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
|
||||
const isTodo = note && !!note.is_todo;
|
||||
|
||||
const borderWidth = 1;
|
||||
|
||||
@@ -568,6 +612,12 @@ class NoteTextComponent extends React.Component {
|
||||
onClick: () => { return this.commandAttachFile(); },
|
||||
});
|
||||
|
||||
toolbarItems.push({
|
||||
title: _('Tags'),
|
||||
iconName: 'fa-tags',
|
||||
onClick: () => { return this.commandSetTags(); },
|
||||
});
|
||||
|
||||
if (note.is_todo) {
|
||||
toolbarItems.push({
|
||||
title: Note.needAlarm(note) ? time.formatMsToLocal(note.todo_due) : _('Set alarm'),
|
||||
@@ -588,6 +638,7 @@ class NoteTextComponent extends React.Component {
|
||||
style={titleEditorStyle}
|
||||
value={note && note.title ? note.title : ''}
|
||||
onChange={(event) => { this.title_changeText(event); }}
|
||||
placeholder={ this.props.newNote ? _('Creating new %s...', isTodo ? _('to-do') : _('note')) : '' }
|
||||
/>
|
||||
|
||||
const titleBarMenuButton = <IconButton style={{
|
||||
|
@@ -1,179 +1,215 @@
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="hlScriptContainer"></div>
|
||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const contentElement = document.getElementById('content');
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
let hljsScriptAdded = false;
|
||||
let hljsLoaded = false;
|
||||
|
||||
function loadHljs(callback) {
|
||||
hljsScriptAdded = true;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.onload = function () {
|
||||
hljsLoaded = true;
|
||||
applyHljs();
|
||||
};
|
||||
script.src = 'highlight/highlight.pack.js';
|
||||
document.getElementById('hlScriptContainer').appendChild(script);
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
link.href = 'highlight/styles/atom-one-light.css';
|
||||
document.getElementById('hlScriptContainer').appendChild(link);
|
||||
}
|
||||
|
||||
function loadAndApplyHljs() {
|
||||
var codeElements = document.getElementsByClassName('code');
|
||||
if (!codeElements.length) return;
|
||||
|
||||
if (!hljsScriptAdded) {
|
||||
this.loadHljs();
|
||||
return;
|
||||
}
|
||||
|
||||
// If HLJS is not loaded yet, no need to do anything. When it loads
|
||||
// it will automatically apply the style to all the code elements.
|
||||
if (hljsLoaded) applyHljs(codeElements);
|
||||
}
|
||||
|
||||
function applyHljs(codeElements) {
|
||||
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
|
||||
|
||||
for (var i = 0; i < codeElements.length; i++) {
|
||||
hljs.highlightBlock(codeElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// / Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
|
||||
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
|
||||
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
|
||||
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
|
||||
// it at any time knowing that it's not going to be changed because the content height has changed.
|
||||
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
|
||||
// one second after the content has been updated.
|
||||
//
|
||||
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
|
||||
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
|
||||
|
||||
let percentScroll_ = 0;
|
||||
let checkScrollIID_ = null;
|
||||
|
||||
function setPercentScroll(percent) {
|
||||
percentScroll_ = percent;
|
||||
contentElement.scrollTop = percentScroll_ * maxScrollTop();
|
||||
}
|
||||
|
||||
function percentScroll() {
|
||||
return percentScroll_;
|
||||
}
|
||||
|
||||
function restorePercentScroll() {
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
contentElement.innerHTML = html;
|
||||
|
||||
loadAndApplyHljs();
|
||||
|
||||
// Remove the bullet from "ul" for checkbox lists and extra padding
|
||||
const checkboxes = document.getElementsByClassName('checkbox');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const cb = checkboxes[i];
|
||||
const ul = cb.parentElement.parentElement;
|
||||
if (!ul) {
|
||||
console.warn('Unexpected layout for checkbox');
|
||||
continue;
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.paddingLeft = 0;
|
||||
|
||||
#content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
<div id="hlScriptContainer"></div>
|
||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const contentElement = document.getElementById('content');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
let hljsScriptAdded = false;
|
||||
let hljsLoaded = false;
|
||||
|
||||
function loadHljs(callback) {
|
||||
hljsScriptAdded = true;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.onload = function () {
|
||||
hljsLoaded = true;
|
||||
applyHljs();
|
||||
};
|
||||
script.src = 'highlight/highlight.pack.js';
|
||||
document.getElementById('hlScriptContainer').appendChild(script);
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
link.href = 'highlight/styles/atom-one-light.css';
|
||||
document.getElementById('hlScriptContainer').appendChild(link);
|
||||
}
|
||||
|
||||
let previousContentHeight = contentElement.scrollHeight;
|
||||
let startTime = Date.now();
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
function loadAndApplyHljs() {
|
||||
var codeElements = document.getElementsByClassName('code');
|
||||
if (!codeElements.length) return;
|
||||
|
||||
if (!checkScrollIID_) {
|
||||
checkScrollIID_ = setInterval(() => {
|
||||
const h = contentElement.scrollHeight;
|
||||
if (h !== previousContentHeight) {
|
||||
previousContentHeight = h;
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
if (!hljsScriptAdded) {
|
||||
this.loadHljs();
|
||||
return;
|
||||
}
|
||||
|
||||
// If HLJS is not loaded yet, no need to do anything. When it loads
|
||||
// it will automatically apply the style to all the code elements.
|
||||
if (hljsLoaded) applyHljs(codeElements);
|
||||
}
|
||||
|
||||
function applyHljs(codeElements) {
|
||||
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
|
||||
|
||||
for (var i = 0; i < codeElements.length; i++) {
|
||||
try {
|
||||
hljs.highlightBlock(codeElements[i]);
|
||||
} catch (error) {
|
||||
console.warn('Cannot highlight code', error);
|
||||
}
|
||||
if (Date.now() - startTime >= 1000) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// / Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
|
||||
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
|
||||
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
|
||||
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
|
||||
// it at any time knowing that it's not going to be changed because the content height has changed.
|
||||
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
|
||||
// one second after the content has been updated.
|
||||
//
|
||||
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
|
||||
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
|
||||
|
||||
let percentScroll_ = 0;
|
||||
let checkScrollIID_ = null;
|
||||
|
||||
function setPercentScroll(percent) {
|
||||
percentScroll_ = percent;
|
||||
contentElement.scrollTop = percentScroll_ * maxScrollTop();
|
||||
}
|
||||
|
||||
function percentScroll() {
|
||||
return percentScroll_;
|
||||
}
|
||||
|
||||
function restorePercentScroll() {
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
updateBodyHeight();
|
||||
|
||||
contentElement.innerHTML = html;
|
||||
|
||||
loadAndApplyHljs();
|
||||
|
||||
// Remove the bullet from "ul" for checkbox lists and extra padding
|
||||
const checkboxes = document.getElementsByClassName('checkbox');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const cb = checkboxes[i];
|
||||
const ul = cb.parentElement.parentElement;
|
||||
if (!ul) {
|
||||
console.warn('Unexpected layout for checkbox');
|
||||
continue;
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.paddingLeft = 0;
|
||||
}
|
||||
|
||||
let ignoreNextScrollEvent = false;
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
if (checkScrollIID_) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
let previousContentHeight = contentElement.scrollHeight;
|
||||
let startTime = Date.now();
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
|
||||
if (!checkScrollIID_) {
|
||||
checkScrollIID_ = setInterval(() => {
|
||||
const h = contentElement.scrollHeight;
|
||||
if (h !== previousContentHeight) {
|
||||
previousContentHeight = h;
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
}
|
||||
if (Date.now() - startTime >= 1000) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
|
||||
let ignoreNextScrollEvent = false;
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
if (checkScrollIID_) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
|
||||
ignoreNextScrollEvent = true;
|
||||
setPercentScroll(percent);
|
||||
});
|
||||
|
||||
function maxScrollTop() {
|
||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
}
|
||||
|
||||
ignoreNextScrollEvent = true;
|
||||
setPercentScroll(percent);
|
||||
});
|
||||
|
||||
function maxScrollTop() {
|
||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
}
|
||||
|
||||
contentElement.addEventListener('scroll', function(e) {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
return;
|
||||
// The body element needs to have a fixed height for the content to be scrollable
|
||||
function updateBodyHeight() {
|
||||
document.getElementById('body').style.height = window.innerHeight + 'px';
|
||||
}
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
setPercentScroll(percent);
|
||||
ipcRenderer.sendToHost('percentScroll', percent);
|
||||
});
|
||||
|
||||
// Disable drag and drop otherwise it's possible to drop a URL
|
||||
// on it and it will open in the view as a website.
|
||||
document.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
contentElement.addEventListener('scroll', function(e) {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
return;
|
||||
}
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
setPercentScroll(percent);
|
||||
ipcRenderer.sendToHost('percentScroll', percent);
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', function(event) {
|
||||
const element = event.target;
|
||||
if (element && element.getAttribute('data-resource-id')) {
|
||||
ipcRenderer.sendToHost('contextMenu', {
|
||||
type: element.getAttribute('src') ? 'image' : 'link',
|
||||
resourceId: element.getAttribute('data-resource-id'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Disable drag and drop otherwise it's possible to drop a URL
|
||||
// on it and it will open in the view as a website.
|
||||
document.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
updateBodyHeight();
|
||||
});
|
||||
|
||||
updateBodyHeight();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
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
1
ElectronClient/app/locales/eu.json
Normal file
1
ElectronClient/app/locales/eu.json
Normal file
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
@@ -3,6 +3,7 @@ locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['es_ES'] = require('./es_ES.json');
|
||||
locales['eu'] = require('./eu.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
locales['hr_HR'] = require('./hr_HR.json');
|
||||
locales['it_IT'] = require('./it_IT.json');
|
||||
|
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
202
ElectronClient/app/package-lock.json
generated
202
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.54",
|
||||
"version": "1.0.63",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -10,17 +10,9 @@
|
||||
"integrity": "sha512-+rr4OgeTNrLuJAf09o3USdttEYiXvZshWMkhD6wR9v1ieXH0JM1Q2yT41/cJuJcqiPpSXlM/g3aR+Y5MWQdr0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"7zip-bin-linux": "1.3.1",
|
||||
"7zip-bin-win": "2.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"7zip-bin-linux": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin-linux/-/7zip-bin-linux-1.3.1.tgz",
|
||||
"integrity": "sha512-Wv1uEEeHbTiS1+ycpwUxYNuIcyohU6Y6vEqY3NquBkeqy0YhVdsNUGsj0XKSRciHR6LoJSEUuqYUexmws3zH7Q==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"7zip-bin-win": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.1.tgz",
|
||||
@@ -245,6 +237,11 @@
|
||||
"integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==",
|
||||
"dev": true
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
|
||||
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -571,8 +568,7 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base-64": {
|
||||
"version": "0.1.0",
|
||||
@@ -617,6 +613,14 @@
|
||||
"readable-stream": "2.3.3"
|
||||
}
|
||||
},
|
||||
"block-stream": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
|
||||
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
|
||||
"requires": {
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
|
||||
@@ -734,7 +738,6 @@
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
|
||||
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -879,8 +882,7 @@
|
||||
"capture-stack-trace": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz",
|
||||
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0="
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.12.0",
|
||||
@@ -937,7 +939,8 @@
|
||||
"chownr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
|
||||
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE="
|
||||
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
|
||||
"dev": true
|
||||
},
|
||||
"chromium-pickle-js": {
|
||||
"version": "0.2.0",
|
||||
@@ -981,9 +984,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"color": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz",
|
||||
"integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz",
|
||||
"integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=",
|
||||
"requires": {
|
||||
"color-convert": "1.9.1",
|
||||
"color-string": "1.5.2"
|
||||
@@ -1034,8 +1037,7 @@
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"concat-stream": {
|
||||
"version": "1.6.0",
|
||||
@@ -1097,7 +1099,6 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
|
||||
"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"capture-stack-trace": "1.0.0"
|
||||
}
|
||||
@@ -1196,14 +1197,6 @@
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
|
||||
"requires": {
|
||||
"mimic-response": "1.0.0"
|
||||
}
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
@@ -1240,11 +1233,6 @@
|
||||
"repeating": "2.0.1"
|
||||
}
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
|
||||
"integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U="
|
||||
},
|
||||
"dmg-builder": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-2.1.6.tgz",
|
||||
@@ -1295,8 +1283,7 @@
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
|
||||
"dev": true
|
||||
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.1",
|
||||
@@ -2024,8 +2011,18 @@
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fstream": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
|
||||
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"inherits": "2.0.3",
|
||||
"mkdirp": "0.5.1",
|
||||
"rimraf": "2.6.2"
|
||||
}
|
||||
},
|
||||
"fullstore": {
|
||||
"version": "1.1.0",
|
||||
@@ -2071,8 +2068,7 @@
|
||||
"get-stream": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
|
||||
},
|
||||
"getpass": {
|
||||
"version": "0.1.7",
|
||||
@@ -2093,7 +2089,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "1.0.0",
|
||||
"inflight": "1.0.6",
|
||||
@@ -2142,7 +2137,6 @@
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"create-error-class": "3.0.2",
|
||||
"duplexer3": "0.1.4",
|
||||
@@ -2160,8 +2154,7 @@
|
||||
"unzip-response": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
|
||||
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2321,7 +2314,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "1.4.0",
|
||||
"wrappy": "1.0.2"
|
||||
@@ -2520,14 +2512,12 @@
|
||||
"is-redirect": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
|
||||
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
|
||||
},
|
||||
"is-retry-allowed": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
|
||||
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
@@ -2680,6 +2670,14 @@
|
||||
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
|
||||
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.9.0-beta1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
|
||||
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
|
||||
"requires": {
|
||||
"match-at": "0.1.1"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
@@ -2812,8 +2810,7 @@
|
||||
"lowercase-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz",
|
||||
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=",
|
||||
"dev": true
|
||||
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "4.1.1",
|
||||
@@ -2860,6 +2857,29 @@
|
||||
"uc.micro": "1.0.3"
|
||||
}
|
||||
},
|
||||
"markdown-it-katex": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz",
|
||||
"integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=",
|
||||
"requires": {
|
||||
"katex": "0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"katex": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz",
|
||||
"integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=",
|
||||
"requires": {
|
||||
"match-at": "0.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"match-at": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
|
||||
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
@@ -2963,16 +2983,10 @@
|
||||
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
|
||||
"dev": true
|
||||
},
|
||||
"mimic-response": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",
|
||||
"integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.8"
|
||||
}
|
||||
@@ -2982,22 +2996,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz",
|
||||
"integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==",
|
||||
"requires": {
|
||||
"yallist": "3.0.2"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.4.tgz",
|
||||
"integrity": "sha512-sN4U9tIJtBRwKbwgFh9qJfrPIQ/GGTRr1MGqkgOeMTLy8/lM0FcWU//FqlnZ3Vb7gJ+Mxh3FOg1EklibdajbaQ==",
|
||||
"requires": {
|
||||
"minipass": "2.2.1"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||
@@ -3300,8 +3298,7 @@
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
@@ -3427,8 +3424,7 @@
|
||||
"prepend-http": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
|
||||
"dev": true
|
||||
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
|
||||
},
|
||||
"preserve": {
|
||||
"version": "0.2.0",
|
||||
@@ -3889,7 +3885,6 @@
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
|
||||
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "7.1.2"
|
||||
}
|
||||
@@ -3946,17 +3941,16 @@
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.18.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.18.4.tgz",
|
||||
"integrity": "sha1-/jKcDwaJbCiqJDdt8f/wKuV/LTQ=",
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.17.3.tgz",
|
||||
"integrity": "sha1-SEzSpwyQA3CUjcxD4WX3gwa/9Io=",
|
||||
"requires": {
|
||||
"caw": "2.0.1",
|
||||
"color": "2.0.1",
|
||||
"detect-libc": "0.2.0",
|
||||
"color": "1.0.3",
|
||||
"got": "6.7.1",
|
||||
"nan": "2.7.0",
|
||||
"semver": "5.4.1",
|
||||
"simple-get": "2.7.0",
|
||||
"tar": "3.2.1"
|
||||
"tar": "2.2.1"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
@@ -3985,21 +3979,6 @@
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
|
||||
"dev": true
|
||||
},
|
||||
"simple-concat": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
|
||||
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
|
||||
},
|
||||
"simple-get": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz",
|
||||
"integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==",
|
||||
"requires": {
|
||||
"decompress-response": "3.3.0",
|
||||
"once": "1.4.0",
|
||||
"simple-concat": "1.0.0"
|
||||
}
|
||||
},
|
||||
"simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -4944,15 +4923,13 @@
|
||||
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
|
||||
},
|
||||
"tar": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz",
|
||||
"integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
|
||||
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
|
||||
"requires": {
|
||||
"chownr": "1.0.1",
|
||||
"minipass": "2.2.1",
|
||||
"minizlib": "1.0.4",
|
||||
"mkdirp": "0.5.1",
|
||||
"yallist": "3.0.2"
|
||||
"block-stream": "0.0.9",
|
||||
"fstream": "1.0.11",
|
||||
"inherits": "2.0.3"
|
||||
}
|
||||
},
|
||||
"tar-fs": {
|
||||
@@ -5070,8 +5047,7 @@
|
||||
"timed-out": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
|
||||
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
|
||||
},
|
||||
"to-fast-properties": {
|
||||
"version": "1.0.3",
|
||||
@@ -5230,7 +5206,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
||||
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prepend-http": "1.0.4"
|
||||
}
|
||||
@@ -5409,11 +5384,6 @@
|
||||
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
|
||||
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.54",
|
||||
"version": "1.0.63",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -8,8 +8,8 @@
|
||||
"pack": "node_modules/.bin/electron-builder --dir",
|
||||
"dist": "node_modules/.bin/electron-builder",
|
||||
"publish": "build -p always",
|
||||
"postinstall": "node compile-jsx.js && node compile-package-info.js",
|
||||
"compile": "node compile-jsx.js && node compile-package-info.js"
|
||||
"postinstall": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
|
||||
"compile": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,6 +22,9 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "net.cozic.joplin-desktop",
|
||||
"extraResources": [
|
||||
"build/icons/*"
|
||||
],
|
||||
"win": {
|
||||
"icon": "../../Assets/Joplin.ico"
|
||||
},
|
||||
@@ -34,7 +37,8 @@
|
||||
"asar": false
|
||||
},
|
||||
"linux": {
|
||||
"asar": false
|
||||
"asar": false,
|
||||
"category": "Office"
|
||||
}
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
@@ -51,6 +55,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"async-mutex": "^0.1.3",
|
||||
"base-64": "^0.1.0",
|
||||
"electron-context-menu": "^0.9.1",
|
||||
"electron-log": "^2.2.11",
|
||||
@@ -62,9 +67,11 @@
|
||||
"highlight.js": "^9.12.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"jssha": "^2.3.1",
|
||||
"katex": "^0.9.0-beta1",
|
||||
"levenshtein": "^1.0.5",
|
||||
"lodash": "^4.17.4",
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"md5": "^2.2.1",
|
||||
"mime": "^2.0.3",
|
||||
"moment": "^2.19.1",
|
||||
@@ -78,7 +85,7 @@
|
||||
"react-dom": "^16.0.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"redux": "^3.7.2",
|
||||
"sharp": "^0.18.4",
|
||||
"sharp": "^0.17.3",
|
||||
"smalltalk": "^2.5.1",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^3.1.13",
|
||||
|
@@ -79,6 +79,10 @@ async function main(argv) {
|
||||
const macOsUrl = downloadUrl(release, 'macos');
|
||||
const linuxUrl = downloadUrl(release, 'linux');
|
||||
|
||||
console.info('Windows: ', winUrl);
|
||||
console.info('macOS: ', macOsUrl);
|
||||
console.info('Linux: ', linuxUrl);
|
||||
|
||||
let content = readmeContent();
|
||||
|
||||
if (winUrl) content = content.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.exe)/, winUrl);
|
||||
|
102
README.md
102
README.md
@@ -6,7 +6,7 @@ Notes exported from Evernote via .enex files [can be imported](#importing-notes-
|
||||
|
||||
The notes can be [synchronised](#synchronisation) with various targets including [Nextcloud](https://nextcloud.com/), the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
|
||||
|
||||
Joplin is still under development but is out of Beta and should be suitable for every day use. The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
|
||||
The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
|
||||
|
||||
<div class="top-screenshot"><img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/AllClients.jpg" style="max-width: 100%; max-height: 35em;"></div>
|
||||
|
||||
@@ -18,15 +18,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
|
||||
|
||||
Operating System | Download
|
||||
-----------------|--------
|
||||
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-Setup-0.10.54.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/v0.10.54/Joplin-0.10.54.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/v0.10.54/Joplin-0.10.54-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
|
||||
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.62/Joplin-Setup-1.0.62.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.62/Joplin-1.0.62.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.62/Joplin-1.0.62-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
|
||||
|
||||
## Mobile applications
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.81/joplin-v0.10.81.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.97/joplin-v1.0.97.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> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -51,13 +51,15 @@ For usage information, please refer to the full [Joplin Terminal Application Doc
|
||||
# Features
|
||||
|
||||
- Desktop, mobile and terminal applications.
|
||||
- Import Enex files (Evernote export format)
|
||||
- Support notes, to-dos, tags and notebooks.
|
||||
- Support for alarms (notifications) in mobile and desktop applications.
|
||||
- Offline first, so the entire data is always available on the device even without an internet connection.
|
||||
- Ability to synchronise with multiple targets, including NextCloud, the file system and OneDrive (Dropbox is planned).
|
||||
- Synchronisation with various services, including NextCloud, WebDAV and OneDrive. Dropbox is planned.
|
||||
- End To End Encryption (E2EE)
|
||||
- Synchronises to a plain text format, which can be easily manipulated, backed up, or exported to a different format.
|
||||
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications.
|
||||
- Tag support
|
||||
- File attachment support (images are displayed, and other files are linked and can be opened in the relevant application).
|
||||
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications. Support for extra features such as math notation and checkboxes.
|
||||
- File attachment support - images are displayed, and other files are linked and can be opened in the relevant application.
|
||||
- Search functionality.
|
||||
- Geo-location support.
|
||||
- Supports multiple languages
|
||||
@@ -91,8 +93,6 @@ Currently, synchronisation is possible with Nextcloud and OneDrive (by default)
|
||||
|
||||
## Nextcloud synchronisation
|
||||
|
||||
**Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup the profile directory of the desktop or terminal application.**
|
||||
|
||||
On the **desktop application** or **mobile application**, go to the config screen and select Nextcloud as the synchronisation target. Then input [the WebDAV URL](https://docs.nextcloud.com/server/9/user_manual/files/access_webdav.html), this is normally `https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin` (make sure to create the "Joplin" directory in Nextcloud and to replace USERNAME by your Nextcloud username), and set the username and password.
|
||||
|
||||
On the **terminal application**, you will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
|
||||
@@ -104,6 +104,16 @@ On the **terminal application**, you will need to set the `sync.target` config v
|
||||
|
||||
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
|
||||
|
||||
## WebDAV synchronisation
|
||||
|
||||
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
|
||||
|
||||
Known compatible services that use WebDAV:
|
||||
|
||||
- [Box.com](https://www.box.com/)
|
||||
- [DriveHQ](https://www.drivehq.com)
|
||||
- [Zimbra](https://www.zimbra.com/)
|
||||
|
||||
## OneDrive synchronisation
|
||||
|
||||
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
|
||||
@@ -138,28 +148,76 @@ On mobile, the alarms will be displayed using the built-in notification system.
|
||||
|
||||
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
|
||||
|
||||
# Markdown
|
||||
|
||||
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
|
||||
|
||||
## Math notation
|
||||
|
||||
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:
|
||||
|
||||
$$
|
||||
EXPRESSION
|
||||
$$
|
||||
|
||||
For example:
|
||||
|
||||
$$
|
||||
f(x) = \int_{-\infty}^\infty
|
||||
\hat f(\xi)\,e^{2 \pi i \xi x}
|
||||
\,d\xi
|
||||
$$
|
||||
|
||||
Here is an example with the Markdown and rendered result side by side:
|
||||
|
||||
<img src="https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/Katex.png" style="max-width: 100%; max-height: 35em;">
|
||||
|
||||
## Checkboxes
|
||||
|
||||
Checkboxes can be added like so:
|
||||
|
||||
-[ ] Milk
|
||||
-[ ] Rice
|
||||
-[ ] Eggs
|
||||
|
||||
The checkboxes can then be ticked in the mobile and desktop applications.
|
||||
|
||||
# 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
|
||||
|
||||
# Localisation
|
||||
|
||||
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
|
||||
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
|
||||
|
||||
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
|
||||
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
|
||||
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
|
||||
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
|
||||
|
||||
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from there: https://github.com/laurent22/joplin/tree/master/CliClient/locales
|
||||
|
||||
This translation will apply to the three applications - desktop, mobile and terminal.
|
||||
|
||||
# Contributing
|
||||
Current translations:
|
||||
|
||||
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
|
||||
|
||||
# Coming features
|
||||
|
||||
- Mobile: manage tags
|
||||
- Windows: Tray icon
|
||||
- Desktop apps: Tag auto-complete
|
||||
- Desktop apps: Dark theme
|
||||
- Linux: Enable auto-update for desktop app
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Code | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
 | Basque | eu | juan.abasolo@ehu.eus | 89%
|
||||
 | Croatian | hr_HR | Hrvoje Mandić <trbuhom@net.hr> | 72%
|
||||
 | Deutsch | de_DE | Tobias Strobel <git@strobeltobias.de> | 91%
|
||||
 | English | en_GB | | 100%
|
||||
 | Español | es_ES | Lucas Vieites | 79%
|
||||
 | Español (Costa Rica) | es_CR | | 68%
|
||||
 | Français | fr_FR | Laurent Cozic | 100%
|
||||
 | Italiano | it_IT | | 75%
|
||||
 | Nederlands | nl_BE | | 89%
|
||||
 | Português (Brasil) | pt_BR | | 74%
|
||||
 | Русский | ru_RU | Artyom Karlov <artyom.karlov@gmail.com> | 94%
|
||||
 | 中文 (简体) | zh_CN | RCJacH <RCJacH@outlook.com> | 75%
|
||||
 | 日本語 | ja_JP | | 73%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
# Known bugs
|
||||
|
||||
|
@@ -126,6 +126,10 @@ You will need to set the `sync.target` config variable and all the `sync.5.path`
|
||||
|
||||
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
|
||||
|
||||
## WebDAV synchronisation
|
||||
|
||||
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
|
||||
|
||||
## OneDrive synchronisation
|
||||
|
||||
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
|
||||
@@ -240,7 +244,7 @@ The following commands are available in [command-line mode](#command-line-mode):
|
||||
Possible values: HH:mm (20:30), h:mm A (8:30 PM).
|
||||
Default: "HH:mm"
|
||||
|
||||
uncompletedTodosOnTop Show uncompleted todos on top of the lists.
|
||||
uncompletedTodosOnTop Show uncompleted to-dos on top of the lists.
|
||||
Type: bool.
|
||||
Default: true
|
||||
|
||||
|
@@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 2097259
|
||||
versionName "0.10.81"
|
||||
versionCode 2097275
|
||||
versionName "1.0.97"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
|
||||
@@ -34,6 +35,7 @@ SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
|
||||
class BaseApplication {
|
||||
|
||||
|
@@ -2,6 +2,7 @@ const { Log } = require('lib/log.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
class BaseModel {
|
||||
|
||||
@@ -247,6 +248,40 @@ class BaseModel {
|
||||
return !Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
static saveMutex(modelOrId) {
|
||||
const noLockMutex = {
|
||||
acquire: function() { return null; }
|
||||
};
|
||||
|
||||
if (!modelOrId) return noLockMutex;
|
||||
|
||||
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
|
||||
|
||||
if (!modelId) return noLockMutex;
|
||||
|
||||
let mutex = BaseModel.saveMutexes_[modelId];
|
||||
if (mutex) return mutex;
|
||||
|
||||
mutex = new Mutex();
|
||||
BaseModel.saveMutexes_[modelId] = mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
static releaseSaveMutex(modelOrId, release) {
|
||||
if (!release) return;
|
||||
if (!modelOrId) return release();
|
||||
|
||||
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
|
||||
|
||||
if (!modelId) return release();
|
||||
|
||||
let mutex = BaseModel.saveMutexes_[modelId];
|
||||
if (!mutex) return release();
|
||||
|
||||
delete BaseModel.saveMutexes_[modelId];
|
||||
release();
|
||||
}
|
||||
|
||||
static saveQuery(o, options) {
|
||||
let temp = {}
|
||||
let fieldNames = this.fieldNames();
|
||||
@@ -320,7 +355,16 @@ class BaseModel {
|
||||
return query;
|
||||
}
|
||||
|
||||
static save(o, options = null) {
|
||||
static async save(o, options = null) {
|
||||
// When saving, there's a mutex per model ID. This is because the model returned from this function
|
||||
// is basically its input `o` (instead of being read from the database, for performance reasons).
|
||||
// This works well in general except if that model is saved simultaneously in two places. In that
|
||||
// case, the output won't be up-to-date and would cause for example display issues with out-dated
|
||||
// notes being displayed. This was an issue when notes were being synchronised while being decrypted
|
||||
// at the same time.
|
||||
|
||||
const mutexRelease = await this.saveMutex(o).acquire();
|
||||
|
||||
options = this.modOptions(options);
|
||||
options.isNew = this.isNew(o, options);
|
||||
|
||||
@@ -348,7 +392,11 @@ class BaseModel {
|
||||
queries = queries.concat(options.nextQueries);
|
||||
}
|
||||
|
||||
return this.db().transactionExecBatch(queries).then(() => {
|
||||
let output = null;
|
||||
|
||||
try {
|
||||
await this.db().transactionExecBatch(queries);
|
||||
|
||||
o = Object.assign({}, o);
|
||||
if (modelId) o.id = modelId;
|
||||
if ('updated_time' in saveQuery.modObject) o.updated_time = saveQuery.modObject.updated_time;
|
||||
@@ -365,10 +413,14 @@ class BaseModel {
|
||||
}
|
||||
}
|
||||
|
||||
return this.filter(o);
|
||||
}).catch((error) => {
|
||||
output = this.filter(o);
|
||||
} catch (error) {
|
||||
Log.error('Cannot save model', error);
|
||||
});
|
||||
}
|
||||
|
||||
this.releaseSaveMutex(o, mutexRelease);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static isNew(object, options) {
|
||||
@@ -447,5 +499,6 @@ BaseModel.TYPE_MASTER_KEY = 9;
|
||||
|
||||
BaseModel.db_ = null;
|
||||
BaseModel.dispatch = function(o) {};
|
||||
BaseModel.saveMutexes_ = {};
|
||||
|
||||
module.exports = BaseModel;
|
@@ -10,6 +10,10 @@ class BaseSyncTarget {
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
option(name, defaultValue = null) {
|
||||
return this.options_ && (name in this.options_) ? this.options_[name] : defaultValue;
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ const Resource = require('lib/models/Resource.js');
|
||||
const ModelCache = require('lib/ModelCache');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const md5 = require('md5');
|
||||
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
|
||||
|
||||
class MdToHtml {
|
||||
|
||||
@@ -116,7 +117,7 @@ class MdToHtml {
|
||||
if (mime == 'image/png' || mime == 'image/jpg' || mime == 'image/jpeg' || mime == 'image/gif') {
|
||||
let src = './' + Resource.filename(resource);
|
||||
if (this.resourceBaseUrl_ !== null) src = this.resourceBaseUrl_ + src;
|
||||
let output = '<img title="' + htmlentities(title) + '" src="' + src + '"/>';
|
||||
let output = '<img data-resource-id="' + resource.id + '" title="' + htmlentities(title) + '" src="' + src + '"/>';
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -156,27 +157,57 @@ class MdToHtml {
|
||||
}
|
||||
}
|
||||
|
||||
renderTokens_(tokens, options) {
|
||||
rendererPlugin_(language) {
|
||||
if (!language) return null;
|
||||
|
||||
const handlers = {};
|
||||
handlers['katex'] = new MdToHtml_Katex();
|
||||
return language in handlers ? handlers[language] : null;
|
||||
}
|
||||
|
||||
parseInlineCodeLanguage_(content) {
|
||||
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
|
||||
if (m && m.length >= 2) {
|
||||
const language = m[1];
|
||||
return {
|
||||
language: language,
|
||||
newContent: content.substr(language.length + 3),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderTokens_(markdownIt, tokens, options) {
|
||||
let output = [];
|
||||
let previousToken = null;
|
||||
let anchorAttrs = [];
|
||||
let extraCssBlocks = {};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const t = tokens[i];
|
||||
let t = tokens[i];
|
||||
const nextToken = i < tokens.length ? tokens[i+1] : null;
|
||||
|
||||
let tag = t.tag;
|
||||
let openTag = null;
|
||||
let closeTag = null;
|
||||
let attrs = t.attrs ? t.attrs : [];
|
||||
let tokenContent = t.content ? t.content : null;
|
||||
const isCodeBlock = tag === 'code' && t.block;
|
||||
const isInlineCode = t.type === 'code_inline';
|
||||
const codeBlockLanguage = t && t.info ? t.info : null;
|
||||
let rendererPlugin = null;
|
||||
let rendererPluginOptions = { tagType: 'inline' };
|
||||
|
||||
// if (t.map) attrs.push(['data-map', t.map.join(':')]);
|
||||
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
|
||||
|
||||
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
|
||||
// Markdown-it render list items as <li><p>Text<p></li> which makes it
|
||||
// complicated to style and layout the HTML, so we remove this extra
|
||||
// <p> here and below in closeTag.
|
||||
openTag = null;
|
||||
} else if (isInlineCode) {
|
||||
openTag = null;
|
||||
} else if (tag && t.type.indexOf('_open') >= 0) {
|
||||
openTag = tag;
|
||||
} else if (tag && t.type.indexOf('_close') >= 0) {
|
||||
@@ -186,7 +217,11 @@ class MdToHtml {
|
||||
} else if (t.type === 'link_open') {
|
||||
openTag = 'a';
|
||||
} else if (isCodeBlock) {
|
||||
openTag = 'pre';
|
||||
if (rendererPlugin) {
|
||||
openTag = null;
|
||||
} else {
|
||||
openTag = 'pre';
|
||||
}
|
||||
}
|
||||
|
||||
if (openTag) {
|
||||
@@ -201,12 +236,35 @@ class MdToHtml {
|
||||
|
||||
if (isCodeBlock) {
|
||||
const codeAttrs = ['code'];
|
||||
if (t.info) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
|
||||
output.push('<code class="' + codeAttrs.join(' ') + '">');
|
||||
if (!rendererPlugin) {
|
||||
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
|
||||
output.push('<code class="' + codeAttrs.join(' ') + '">');
|
||||
}
|
||||
} else if (isInlineCode) {
|
||||
const result = this.parseInlineCodeLanguage_(tokenContent);
|
||||
if (result) {
|
||||
rendererPlugin = this.rendererPlugin_(result.language);
|
||||
tokenContent = result.newContent;
|
||||
}
|
||||
|
||||
if (!rendererPlugin) {
|
||||
output.push('<code>');
|
||||
}
|
||||
}
|
||||
|
||||
if (t.type === 'math_inline' || t.type === 'math_block') {
|
||||
rendererPlugin = this.rendererPlugin_('katex');
|
||||
rendererPluginOptions = { tagType: t.type === 'math_block' ? 'block' : 'inline' };
|
||||
}
|
||||
|
||||
if (rendererPlugin) {
|
||||
rendererPlugin.loadAssets().catch((error) => {
|
||||
console.warn('MdToHtml: Error loading assets for ' + rendererPlugin.name() + ': ', error.message);
|
||||
});
|
||||
}
|
||||
|
||||
if (t.type === 'image') {
|
||||
if (t.content) attrs.push(['title', t.content]);
|
||||
if (tokenContent) attrs.push(['title', tokenContent]);
|
||||
output.push(this.renderImage_(attrs, options));
|
||||
} else if (t.type === 'softbreak') {
|
||||
output.push('<br/>');
|
||||
@@ -214,11 +272,17 @@ class MdToHtml {
|
||||
output.push('<hr/>');
|
||||
} else {
|
||||
if (t.children) {
|
||||
const parsedChildren = this.renderTokens_(t.children, options);
|
||||
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
|
||||
output = output.concat(parsedChildren);
|
||||
} else {
|
||||
if (t.content) {
|
||||
output.push(htmlentities(t.content));
|
||||
if (tokenContent) {
|
||||
if ((isCodeBlock || isInlineCode) && rendererPlugin) {
|
||||
output = rendererPlugin.processContent(output, tokenContent, isCodeBlock ? 'block' : 'inline');
|
||||
} else if (rendererPlugin) {
|
||||
output = rendererPlugin.processContent(output, tokenContent, rendererPluginOptions.tagType);
|
||||
} else {
|
||||
output.push(htmlentities(tokenContent));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,10 +294,18 @@ class MdToHtml {
|
||||
} else if (tag && t.type.indexOf('inline') >= 0) {
|
||||
closeTag = openTag;
|
||||
} else if (isCodeBlock) {
|
||||
closeTag = openTag;
|
||||
if (!rendererPlugin) closeTag = openTag;
|
||||
}
|
||||
|
||||
if (isCodeBlock) output.push('</code>');
|
||||
if (isCodeBlock) {
|
||||
if (!rendererPlugin) {
|
||||
output.push('</code>');
|
||||
}
|
||||
} else if (isInlineCode) {
|
||||
if (!rendererPlugin) {
|
||||
output.push('</code>');
|
||||
}
|
||||
}
|
||||
|
||||
if (closeTag) {
|
||||
if (closeTag === 'a') {
|
||||
@@ -243,8 +315,28 @@ class MdToHtml {
|
||||
}
|
||||
}
|
||||
|
||||
if (rendererPlugin) {
|
||||
const extraCss = rendererPlugin.extraCss();
|
||||
const name = rendererPlugin.name();
|
||||
if (extraCss && !(name in extraCssBlocks)) {
|
||||
extraCssBlocks[name] = extraCss;
|
||||
}
|
||||
}
|
||||
|
||||
previousToken = t;
|
||||
}
|
||||
|
||||
// Insert the extra CSS at the top of the HTML
|
||||
|
||||
const temp = ['<style>'];
|
||||
for (let n in extraCssBlocks) {
|
||||
if (!extraCssBlocks.hasOwnProperty(n)) continue;
|
||||
temp.push(extraCssBlocks[n]);
|
||||
}
|
||||
temp.push('</style>');
|
||||
|
||||
output = temp.concat(output);
|
||||
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
@@ -260,7 +352,13 @@ class MdToHtml {
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
});
|
||||
const env = {};
|
||||
|
||||
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
|
||||
// to math_inline and math_block blocks. These blocks are then processed directly with the Katex
|
||||
// library. It is better this way as then it is possible to conditionally load the CSS required by
|
||||
// Katex and use an up-to-date version of Katex (as of 2018, the plugin is still using 0.6, which is
|
||||
// buggy instead of 0.9).
|
||||
md.use(require('markdown-it-katex'));
|
||||
|
||||
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
|
||||
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
|
||||
@@ -278,12 +376,14 @@ class MdToHtml {
|
||||
}
|
||||
}
|
||||
|
||||
const env = {};
|
||||
const tokens = md.parse(body, env);
|
||||
|
||||
let renderedBody = this.renderTokens_(md, tokens, options);
|
||||
|
||||
// console.info(body);
|
||||
// console.info(tokens);
|
||||
|
||||
let renderedBody = this.renderTokens_(tokens, options);
|
||||
// console.info(renderedBody);
|
||||
|
||||
if (HORRIBLE_HACK) {
|
||||
let loopCount = 0;
|
||||
@@ -374,9 +474,14 @@ class MdToHtml {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.katex .mfrac .frac-line:before {
|
||||
/* top: 50%; */
|
||||
/* padding-bottom: .7em; */
|
||||
}
|
||||
`;
|
||||
|
||||
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
|
||||
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 output = styleHtml + renderedBody;
|
||||
|
||||
|
48
ReactNativeClient/lib/MdToHtml_Katex.js
Normal file
48
ReactNativeClient/lib/MdToHtml_Katex.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const { shim } = require('lib/shim');
|
||||
const katex = require('katex');
|
||||
const katexCss = require('lib/csstojs/katex.css.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
|
||||
class MdToHtml_Katex {
|
||||
|
||||
name() {
|
||||
return 'katex';
|
||||
}
|
||||
|
||||
processContent(renderedTokens, content, tagType) {
|
||||
try {
|
||||
let renderered = katex.renderToString(content);
|
||||
|
||||
if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
|
||||
|
||||
renderedTokens.push(renderered);
|
||||
} catch (error) {
|
||||
renderedTokens.push('Cannot render Katex content: ' + error.message);
|
||||
}
|
||||
return renderedTokens;
|
||||
}
|
||||
|
||||
extraCss() {
|
||||
return katexCss;
|
||||
}
|
||||
|
||||
async loadAssets() {
|
||||
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
|
||||
|
||||
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
|
||||
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
|
||||
|
||||
if (shim.isReactNative()) {
|
||||
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
|
||||
const baseDir = Setting.value('resourceDir');
|
||||
await shim.fsDriver().mkdir(baseDir + '/fonts');
|
||||
|
||||
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
|
||||
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
|
||||
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = MdToHtml_Katex;
|
@@ -1,9 +1,13 @@
|
||||
// The Nextcloud sync target is essentially a wrapper over the WebDAV sync target,
|
||||
// thus all the calls to SyncTargetWebDAV to avoid duplicate code.
|
||||
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const WebDavApi = require('lib/WebDavApi');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
|
||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
|
||||
|
||||
class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
@@ -12,9 +16,8 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
return 5;
|
||||
}
|
||||
|
||||
constructor(db, options = null) {
|
||||
super(db, options);
|
||||
// this.authenticated_ = false;
|
||||
static supportsConfigCheck() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
@@ -22,26 +25,26 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('Nextcloud (Beta)');
|
||||
return _('Nextcloud');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return true;
|
||||
//return this.authenticated_;
|
||||
}
|
||||
|
||||
static async checkConfig(options) {
|
||||
return SyncTargetWebDAV.checkConfig(options);
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const options = {
|
||||
baseUrl: () => Setting.value('sync.5.path'),
|
||||
username: () => Setting.value('sync.5.username'),
|
||||
password: () => Setting.value('sync.5.password'),
|
||||
};
|
||||
const fileApi = await SyncTargetWebDAV.initFileApi_({
|
||||
path: Setting.value('sync.5.path'),
|
||||
username: Setting.value('sync.5.username'),
|
||||
password: Setting.value('sync.5.password'),
|
||||
});
|
||||
|
||||
const api = new WebDavApi(options);
|
||||
const driver = new FileApiDriverWebDav(api);
|
||||
const fileApi = new FileApi('', driver);
|
||||
fileApi.setSyncTargetId(SyncTargetNextcloud.id());
|
||||
fileApi.setLogger(this.logger());
|
||||
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,7 @@ class SyncTargetRegistry {
|
||||
name: SyncTargetClass.targetName(),
|
||||
label: SyncTargetClass.label(),
|
||||
classRef: SyncTargetClass,
|
||||
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
|
||||
};
|
||||
}
|
||||
|
||||
|
83
ReactNativeClient/lib/SyncTargetWebDAV.js
Normal file
83
ReactNativeClient/lib/SyncTargetWebDAV.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const WebDavApi = require('lib/WebDavApi');
|
||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
|
||||
|
||||
class SyncTargetWebDAV extends BaseSyncTarget {
|
||||
|
||||
static id() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'webdav';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('WebDAV');
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async initFileApi_(options) {
|
||||
const apiOptions = {
|
||||
baseUrl: () => options.path,
|
||||
username: () => options.username,
|
||||
password: () => options.password,
|
||||
};
|
||||
|
||||
const api = new WebDavApi(apiOptions);
|
||||
const driver = new FileApiDriverWebDav(api);
|
||||
const fileApi = new FileApi('', driver);
|
||||
fileApi.setSyncTargetId(this.id());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
static async checkConfig(options) {
|
||||
const fileApi = await SyncTargetWebDAV.initFileApi_(options);
|
||||
|
||||
const output = {
|
||||
ok: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await fileApi.stat('');
|
||||
if (!result) throw new Error('Could not access WebDAV directory');
|
||||
output.ok = true;
|
||||
} catch (error) {
|
||||
output.errorMessage = error.message;
|
||||
if (error.code) output.errorMessage += ' (Code ' + error.code + ')';
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const fileApi = await SyncTargetWebDAV.initFileApi_({
|
||||
path: Setting.value('sync.6.path'),
|
||||
username: Setting.value('sync.6.username'),
|
||||
password: Setting.value('sync.6.password'),
|
||||
});
|
||||
|
||||
fileApi.setLogger(this.logger());
|
||||
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SyncTargetWebDAV;
|
@@ -42,18 +42,34 @@ class WebDavApi {
|
||||
}
|
||||
|
||||
async xmlToJson(xml) {
|
||||
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
|
||||
|
||||
const nameProcessor = (name) => {
|
||||
// const idx = name.indexOf(':');
|
||||
// if (idx >= 0) {
|
||||
// if (name.indexOf('xmlns:') !== 0) name = name.substr(idx + 1);
|
||||
// }
|
||||
if (name.indexOf('xmlns:') !== 0) {
|
||||
// Check if the current name is within the DAV namespace. If it is, normalise it
|
||||
// by moving it to the "d:" namespace, which is what all the functions are using.
|
||||
const p = name.split(':');
|
||||
if (p.length == 2) {
|
||||
const ns = p[0];
|
||||
if (davNamespaces.indexOf(ns) >= 0) {
|
||||
name = 'd:' + p[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
const attrValueProcessor = (value, name) => {
|
||||
if (value.toLowerCase() === 'dav:') {
|
||||
const p = name.split(':');
|
||||
davNamespaces.push(p[p.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
tagNameProcessors: [nameProcessor],
|
||||
attrNameProcessors: [nameProcessor],
|
||||
attrValueProcessors: [attrValueProcessor]
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -81,6 +97,14 @@ class WebDavApi {
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
// If the XML has not attribute the value is directly a string
|
||||
// If the XML node has attributes, the value is under "_".
|
||||
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
|
||||
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
|
||||
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
|
||||
if (typeof output === 'object' && '_' in output) output = output['_'];
|
||||
if (typeof output !== 'string') return null;
|
||||
return output;
|
||||
}
|
||||
@@ -109,6 +133,42 @@ class WebDavApi {
|
||||
return this.valueFromJson(json, keys, 'array');
|
||||
}
|
||||
|
||||
resourcePropByName(resource, outputType, propName) {
|
||||
const propStats = resource['d:propstat'];
|
||||
let output = null;
|
||||
if (!Array.isArray(propStats)) throw new Error('Missing d:propstat property');
|
||||
for (let i = 0; i < propStats.length; i++) {
|
||||
const props = propStats[i]['d:prop'];
|
||||
if (!Array.isArray(props) || !props.length) continue;
|
||||
const prop = props[0];
|
||||
if (Array.isArray(prop[propName])) {
|
||||
output = prop[propName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (outputType === 'string') {
|
||||
// If the XML has not attribute the value is directly a string
|
||||
// If the XML node has attributes, the value is under "_".
|
||||
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
|
||||
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
|
||||
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
|
||||
output = output[0];
|
||||
|
||||
if (typeof output === 'object' && '_' in output) output = output['_'];
|
||||
if (typeof output !== 'string') return null;
|
||||
return output;
|
||||
}
|
||||
|
||||
if (outputType === 'array') {
|
||||
return output;
|
||||
}
|
||||
|
||||
throw new Error('Invalid output type: ' + outputType);
|
||||
}
|
||||
|
||||
async execPropFind(path, depth, fields = null, options = null) {
|
||||
if (fields === null) fields = ['d:getlastmodified'];
|
||||
|
||||
@@ -134,6 +194,22 @@ class WebDavApi {
|
||||
return this.exec('PROPFIND', path, body, { 'Depth': depth }, options);
|
||||
}
|
||||
|
||||
requestToCurl_(url, options) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
if (options.method) output.push('-X ' + options.method);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push('-H ' + '"' + n + ': ' + options.headers[n] + '"');
|
||||
}
|
||||
}
|
||||
if (options.body) output.push('--data ' + "'" + options.body + "'");
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
|
||||
// <d:propfind xmlns:d="DAV:">
|
||||
// <d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
@@ -151,7 +227,10 @@ class WebDavApi {
|
||||
|
||||
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
|
||||
|
||||
if (typeof body === 'string') headers['Content-length'] = body.length;
|
||||
// /!\ Doesn't work with UTF-8 strings as it results in truncated content. Content-Length
|
||||
// /!\ should not be needed anyway, but was required by one service. If re-implementing this
|
||||
// /!\ test with various content, including binary blobs.
|
||||
// if (typeof body === 'string') headers['Content-Length'] = body.length;
|
||||
|
||||
const fetchOptions = {};
|
||||
fetchOptions.headers = headers;
|
||||
@@ -163,7 +242,8 @@ class WebDavApi {
|
||||
|
||||
let response = null;
|
||||
|
||||
// console.info('WebDAV', method + ' ' + path, headers, options);
|
||||
// console.info('WebDAV Call', method + ' ' + url, headers, options);
|
||||
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
@@ -175,6 +255,8 @@ class WebDavApi {
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
// console.info('WebDAV Response', responseText);
|
||||
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = () => {
|
||||
@@ -202,7 +284,7 @@ class WebDavApi {
|
||||
throw new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + ')', response.status);
|
||||
}
|
||||
|
||||
throw new JoplinError(shortResponseText(), response.status);
|
||||
throw new JoplinError(method + ' ' + path + ': ' + shortResponseText(), response.status);
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
@@ -2,7 +2,7 @@ const React = require('react'); const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { NotesScreen } = require('lib/components/screens/notes.js');
|
||||
const { SearchScreen } = require('lib/components/screens/search.js');
|
||||
const { View } = require('react-native');
|
||||
const { KeyboardAvoidingView, Keyboard, Platform, View } = require('react-native');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
@@ -11,6 +11,31 @@ class AppNavComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.previousRouteName_ = null;
|
||||
this.state = {
|
||||
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidShowListener) this.keyboardDidShowListener.remove();
|
||||
if (this.keyboardDidHideListener) this.keyboardDidHideListener.remove();
|
||||
this.keyboardDidShowListener = null;
|
||||
this.keyboardDidHideListener = null;
|
||||
}
|
||||
|
||||
keyboardDidShow () {
|
||||
this.setState({ autoCompletionBarExtraHeight: 30 })
|
||||
}
|
||||
|
||||
keyboardDidHide () {
|
||||
this.setState({ autoCompletionBarExtraHeight:0 })
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -44,11 +69,12 @@ class AppNavComponent extends Component {
|
||||
const style = { flex: 1, backgroundColor: theme.backgroundColor }
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<KeyboardAvoidingView behavior={ Platform.OS === 'ios' ? "padding" : null } style={style}>
|
||||
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
|
||||
{ searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} /> }
|
||||
{ (!notesScreenVisible && !searchScreenVisible) && <Screen navigation={{ state: route }} /> }
|
||||
</View>
|
||||
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -51,7 +51,19 @@ class NoteBodyViewer extends Component {
|
||||
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
|
||||
};
|
||||
|
||||
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
|
||||
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
|
||||
|
||||
html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
` + html + `
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
let webViewStyle = {}
|
||||
// On iOS, the onLoadEnd() event is never fired so always
|
||||
|
@@ -372,7 +372,7 @@ class ScreenHeaderComponent extends Component {
|
||||
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
|
||||
for (let i = 0; i < this.props.folders.length; i++) {
|
||||
let f = this.props.folders[i];
|
||||
output.push({ label: f.title, value: f.id });
|
||||
output.push({ label: Folder.displayTitle(f), value: f.id });
|
||||
}
|
||||
output.sort((a, b) => {
|
||||
if (a.value === null) return -1;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
|
||||
const { Platform, TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
@@ -7,6 +7,8 @@ const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
|
||||
@@ -18,17 +20,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
|
||||
this.state = {
|
||||
settings: {},
|
||||
settingsChanged: false,
|
||||
};
|
||||
shared.init(this);
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
this.saveButton_press = () => {
|
||||
for (let n in this.state.settings) {
|
||||
if (!this.state.settings.hasOwnProperty(n)) continue;
|
||||
Setting.setValue(n, this.state.settings[n]);
|
||||
}
|
||||
this.setState({settingsChanged:false});
|
||||
return shared.saveSettings(this);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,12 +65,22 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
},
|
||||
descriptionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
},
|
||||
settingControl: {
|
||||
color: theme.color,
|
||||
flex: 1,
|
||||
},
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
styles.settingControl.borderBottomWidth = 1;
|
||||
styles.settingControl.borderBottomColor = theme.dividerColor;
|
||||
}
|
||||
|
||||
styles.switchSettingText = Object.assign({}, styles.settingText);
|
||||
styles.switchSettingText.width = '80%';
|
||||
|
||||
@@ -100,14 +109,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
let output = null;
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = value;
|
||||
this.setState({
|
||||
settings: settings,
|
||||
settingsChanged: true,
|
||||
});
|
||||
|
||||
console.info(settings['sync.5.path']);
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
}
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
@@ -163,7 +165,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
|
||||
<TextInput key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
||||
<TextInput autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
@@ -176,19 +178,27 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
render() {
|
||||
const settings = this.state.settings;
|
||||
|
||||
const keys = Setting.keys(true, 'mobile');
|
||||
let settingComps = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
//if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
|
||||
if (!Setting.isPublic(key)) continue;
|
||||
const settingComps = shared.settingsToComponents(this, 'mobile', settings);
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
|
||||
const comp = this.settingToComponent(key, settings[key]);
|
||||
if (!comp) continue;
|
||||
settingComps.push(comp);
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusComp = !messages.length ? null : (
|
||||
<View style={{flex:1, marginTop: 10}}>
|
||||
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
|
||||
{messages.length >= 1 ? (<View style={{marginTop:10}}><Text style={this.styles().descriptionText}>{messages[1]}</Text></View>) : null}
|
||||
</View>);
|
||||
|
||||
settingComps.push(
|
||||
<View key="check_sync_config_button" style={this.styles().settingContainer}>
|
||||
<View style={{flex:1, flexDirection: 'column'}}>
|
||||
<View style={{flex:1}}>
|
||||
<Button title={_('Check synchronisation configuration')} onPress={this.checkSyncConfig_}/>
|
||||
</View>
|
||||
{ statusComp }
|
||||
</View>
|
||||
</View>);
|
||||
}
|
||||
|
||||
settingComps.push(
|
||||
@@ -212,7 +222,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
<ScreenHeader
|
||||
title={_('Configuration')}
|
||||
showSaveButton={true}
|
||||
saveButtonDisabled={!this.state.settingsChanged}
|
||||
saveButtonDisabled={!this.state.changedSettingKeys.length}
|
||||
onSaveButtonPress={this.saveButton_press}
|
||||
/>
|
||||
<ScrollView >
|
||||
|
@@ -159,9 +159,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
|
||||
|
||||
const mkComps = [];
|
||||
|
||||
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(i+1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
@@ -183,6 +189,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
};
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('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 they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{marginTop: 10}}>{rows}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
|
||||
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
|
||||
|
||||
@@ -191,12 +215,12 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
<ScreenHeader title={_('Encryption Config')}/>
|
||||
<ScrollView style={this.styles().container}>
|
||||
|
||||
<View style={{backgroundColor: theme.warningBackgroundColor, padding: 5}}>
|
||||
{/*<View style={{backgroundColor: theme.warningBackgroundColor, padding: 5}}>
|
||||
<Text>Important: This is a *beta* feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain.</Text>
|
||||
<Text>If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup your notes from the desktop or terminal application.</Text>
|
||||
<Text>For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation:</Text>
|
||||
<TouchableOpacity onPress={() => { Linking.openURL('http://joplin.cozic.net/help/e2ee.html') }}><Text>http://joplin.cozic.net/help/e2ee.html</Text></TouchableOpacity>
|
||||
</View>
|
||||
</View>*/}
|
||||
|
||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
@@ -204,6 +228,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{flex:1, height: 20}}></View>
|
||||
</ScrollView>
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
@@ -221,6 +246,7 @@ const EncryptionConfigScreen = connect(
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: state.settings['encryption.enabled'],
|
||||
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
};
|
||||
}
|
||||
)(EncryptionConfigScreenComponent)
|
||||
|
@@ -23,6 +23,7 @@ class LogScreenComponent extends BaseScreenComponent {
|
||||
});
|
||||
this.state = {
|
||||
dataSource: ds,
|
||||
showErrorsOnly: false,
|
||||
};
|
||||
this.styles_ = {};
|
||||
}
|
||||
@@ -62,13 +63,24 @@ class LogScreenComponent extends BaseScreenComponent {
|
||||
this.resfreshLogEntries();
|
||||
}
|
||||
|
||||
resfreshLogEntries() {
|
||||
reg.logger().lastEntries(1000).then((entries) => {
|
||||
resfreshLogEntries(showErrorsOnly = null) {
|
||||
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
|
||||
|
||||
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
|
||||
|
||||
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
|
||||
const newDataSource = this.state.dataSource.cloneWithRows(entries);
|
||||
this.setState({ dataSource: newDataSource });
|
||||
});
|
||||
}
|
||||
|
||||
toggleErrorsOnly() {
|
||||
const showErrorsOnly = !this.state.showErrorsOnly;
|
||||
this.setState({ showErrorsOnly: showErrorsOnly });
|
||||
this.resfreshLogEntries(showErrorsOnly);
|
||||
}
|
||||
|
||||
render() {
|
||||
let renderRow = (item) => {
|
||||
let textStyle = this.styles().rowText;
|
||||
@@ -91,7 +103,14 @@ class LogScreenComponent extends BaseScreenComponent {
|
||||
renderRow={renderRow}
|
||||
enableEmptySections={true}
|
||||
/>
|
||||
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{flex:1, marginRight: 5 }}>
|
||||
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
|
||||
</View>
|
||||
<View style={{flex:1}}>
|
||||
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, KeyboardAvoidingView } = require('react-native');
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { Log } = require('lib/log.js');
|
||||
@@ -149,12 +149,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
await shared.initState(this);
|
||||
|
||||
this.refreshNoteMetadata();
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshNoteMetadata(force = null) {
|
||||
@@ -163,19 +157,6 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.backHandler);
|
||||
|
||||
if (Platform.OS === 'ios'){
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardDidShow () {
|
||||
this.setState({ heightBumpView:30 })
|
||||
}
|
||||
|
||||
_keyboardDidHide () {
|
||||
this.setState({ heightBumpView:0 })
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
@@ -542,7 +523,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior= {(Platform.OS === 'ios')? "padding" : null} style={this.rootStyle(this.props.theme).root}>
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader
|
||||
folderPickerOptions={{
|
||||
enabled: true,
|
||||
@@ -578,8 +559,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
/>
|
||||
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
<View style={{ height: this.state.heightBumpView }} />
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
81
ReactNativeClient/lib/components/shared/config-shared.js
Normal file
81
ReactNativeClient/lib/components/shared/config-shared.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
const shared = {}
|
||||
|
||||
shared.init = function(comp) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
}
|
||||
|
||||
shared.checkSyncConfig = async function(comp, settings) {
|
||||
const syncTargetId = settings['sync.target'];
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
|
||||
const options = Setting.subValues('sync.' + syncTargetId, settings);
|
||||
comp.setState({ checkSyncConfigResult: 'checking' });
|
||||
const result = await SyncTargetClass.checkConfig(options);
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
}
|
||||
|
||||
shared.checkSyncConfigMessages = function(comp) {
|
||||
const result = comp.state.checkSyncConfigResult;
|
||||
const output = [];
|
||||
|
||||
if (result === 'checking') {
|
||||
output.push(_('Checking... Please wait.'));
|
||||
} else if (result && result.ok) {
|
||||
output.push(_('Success! Synchronisation configuration appears to be correct.'));
|
||||
} else if (result && !result.ok) {
|
||||
output.push(_('Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:'));
|
||||
output.push(result.errorMessage);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
shared.updateSettingValue = function(comp, key, value) {
|
||||
const settings = Object.assign({}, comp.state.settings);
|
||||
const changedSettingKeys = comp.state.changedSettingKeys.slice();
|
||||
settings[key] = Setting.formatValue(key, value);
|
||||
if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key);
|
||||
|
||||
comp.setState({
|
||||
settings: settings,
|
||||
changedSettingKeys: changedSettingKeys,
|
||||
});
|
||||
}
|
||||
|
||||
shared.saveSettings = function(comp) {
|
||||
for (let key in comp.state.settings) {
|
||||
if (!comp.state.settings.hasOwnProperty(key)) continue;
|
||||
if (comp.state.changedSettingKeys.indexOf(key) < 0) continue;
|
||||
console.info("Saving", key, comp.state.settings[key]);
|
||||
Setting.setValue(key, comp.state.settings[key]);
|
||||
}
|
||||
|
||||
comp.setState({ changedSettingKeys: [] });
|
||||
}
|
||||
|
||||
shared.settingsToComponents = function(comp, device, settings) {
|
||||
const keys = Setting.keys(true, device);
|
||||
const settingComps = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!Setting.isPublic(key)) continue;
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
|
||||
const settingComp = comp.settingToComponent(key, settings[key]);
|
||||
if (!settingComp) continue;
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
|
||||
return settingComps
|
||||
}
|
||||
|
||||
module.exports = shared;
|
@@ -30,7 +30,6 @@ shared.saveNoteButton_press = async function(comp) {
|
||||
options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note);
|
||||
}
|
||||
|
||||
|
||||
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title);
|
||||
if (hasAutoTitle) {
|
||||
note.title = Note.defaultTitle(note);
|
||||
|
@@ -42,7 +42,7 @@ class FileApiDriverLocal {
|
||||
metadataFromStat_(stat) {
|
||||
return {
|
||||
path: stat.path,
|
||||
created_time: stat.birthtime.getTime(),
|
||||
// created_time: stat.birthtime.getTime(),
|
||||
updated_time: stat.mtime.getTime(),
|
||||
isDir: stat.isDirectory(),
|
||||
};
|
||||
|
@@ -39,7 +39,7 @@ class FileApiDriverMemory {
|
||||
path: path,
|
||||
isDir: isDir,
|
||||
updated_time: now, // In milliseconds!!
|
||||
created_time: now, // In milliseconds!!
|
||||
// created_time: now, // In milliseconds!!
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ class FileApiDriverOneDrive {
|
||||
if ('deleted' in odItem) {
|
||||
output.isDeleted = true;
|
||||
} else {
|
||||
output.created_time = moment(odItem.fileSystemInfo.createdDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
||||
// output.created_time = moment(odItem.fileSystemInfo.createdDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
||||
output.updated_time = moment(odItem.fileSystemInfo.lastModifiedDateTime, 'YYYY-MM-DDTHH:mm:ss.SSSZ').format('x');
|
||||
}
|
||||
|
||||
|
@@ -6,6 +6,7 @@ const Entities = require('html-entities').AllHtmlEntities;
|
||||
const html_entity_decode = (new Entities()).decode;
|
||||
const { shim } = require('lib/shim');
|
||||
const { basename } = require('lib/path-utils');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
|
||||
class FileApiDriverWebDav {
|
||||
|
||||
@@ -17,12 +18,16 @@ class FileApiDriverWebDav {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
requestRepeatCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const result = await this.api().execPropFind(path, 0, [
|
||||
'd:getlastmodified',
|
||||
'd:resourcetype',
|
||||
'd:getcontentlength', // Remove this once PUT call issue is sorted out
|
||||
// 'd:getcontentlength', // Remove this once PUT call issue is sorted out
|
||||
]);
|
||||
|
||||
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
|
||||
@@ -34,23 +39,41 @@ class FileApiDriverWebDav {
|
||||
}
|
||||
|
||||
statFromResource_(resource, path) {
|
||||
const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]);
|
||||
const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
|
||||
// WebDAV implementations are always slighly different from one server to another but, at the minimum,
|
||||
// a resource should have a propstat key - if not it's probably an error.
|
||||
const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
|
||||
if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource));
|
||||
|
||||
const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
|
||||
if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
|
||||
const resourceTypes = this.api().resourcePropByName(resource, 'array', 'd:resourcetype');
|
||||
let isDir = false;
|
||||
if (Array.isArray(resourceTypes)) {
|
||||
for (let i = 0; i < resourceTypes.length; i++) {
|
||||
const t = resourceTypes[i];
|
||||
if (typeof t === 'object' && 'd:collection' in t) {
|
||||
isDir = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
|
||||
const lastModifiedString = this.api().resourcePropByName(resource, 'string', 'd:getlastmodified');
|
||||
|
||||
const lastModifiedDate = new Date(lastModifiedString);
|
||||
// const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
|
||||
// if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
|
||||
|
||||
|
||||
// Note: Not all WebDAV servers return a getlastmodified date (eg. Seafile, which doesn't return the
|
||||
// property for folders) so we can only throw an error if it's a file.
|
||||
if (!lastModifiedString && !isDir) throw new Error('Could not get lastModified date for resource: ' + JSON.stringify(resource));
|
||||
const lastModifiedDate = lastModifiedString ? new Date(lastModifiedString) : new Date();
|
||||
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
|
||||
|
||||
return {
|
||||
path: path,
|
||||
created_time: lastModifiedDate.getTime(),
|
||||
// created_time: lastModifiedDate.getTime(),
|
||||
updated_time: lastModifiedDate.getTime(),
|
||||
isDir: isCollection === '',
|
||||
sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
|
||||
isDir: isDir,
|
||||
// sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,8 +90,41 @@ class FileApiDriverWebDav {
|
||||
return await basicDelta(path, getDirStats, options);
|
||||
}
|
||||
|
||||
async list(path, options) {
|
||||
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
|
||||
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
|
||||
// works with paths relative to the base URL.
|
||||
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
|
||||
let output = '';
|
||||
if (href.indexOf(baseUrl) === 0) {
|
||||
output = href.substr(baseUrl.length);
|
||||
} else if (href.indexOf(relativeBaseUrl) === 0) {
|
||||
output = href.substr(relativeBaseUrl.length);
|
||||
} else {
|
||||
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
|
||||
}
|
||||
|
||||
return rtrimSlashes(ltrimSlashes(output));
|
||||
}
|
||||
|
||||
statsFromResources_(resources) {
|
||||
const relativeBaseUrl = this.api().relativeBaseUrl();
|
||||
const baseUrl = this.api().baseUrl();
|
||||
let output = [];
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
const resource = resources[i];
|
||||
const href = this.api().stringFromJson(resource, ['d:href', 0]);
|
||||
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
|
||||
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
|
||||
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
|
||||
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
|
||||
const stat = this.statFromResource_(resources[i], path);
|
||||
output.push(stat);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async list(path, options) {
|
||||
// const relativeBaseUrl = this.api().relativeBaseUrl();
|
||||
|
||||
// function parsePropFindXml(xmlString) {
|
||||
// return new Promise(async (resolve, reject) => {
|
||||
@@ -176,39 +232,53 @@ class FileApiDriverWebDav {
|
||||
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
|
||||
// and it means the mobile app does not freeze during sync.
|
||||
|
||||
async function parsePropFindXml2(xmlString) {
|
||||
const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
|
||||
// async function parsePropFindXml2(xmlString) {
|
||||
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
|
||||
|
||||
let output = [];
|
||||
let match = null;
|
||||
// let output = [];
|
||||
// let match = null;
|
||||
|
||||
while (match = regex.exec(xmlString)) {
|
||||
const href = html_entity_decode(match[1]);
|
||||
if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
|
||||
const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
|
||||
// while (match = regex.exec(xmlString)) {
|
||||
// const href = html_entity_decode(match[1]);
|
||||
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
|
||||
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
|
||||
|
||||
if (!path) continue; // The list of resources includes the root dir too, which we don't want
|
||||
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
|
||||
|
||||
const lastModifiedDate = new Date(match[2]);
|
||||
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
|
||||
// const lastModifiedDate = new Date(match[2]);
|
||||
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
|
||||
|
||||
output.push({
|
||||
path: path,
|
||||
updated_time: lastModifiedDate.getTime(),
|
||||
created_time: lastModifiedDate.getTime(),
|
||||
isDir: !BaseItem.isSystemPath(path),
|
||||
});
|
||||
}
|
||||
// output.push({
|
||||
// path: path,
|
||||
// updated_time: lastModifiedDate.getTime(),
|
||||
// created_time: lastModifiedDate.getTime(),
|
||||
// isDir: !BaseItem.isSystemPath(path),
|
||||
// });
|
||||
// }
|
||||
|
||||
return output;
|
||||
}
|
||||
// return output;
|
||||
// }
|
||||
|
||||
const resultXml = await this.api().execPropFind(path, 1, [
|
||||
// const resultXml = await this.api().execPropFind(path, 1, [
|
||||
// 'd:getlastmodified',
|
||||
// //'d:resourcetype', // Include this to use parsePropFindXml()
|
||||
// ], { responseFormat: 'text' });
|
||||
|
||||
// const stats = await parsePropFindXml2(resultXml);
|
||||
|
||||
// return {
|
||||
// items: stats,
|
||||
// hasMore: false,
|
||||
// context: null,
|
||||
// };
|
||||
|
||||
const result = await this.api().execPropFind(path, 1, [
|
||||
'd:getlastmodified',
|
||||
//'d:resourcetype', // Include this to use parsePropFindXml()
|
||||
], { responseFormat: 'text' });
|
||||
'd:resourcetype',
|
||||
]);
|
||||
|
||||
const stats = await parsePropFindXml2(resultXml);
|
||||
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
|
||||
const stats = this.statsFromResources_(resources);
|
||||
|
||||
return {
|
||||
items: stats,
|
||||
@@ -221,7 +291,13 @@ class FileApiDriverWebDav {
|
||||
if (!options) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = 'text';
|
||||
try {
|
||||
return await this.api().exec('GET', path, null, null, options);
|
||||
const response = await this.api().exec('GET', path, null, null, options);
|
||||
|
||||
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
|
||||
// with a response body "The specified file doesn't exist." for non-existing files,
|
||||
// so we need to check for this.
|
||||
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error.code !== 404) throw error;
|
||||
}
|
||||
@@ -231,37 +307,22 @@ class FileApiDriverWebDav {
|
||||
try {
|
||||
await this.api().exec('MKCOL', path);
|
||||
} catch (error) {
|
||||
if (error.code !== 405) throw error; // 405 means that the collection already exists (Method Not Allowed)
|
||||
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
|
||||
|
||||
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
|
||||
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
|
||||
// we get this code, verify that indeed the directory already exists and exit if it does.
|
||||
if (error.code === 409) {
|
||||
const stat = await this.stat(path);
|
||||
if (stat) return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
// In theory, if a client doesn't complete an upload, the file will not appear in the Nextcloud app. Likewise if
|
||||
// the server interrupts the upload midway, the client should receive some kind of error and try uploading the
|
||||
// file again next time. At the very least the file should not appear half-uploaded on the server. In practice
|
||||
// however it seems some files might end up half uploaded on the server (at least on ocloud.de) so, for now,
|
||||
// instead of doing a simple PUT, we do it to a temp file on Nextcloud, then check the file size and, if it
|
||||
// matches, move it its actual place (hoping the server won't mess up and only copy half of the file).
|
||||
// This is innefficient so once the bug is better understood it should hopefully be possible to go back to
|
||||
// using a single PUT call.
|
||||
|
||||
let contentSize = 0;
|
||||
if (content) contentSize = content.length;
|
||||
if (options && options.path) {
|
||||
const stat = await shim.fsDriver().stat(options.path);
|
||||
contentSize = stat.size;
|
||||
}
|
||||
|
||||
const tempPath = this.fileApi_.tempDirName() + '/' + basename(path) + '_' + Date.now();
|
||||
await this.api().exec('PUT', tempPath, content, null, options);
|
||||
|
||||
const stat = await this.stat(tempPath);
|
||||
if (stat.sizeDONOTUSE != contentSize) {
|
||||
// await this.delete(tempPath);
|
||||
throw new Error('WebDAV PUT - Size check failed for ' + tempPath + ' Expected: ' + contentSize + '. Found: ' + stat.sizeDONOTUSE);
|
||||
}
|
||||
|
||||
await this.move(tempPath, path);
|
||||
return await this.api().exec('PUT', path, content, null, options);
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
@@ -275,6 +336,7 @@ class FileApiDriverWebDav {
|
||||
async move(oldPath, newPath) {
|
||||
await this.api().exec('MOVE', oldPath, null, {
|
||||
'Destination': this.api().baseUrl() + '/' + newPath,
|
||||
'Overwrite': 'T',
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,31 @@ const { shim } = require('lib/shim');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const ArrayUtils = require('lib/ArrayUtils');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
function requestCanBeRepeated(error) {
|
||||
const errorCode = typeof error === 'object' && error.code ? error.code : null;
|
||||
|
||||
if (errorCode === 'rejectedByTarget') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryAndRepeat(fn, count) {
|
||||
let retryCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (retryCount >= count) throw error;
|
||||
if (!requestCanBeRepeated(error)) throw error;
|
||||
retryCount++;
|
||||
await time.sleep(1 + retryCount * 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FileApi {
|
||||
|
||||
@@ -16,6 +41,14 @@ class FileApi {
|
||||
this.driver_.fileApi_ = this;
|
||||
}
|
||||
|
||||
// Ideally all requests repeating should be done at the FileApi level to remove duplicate code in the drivers, but
|
||||
// historically some drivers (eg. OneDrive) are already handling request repeating, so this is optional, per driver,
|
||||
// and it defaults to no repeating.
|
||||
requestRepeatCount() {
|
||||
if (this.driver_.requestRepeatCount) return this.driver_.requestRepeatCount();
|
||||
return 0;
|
||||
}
|
||||
|
||||
tempDirName() {
|
||||
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
|
||||
return this.tempDirName_;
|
||||
@@ -43,6 +76,7 @@ class FileApi {
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
if (!l) l = new Logger();
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
@@ -58,50 +92,70 @@ class FileApi {
|
||||
}
|
||||
|
||||
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
|
||||
list(path = '', options = null) {
|
||||
async list(path = '', options = null) {
|
||||
if (!options) options = {};
|
||||
if (!('includeHidden' in options)) options.includeHidden = false;
|
||||
if (!('context' in options)) options.context = null;
|
||||
|
||||
this.logger().debug('list ' + this.baseDir_);
|
||||
|
||||
return this.driver_.list(this.baseDir_, options).then((result) => {
|
||||
if (!options.includeHidden) {
|
||||
let temp = [];
|
||||
for (let i = 0; i < result.items.length; i++) {
|
||||
if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
|
||||
}
|
||||
result.items = temp;
|
||||
const result = await tryAndRepeat(() => this.driver_.list(this.baseDir_, options), this.requestRepeatCount());
|
||||
|
||||
if (!options.includeHidden) {
|
||||
let temp = [];
|
||||
for (let i = 0; i < result.items.length; i++) {
|
||||
if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
result.items = temp;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
// return this.driver_.list(this.baseDir_, options).then((result) => {
|
||||
// if (!options.includeHidden) {
|
||||
// let temp = [];
|
||||
// for (let i = 0; i < result.items.length; i++) {
|
||||
// if (!isHidden(result.items[i].path)) temp.push(result.items[i]);
|
||||
// }
|
||||
// result.items = temp;
|
||||
// }
|
||||
// return result;
|
||||
// });
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
setTimestamp(path, timestampMs) {
|
||||
this.logger().debug('setTimestamp ' + this.fullPath_(path));
|
||||
return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
|
||||
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath_(path), timestampMs), this.requestRepeatCount());
|
||||
//return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
|
||||
}
|
||||
|
||||
mkdir(path) {
|
||||
this.logger().debug('mkdir ' + this.fullPath_(path));
|
||||
return this.driver_.mkdir(this.fullPath_(path));
|
||||
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath_(path)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
stat(path) {
|
||||
async stat(path) {
|
||||
this.logger().debug('stat ' + this.fullPath_(path));
|
||||
return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
||||
if (!output) return output;
|
||||
output.path = path;
|
||||
return output;
|
||||
});
|
||||
|
||||
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath_(path)), this.requestRepeatCount());
|
||||
|
||||
if (!output) return output;
|
||||
output.path = path;
|
||||
return output;
|
||||
|
||||
// return this.driver_.stat(this.fullPath_(path)).then((output) => {
|
||||
// if (!output) return output;
|
||||
// output.path = path;
|
||||
// return output;
|
||||
// });
|
||||
}
|
||||
|
||||
get(path, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.encoding) options.encoding = 'utf8';
|
||||
this.logger().debug('get ' + this.fullPath_(path));
|
||||
return this.driver_.get(this.fullPath_(path), options);
|
||||
return tryAndRepeat(() => this.driver_.get(this.fullPath_(path), options), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
@@ -111,32 +165,32 @@ class FileApi {
|
||||
if (!await this.fsDriver().exists(options.path)) throw new JoplinError('File not found: ' + options.path, 'fileNotFound');
|
||||
}
|
||||
|
||||
return this.driver_.put(this.fullPath_(path), content, options);
|
||||
return tryAndRepeat(() => this.driver_.put(this.fullPath_(path), content, options), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
this.logger().debug('delete ' + this.fullPath_(path));
|
||||
return this.driver_.delete(this.fullPath_(path));
|
||||
return tryAndRepeat(() => this.driver_.delete(this.fullPath_(path)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
move(oldPath, newPath) {
|
||||
this.logger().debug('move ' + this.fullPath_(oldPath) + ' => ' + this.fullPath_(newPath));
|
||||
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
|
||||
return tryAndRepeat(() => this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
// Deprectated
|
||||
format() {
|
||||
return this.driver_.format();
|
||||
return tryAndRepeat(() => this.driver_.format(), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
clearRoot() {
|
||||
return this.driver_.clearRoot(this.baseDir_);
|
||||
return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir_), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
delta(path, options = null) {
|
||||
this.logger().debug('delta ' + this.fullPath_(path));
|
||||
return this.driver_.delta(this.fullPath_(path), options);
|
||||
return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -256,7 +256,7 @@ function countryDisplayName(canonicalName) {
|
||||
|
||||
let extraString;
|
||||
|
||||
if (countryCode != "") {
|
||||
if (countryCode) {
|
||||
if (languageCode == "zh" && countryCode == "CN") {
|
||||
extraString = "简体"; // "Simplified" in "Simplified Chinese"
|
||||
} else {
|
||||
@@ -266,7 +266,7 @@ function countryDisplayName(canonicalName) {
|
||||
|
||||
if (languageCode == "zh" && (countryCode == "" || countryCode == "TW")) extraString = "繁體"; // "Traditional" in "Traditional Chinese"
|
||||
|
||||
if (extraString != "") output += " (" + extraString + ")";
|
||||
if (extraString) output += " (" + extraString + ")";
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -294,7 +294,11 @@ function _(s, ...args) {
|
||||
let strings = localeStrings(currentLocale_);
|
||||
let result = strings[s];
|
||||
if (result === '' || result === undefined) result = s;
|
||||
return sprintf(result, ...args);
|
||||
try {
|
||||
return sprintf(result, ...args);
|
||||
} catch (error) {
|
||||
return result + ' ' + args.join(', ') + ' (Translation error: ' + error.message + ')';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { _, supportedLocales, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode };
|
||||
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
|
@@ -85,11 +85,15 @@ class Logger {
|
||||
}
|
||||
|
||||
// Only for database at the moment
|
||||
async lastEntries(limit = 100) {
|
||||
async lastEntries(limit = 100, options = null) {
|
||||
if (options === null) options = {};
|
||||
if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
if (!options.levels.length) return [];
|
||||
|
||||
for (let i = 0; i < this.targets_.length; i++) {
|
||||
const target = this.targets_[i];
|
||||
if (target.type == 'database') {
|
||||
let sql = 'SELECT * FROM logs ORDER BY timestamp DESC';
|
||||
let sql = 'SELECT * FROM logs WHERE level IN (' + options.levels.join(',') + ') ORDER BY timestamp DESC';
|
||||
if (limit !== null) sql += ' LIMIT ' + limit
|
||||
return await target.database.selectAll(sql);
|
||||
}
|
||||
|
@@ -381,26 +381,26 @@ class Note extends BaseItem {
|
||||
return this.save(newNote);
|
||||
}
|
||||
|
||||
static save(o, options = null) {
|
||||
static async save(o, options = null) {
|
||||
let isNew = this.isNew(o, options);
|
||||
if (isNew && !o.source) o.source = Setting.value('appName');
|
||||
if (isNew && !o.source_application) o.source_application = Setting.value('appId');
|
||||
|
||||
return super.save(o, options).then((note) => {
|
||||
this.dispatch({
|
||||
type: 'NOTE_UPDATE_ONE',
|
||||
note: note,
|
||||
});
|
||||
const note = await super.save(o, options);
|
||||
|
||||
if ('todo_due' in o || 'todo_completed' in o || 'is_todo' in o || 'is_conflict' in o) {
|
||||
this.dispatch({
|
||||
type: 'EVENT_NOTE_ALARM_FIELD_CHANGE',
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
return note;
|
||||
this.dispatch({
|
||||
type: 'NOTE_UPDATE_ONE',
|
||||
note: note,
|
||||
});
|
||||
|
||||
if ('todo_due' in o || 'todo_completed' in o || 'is_todo' in o || 'is_conflict' in o) {
|
||||
this.dispatch({
|
||||
type: 'EVENT_NOTE_ALARM_FIELD_CHANGE',
|
||||
id: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
static async delete(id, options = null) {
|
||||
|
@@ -58,7 +58,7 @@ class Setting extends BaseModel {
|
||||
// recent: _('Non-completed and recently completed ones'),
|
||||
// nonCompleted: _('Non-completed ones only'),
|
||||
// })},
|
||||
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') },
|
||||
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted to-dos on top of the lists') },
|
||||
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') },
|
||||
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
|
||||
return {
|
||||
@@ -72,6 +72,7 @@ class Setting extends BaseModel {
|
||||
'body': _('Focus body'),
|
||||
};
|
||||
}},
|
||||
'showTrayIcon': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon') },
|
||||
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
|
||||
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false },
|
||||
@@ -105,6 +106,11 @@ class Setting extends BaseModel {
|
||||
'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud WebDAV URL') },
|
||||
'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud username') },
|
||||
'sync.5.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud password'), secure: true },
|
||||
|
||||
'sync.6.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV URL') },
|
||||
'sync.6.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV username') },
|
||||
'sync.6.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV password'), secure: true },
|
||||
|
||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
@@ -386,26 +392,42 @@ class Setting extends BaseModel {
|
||||
return !!options[value];
|
||||
}
|
||||
|
||||
// Currently only supports objects with properties one level deep
|
||||
static object(key) {
|
||||
// For example, if settings is:
|
||||
// { sync.5.path: 'http://example', sync.5.username: 'testing' }
|
||||
// and baseKey is 'sync.5', the function will return
|
||||
// { path: 'http://example', username: 'testing' }
|
||||
static subValues(baseKey, settings) {
|
||||
let output = {};
|
||||
let keys = this.keys();
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let k = keys[i].split('.');
|
||||
if (k[0] == key) {
|
||||
output[k[1]] = this.value(keys[i]);
|
||||
for (let key in settings) {
|
||||
if (!settings.hasOwnProperty(key)) continue;
|
||||
if (key.indexOf(baseKey) === 0) {
|
||||
const subKey = key.substr(baseKey.length + 1);
|
||||
output[subKey] = settings[key];
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Currently only supports objects with properties one level deep
|
||||
static setObject(key, object) {
|
||||
for (let n in object) {
|
||||
if (!object.hasOwnProperty(n)) continue;
|
||||
this.setValue(key + '.' + n, object[n]);
|
||||
}
|
||||
}
|
||||
// static object(key) {
|
||||
// let output = {};
|
||||
// let keys = this.keys();
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// let k = keys[i].split('.');
|
||||
// if (k[0] == key) {
|
||||
// output[k[1]] = this.value(keys[i]);
|
||||
// }
|
||||
// }
|
||||
// return output;
|
||||
// }
|
||||
|
||||
// Currently only supports objects with properties one level deep
|
||||
// static setObject(key, object) {
|
||||
// for (let n in object) {
|
||||
// if (!object.hasOwnProperty(n)) continue;
|
||||
// this.setValue(key + '.' + n, object[n]);
|
||||
// }
|
||||
// }
|
||||
|
||||
static saveAll() {
|
||||
if (!this.saveTimeoutId_) return Promise.resolve();
|
||||
|
@@ -72,8 +72,15 @@ class DecryptionWorker {
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
|
||||
// Temp hack
|
||||
if (['edf44b7a0e4f8cbf248e206cd8dfa800', '2ccb3c9af0b1adac2ec6b66a5961fbb1'].indexOf(item.id) >= 0) {
|
||||
excludedIds.push(item.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ItemClass = BaseItem.itemClass(item);
|
||||
this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
|
||||
this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
|
||||
try {
|
||||
await ItemClass.decrypt(item);
|
||||
} catch (error) {
|
||||
|
@@ -45,12 +45,20 @@ function shimInit() {
|
||||
|
||||
let headers = options.headers ? options.headers : {};
|
||||
let method = options.method ? options.method : 'GET';
|
||||
const overwrite = 'overwrite' in options ? options.overwrite : true;
|
||||
|
||||
let dirs = RNFetchBlob.fs.dirs;
|
||||
let localFilePath = options.path;
|
||||
if (localFilePath.indexOf('/') !== 0) localFilePath = dirs.DocumentDir + '/' + localFilePath;
|
||||
|
||||
if (!overwrite) {
|
||||
if (await shim.fsDriver().exists(localFilePath)) {
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
||||
delete options.path;
|
||||
delete options.overwrite;
|
||||
|
||||
const doFetchBlob = () => {
|
||||
return RNFetchBlob.config({
|
||||
|
@@ -450,7 +450,7 @@ class Synchronizer {
|
||||
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
|
||||
|
||||
const loadContent = async () => {
|
||||
content = await this.api().get(path);
|
||||
let content = await this.api().get(path);
|
||||
if (!content) return null;
|
||||
return await BaseItem.unserialize(content);
|
||||
}
|
||||
|
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
1
ReactNativeClient/locales/eu.json
Normal file
1
ReactNativeClient/locales/eu.json
Normal file
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
@@ -3,6 +3,7 @@ locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['es_ES'] = require('./es_ES.json');
|
||||
locales['eu'] = require('./eu.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
locales['hr_HR'] = require('./hr_HR.json');
|
||||
locales['it_IT'] = require('./it_IT.json');
|
||||
|
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
36
ReactNativeClient/package-lock.json
generated
36
ReactNativeClient/package-lock.json
generated
@@ -228,6 +228,11 @@
|
||||
"lodash": "4.17.4"
|
||||
}
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
|
||||
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -3397,6 +3402,14 @@
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"katex": {
|
||||
"version": "0.9.0-beta1",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
|
||||
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
|
||||
"requires": {
|
||||
"match-at": "0.1.1"
|
||||
}
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
@@ -3658,6 +3671,29 @@
|
||||
"uc.micro": "1.0.3"
|
||||
}
|
||||
},
|
||||
"markdown-it-katex": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it-katex/-/markdown-it-katex-2.0.3.tgz",
|
||||
"integrity": "sha1-17hqGuoLnWSW+rTnkZoY/e9YnDk=",
|
||||
"requires": {
|
||||
"katex": "0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"katex": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.6.0.tgz",
|
||||
"integrity": "sha1-EkGOCRIcBckgQbazuftrqyE8tvM=",
|
||||
"requires": {
|
||||
"match-at": "0.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"match-at": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
|
||||
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
|
@@ -6,15 +6,19 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"postinstall": "node ../Tools/copycss.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.1.3",
|
||||
"base-64": "^0.1.0",
|
||||
"buffer": "^5.0.8",
|
||||
"events": "^1.1.1",
|
||||
"form-data": "^2.1.4",
|
||||
"html-entities": "^1.2.1",
|
||||
"katex": "^0.9.0-beta1",
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"md5": "^2.2.1",
|
||||
"moment": "^2.18.1",
|
||||
"prop-types": "^15.6.0",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Keyboard, NativeModules, BackHandler } = require('react-native');
|
||||
const { AppState, Keyboard, NativeModules, BackHandler } = require('react-native');
|
||||
const { SafeAreaView } = require('react-navigation');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
@@ -52,9 +52,11 @@ const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
|
||||
// Disabled because not fully working
|
||||
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
@@ -467,6 +469,10 @@ class AppComponent extends React.Component {
|
||||
this.backButtonHandler_ = () => {
|
||||
return this.backButtonHandler();
|
||||
}
|
||||
|
||||
this.onAppStateChange_ = () => {
|
||||
PoorManIntervals.update();
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@@ -491,6 +497,12 @@ class AppComponent extends React.Component {
|
||||
const notification = await Alarm.makeNotification(alarm);
|
||||
this.dropdownAlert_.alertWithType('info', notification.title, notification.body ? notification.body : '');
|
||||
});
|
||||
|
||||
AppState.addEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
async backButtonHandler() {
|
||||
|
@@ -1,18 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const toolUtils = require('./tool-utils.js');
|
||||
|
||||
const rnDir = __dirname + '/../ReactNativeClient';
|
||||
|
||||
function androidVersionNumber() {
|
||||
let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8');
|
||||
const r = content.match(/versionName\s+"(\d+?\.\d+?\.\d+)"/)
|
||||
if (r.length !== 2) throw new Error('Could not get version number');
|
||||
return r[1];
|
||||
}
|
||||
|
||||
// const
|
||||
|
||||
// '/android/app/build/outputs/apk/app-armeabi-v7a-release.apk'
|
||||
|
||||
|
||||
console.info(androidVersionNumber());
|
@@ -1,5 +1,7 @@
|
||||
"use strict"
|
||||
|
||||
require('app-module-path').addPath(__dirname + '/../ReactNativeClient');
|
||||
|
||||
const rootDir = __dirname + '/..';
|
||||
|
||||
const processArgs = process.argv.splice(2, process.argv.length);
|
||||
@@ -16,6 +18,7 @@ const rnDir = rootDir + '/ReactNativeClient';
|
||||
const electronDir = rootDir + '/ElectronClient/app';
|
||||
|
||||
const { execCommand } = require('./tool-utils.js');
|
||||
const { countryDisplayName, countryCodeOnly } = require('lib/locale.js');
|
||||
|
||||
function parsePoFile(filePath) {
|
||||
const content = fs.readFileSync(filePath);
|
||||
@@ -105,6 +108,95 @@ function availableLocales(defaultLocale) {
|
||||
return output;
|
||||
}
|
||||
|
||||
function extractTranslator(regex, poContent) {
|
||||
const translatorMatch = poContent.match(regex);
|
||||
let translatorName = '';
|
||||
|
||||
if (translatorMatch && translatorMatch.length >= 1) {
|
||||
translatorName = translatorMatch[1];
|
||||
translatorName = translatorName.replace(/["\s]+$/, '');
|
||||
translatorName = translatorName.replace(/\\n$/, '');
|
||||
translatorName = translatorName.replace(/^\s*/, '');
|
||||
}
|
||||
|
||||
if (translatorName.indexOf('FULL NAME') >= 0) return '';
|
||||
if (translatorName.indexOf('LL@li.org') >= 0) return '';
|
||||
|
||||
return translatorName;
|
||||
}
|
||||
|
||||
async function translationStatus(isDefault, poFile) {
|
||||
// "apt install translate-toolkit" to have pocount
|
||||
const command = 'pocount "' + poFile + '"';
|
||||
const result = await execCommand(command);
|
||||
const matches = result.match(/translated:\s*?(\d+)\s*\((.+?)%\)/);
|
||||
if (matches.length < 3) throw new Error('Cannot extract status: ' + command + ':\n' + result);
|
||||
|
||||
const percentDone = Number(matches[2]);
|
||||
if (isNaN(percentDone)) throw new Error('Cannot extract percent translated: ' + command + ':\n' + result);
|
||||
|
||||
let translatorName = '';
|
||||
const content = await fs.readFile(poFile, 'utf-8');
|
||||
|
||||
translatorName = extractTranslator(/Last-Translator:\s*?(.*)/, content);
|
||||
if (!translatorName) {
|
||||
translatorName = extractTranslator(/Language-Team:\s*?(.*)/, content);
|
||||
}
|
||||
|
||||
// "Last-Translator: Hrvoje Mandić <trbuhom@net.hr>\n"
|
||||
// let translatorMatch = content.match(/Last-Translator:\s*?(.*)/);
|
||||
// if (translatorMatch.length < 1) {
|
||||
// translatorMatch = content.match(/Last-Team:\s*?(.*)/);
|
||||
// }
|
||||
|
||||
// if (translatorMatch.length >= 1) {
|
||||
// translatorName = translatorMatch[1];
|
||||
// translatorName = translatorName.replace(/["\s]+$/, '');
|
||||
// translatorName = translatorName.replace(/\\n$/, '');
|
||||
// translatorName = translatorName.replace(/^\s*/, '');
|
||||
// }
|
||||
|
||||
// if (translatorName.indexOf('FULL NAME') >= 0) translatorName = '';
|
||||
|
||||
return {
|
||||
percentDone: isDefault ? 100 : percentDone,
|
||||
translatorName: translatorName,
|
||||
};
|
||||
}
|
||||
|
||||
function flagImageUrl(locale) {
|
||||
if (locale === 'eu') {
|
||||
return 'https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/es/basque_country.png';
|
||||
} else {
|
||||
return 'https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/' + countryCodeOnly(locale).toLowerCase() + '.png'
|
||||
}
|
||||
}
|
||||
|
||||
function translationStatusToMdTable(status) {
|
||||
let output = [];
|
||||
output.push([' ', 'Language', 'Code', 'Last translator', 'Percent done'].join(' | '));
|
||||
output.push(['---', '---', '---', '---', '---'].join('|'));
|
||||
for (let i = 0; i < status.length; i++) {
|
||||
const stat = status[i];
|
||||
const flagUrl = flagImageUrl(stat.locale); //'https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/' + countryCodeOnly(stat.locale).toLowerCase() + '.png';
|
||||
output.push(['', stat.languageName, stat.locale, stat.translatorName, stat.percentDone + '%'].join(' | '));
|
||||
}
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
async function updateReadmeWithStats(stats) {
|
||||
const mdTableMarkerOpen = '<!-- LOCALE-TABLE-AUTO-GENERATED -->\n';
|
||||
const mdTableMarkerClose = '\n<!-- LOCALE-TABLE-AUTO-GENERATED -->';
|
||||
let mdTable = translationStatusToMdTable(stats);
|
||||
mdTable = mdTableMarkerOpen + mdTable + mdTableMarkerClose;
|
||||
|
||||
let content = await fs.readFile(rootDir + '/README.md', 'utf-8');
|
||||
// [^]* matches any character including new lines
|
||||
const regex = new RegExp(mdTableMarkerOpen + '[^]*?' + mdTableMarkerClose);
|
||||
content = content.replace(regex, mdTable);
|
||||
await fs.writeFile(rootDir + '/README.md', content);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let potFilePath = cliLocalesDir + '/joplin.pot';
|
||||
let jsonLocalesDir = cliDir + '/build/locales';
|
||||
@@ -126,6 +218,8 @@ async function main() {
|
||||
|
||||
fs.mkdirpSync(jsonLocalesDir, 0o755);
|
||||
|
||||
let stats = [];
|
||||
|
||||
let locales = availableLocales(defaultLocale);
|
||||
for (let i = 0; i < locales.length; i++) {
|
||||
const locale = locales[i];
|
||||
@@ -133,8 +227,15 @@ async function main() {
|
||||
const jsonFilePath = jsonLocalesDir + '/' + locale + '.json';
|
||||
if (locale != defaultLocale) await mergePotToPo(potFilePath, poFilePäth);
|
||||
buildLocale(poFilePäth, jsonFilePath);
|
||||
|
||||
const stat = await translationStatus(defaultLocale === locale, poFilePäth);
|
||||
stat.locale = locale;
|
||||
stat.languageName = countryDisplayName(locale);
|
||||
stats.push(stat);
|
||||
}
|
||||
|
||||
stats.sort((a, b) => a.languageName < b.languageName ? -1 : +1);
|
||||
|
||||
saveToFile(jsonLocalesDir + '/index.js', buildIndex(locales));
|
||||
|
||||
const rnJsonLocaleDir = rnDir + '/locales';
|
||||
@@ -142,6 +243,8 @@ async function main() {
|
||||
|
||||
const electronJsonLocaleDir = electronDir + '/locales';
|
||||
await execCommand('rsync -a "' + jsonLocalesDir + '/" "' + electronJsonLocaleDir + '"');
|
||||
|
||||
await updateReadmeWithStats(stats);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user