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

Compare commits

...

46 Commits

Author SHA1 Message Date
Laurent Cozic
5c66042a2d CLI v0.10.91 2018-01-28 17:59:34 +00:00
Laurent Cozic
ae75181b02 Electron release v0.10.50 2018-01-28 17:52:15 +00:00
Laurent Cozic
9dc3238182 Added beta notice 2018-01-28 17:51:38 +00:00
Laurent Cozic
0a68749373 Android release v0.10.79 2018-01-28 17:45:54 +00:00
Laurent Cozic
1519116291 Updated French translation 2018-01-28 17:43:21 +00:00
Laurent Cozic
d023d841e2 Update translations 2018-01-28 17:38:30 +00:00
Laurent Cozic
d7a1465d8e Skip sync report events in log 2018-01-28 17:38:17 +00:00
Laurent Cozic
15848fc696 Closing a resource is async 2018-01-28 17:37:51 +00:00
Laurent Cozic
837ae2c9f2 Send only one NOT_LOADED event per master key 2018-01-28 17:37:29 +00:00
Laurent Cozic
6789b98ead Return fileNotFound error when file cannot be opened so that it is skipped by synchroniser 2018-01-28 17:37:03 +00:00
Laurent Cozic
29f6e74ee3 Convert fs errors to normal errors 2018-01-28 17:36:36 +00:00
Laurent Cozic
2780c38c45 Fixed WebDAV error handling 2018-01-28 17:36:11 +00:00
Laurent Cozic
4531838217 Display stars for secure config value 2018-01-28 17:35:20 +00:00
Laurent Cozic
c6830499f7 Fixed Travis 2018-01-28 11:48:29 +00:00
Laurent Cozic
d9f00a2539 Electron release v0.10.49 2018-01-26 17:21:34 +00:00
Laurent Cozic
def83c9119 Merge branch 'master' of github.com:laurent22/joplin 2018-01-26 17:19:58 +00:00
Laurent Cozic
b6cb0056c7 CLI v0.10.90 2018-01-26 17:19:42 +00:00
Laurent Cozic
1669b5258a Fixed detection of encrypted item 2018-01-25 23:01:18 +00:00
Laurent Cozic
5a9e0bfc26 Handle password text input in mobile and desktop 2018-01-25 22:44:09 +00:00
Laurent Cozic
8f3fdb3afe Tweaks to make sure Nextcloud driver passes all test units 2018-01-25 21:15:58 +00:00
Laurent Cozic
7ab135c099 Various tweaks to get Nextcloud working in mobile 2018-01-25 20:48:01 +00:00
Laurent Cozic
1cc27f2509 Got Nextcloud sync to work in Electron 2018-01-25 19:01:14 +00:00
Laurent Cozic
ef700b421c Update CONTRIBUTING.md 2018-01-25 13:35:04 +00:00
Laurent Cozic
b9af5ac052 Update issue_template.md 2018-01-25 13:34:07 +00:00
Laurent Cozic
173f2d421d Update PULL_REQUEST_TEMPLATE 2018-01-25 13:32:19 +00:00
Laurent Cozic
9f82c069c9 Update CONTRIBUTING.md 2018-01-25 13:31:17 +00:00
Laurent Cozic
6ade09c228 Merge branch 'master' into webdav 2018-01-24 17:25:34 +00:00
Laurent Cozic
5393a1399c Finished WebDAV driver 2018-01-23 20:10:20 +00:00
Laurent Cozic
fd29f20b2e Electron: Fix checkbox issue in config screen 2018-01-23 18:31:49 +00:00
Laurent Cozic
c011b53d1f Electron: Upgraded Electron to 1.7.11 to fix security vulnerability 2018-01-23 18:10:30 +00:00
Laurent Cozic
26e3a7b68c Merge branch 'master' of github.com:laurent22/joplin 2018-01-23 17:53:19 +00:00
Laurent Cozic
e70a291698 Merge pull request #174 from gabcoh/fix171
Fix #171
2018-01-23 11:26:11 +00:00
Laurent Cozic
511bd57726 Merge pull request #175 from alexdevero/add-font-size-settings
Add font size settings
2018-01-23 11:25:47 +00:00
Laurent Cozic
c6de8598dc Electron release v0.10.48 2018-01-22 19:10:29 +00:00
Laurent Cozic
7bee25599d Removed uneeded code 2018-01-22 19:06:50 +00:00
Laurent Cozic
773a1ad829 Travis: only build tags 2018-01-21 20:03:40 +00:00
Laurent Cozic
1a1e264fa4 All: Refactored so that memory and file sync target use same delta logic 2018-01-21 19:45:32 +00:00
Laurent Cozic
5b99ecefca Merge branch 'master' into webdav 2018-01-21 19:10:39 +00:00
Laurent Cozic
1bfeed377a All: Optimised file sync logic so that it doesn't fetch the content of
all the items on each sync. Also limit the number of items in a batch
to 1000
2018-01-21 18:54:47 +00:00
Laurent Cozic
86eee376bb All: Handle case where resource blob is missing during sync 2018-01-21 17:48:50 +00:00
Laurent Cozic
6a7d368184 All: Started Nextcloud support 2018-01-21 17:01:37 +00:00
Alex Devero
1da19ae98d Fix indentation 2018-01-19 14:11:40 +01:00
Alex Devero
f52c117b09 Add font size settings 2018-01-19 13:27:44 +01:00
Laurent Cozic
2551f96149 Fixed Readme 2018-01-18 22:43:37 +00:00
Gabe Cohen
af50d80541 Fix #171 2018-01-18 14:29:13 -06:00
Laurent Cozic
e355f4e49b Fixed license 2018-01-18 20:14:05 +00:00
95 changed files with 1798 additions and 455 deletions

View File

@@ -1,3 +1,6 @@
# Only build tags (Doesn't work - doesn't build anything)
# if: tag IS present
rvm: 2.3.3
matrix:

View File

@@ -1,6 +1,20 @@
# Reporting a bug
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/README_debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
# Feature requests
Again, please check that it has not already been requested. If it has, simply **up-vote the issue** - the ones with the most up-votes are likely to be implemented. Adding a "+1" comment does nothing.
# Adding new features
If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
# Style
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
# Coding style
- Only use tabs for indentation, not spaces.
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.

View File

