You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
46 Commits
android-v0
...
cli-v0.10.
Author | SHA1 | Date | |
---|---|---|---|
|
5c66042a2d | ||
|
ae75181b02 | ||
|
9dc3238182 | ||
|
0a68749373 | ||
|
1519116291 | ||
|
d023d841e2 | ||
|
d7a1465d8e | ||
|
15848fc696 | ||
|
837ae2c9f2 | ||
|
6789b98ead | ||
|
29f6e74ee3 | ||
|
2780c38c45 | ||
|
4531838217 | ||
|
c6830499f7 | ||
|
d9f00a2539 | ||
|
def83c9119 | ||
|
b6cb0056c7 | ||
|
1669b5258a | ||
|
5a9e0bfc26 | ||
|
8f3fdb3afe | ||
|
7ab135c099 | ||
|
1cc27f2509 | ||
|
ef700b421c | ||
|
b9af5ac052 | ||
|
173f2d421d | ||
|
9f82c069c9 | ||
|
6ade09c228 | ||
|
5393a1399c | ||
|
fd29f20b2e | ||
|
c011b53d1f | ||
|
26e3a7b68c | ||
|
e70a291698 | ||
|
511bd57726 | ||
|
c6de8598dc | ||
|
7bee25599d | ||
|
773a1ad829 | ||
|
1a1e264fa4 | ||
|
5b99ecefca | ||
|
1bfeed377a | ||
|
86eee376bb | ||
|
6a7d368184 | ||
|
1da19ae98d | ||
|
f52c117b09 | ||
|
2551f96149 | ||
|
af50d80541 | ||
|
e355f4e49b |
@@ -1,3 +1,6 @@
|
||||
# Only build tags (Doesn't work - doesn't build anything)
|
||||
# if: tag IS present
|
||||
|
||||
rvm: 2.3.3
|
||||
|
||||
matrix:
|
||||
|
@@ -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.
|
||||
|
@@ -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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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 ""
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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 :"
|
||||
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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 "ノートの題名:"
|
||||
|
||||
|
@@ -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 ""
|
||||
|
@@ -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."
|
||||
|
@@ -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:"
|
||||
|
||||
|
@@ -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 "Название заметки:"
|
||||
|
||||
|
@@ -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 "笔记标题:"
|
||||
|
||||
|
40
CliClient/package-lock.json
generated
40
CliClient/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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": {
|
||||
|
@@ -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"
|
||||
|
@@ -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 });
|
||||
|
@@ -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) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
@@ -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');
|
||||
|
64
ElectronClient/app/package-lock.json
generated
64
ElectronClient/app/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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
20
LICENSE
@@ -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.
|
@@ -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 :
|
||||
|
38
README.md
38
README.md
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
||||
|
@@ -30,6 +30,10 @@ class BaseSyncTarget {
|
||||
return false;
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
static id() {
|
||||
throw new Error('id() not implemented');
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
54
ReactNativeClient/lib/SyncTargetNextcloud.js
Normal file
54
ReactNativeClient/lib/SyncTargetNextcloud.js
Normal 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;
|
@@ -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_;
|
||||
|
||||
|
@@ -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_) {
|
||||
|
217
ReactNativeClient/lib/WebDavApi.js
Normal file
217
ReactNativeClient/lib/WebDavApi.js
Normal 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;
|
@@ -13,7 +13,7 @@ const globalStyle = {
|
||||
fontSizeSmaller: 14,
|
||||
dividerColor: "#dddddd",
|
||||
selectedColor: '#e5e5e5',
|
||||
disabledOpacity: 0.3,
|
||||
disabledOpacity: 0.2,
|
||||
|
||||
raisedBackgroundColor: "#0080EF",
|
||||
raisedColor: "#003363",
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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 };
|
@@ -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 };
|
@@ -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,
|
||||
|
136
ReactNativeClient/lib/file-api-driver-webdav.js
Normal file
136
ReactNativeClient/lib/file-api-driver-webdav.js
Normal 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 };
|
@@ -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 };
|
@@ -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') {
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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 },
|
||||
|
@@ -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 };
|
@@ -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() + ')');
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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'); }
|
||||
|
@@ -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
72
ReactNativeClient/package-lock.json
generated
72
ReactNativeClient/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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);
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
-->
|
||||
|
@@ -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 "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.</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 "Synchronise" 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 "Joplin" 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 "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).</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>
|
||||
|
@@ -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
|
||||
-->
|
||||
|
@@ -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 "Synchronise" 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>
|
||||
|
Reference in New Issue
Block a user