@@ -48,7 +48,7 @@ async function handleAutocompletionPromise(line) {
if (options.length > 1 && options[1].indexOf(next) === 0) {
l.push(options[1]);
} else if (options[0].indexOf(next) === 0) {
l.push(options[2]);
l.push(options[0]);
}
}
if (l.length === 0) {

View File

@@ -23,8 +23,11 @@ class Command extends BaseCommand {
const verbose = args.options.verbose;
const renderKeyValue = (name) => {
const md = Setting.settingMetadata(name);
let value = Setting.value(name);
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
if (md.secure) value = '********';
if (Setting.isEnum(name)) {
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
} else {

View File

@@ -131,7 +131,6 @@ class Command extends BaseCommand {
} else if (stat.isDirectory()) {
continue;
} else {
itemCount++;
const content = await fs.readFile(fullPath, 'utf8');
const item = await BaseItem.unserialize(content);
const ItemClass = BaseItem.itemClass(item);
@@ -141,6 +140,8 @@ class Command extends BaseCommand {
continue;
}
itemCount++;
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
if (isEncrypted) {

View File

@@ -11,6 +11,7 @@ const md5 = require('md5');
const locker = require('proper-lockfile');
const fs = require('fs-extra');
const osTmpdir = require('os-tmpdir');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
class Command extends BaseCommand {
@@ -61,14 +62,28 @@ class Command extends BaseCommand {
});
}
async doAuth(syncTargetId) {
async doAuth() {
const syncTarget = reg.syncTarget(this.syncTargetId_);
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
return auth;
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
const auth = await this.oneDriveApiUtils_.oauthDance({
log: (...s) => { return this.stdout(...s); }
});
this.oneDriveApiUtils_ = null;
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return false;
}
return true;
}
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
return false;
}
cancelAuth() {
@@ -120,12 +135,8 @@ class Command extends BaseCommand {
app().gui().showConsole();
app().gui().maximizeConsole();
const auth = await this.doAuth(this.syncTargetId_);
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
if (!auth) {
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
return cleanUp();
}
const authDone = await this.doAuth();
if (!authDone) return cleanUp();
}
const sync = await syncTarget.synchronizer();

View File

@@ -66,6 +66,53 @@ process.stdout.on('error', function( err ) {
}
});
// async function main() {
// const WebDavApi = require('lib/WebDavApi');
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
// const driver = new FileApiDriverWebDav(api);
// const stat = await driver.stat('');
// console.info(stat);
// // const stat = await driver.stat('testing.txt');
// // console.info(stat);
// // const content = await driver.get('testing.txta');
// // console.info(content);
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
// // console.info(content);
// // const content = await driver.mkdir('newdir5');
// // console.info(content);
// //await driver.put('myfile4.md', 'this is my content');
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
// // await driver.delete('myfile4.md');
// // const deltaResult = await driver.delta('', {
// // allItemIdsHandler: () => { return []; }
// // });
// // console.info(deltaResult);
// }
// main().catch((error) => { console.error(error); });
application.start(process.argv).catch((error) => {
console.error(_('Fatal error:'));
console.error(error);

View File

@@ -234,6 +234,10 @@ msgstr ""
"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um "
"zurück zum Terminal zu gelangen."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@@ -470,6 +474,16 @@ msgstr ""
"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target "
"Optionswert)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
@@ -483,12 +497,6 @@ msgstr ""
"Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und "
"fortfahren."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisationsziel: %s (%s)"
@@ -884,6 +892,9 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system"
msgstr "Dateisystem"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1035,6 +1046,12 @@ msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Synchronisation interval"
msgstr "Synchronisationsinterval"
@@ -1050,9 +1067,6 @@ msgstr "%d Stunde"
msgid "%d hours"
msgstr "%d Stunden"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Show advanced options"
msgstr "Erweiterte Optionen anzeigen"
@@ -1060,12 +1074,9 @@ msgid "Synchronisation target"
msgstr "Synchronisationsziel"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem "
"Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um "
"den Zielpfad zu spezifizieren."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@@ -1077,6 +1088,15 @@ msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
@@ -1248,6 +1268,14 @@ msgstr ""
msgid "Welcome"
msgstr "Willkommen"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit "
#~ "dem Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2."
#~ "path`, um den Zielpfad zu spezifizieren."
#~ msgid "Note title:"
#~ msgstr "Notizen Titel:"

View File

@@ -214,6 +214,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -413,6 +417,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -785,6 +793,9 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -923,6 +934,12 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -938,9 +955,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +962,8 @@ msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Directory to synchronise with (absolute path)"
@@ -960,6 +974,15 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""

View File

@@ -224,6 +224,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -449,6 +453,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -462,10 +474,6 @@ msgstr ""
"curso, puedes eliminar el archivo de bloqueo en \"%s\" y reanudar la "
"operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -854,6 +862,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1009,6 +1020,12 @@ msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -1024,22 +1041,16 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Show advanced options"
msgstr "Mostrar opciones "
msgid "Synchronisation target"
msgstr "Sincronización de objetivo"
#, fuzzy
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"El objetivo para sincronizarse a. Si sincronizando con el sistema de "
"archivos, establecer `sync.2.path` especifique el directorio destino."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -1051,6 +1062,15 @@ msgstr ""
"La ubicacion para sincronizar cuando el sistema de archivo tenga habilitada "
"la sincronización. Ver `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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."
@@ -1225,6 +1245,14 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#, fuzzy
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El objetivo para sincronizarse a. Si sincronizando con el sistema de "
#~ "archivos, establecer `sync.2.path` especifique el directorio destino."
#~ msgid "Note title:"
#~ msgstr "Título de nota:"

View File

@@ -225,6 +225,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -458,6 +462,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -471,10 +483,6 @@ msgstr ""
"sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar "
"la operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -858,6 +866,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1008,6 +1019,12 @@ msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -1023,9 +1040,6 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Show advanced options"
msgstr "Mostrar opciones avanzadas"
@@ -1033,11 +1047,9 @@ msgid "Synchronisation target"
msgstr "Destino de sincronización"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"El destino de la sincronización. Si se sincroniza con el sistema de "
"archivos, indique el directorio destino en «sync.2.path»."
msgid "Directory to synchronise with (absolute path)"
msgstr "Directorio con el que sincronizarse (ruta completa)"
@@ -1049,6 +1061,15 @@ msgstr ""
"La ruta a la que sincronizar cuando se activa la sincronización con sistema "
"de archivos. Vea «sync.target»."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1215,6 +1236,13 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El destino de la sincronización. Si se sincroniza con el sistema de "
#~ "archivos, indique el directorio destino en «sync.2.path»."
#~ msgid "Note title:"
#~ msgstr "Título de la nota:"

View File

@@ -228,6 +228,10 @@ msgstr ""
"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à "
"l'invite de commande."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Erreur lors de l'ouverture de la note dans l'éditeur de texte : %s"
msgid "Note has been saved."
msgstr "La note a été enregistrée."
@@ -458,6 +462,16 @@ msgstr ""
"Synchroniser avec la cible donnée (par défaut, la valeur de configuration "
"`sync.target`)."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Non-connecté à %s. Veuillez fournir les identifiants et mots de passe "
"manquants."
msgid "Synchronisation is already in progress."
msgstr "La synchronisation est déjà en cours."
@@ -471,10 +485,6 @@ msgstr ""
"correctement. Si vous savez qu'aucune autre synchronisation est en cours, "
"vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Cible de la synchronisation : %s (%s)"
@@ -665,7 +675,7 @@ msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annulation"
msgstr "Annuler"
#, javascript-format
msgid "Notes and settings are stored in: %s"
@@ -876,6 +886,9 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
@@ -1024,6 +1037,12 @@ msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
msgid "Set application zoom percentage"
msgstr "Niveau de zoom"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Synchronisation interval"
msgstr "Intervalle de synchronisation"
@@ -1039,9 +1058,6 @@ msgstr "%d heure"
msgid "%d hours"
msgstr "%d heures"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Show advanced options"
msgstr "Montrer les options avancées"
@@ -1049,11 +1065,12 @@ msgid "Synchronisation target"
msgstr "Cible de la synchronisation"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
"La cible avec laquelle synchroniser. Chaque cible de synchronisation peut "
"avoir des paramètres supplémentaires sous le nom `sync.NUM.NOM` (documentés "
"ci-dessous)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
@@ -1065,6 +1082,15 @@ msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nextcloud : URL WebDAV"
msgid "Nexcloud username"
msgstr "Nextcloud : Nom utilisateur"
msgid "Nexcloud password"
msgstr "Nextcloud : Mot de passe"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
@@ -1236,6 +1262,13 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
#~ "fichier, veuillez spécifier le répertoire avec `sync.2.path`."
#~ msgid "Note title:"
#~ msgstr "Titre de la note :"

View File

@@ -235,6 +235,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Počinjem uređivati bilješku. Za povratak u naredbeni redak, zatvori uređivač."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Bilješka je spremljena."
@@ -466,6 +470,16 @@ msgstr "Sinkronizira sa udaljenom pohranom podataka."
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)"
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sinkronizacija je već u toku."
@@ -477,12 +491,6 @@ msgid ""
msgstr ""
"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi..."
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Meta sinkronizacije: %s (%s)"
@@ -868,6 +876,9 @@ msgstr "Nepoznata zastavica: %s"
msgid "File system"
msgstr "Datotečni sustav"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1015,6 +1026,12 @@ msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Save geo-location with notes"
msgstr "Spremi geolokacijske podatke sa bilješkama"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Synchronisation interval"
msgstr "Interval sinkronizacije"
@@ -1030,9 +1047,6 @@ msgstr "%d sat"
msgid "%d hours"
msgstr "%d sati"
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Show advanced options"
msgstr "Prikaži napredne opcije"
@@ -1040,11 +1054,9 @@ msgid "Synchronisation target"
msgstr "Sinkroniziraj sa"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
"sustavom, postavi `sync.2.path` na ciljani direktorij."
msgid "Directory to synchronise with (absolute path)"
msgstr "Direktorij za sinkroniziranje (apsolutna putanja)"
@@ -1056,6 +1068,15 @@ msgstr ""
"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa "
"datotečnim sustavom omogućena. Vidi `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@@ -1221,6 +1242,13 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
msgid "Welcome"
msgstr "Dobro došli"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
#~ "sustavom, postavi `sync.2.path` na ciljani direktorij."
#~ msgid "Note title:"
#~ msgstr "Naslov bilješke:"

View File

@@ -226,6 +226,10 @@ msgstr "Non esiste la nota: \"%s\". Desideri crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Comincia a modificare la nota. Chiudi l'editor per tornare al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota è stata salvata."
@@ -449,6 +453,16 @@ msgstr ""
"Sincronizza con l'obiettivo fornito (come predefinito il valore di "
"configurazione sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "La sincronizzazione è in corso."
@@ -462,12 +476,6 @@ msgstr ""
"sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e "
"riprendere l'operazione."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Posizione di sincronizzazione: %s (%s)"
@@ -848,6 +856,9 @@ msgstr "Etichetta sconosciuta: %s"
msgid "File system"
msgstr "File system"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -997,6 +1008,12 @@ msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Save geo-location with notes"
msgstr "Salva geo-localizzazione con le note"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Synchronisation interval"
msgstr "Intervallo di sincronizzazione"
@@ -1012,9 +1029,6 @@ msgstr "%d ora"
msgid "%d hours"
msgstr "%d ore"
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Show advanced options"
msgstr "Mostra opzioni avanzate"
@@ -1022,12 +1036,9 @@ msgid "Synchronisation target"
msgstr "Destinazione di sincronizzazione"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"La destinazione della sincronizzazione. Se si sincronizza con il file "
"system, impostare ' Sync. 2. Path ' per specificare la directory di "
"destinazione."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -1039,6 +1050,15 @@ msgstr ""
"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
@@ -1206,6 +1226,14 @@ msgstr ""
msgid "Welcome"
msgstr "Benvenuto"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "La destinazione della sincronizzazione. Se si sincronizza con il file "
#~ "system, impostare ' Sync. 2. Path ' per specificare la directory di "
#~ "destinazione."
#~ msgid "Note title:"
#~ msgstr "Titolo della Nota:"

View File

@@ -223,6 +223,10 @@ msgstr "\"%s\"というノートはありません。お作りいたしますか
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "ノートの編集の開始。エディタを閉じると元の画面に戻ることが出来ます。"
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "ノートは保存されました。"
@@ -446,6 +450,14 @@ msgstr "リモート保存領域と同期します。"
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "指定のターゲットと同期します。(標準: sync.targetの設定値)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "認証は完了していません(認証トークンが得られませんでした)"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "同期はすでに実行中です。"
@@ -458,10 +470,6 @@ msgstr ""
"ロックファイルがすでに保持されています。同期作業が行われていない場合は、\"%s"
"\"にあるロックファイルを削除して、作業を再度行ってください。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "認証は完了していません(認証トークンが得られませんでした)"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同期先: %s (%s)"
@@ -849,6 +857,9 @@ msgstr "不明なフラグ: %s"
msgid "File system"
msgstr "ファイルシステム"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -1000,6 +1011,12 @@ msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Synchronisation interval"
msgstr "同期間隔"
@@ -1015,9 +1032,6 @@ msgstr "%d 時間"
msgid "%d hours"
msgstr "%d 時間"
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Show advanced options"
msgstr "詳細な設定の表示"
@@ -1025,11 +1039,9 @@ msgid "Synchronisation target"
msgstr "同期先"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同期先"
"のディレクトリに設定してください。"
msgid "Directory to synchronise with (absolute path)"
msgstr "同期先のディレクトリ(絶対パス)"
@@ -1041,6 +1053,15 @@ msgstr ""
"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく"
"ださい。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1208,6 +1229,13 @@ msgstr ""
msgid "Welcome"
msgstr "ようこそ"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "同期先です。ローカルのファイルシステムと同期する場合は、`sync.2.path`を同"
#~ "期先のディレクトリに設定してください。"
#~ msgid "Note title:"
#~ msgstr "ノートの題名:"

View File

@@ -214,6 +214,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@@ -413,6 +417,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -785,6 +793,9 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
@@ -923,6 +934,12 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@@ -938,9 +955,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +962,8 @@ msgid "Synchronisation target"
msgstr ""
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Directory to synchronise with (absolute path)"
@@ -960,6 +974,15 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""

View File

@@ -229,6 +229,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Bewerken notitie gestart. Sluit de editor om terug naar de prompt te gaan."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Notitie is opgeslaan."
@@ -459,6 +463,14 @@ msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
"Synchroniseer naar opgegeven doel (standaard sync.target configuratie optie)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Synchronisatie reeds bezig."
@@ -472,10 +484,6 @@ msgstr ""
"is, kan de lock file verwijderd worden op \"%s\" en verder gegaan worden met "
"de synchronisatie. "
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisatiedoel: %s (%s)"
@@ -875,6 +883,9 @@ msgstr "Onbekende optie: %s"
msgid "File system"
msgstr "Bestandssysteem"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1026,6 +1037,12 @@ msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
msgid "Save geo-location with notes"
msgstr "Sla geo-locatie op bij notities"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Synchronisation interval"
msgstr "Synchronisatie interval"
@@ -1041,9 +1058,6 @@ msgstr "%d uur"
msgid "%d hours"
msgstr "%d uren"
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Show advanced options"
msgstr "Toon geavanceerde opties"
@@ -1051,11 +1065,9 @@ msgid "Synchronisation target"
msgstr "Synchronisatiedoel"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"Het doel om mee te synchroniseren. Indien synchroniseren met het "
"bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."
msgid "Directory to synchronise with (absolute path)"
msgstr "Folder om mee te synchroniseren (absolute pad)"
@@ -1067,6 +1079,16 @@ msgstr ""
"Het pad om mee te synchroniseren als bestandssysteem synchronisatie is "
"ingeschakeld. Zie `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
#, fuzzy
msgid "Nexcloud password"
msgstr "Stel wachtwoord in"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1236,3 +1258,10 @@ msgstr ""
msgid "Welcome"
msgstr "Welkom"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Het doel om mee te synchroniseren. Indien synchroniseren met het "
#~ "bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."

View File

@@ -223,6 +223,10 @@ msgstr "A nota não existe: \"%s\". Criar?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Começando a editar a nota. Feche o editor para voltar ao prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Nota gravada."
@@ -445,6 +449,15 @@ msgstr ""
"Sincronizar para destino fornecido (p padrão é o valor de configuração sync."
"target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "A sincronização já está em andamento."
@@ -458,11 +471,6 @@ msgstr ""
"está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar "
"a operação."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Alvo de sincronização: %s (%s)"
@@ -846,6 +854,9 @@ msgstr "Flag desconhecido: %s"
msgid "File system"
msgstr "Sistema de arquivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -996,6 +1007,12 @@ msgstr "Mostrar tarefas incompletas no topo das listas"
msgid "Save geo-location with notes"
msgstr "Salvar geolocalização com notas"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronização"
@@ -1011,9 +1028,6 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Show advanced options"
msgstr "Mostrar opções avançadas"
@@ -1021,11 +1035,9 @@ msgid "Synchronisation target"
msgstr "Alvo de sincronização"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"O alvo para sincronizar. Se estiver sincronizando com o sistema de arquivos, "
"configure `sync.2.path` para especificar o diretório de destino."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -1037,6 +1049,15 @@ msgstr ""
"O caminho para sincronizar, quando a sincronização do sistema de arquivos "
"está habilitada. Veja `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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."
@@ -1202,6 +1223,13 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome"
msgstr "Bem-vindo"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "O alvo para sincronizar. Se estiver sincronizando com o sistema de "
#~ "arquivos, configure `sync.2.path` para especificar o diretório de destino."
#~ msgid "Note title:"
#~ msgstr "Título da nota:"

View File

@@ -228,6 +228,10 @@ msgstr ""
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
"командной строке."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Заметка сохранена."
@@ -455,6 +459,14 @@ msgstr ""
"Синхронизация с заданной целью (по умолчанию — значение конфигурации sync."
"target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Аутентификация не была завершена (не получен токен аутентификации)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Синхронизация уже выполняется."
@@ -468,10 +480,6 @@ msgstr ""
"производится, вы можете удалить файл блокировки в «%s» и возобновить "
"операцию."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Аутентификация не была завершена (не получен токен аутентификации)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Цель синхронизации: %s (%s)"
@@ -866,6 +874,9 @@ msgstr "Неизвестный флаг: %s"
msgid "File system"
msgstr "Файловая система"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -1016,6 +1027,12 @@ msgstr "Показывать незавершённые задачи вверх
msgid "Save geo-location with notes"
msgstr "Сохранять информацию о геолокации в заметках"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Synchronisation interval"
msgstr "Интервал синхронизации"
@@ -1031,9 +1048,6 @@ msgstr "%d час"
msgid "%d hours"
msgstr "%d часов"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Show advanced options"
msgstr "Показывать расширенные настройки"
@@ -1041,11 +1055,9 @@ msgid "Synchronisation target"
msgstr "Цель синхронизации"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
"То, с чем будет осуществляться синхронизация. При синхронизации с файловой "
"системой в `sync.2.path` указывается целевой каталог."
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"
@@ -1057,6 +1069,16 @@ msgstr ""
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
#, fuzzy
msgid "Nexcloud password"
msgstr "Установить пароль"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
@@ -1224,6 +1246,13 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome"
msgstr "Добро пожаловать"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "То, с чем будет осуществляться синхронизация. При синхронизации с "
#~ "файловой системой в `sync.2.path` указывается целевой каталог."
#~ msgid "Note title:"
#~ msgstr "Название заметки:"

View File

@@ -218,6 +218,10 @@ msgstr "此笔记不存在:\"%s\"。是否创建?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "开始编辑笔记。关闭编辑器则返回提示。"
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "笔记已被保存。"
@@ -426,6 +430,14 @@ msgstr "与远程储存空间同步。"
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "同步至所提供的目标(默认为同步目标配置值)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "认证未完成(未收到认证令牌)。"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "同步正在进行中。"
@@ -438,10 +450,6 @@ msgstr ""
"锁定文件已被保留。若当前没有任何正在进行的同步,您可以在\"%s\"删除锁定文件并"
"继续操作。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "认证未完成(未收到认证令牌)。"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同步目标:%s (%s)"
@@ -814,6 +822,9 @@ msgstr "未知标记:%s"
msgid "File system"
msgstr "文件系统"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -958,6 +969,12 @@ msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Synchronisation interval"
msgstr "同步间隔"
@@ -973,9 +990,6 @@ msgstr "%d小时"
msgid "%d hours"
msgstr "%d小时"
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Show advanced options"
msgstr "显示高级选项"
@@ -983,9 +997,9 @@ msgid "Synchronisation target"
msgstr "同步目标"
msgid ""
"The target to synchonise to. If synchronising with the file system, set "
"`sync.2.path` to specify the target directory."
msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
"The target to synchonise to. Each sync target may have additional parameters "
"which are named as `sync.NUM.NAME` (all documented below)."
msgstr ""
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -995,6 +1009,15 @@ msgid ""
"See `sync.target`."
msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
@@ -1158,6 +1181,11 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
msgid "Welcome"
msgstr "欢迎"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
#~ msgid "Note title:"
#~ msgstr "笔记标题:"

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "0.10.88",
"version": "0.10.91",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -85,6 +85,11 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
@@ -1050,6 +1055,11 @@
"strict-uri-encode": "1.1.0"
}
},
"querystringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"readable-stream": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
@@ -1104,6 +1114,11 @@
"uuid": "3.1.0"
}
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"retry": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
@@ -2100,6 +2115,15 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
@@ -2144,6 +2168,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
}
},
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
},
"xregexp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.88",
"version": "0.10.91",
"bin": {
"joplin": "./main.js"
},
@@ -28,6 +28,7 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
@@ -57,8 +58,10 @@
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.21",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {

View File

@@ -11,7 +11,7 @@ cp "$SCRIPT_DIR/../README.md" build/
cd "$SCRIPT_DIR/build"
npm publish
NEW_VERSION=$("cat package.json | jq -r .version")
NEW_VERSION=$(cat package.json | jq -r .version)
git add -A
git commit -m "CLI v$NEW_VERSION"
git tag "cli-v$NEW_VERSION"

View File

@@ -74,11 +74,11 @@ async function localItemsSameAsRemote(locals, expect) {
expect(!!remote).toBe(true);
if (!remote) continue;
if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
} else {
expect(remote.updated_time).toBe(dbItem.updated_time);
}
// if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
// expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
// } else {
// expect(remote.updated_time).toBe(dbItem.updated_time);
// }
let remoteContent = await fileApi().get(path);
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
@@ -304,7 +304,7 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
}));
it('should delete remote folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -322,8 +322,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
localItemsSameAsRemote(all, expect);
}));
await localItemsSameAsRemote(all, expect);
}));
it('should delete local folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -345,8 +345,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items = await allItems();
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -388,8 +388,8 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
it('should cross delete all folders', asyncTest(async () => {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
@@ -683,12 +683,12 @@ describe('Synchronizer', function() {
await switchClient(2);
synchronizer().debugFlags_ = ['cancelDeltaLoop2'];
synchronizer().testingHooks_ = ['cancelDeltaLoop2'];
let context = await synchronizer().start();
let notes = await Note.all();
expect(notes.length).toBe(0);
synchronizer().debugFlags_ = [];
synchronizer().testingHooks_ = [];
await synchronizer().start({ context: context });
notes = await Note.all();
expect(notes.length).toBe(1);
@@ -702,9 +702,9 @@ describe('Synchronizer', function() {
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['rejectedByTarget'];
synchronizer().testingHooks_ = ['rejectedByTarget'];
await synchronizer().start();
synchronizer().debugFlags_ = [];
synchronizer().testingHooks_ = [];
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
await switchClient(2);
@@ -848,7 +848,7 @@ describe('Synchronizer', function() {
}));
it('should sync resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
while (insideBeforeEach) await time.msleep(500);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });

View File

@@ -15,6 +15,7 @@ const { Synchronizer } = require('lib/synchronizer.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { time } = require('lib/time-utils.js');
const { shimInit } = require('lib/shim-init-node.js');
@@ -22,8 +23,10 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const EncryptionService = require('lib/services/EncryptionService.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const WebDavApi = require('lib/WebDavApi');
let databases_ = [];
let synchronizers_ = [];
@@ -46,13 +49,17 @@ fs.mkdirpSync(logDir, 0o755);
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
//const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const logger = new Logger();
logger.addTarget('console');
logger.addTarget('file', { path: logDir + '/log.txt' });
@@ -174,12 +181,7 @@ async function setupDatabaseAndSynchronizer(id = null) {
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
} else {
await fileApi().format();
}
await fileApi().clearRoot();
}
function db(id = null) {
@@ -230,7 +232,17 @@ function fileApi() {
fileApi_ = new FileApi(syncDir, new FileApiDriverLocal());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
fileApi_ = new FileApi('/root', new FileApiDriverMemory());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('nextcloud')) {
const options = {
baseUrl: () => 'http://nextcloud.local/remote.php/dav/files/admin/JoplinTest',
username: () => 'admin',
password: () => '123456',
};
const api = new WebDavApi(options);
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
}
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
// let auth = require('./onedrive-auth.json');
// if (!auth) {

View File

@@ -89,24 +89,38 @@ class ConfigScreenComponent extends React.Component {
updateSettingValue(key, !value)
}
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
// There's probably a better way to do this but can't figure it out.
return (
<div key={key} style={rowStyle}>
<div key={key+value.toString()} style={rowStyle}>
<div style={controlStyle}>
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
</div>
</div>
);
} else if (md.type === Setting.TYPE_STRING) {
const onTextChange = (event) => {
const settings = Object.assign({}, this.state.settings);
settings[key] = event.target.value;
this.setState({ settings: settings });
updateSettingValue(key, event.target.value);
}
const inputType = md.secure === true ? 'password' : 'text';
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
<input type={inputType} style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
</div>
);
} else if (md.type === Setting.TYPE_INT) {
const onNumChange = (event) => {
updateSettingValue(key, event.target.value);
};
return (
<div key={key} style={rowStyle}>
<div style={labelStyle}><label>{md.label()}</label></div>
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
</div>
);
} else {

View File

@@ -121,7 +121,7 @@ class EncryptionConfigScreenComponent extends React.Component {
}
}
const decryptedItemsInfo = this.props.encryptionEnabled ? <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p> : null;
const decryptedItemsInfo = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
const toggleButton = <button onClick={() => { onToggleButtonClick() }}>{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}</button>
let masterKeySection = null;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,15 @@
// Make it possible to require("/lib/...") without specifying full path
require('app-module-path').addPath(__dirname);
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
};
const { app } = require('./app.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "0.10.50",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -10,9 +10,17 @@
"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",
@@ -29,9 +37,9 @@
"optional": true
},
"@types/node": {
"version": "7.0.46",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz",
"integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==",
"version": "7.0.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.52.tgz",
"integrity": "sha512-jjpyQsKGsOF/wUElNjfPULk+d8PKvJOIXk3IUeBYYmNCy5dMWfrI+JiixYNw8ppKOlcRwWTXFl0B+i5oGrf95Q==",
"dev": true
},
"ajv": {
@@ -566,6 +574,11 @@
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"base64-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz",
@@ -1302,12 +1315,12 @@
"dev": true
},
"electron": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
"integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.11.tgz",
"integrity": "sha1-mTtqp54OeafPzDafTIE/vZoLCNk=",
"dev": true,
"requires": {
"@types/node": "7.0.46",
"@types/node": "7.0.52",
"electron-download": "3.3.0",
"extract-zip": "1.6.6"
}
@@ -3526,6 +3539,11 @@
"strict-uri-encode": "1.1.0"
}
},
"querystringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"rabin-bindings": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/rabin-bindings/-/rabin-bindings-1.7.3.tgz",
@@ -3862,6 +3880,11 @@
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
@@ -5194,6 +5217,15 @@
}
}
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"url-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
@@ -5334,6 +5366,22 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
},
"dependencies": {
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
}
}
},
"xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "0.10.50",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -41,7 +41,7 @@
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^1.7.9",
"electron": "^1.7.11",
"electron-builder": "^19.45.4"
},
"optionalDependencies": {
@@ -51,6 +51,7 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"base-64": "^0.1.0",
"electron-context-menu": "^0.9.1",
"electron-log": "^2.2.11",
"electron-updater": "^2.16.1",
@@ -84,6 +85,8 @@
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tcp-port-used": "^0.1.2",
"uuid": "^3.1.0"
"url-parse": "^1.2.0",
"uuid": "^3.1.0",
"xml2js": "^0.4.19"
}
}

View File

@@ -1,7 +1,7 @@
const Setting = require('lib/models/Setting.js');
const globalStyle = {
fontSize: 12,
fontSize: 12 * Setting.value('style.zoom')/100,
fontFamily: 'sans-serif',
margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10,

20
LICENSE
View File

@@ -1,7 +1,21 @@
MIT License
Copyright (c) 2016-2018 Laurent Cozic
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1,5 @@
License MIT
Copyright (c) 2016-2018 Laurent Cozic
L'autorisation est accordée, gracieusement, à toute personne acquérant une copie de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de commercialiser le Logiciel sans restriction, notamment les droits d'utiliser, de copier, de modifier, de fusionner, de publier, de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel, ainsi que d'autoriser les personnes auxquelles le Logiciel est fourni à le faire, sous réserve des conditions suivantes :

View File

@@ -4,7 +4,7 @@ Joplin is a free, open source note taking and to-do application, which can handl
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
The notes can be [synchronised](#synchronisation) with various targets including 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.
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/).
@@ -25,8 +25,8 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/
## 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.78/joplin-v0.10.78.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/releases/download/android-v0.10.79/joplin-v0.10.79.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
@@ -53,7 +53,7 @@ For usage information, please refer to the full [Joplin Terminal Application Doc
- Desktop, mobile and terminal applications.
- Support notes, to-dos, tags and notebooks.
- Offline first, so the entire data is always available on the device even without an internet connection.
- Ability to synchronise with multiple targets, including the file system and OneDrive (NextCloud and Dropbox are planned).
- Ability to synchronise with multiple targets, including NextCloud, the file system and OneDrive (Dropbox is planned).
- 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
@@ -85,13 +85,32 @@ In general the way to import notes from any application into Joplin is to conver
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. 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.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
On the **desktop application**, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
## Nextcloud synchronisation
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
**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:
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
:config sync.target 5
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.
## 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.
On the **desktop application** or **mobile application**, select "OneDrive" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
@@ -136,8 +155,7 @@ Please see the guide for information on how to contribute to the development of
# Coming features
- NextCloud support
- All: End to end encryption
- Mobile: manage tags
- Windows: Tray icon
- Desktop apps: Tag auto-complete
- Desktop apps: Dark theme

View File

@@ -111,11 +111,26 @@ To import Evernote data, follow these steps:
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A Dropbox driver will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. 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.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
## Nextcloud synchronisation
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:
:config sync.target 5
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
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.
## 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.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 2097256
versionName "0.10.78"
versionCode 2097257
versionName "0.10.79"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -26,12 +26,14 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
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 EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
class BaseApplication {

View File

@@ -30,6 +30,10 @@ class BaseSyncTarget {
return false;
}
authRouteName() {
return null;
}
static id() {
throw new Error('id() not implemented');
}

View File

@@ -2,11 +2,7 @@ class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code_ = code;
}
get code() {
return this.code_;
this.code = code;
}
}

View File

@@ -73,7 +73,7 @@ class MdToHtml {
renderImage_(attrs, options) {
const loadResource = async (id) => {
console.info('Loading resource: ' + id);
// console.info('Loading resource: ' + id);
// Initially set to to an empty object to make
// it clear that it is being loaded. Otherwise

View File

@@ -0,0 +1,54 @@
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 SyncTargetNextcloud extends BaseSyncTarget {
static id() {
return 5;
}
constructor(db, options = null) {
super(db, options);
// this.authenticated_ = false;
}
static targetName() {
return 'nextcloud';
}
static label() {
return _('Nextcloud (Beta)');
}
isAuthenticated() {
return true;
//return this.authenticated_;
}
async initFileApi() {
const options = {
baseUrl: () => 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;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetNextcloud;

View File

@@ -9,15 +9,15 @@ const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDrive extends BaseSyncTarget {
static id() {
return 3;
}
constructor(db, options = null) {
super(db, options);
this.api_ = null;
}
static id() {
return 3;
}
static targetName() {
return 'onedrive';
}
@@ -38,6 +38,10 @@ class SyncTargetOneDrive extends BaseSyncTarget {
return parameters().oneDrive;
}
authRouteName() {
return 'OneDriveLogin';
}
api() {
if (this.api_) return this.api_;

View File

@@ -23,6 +23,18 @@ class SyncTargetRegistry {
throw new Error('Name not found: ' + name);
}
static idToMetadata(id) {
for (let n in this.reg_) {
if (!this.reg_.hasOwnProperty(n)) continue;
if (this.reg_[n].id === id) return this.reg_[n];
}
throw new Error('ID not found: ' + id);
}
static idToName(id) {
return this.idToMetadata(id).name;
}
static idAndLabelPlainObject() {
let output = {};
for (let n in this.reg_) {

View File

@@ -0,0 +1,217 @@
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim.js');
const parseXmlString = require('xml2js').parseString;
const JoplinError = require('lib/JoplinError');
const URL = require('url-parse');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const base64 = require('base-64');
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
// implementations use the same namespaces. If not, extra processing can be done in `nameProcessor`, for
// example to convert a custom namespace to "d:" so that it can be used by the rest of the code.
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
class WebDavApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
authToken() {
if (!this.options_.username() || !this.options_.password()) return null;
return base64.encode(this.options_.username() + ':' + this.options_.password());
}
baseUrl() {
return this.options_.baseUrl();
}
relativeBaseUrl() {
const url = new URL(this.baseUrl());
return url.pathname + url.query;
}
async xmlToJson(xml) {
const nameProcessor = (name) => {
// const idx = name.indexOf(':');
// if (idx >= 0) {
// if (name.indexOf('xmlns:') !== 0) name = name.substr(idx + 1);
// }
return name.toLowerCase();
};
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
}
return new Promise((resolve, reject) => {
parseXmlString(xml, options, (error, result) => {
if (error) {
resolve(null); // Error handled by caller which will display the XML text (or plain text) if null is returned from this function
return;
}
resolve(result);
});
});
}
valueFromJson(json, keys, type) {
let output = json;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// console.info(key, typeof key, typeof output, typeof output === 'object' && (key in output), Array.isArray(output));
if (typeof key === 'number' && !Array.isArray(output)) return null;
if (typeof key === 'string' && (typeof output !== 'object' || !(key in output))) return null;
output = output[key];
}
if (type === 'string') {
if (typeof output !== 'string') return null;
return output;
}
if (type === 'object') {
if (!Array.isArray(output) && typeof output === 'object') return output;
return null;
}
if (type === 'array') {
return Array.isArray(output) ? output : null;
}
return null;
}
stringFromJson(json, keys) {
return this.valueFromJson(json, keys, 'string');
}
objectFromJson(json, keys) {
return this.valueFromJson(json, keys, 'object');
}
arrayFromJson(json, keys) {
return this.valueFromJson(json, keys, 'array');
}
async execPropFind(path, depth, fields = null) {
if (fields === null) fields = ['d:getlastmodified'];
let fieldsXml = '';
for (let i = 0; i < fields.length; i++) {
fieldsXml += '<' + fields[i] + '/>';
}
// To find all available properties:
//
// const body=`<?xml version="1.0" encoding="utf-8" ?>
// <propfind xmlns="DAV:">
// <propname/>
// </propfind>`;
const body = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
<d:prop xmlns:oc="http://owncloud.org/ns">
` + fieldsXml + `
</d:prop>
</d:propfind>`;
return this.exec('PROPFIND', path, body, { 'Depth': depth });
}
// 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">
// <d:getlastmodified/>
// </d:prop>
// </d:propfind>'
async exec(method, path = '', body = null, headers = null, options = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = 'json';
if (!options.target) options.target = 'string';
const authToken = this.authToken();
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
const fetchOptions = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
const url = this.baseUrl() + '/' + path;
let response = null;
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
response = await shim.fetch(url, fetchOptions);
} else { // file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// 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 = () => {
return (responseText + '').substr(0, 1024);
}
let responseJson_ = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
responseJson_ = await this.xmlToJson(responseText);
if (!responseJson_) throw new JoplinError('Cannot parse JSON response: ' + shortResponseText(), response.status);
return responseJson_;
}
if (!response.ok) {
// When using fetchBlob we only get a string (not xml or json) back
if (options.target === 'file') throw new JoplinError(shortResponseText(), response.status);
const json = await loadResponseJson();
if (json && json['d:error']) {
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : shortResponseText();
throw new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + ')', response.status);
}
throw new JoplinError(shortResponseText(), response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
// null responses are possible, for example for DELETE calls
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw new Error('Not a valid JSON response: ' + shortResponseText());
return output;
}
}
module.exports = WebDavApi;

View File

@@ -13,7 +13,7 @@ const globalStyle = {
fontSizeSmaller: 14,
dividerColor: "#dddddd",
selectedColor: '#e5e5e5',
disabledOpacity: 0.3,
disabledOpacity: 0.2,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { 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');
@@ -17,6 +17,23 @@ class ConfigScreenComponent extends BaseScreenComponent {
constructor() {
super();
this.styles_ = {};
this.state = {
settings: {},
settingsChanged: false,
};
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});
};
}
componentWillMount() {
this.setState({ settings: this.props.settings });
}
styles() {
@@ -83,7 +100,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
let output = null;
const updateSettingValue = (key, value) => {
Setting.setValue(key, value);
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
this.setState({
settings: settings,
settingsChanged: true,
});
console.info(settings['sync.5.path']);
}
const md = Setting.settingMetadata(key);
@@ -135,23 +159,33 @@ class ConfigScreenComponent extends BaseScreenComponent {
<Slider key="control" style={this.styles().settingControl} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
</View>
);
} else if (md.type == Setting.TYPE_STRING) {
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} />
</View>
);
} else {
//throw new Error('Unsupported setting type: ' + setting.type);
//throw new Error('Unsupported setting type: ' + md.type);
}
return output;
}
render() {
const settings = this.props.settings;
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 (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(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);
@@ -173,11 +207,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
);
//style={this.styles().body}
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Configuration')}/>
<ScreenHeader
title={_('Configuration')}
showSaveButton={true}
saveButtonDisabled={!this.state.settingsChanged}
onSaveButtonPress={this.saveButton_press}
/>
<ScrollView >
{ settingComps }
</ScrollView>

View File

@@ -36,12 +36,17 @@ shared.synchronize_press = async function(comp) {
const action = comp.props.syncStarted ? 'cancel' : 'start';
if (!reg.syncTarget().isAuthenticated()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: 'OneDriveLogin',
});
return 'auth';
if (!reg.syncTarget().isAuthenticated()) {
if (reg.syncTarget().authRouteName()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: reg.syncTarget().authRouteName(),
});
return 'auth';
}
reg.logger().info('Not authentified with sync target - please check your credential.');
return 'error';
}
let sync = null;

View File

@@ -1,5 +1,5 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
@@ -44,8 +44,6 @@ class FileApiDriverLocal {
path: stat.path,
created_time: stat.birthtime.getTime(),
updated_time: stat.mtime.getTime(),
created_time_orig: stat.birthtime,
updated_time_orig: stat.mtime,
isDir: stat.isDirectory(),
};
}
@@ -68,41 +66,14 @@ class FileApiDriverLocal {
}
async delta(path, options) {
const itemIds = await options.allItemIdsHandler();
const getStatFn = async (path) => {
const stats = await this.fsDriver().readDirStats(path);
return this.metadataFromStats_(stats);
};
try {
const stats = await this.fsDriver().readDirStats(path);
let output = this.metadataFromStats_(stats);
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
let found = false;
for (let j = 0; j < output.length; j++) {
const item = output[j];
if (BaseItem.pathToId(item.path) == itemId) {
found = true;
break;
}
}
if (!found) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
return {
hasMore: false,
context: null,
items: output,
};
const output = await basicDelta(path, getStatFn, options);
return output;
} catch(error) {
throw this.fsErrorToJsError_(error, path);
}
@@ -253,6 +224,11 @@ class FileApiDriverLocal {
throw new Error('Not supported');
}
async clearRoot(baseDir) {
await this.fsDriver().remove(baseDir);
await this.fsDriver().mkdir(baseDir);
}
}
module.exports = { FileApiDriverLocal };

View File

@@ -1,5 +1,6 @@
const { time } = require('lib/time-utils.js');
const fs = require('fs-extra');
const { basicDelta } = require('lib/file-api');
class FileApiDriverMemory {
@@ -144,51 +145,25 @@ class FileApiDriverMemory {
}
async delta(path, options = null) {
let limit = 3;
let output = {
hasMore: false,
context: {},
items: [],
const getStatFn = async (path) => {
let output = this.items_.slice();
for (let i = 0; i < output.length; i++) {
const item = Object.assign({}, output[i]);
item.path = item.path.substr(path.length + 1);
output[i] = item;
}
return output;
};
let context = options ? options.context : null;
let fromTime = 0;
if (context) fromTime = context.fromTime;
let sortedItems = this.items_.slice().concat(this.deletedItems_);
sortedItems.sort((a, b) => {
if (a.updated_time < b.updated_time) return -1;
if (a.updated_time > b.updated_time) return +1;
return 0;
});
let hasMore = false;
let items = [];
let maxTime = 0;
for (let i = 0; i < sortedItems.length; i++) {
let item = sortedItems[i];
if (item.updated_time >= fromTime) {
item = Object.assign({}, item);
item.path = item.path.substr(path.length + 1);
items.push(item);
if (item.updated_time > maxTime) maxTime = item.updated_time;
}
if (items.length >= limit) {
hasMore = true;
break;
}
}
output.items = items;
output.hasMore = hasMore;
output.context = { fromTime: maxTime };
const output = await basicDelta(path, getStatFn, options);
return output;
}
clearRoot() {
this.items_ = [];
return Promise.resolve();
}
}
module.exports = { FileApiDriverMemory };

View File

@@ -189,6 +189,10 @@ class FileApiDriverOneDrive {
return this.pathCache_[path];
}
clearRoot() {
throw new Error('Not implemented');
}
async delta(path, options = null) {
let output = {
hasMore: false,

View File

@@ -0,0 +1,136 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
class FileApiDriverWebDav {
constructor(api) {
this.api_ = api;
}
api() {
return this.api_;
}
async stat(path) {
try {
const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified',
'd:resourcetype',
]);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
return this.statFromResource_(resource, path);
} catch (error) {
if (error.code === 404) return null;
throw error;
}
}
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]);
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
const lastModifiedDate = new Date(lastModifiedString);
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
return {
path: path,
created_time: lastModifiedDate.getTime(),
updated_time: lastModifiedDate.getTime(),
isDir: isCollection === '',
};
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
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
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async setTimestamp(path, timestampMs) {
throw new Error('Not implemented'); // Not needed anymore
}
async delta(path, options) {
const getDirStats = async (path) => {
const result = await this.list(path);
return result.items;
};
return await basicDelta(path, getDirStats, options);
}
async list(path, options) {
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
return {
items: this.statsFromResources_(resources),
hasMore: false,
context: null,
};
}
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
return await this.api().exec('GET', path, null, null, options);
} catch (error) {
if (error.code !== 404) throw error;
}
}
async mkdir(path) {
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)
}
}
async put(path, content, options = null) {
await this.api().exec('PUT', path, content, null, options);
}
async delete(path) {
try {
await this.api().exec('DELETE', path);
} catch (error) {
if (error.code !== 404) throw error;
}
}
async move(oldPath, newPath) {
throw new Error('Not implemented');
}
format() {
throw new Error('Not supported');
}
async clearRoot() {
await this.delete('');
await this.mkdir('');
}
}
module.exports = { FileApiDriverWebDav };

View File

@@ -1,5 +1,8 @@
const { isHidden } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');
class FileApi {
@@ -10,6 +13,10 @@ class FileApi {
this.syncTargetId_ = null;
}
fsDriver() {
return shim.fsDriver();
}
driver() {
return this.driver_;
}
@@ -57,6 +64,7 @@ class FileApi {
});
}
// Deprectated
setTimestamp(path, timestampMs) {
this.logger().debug('setTimestamp ' + this.fullPath_(path));
return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
@@ -83,8 +91,13 @@ class FileApi {
return this.driver_.get(this.fullPath_(path), options);
}
put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path));
async put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path), options);
if (options && options.source === 'file') {
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);
}
@@ -93,15 +106,21 @@ class FileApi {
return this.driver_.delete(this.fullPath_(path));
}
// Deprectated
move(oldPath, newPath) {
this.logger().debug('move ' + this.fullPath_(oldPath) + ' => ' + this.fullPath_(newPath));
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
}
// Deprectated
format() {
return this.driver_.format();
}
clearRoot() {
return this.driver_.clearRoot(this.baseDir_);
}
delta(path, options = null) {
this.logger().debug('delta ' + this.fullPath_(path));
return this.driver_.delta(this.fullPath_(path), options);
@@ -109,4 +128,115 @@ class FileApi {
}
module.exports = { FileApi };
function basicDeltaContextFromOptions_(options) {
let output = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
};
if (!options || !options.context) return output;
const d = new Date(options.context.timestamp);
output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
return output;
}
// This is the basic delta algorithm, which can be used in case the cloud service does not have
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
// the file system do not.
async function basicDelta(path, getDirStatFn, options) {
const outputLimit = 1000;
const itemIds = await options.allItemIdsHandler();
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
const context = basicDeltaContextFromOptions_(options);
let newContext = {
timestamp: context.timestamp,
filesAtTimestamp: context.filesAtTimestamp.slice(),
statsCache: context.statsCache,
};
// Stats are cached until all items have been processed (until hasMore is false)
if (newContext.statsCache === null) {
newContext.statsCache = await getDirStatFn(path);
newContext.statsCache.sort(function(a, b) {
return a.updated_time - b.updated_time;
});
}
let output = [];
// Find out which files have been changed since the last time. Note that we keep
// both the timestamp of the most recent change, *and* the items that exactly match
// this timestamp. This to handle cases where an item is modified while this delta
// function is running. For example:
// t0: Item 1 is changed
// t0: Sync items - run delta function
// t0: While delta() is running, modify Item 2
// Since item 2 was modified within the same millisecond, it would be skipped in the
// next sync if we relied exclusively on a timestamp.
for (let i = 0; i < newContext.statsCache.length; i++) {
const stat = newContext.statsCache[i];
if (stat.isDir) continue;
if (stat.updated_time < context.timestamp) continue;
// Special case for items that exactly match the timestamp
if (stat.updated_time === context.timestamp) {
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
}
if (stat.updated_time > newContext.timestamp) {
newContext.timestamp = stat.updated_time;
newContext.filesAtTimestamp = [];
}
newContext.filesAtTimestamp.push(stat.path);
output.push(stat);
if (output.length >= outputLimit) break;
}
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
if (output.length + deletedItems.length >= outputLimit) break;
const itemId = itemIds[i];
let found = false;
for (let j = 0; j < newContext.statsCache.length; j++) {
const item = newContext.statsCache[j];
if (BaseItem.pathToId(item.path) == itemId) {
found = true;
break;
}
}
if (!found) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
const hasMore = output.length >= outputLimit;
if (!hasMore) newContext.statsCache = null;
return {
hasMore: hasMore,
context: newContext,
items: output,
};
}
module.exports = { FileApi, basicDelta };

View File

@@ -3,21 +3,50 @@ const { time } = require('lib/time-utils.js');
class FsDriverNode {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += '. Path: ' + path;
let output = new Error(msg);
if (error.code) output.code = error.code;
return output;
}
appendFileSync(path, string) {
return fs.appendFileSync(path, string);
}
appendFile(path, string, encoding = 'base64') {
return fs.appendFile(path, string, { encoding: encoding });
async appendFile(path, string, encoding = 'base64') {
try {
return await fs.appendFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeBinaryFile(path, content) {
let buffer = new Buffer(content);
return fs.writeFile(path, buffer);
async writeBinaryFile(path, content) {
try {
let buffer = new Buffer(content);
return await fs.writeFile(path, buffer);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeFile(path, string, encoding = 'base64') {
return fs.writeFile(path, string, { encoding: encoding });
async writeFile(path, string, encoding = 'base64') {
try {
return await fs.writeFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
// same as rm -rf
async remove(path) {
try {
return await fs.remove(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async move(source, dest) {
@@ -35,7 +64,7 @@ class FsDriverNode {
await time.sleep(1);
continue;
}
throw error;
throw this.fsErrorToJsError_(error);
}
}
@@ -77,12 +106,20 @@ class FsDriverNode {
return output;
}
open(path, mode) {
return fs.open(path, mode);
async open(path, mode) {
try {
return await fs.open(path, mode);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
close(handle) {
return fs.close(handle);
async close(handle) {
try {
return await fs.close(handle);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
readFile(path, encoding = 'utf8') {

View File

@@ -14,6 +14,11 @@ class FsDriverRN {
return RNFS.writeFile(path, string, encoding);
}
// same as rm -rf
async remove(path) {
throw new Error('Not implemented');
}
writeBinaryFile(path, content) {
throw new Error('Not implemented');
}

View File

@@ -7,6 +7,7 @@ const { mime } = require('lib/mime-utils.js');
const { filename } = require('lib/path-utils.js');
const { FsDriverDummy } = require('lib/fs-driver-dummy.js');
const { markdownUtils } = require('lib/markdown-utils.js');
const JoplinError = require('lib/JoplinError');
class Resource extends BaseItem {
@@ -35,7 +36,7 @@ class Resource extends BaseItem {
static async serialize(item, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
fieldNames.push('type_');
//fieldNames = ArrayUtils.removeElement(fieldNames, 'encryption_blob_encrypted');
return super.serialize(item, 'resource', fieldNames);
}
@@ -92,7 +93,13 @@ class Resource extends BaseItem {
const encryptedPath = this.fullPath(resource, true);
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
try {
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
} catch (error) {
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
throw error;
}
const resourceCopy = Object.assign({}, resource);
resourceCopy.encryption_blob_encrypted = 1;

View File

@@ -63,6 +63,8 @@ class Setting extends BaseModel {
'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 },
'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Set application zoom percentage'), minimum: "50", maximum: "500", step: "10"},
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
@@ -75,12 +77,22 @@ class Setting extends BaseModel {
};
}},
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.'), options: () => {
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).'), options: () => {
return SyncTargetRegistry.idAndLabelPlainObject();
}},
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem') }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => {
try {
return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem')
} catch (error) {
return false;
}
}, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
'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.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 },

View File

@@ -45,4 +45,12 @@ function toSystemSlashes(path, os) {
return path.replace(/\\/g, "/");
}
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes };
function rtrimSlashes(path) {
return path.replace(/\/+$/, '');
}
function ltrimSlashes(path) {
return path.replace(/^\/+/, '');
}
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes, rtrimSlashes, ltrimSlashes };

View File

@@ -64,6 +64,8 @@ class DecryptionWorker {
let excludedIds = [];
try {
const notLoadedMasterKeyDisptaches = [];
while (true) {
const result = await BaseItem.itemsThatNeedDecryption(excludedIds);
const items = result.items;
@@ -77,10 +79,13 @@ class DecryptionWorker {
} catch (error) {
if (error.code === 'masterKeyNotLoaded' && options.materKeyNotLoadedHandler === 'dispatch') {
excludedIds.push(item.id);
this.dispatch({
type: 'MASTERKEY_ADD_NOT_LOADED',
id: error.masterKeyId,
});
if (notLoadedMasterKeyDisptaches.indexOf(error.masterKeyId) < 0) {
this.dispatch({
type: 'MASTERKEY_ADD_NOT_LOADED',
id: error.masterKeyId,
});
notLoadedMasterKeyDisptaches.push(error.masterKeyId);
}
continue;
}
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')');

View File

@@ -378,8 +378,8 @@ class EncryptionService {
read: async (size) => {
return this.fsDriver().readFileChunk(reader.handle, size, encoding);
},
close: () => {
this.fsDriver().close(reader.handle);
close: async () => {
await this.fsDriver().close(reader.handle);
},
};
return reader;
@@ -412,9 +412,9 @@ class EncryptionService {
let source = await this.fileReader_(srcPath, 'base64');
let destination = await this.fileWriter_(destPath, 'ascii');
const cleanUp = () => {
if (source) source.close();
if (destination) destination.close();
const cleanUp = async () => {
if (source) await source.close();
if (destination) await destination.close();
source = null;
destination = null;
}
@@ -428,16 +428,16 @@ class EncryptionService {
throw error;
}
cleanUp();
await cleanUp();
}
async decryptFile(srcPath, destPath) {
let source = await this.fileReader_(srcPath, 'ascii');
let destination = await this.fileWriter_(destPath, 'base64');
const cleanUp = () => {
if (source) source.close();
if (destination) destination.close();
const cleanUp = async () => {
if (source) await source.close();
if (destination) await destination.close();
source = null;
destination = null;
}
@@ -451,7 +451,7 @@ class EncryptionService {
throw error;
}
cleanUp();
await cleanUp();
}
decodeHeaderVersion_(hexaByte) {
@@ -507,7 +507,8 @@ class EncryptionService {
}
isValidHeaderIdentifier(id, ignoreTooLongLength = false) {
if (!ignoreTooLongLength && !id || id.length !== 5) return false;
if (!id) return false;
if (!ignoreTooLongLength && id.length !== 5) return false;
return /JED\d\d/.test(id);
}
@@ -515,7 +516,7 @@ class EncryptionService {
if (!item) throw new Error('No item');
const ItemClass = BaseItem.itemClass(item);
if (!ItemClass.encryptionSupported()) return false;
return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text);
return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text, true);
}
async fileIsEncrypted(path) {

View File

@@ -4,14 +4,20 @@ const { GeolocationNode } = require('lib/geolocation-node.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { time } = require('lib/time-utils.js');
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
function shimInit() {
shim.fs = fs;
shim.fsDriver = () => { throw new Error('Not implemented') }
shim.FileApiDriverLocal = FileApiDriverLocal;
shim.Geolocation = GeolocationNode;
shim.FormData = require('form-data');
shim.sjclModule = require('lib/vendor/sjcl.js');
shim.fsDriver = () => {
if (!shim.fsDriver_) shim.fsDriver_ = new FsDriverNode();
return shim.fsDriver_;
}
shim.randomBytes = async (count) => {
const buffer = require('crypto').randomBytes(count);
return Array.from(buffer);

View File

@@ -3,6 +3,7 @@ const { GeolocationReact } = require('lib/geolocation-react.js');
const { PoorManIntervals } = require('lib/poor-man-intervals.js');
const RNFetchBlob = require('react-native-fetch-blob').default;
const { generateSecureRandom } = require('react-native-securerandom');
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
function shimInit() {
shim.Geolocation = GeolocationReact;
@@ -10,6 +11,11 @@ function shimInit() {
shim.clearInterval = PoorManIntervals.clearInterval;
shim.sjclModule = require('lib/vendor/sjcl-rn.js');
shim.fsDriver = () => {
if (!shim.fsDriver_) shim.fsDriver_ = new FsDriverRN();
return shim.fsDriver_;
}
shim.randomBytes = async (count) => {
const randomBytes = await generateSecureRandom(count);
let temp = [];
@@ -22,7 +28,19 @@ function shimInit() {
shim.fetch = async function(url, options = null) {
return shim.fetchWithRetry(() => {
return shim.nativeFetch_(url, options)
// The native fetch() throws an uncatable error if calling it with an invalid URL
// such as '//.resource' so detect if the URL is valid beforehand and throw
// a catchable error.
if (typeof url !== 'string') {
console.info('NOT A STRING: ', url);
throw new Error('shim.fetch: URL is not a string');
}
const lcUrl = url.toLowerCase();
if (lcUrl.indexOf('http:') !== 0 && lcUrl.indexOf('https:') !== 0) throw new Error('shim.fetch: Invalid URL: ' + lcUrl);
return shim.nativeFetch_(url, options);
}, options);
}

View File

@@ -107,7 +107,7 @@ shim.fetchWithRetry = async function(fetchFn, options = null) {
shim.nativeFetch_ = typeof fetch !== 'undefined' ? fetch : null;
shim.fetch = () => { throw new Error('Not implemented'); }
shim.FormData = typeof FormData !== 'undefined' ? FormData : null;
shim.fs = null;
shim.fsDriver = () => { throw new Error('Not implemented') }
shim.FileApiDriverLocal = null;
shim.readLocalFileBase64 = (path) => { throw new Error('Not implemented'); }
shim.uploadBlob = () => { throw new Error('Not implemented'); }

View File

@@ -10,7 +10,7 @@ const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
const { shim } = require('lib/shim.js');
const moment = require('moment');
const JoplinError = require('lib/JoplinError');
class Synchronizer {
@@ -27,7 +27,7 @@ class Synchronizer {
// Debug flags are used to test certain hard-to-test conditions
// such as cancelling in the middle of a loop.
this.debugFlags_ = [];
this.testingHooks_ = [];
this.onProgress_ = function(s) {};
this.progressReport_ = {};
@@ -279,7 +279,7 @@ class Synchronizer {
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
} catch (error) {
if (error && error.code === 'rejectedByTarget') {
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
await handleCannotSyncItem(syncTargetId, local, error.message);
action = null;
} else {
@@ -289,25 +289,9 @@ class Synchronizer {
}
if (action == 'createRemote' || action == 'updateRemote') {
// Make the operation atomic by doing the work on a copy of the file
// and then copying it back to the original location.
// let tempPath = this.syncDirName_ + '/' + path + '_' + time.unixMs();
//
// Atomic operation is disabled for now because it's not possible
// to do an atomic move with OneDrive (see file-api-driver-onedrive.js)
// await this.api().put(tempPath, content);
// await this.api().setTimestamp(tempPath, local.updated_time);
// await this.api().move(tempPath, path);
let canSync = true;
try {
if (this.debugFlags_.indexOf('rejectedByTarget') >= 0) {
const error = new Error('Testing rejectedByTarget');
error.code = 'rejectedByTarget';
throw error;
}
if (this.testingHooks_.indexOf('rejectedByTarget') >= 0) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
const content = await ItemClass.serializeForSync(local);
await this.api().put(path, content);
} catch (error) {
@@ -334,7 +318,12 @@ class Synchronizer {
// change is uniquely identified. Leaving it like this for now.
if (canSync) {
await this.api().setTimestamp(path, local.updated_time);
// 2018-01-21: Setting timestamp is not needed because the delta() logic doesn't rely
// on it (instead it uses a more reliable `context` object) and the itemsThatNeedSync loop
// above also doesn't use it because it fetches the whole remote object and read the
// more reliable 'updated_time' property. Basically remote.updated_time is deprecated.
// await this.api().setTimestamp(path, local.updated_time);
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
}
@@ -449,7 +438,7 @@ class Synchronizer {
this.logSyncOperation('fetchingTotal', null, null, 'Fetching delta items from sync target', remotes.length);
for (let i = 0; i < remotes.length; i++) {
if (this.cancelling() || this.debugFlags_.indexOf('cancelDeltaLoop2') >= 0) {
if (this.cancelling() || this.testingHooks_.indexOf('cancelDeltaLoop2') >= 0) {
hasCancelled = true;
break;
}
@@ -500,7 +489,7 @@ class Synchronizer {
if (action == 'createLocal' || action == 'updateLocal') {
if (content === null) {
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
this.logger().warn('Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: ' + path);
continue;
}
content = ItemClass.filter(content);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1143,6 +1143,15 @@
}
}
},
"buffer": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.0.8.tgz",
"integrity": "sha512-xXvjQhVNz50v2nPeoOsNqWCLGfiv4ji/gXZM28jnVwdLJxH4mFyqgqCKfaK9zf1KUbG6zTkjLOy7ou+jSMarGA==",
"requires": {
"base64-js": "1.2.1",
"ieee754": "1.1.8"
}
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@@ -1672,6 +1681,11 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"emitter-component": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.1.tgz",
"integrity": "sha1-Bl4tvtaVm/RwZ57avq95gdEAOrY="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
@@ -1794,6 +1808,11 @@
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz",
"integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE="
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"exec-sh": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.1.tgz",
@@ -2376,6 +2395,11 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"ieee754": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz",
"integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q="
},
"image-size": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.1.tgz",
@@ -4418,6 +4442,11 @@
"strict-uri-encode": "1.1.0"
}
},
"querystringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -5249,6 +5278,11 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
@@ -5632,6 +5666,14 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"stream": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
"integrity": "sha1-f1Nj8Ff2WSxVlfALyAon9c7B8O8=",
"requires": {
"emitter-component": "1.1.1"
}
},
"stream-buffers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
@@ -5808,6 +5850,11 @@
"resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz",
"integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM="
},
"timers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/timers/-/timers-0.1.1.tgz",
"integrity": "sha1-hqxceMHuQZaU81pY3k/UGDz7nB4="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -5954,6 +6001,15 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"utf8": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/utf8/-/utf8-2.1.2.tgz",
@@ -6194,6 +6250,22 @@
"integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=",
"dev": true
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.1.6",
"xmlbuilder": "9.0.4"
},
"dependencies": {
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
}
}
},
"xmlbuilder": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.0.0.tgz",

View File

@@ -9,6 +9,9 @@
"test": "jest"
},
"dependencies": {
"base-64": "^0.1.0",
"buffer": "^5.0.8",
"events": "^1.1.1",
"form-data": "^2.1.4",
"html-entities": "^1.2.1",
"markdown-it": "^8.4.0",
@@ -38,8 +41,12 @@
"react-navigation": "^1.0.0-beta.21",
"react-redux": "4.4.8",
"redux": "3.6.0",
"stream": "0.0.2",
"timers": "^0.1.1",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3"
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19"
},
"devDependencies": {
"babel-jest": "19.0.0",

View File

@@ -51,8 +51,10 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
// Disabled because not fully working
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
@@ -64,7 +66,7 @@ const EncryptionService = require('lib/services/EncryptionService');
let storeDispatch = function(action) {};
const generalMiddleware = store => next => async (action) => {
if (action.type !== 'SIDE_MENU_OPEN_PERCENT') reg.logger().info('Reducer action', action.type);
if (['SIDE_MENU_OPEN_PERCENT', 'SYNC_REPORT_UPDATE'].indexOf(action.type) < 0) reg.logger().info('Reducer action', action.type);
PoorManIntervals.update(); // This function needs to be called regularly so put it here
const result = next(action);

View File

@@ -80,11 +80,11 @@ async function main() {
readmeContent = readmeContent.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.apk)/, downloadUrl);
await fs.writeFile('README.md', readmeContent);
await execCommand('git add -A');
await execCommand('git commit -m "Android release v' + version + '"');
await execCommand('git tag ' + tagName);
await execCommand('git push');
await execCommand('git push --tags');
console.info(await execCommand('git add -A'));
console.info(await execCommand('git commit -m "Android release v' + version + '"'));
console.info(await execCommand('git tag ' + tagName));
console.info(await execCommand('git push'));
console.info(await execCommand('git push --tags'));
console.info('Creating GitHub release ' + tagName + '...');
@@ -93,7 +93,7 @@ async function main() {
body: JSON.stringify({
tag_name: tagName,
name: tagName,
draft: true,
draft: false,
}),
headers: {
'Content-Type': 'application/json',

View File

@@ -1,14 +1,3 @@
<!--
# Adding new features
If you want to add a new feature, consider asking about it before implementing it to
make sure it is within the scope of the project. Of course you are free to create the
pull request directly but it is not guaranteed it is going to be accepted.
# Style
- Only use tabs for indentation, not spaces.
- Do not remove or add optional characters from other lines (such as colons or new line
characters) as it can make the commit needlessly big, and create conflicts with other
changes.
PLEASE READ THE GUIDE FIRST: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
-->

View File

@@ -201,7 +201,7 @@
<div class="content">
<p>Joplin is a free, open source note taking and to-do application, which can handle a large number of notes organised into notebooks. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in <a href="https://daringfireball.net/projects/markdown/basics">Markdown format</a>.</p>
<p>Notes exported from Evernote via .enex files <a href="#importing-notes-from-evernote">can be imported</a> into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).</p>
<p>The notes can be <a href="#synchronisation">synchronised</a> with various targets including 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.</p>
<p>The notes can be <a href="#synchronisation">synchronised</a> with various targets including <a href="https://nextcloud.com/">Nextcloud</a>, 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.</p>
<p>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 <a href="https://github.com/cronvel/terminal-kit">terminal-kit</a> library, the desktop client using <a href="https://electronjs.org/">Electron</a>, and the Android client front end is done using <a href="https://facebook.github.io/react-native/">React Native</a>.</p>
<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>
@@ -243,7 +243,7 @@
<tr>
<td>Android</td>
<td><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></td>
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.78/joplin-v0.10.78.apk">Download APK File</a></td>
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.79/joplin-v0.10.79.apk">Download APK File</a></td>
</tr>
<tr>
<td>iOS</td>
@@ -267,7 +267,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<li>Desktop, mobile and terminal applications.</li>
<li>Support notes, to-dos, tags and notebooks.</li>
<li>Offline first, so the entire data is always available on the device even without an internet connection.</li>
<li>Ability to synchronise with multiple targets, including the file system and OneDrive (NextCloud and Dropbox are planned).</li>
<li>Ability to synchronise with multiple targets, including NextCloud, the file system and OneDrive (Dropbox is planned).</li>
<li>Synchronises to a plain text format, which can be easily manipulated, backed up, or exported to a different format.</li>
<li>Markdown notes, which are rendered with images and formatting in the desktop and mobile applications.</li>
<li>Tag support</li>
@@ -294,10 +294,21 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<li>Tomboy Notes: Export the notes to ENEX files <a href="https://askubuntu.com/questions/243691/how-can-i-export-my-tomboy-notes-into-evernote/608551">as described here</a> for example, and import these ENEX files into Joplin.</li>
</ul>
<h1 id="synchronisation">Synchronisation</h1>
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
<p>Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once <a href="https://github.com/facebook/react-native/issues/14445">this React Native bug</a> is fixed. 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.</p>
<p>On the <strong>desktop application</strong>, to initiate the synchronisation process, click on the &quot;Synchronise&quot; button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on &quot;Synchronise&quot; to start a synchronisation manually.</p>
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
<p>Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once <a href="https://github.com/facebook/react-native/issues/14445">this React Native bug</a> is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on &quot;Synchronise&quot; to start a synchronisation manually.</p>
<h2 id="nextcloud-synchronisation">Nextcloud synchronisation</h2>
<p><strong>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.</strong></p>
<p>On the <strong>desktop application</strong> or <strong>mobile application</strong>, go to the config screen and select Nextcloud as the synchronisation target. Then input <a href="https://docs.nextcloud.com/server/9/user_manual/files/access_webdav.html">the WebDAV URL</a>, this is normally <code>https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin</code> (make sure to create the &quot;Joplin&quot; directory in Nextcloud and to replace USERNAME by your Nextcloud username), and set the username and password.</p>
<p>On the <strong>terminal application</strong>, you will need to set the <code>sync.target</code> config variable and all the <code>sync.5.path</code>, <code>sync.5.username</code> and <code>sync.5.password</code> config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:</p>
<pre><code>:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
:config sync.target 5
</code></pre><p>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.</p>
<h2 id="onedrive-synchronisation">OneDrive synchronisation</h2>
<p>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.</p>
<p>On the <strong>desktop application</strong> or <strong>mobile application</strong>, select &quot;OneDrive&quot; as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the &quot;Synchronise&quot; button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).</p>
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
<pre><code>*/30 * * * * /path/to/joplin sync
</code></pre><h1 id="encryption">Encryption</h1>
<p>Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the <a href="http://joplin.cozic.net/help/e2ee">End-To-End Encryption Tutorial</a> for more information about this feature and how to enable it.</p>
@@ -327,8 +338,7 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<p>Please see the guide for information on how to contribute to the development of Joplin: <a href="https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md">https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md</a></p>
<h1 id="coming-features">Coming features</h1>
<ul>
<li>NextCloud support</li>
<li>All: End to end encryption</li>
<li>Mobile: manage tags</li>
<li>Windows: Tray icon</li>
<li>Desktop apps: Tag auto-complete</li>
<li>Desktop apps: Dark theme</li>

View File

@@ -12,8 +12,6 @@
- [ ] Mobile
- [ ] Terminal
*If it is a bug if possible please provide a screenshot showing the problem*
*If there is an error, please copy and paste the full error message*
---
<!--
Please the guide first! https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
-->

View File

@@ -296,9 +296,18 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
<li>Then repeat the process for each notebook that needs to be imported.</li>
</ul>
<h1 id="synchronisation">Synchronisation</h1>
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
<p>Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A Dropbox driver will also be available once <a href="https://github.com/facebook/react-native/issues/14445">this React Native bug</a> is fixed. 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.</p>
<p>To initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
<p>One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.</p>
<p>Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once <a href="https://github.com/facebook/react-native/issues/14445">this React Native bug</a> is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on &quot;Synchronise&quot; to start a synchronisation manually.</p>
<h2 id="nextcloud-synchronisation">Nextcloud synchronisation</h2>
<p>You will need to set the <code>sync.target</code> config variable and all the <code>sync.5.path</code>, <code>sync.5.username</code> and <code>sync.5.password</code> config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:</p>
<pre><code>:config sync.target 5
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
</code></pre><p>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.</p>
<h2 id="onedrive-synchronisation">OneDrive synchronisation</h2>
<p>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.</p>
<p>To initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
<pre><code>*/30 * * * * /path/to/joplin sync
</code></pre><h1 id="urls">URLs</h1>
<p>When Ctrl+Clicking a URL, most terminals will open that URL in the default browser. However, one issue, especially with long URLs, is that they can end up like this:</p>