1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Merge branch 'master' into math-support

This commit is contained in:
Laurent Cozic 2018-02-02 20:35:32 +00:00
commit e8416042d4
132 changed files with 6020 additions and 1039 deletions

4
.gitignore vendored
View File

@ -35,4 +35,6 @@ _vieux/
_mydocs
.DS_Store
Assets/DownloadBadges*.psd
node_modules
node_modules
Tools/github_oauth_token.txt
_releases

View File

@ -1,3 +1,6 @@
# Only build tags (Doesn't work - doesn't build anything)
if: tag IS present
rvm: 2.3.3
matrix:
@ -45,5 +48,4 @@ script:
- |
cd ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install
yarn dist
npm install && yarn dist

View File

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

View File

@ -21,6 +21,7 @@ const os = require('os');
const fs = require('fs-extra');
const { cliUtils } = require('./cli-utils.js');
const EventEmitter = require('events');
const Cache = require('lib/Cache');
class Application extends BaseApplication {
@ -34,6 +35,7 @@ class Application extends BaseApplication {
this.allCommandsLoaded_ = false;
this.showStackTraces_ = false;
this.gui_ = null;
this.cache_ = new Cache();
}
gui() {
@ -223,12 +225,8 @@ class Application extends BaseApplication {
async commandMetadata() {
if (this.commandMetadata_) return this.commandMetadata_;
const osTmpdir = require('os-tmpdir');
const storage = require('node-persist');
await storage.init({ dir: osTmpdir() + '/commandMetadata', ttl: 1000 * 60 * 60 * 24 });
let output = await storage.getItem('metadata');
if (Setting.value('env') != 'dev' && output) {
let output = await this.cache_.getItem('metadata');
if (output) {
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);
}
@ -242,7 +240,7 @@ class Application extends BaseApplication {
output[n] = cmd.metadata();
}
await storage.setItem('metadata', output);
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
this.commandMetadata_ = output;
return Object.assign({}, this.commandMetadata_);

View File

@ -4,6 +4,7 @@ var Folder = require('lib/models/Folder.js');
var Tag = require('lib/models/Tag.js');
var { cliUtils } = require('./cli-utils.js');
var yargParser = require('yargs-parser');
var fs = require('fs-extra');
async function handleAutocompletionPromise(line) {
// Auto-complete the command name
@ -48,7 +49,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) {
@ -71,8 +72,10 @@ async function handleAutocompletionPromise(line) {
let argName = cmdUsage[positionalArgs - 1];
argName = cliUtils.parseCommandArg(argName).name;
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
const currentFolder = app().currentFolder();
if (argName == 'note' || argName == 'note-pattern') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
l.push(...notes.map((n) => n.title));
}
@ -81,11 +84,22 @@ async function handleAutocompletionPromise(line) {
l.push(...folders.map((n) => n.title));
}
if (argName == 'item') {
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
const folders = await Folder.search({ titlePattern: next + '*' });
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
}
if (argName == 'tag') {
let tags = await Tag.search({ titlePattern: next + '*' });
l.push(...tags.map((n) => n.title));
}
if (argName == 'file') {
let files = await fs.readdir('.');
l.push(...files);
}
if (argName == 'tag-command') {
let c = filterList(['add', 'remove', 'list'], next);
l.push(...c);

View File

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

View File

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

View File

@ -81,7 +81,9 @@ class Command extends BaseCommand {
const termState = app().gui().termSaveState();
const spawnSync = require('child_process').spawnSync;
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
app().gui().termRestoreState(termState);
app().gui().hideModalOverlay();

View File

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

View File

@ -23,12 +23,14 @@ const { Logger } = require('lib/logger.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
const { _ } = require('lib/locale.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const EncryptionService = require('lib/services/EncryptionService');
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.
@ -64,6 +66,53 @@ process.stdout.on('error', function( err ) {
}
});
// async function main() {
// const WebDavApi = require('lib/WebDavApi');
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
// const driver = new FileApiDriverWebDav(api);
// const stat = await driver.stat('');
// console.info(stat);
// // const stat = await driver.stat('testing.txt');
// // console.info(stat);
// // const content = await driver.get('testing.txta');
// // console.info(content);
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
// // console.info(content);
// // const content = await driver.mkdir('newdir5');
// // console.info(content);
// //await driver.put('myfile4.md', 'this is my content');
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
// // await driver.delete('myfile4.md');
// // const deltaResult = await driver.delta('', {
// // allItemIdsHandler: () => { return []; }
// // });
// // console.info(deltaResult);
// }
// main().catch((error) => { console.error(error); });
application.start(process.argv).catch((error) => {
console.error(_('Fatal error:'));
console.error(error);

View File

@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Samuel Blickle <blickle.samuel@gmail.com>\n"
"Last-Translator: Tobias Strobel <git@strobeltobias.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
@ -50,7 +50,7 @@ msgstr ""
"soll."
msgid "Set a to-do as completed / not completed"
msgstr "Ein To-Do as abgeschlossen / nicht abgeschlossen markieren"
msgstr "Ein To-Do als abgeschlossen / nicht abgeschlossen markieren"
#, fuzzy
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
@ -117,7 +117,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Der Befehl \"%s\" ist nur im GUI Modus verfügbar"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Kann verschlüsseltes Objekt nicht ändern"
#, javascript-format
msgid "Missing required argument: %s"
@ -187,31 +187,34 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Verwaltet die E2EE-Konfiguration. Die Befehle sind `enable`, `disable`, "
"`decrypt`, `status` und `target-status`."
msgid "Enter master password:"
msgstr ""
msgstr "Master-Passwort eingeben:"
msgid "Operation cancelled"
msgstr ""
msgstr "Vorgang abgebrochen"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Entschlüsselung starten.... Warte bitte, da es einige Minuten dauern kann, "
"je nachdem, wie viel es zu entschlüsseln gibt."
msgid "Completed decryption."
msgstr ""
msgstr "Entschlüsselung abgeschlossen."
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgstr "Aktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgstr "Die Verschlüsselung ist: %s"
msgid "Edit note."
msgstr "Notiz bearbeiten."
@ -234,6 +237,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 "Fehler beim Öffnen der Notiz im Editor: %s"
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@ -263,13 +270,12 @@ msgstr "Zeigt die Nutzungsstatistik an."
msgid "Shortcuts are not available in CLI mode."
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
#, fuzzy
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
"Tippe `help [Befehl]` ein, um mehr Informationen über einen Befehl zu "
"erhalten."
"Tippe `help [Befehl]` für weitere Informationen über einen Befehl; oder "
"tippe `help all` für die vollständigen Informationen zur Befehlsverwendung."
msgid "The possible commands are:"
msgstr "Mögliche Befehle sind:"
@ -470,8 +476,19 @@ 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 ""
"Keine Authentifizierung mit %s. Gib bitte alle fehlenden Zugangsdaten an."
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
msgstr "Synchronisation wird bereits ausgeführt."
#, javascript-format
msgid ""
@ -483,12 +500,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)"
@ -500,9 +511,8 @@ msgid "Starting synchronisation..."
msgstr "Starte Synchronisation..."
msgid "Cancelling... Please wait."
msgstr "Breche ab... Bitte warten."
msgstr "Abbrechen... Bitte warten."
#, fuzzy
msgid ""
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
"[tag] from [note], or to list the notes associated with [tag]. The command "
@ -511,7 +521,7 @@ msgstr ""
"<tag-command> kann \"add\", \"remove\" or \"list\" sein, um eine "
"[Markierung] zu [Notiz] zuzuweisen oder zu entfernen, oder um mit "
"[Markierung] markierte Notizen anzuzeigen. Mit dem Befehl `tag list` können "
"alle Notizen angezeigt werden."
"alle Markierungen angezeigt werden."
#, javascript-format
msgid "Invalid command: \"%s\""
@ -546,7 +556,7 @@ msgid "%s %s (%s)"
msgstr "%s %s (%s)"
msgid "Enum"
msgstr ""
msgstr "Aufzählung"
#, javascript-format
msgid "Type: %s."
@ -612,13 +622,14 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Ein oder mehrere Objekte sind derzeit verschlüsselt und es kann erforderlich "
"sein, ein Master-Passwort zu hinterlegen. Gib dazu bitte `e2ee decrypt` ein. "
"Wenn du das Passwort bereits eingegeben hast, werden die verschlüsselten "
"Objekte im Hintergrund entschlüsselt und stehen in Kürze zur Verfügung."
msgid "File"
msgstr "Datei"
msgid "Save"
msgstr "Speichern"
msgid "New note"
msgstr "Neue Notiz"
@ -659,11 +670,10 @@ msgid "Synchronisation status"
msgstr "Status der Synchronisation"
msgid "Encryption options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
#, fuzzy
msgid "General Options"
msgstr "Optionen"
msgstr "Allgemeine Einstellungen"
msgid "Help"
msgstr "Hilfe"
@ -671,6 +681,9 @@ msgstr "Hilfe"
msgid "Website and documentation"
msgstr "Webseite und Dokumentation"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Über Joplin"
@ -678,21 +691,61 @@ msgstr "Über Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Auf %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Abbrechen"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen gespeichert in: %s"
msgid "Save"
msgstr "Speichern"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Durch die Deaktivierung der Verschlüsselung werden *alle* Notizen und "
"Anhänge neu synchronisiert und unverschlüsselt an das Synchronisierungsziel "
"gesendet. Möchtest du fortfahren?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@ -700,15 +753,20 @@ msgid ""
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
"Durch das Aktivieren der Verschlüsselung werden alle Notizen und Anhänge neu "
"synchronisiert und verschlüsselt an das Synchronisationsziel gesendet. Achte "
"darauf, dass du das Passwort nicht verlierst, da dies aus Sicherheitsgründen "
"die einzige Möglichkeit ist, deine Daten zu entschlüsseln! Um die "
"Verschlüsselung zu aktivieren, gib bitte unten dein Passwort ein."
msgid "Disable encryption"
msgstr ""
msgstr "Verschlüsselung deaktivieren"
msgid "Enable encryption"
msgstr ""
msgstr "Verschlüsselung aktivieren"
msgid "Master Keys"
msgstr ""
msgstr "Hauptschlüssel"
msgid "Active"
msgstr "Aktiv"
@ -741,11 +799,21 @@ msgstr ""
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
"Notizbücher ursprünglich verschlüsselt wurden."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hauptschlüssel"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
msgid "Encryption is:"
msgstr ""
msgstr "Die Verschlüsselung ist:"
msgid "Back"
msgstr "Zurück"
@ -760,15 +828,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Bitte erstelle zuerst ein Notizbuch."
msgid "Note title:"
msgstr "Notizen Titel:"
msgid "Please create a notebook first"
msgstr "Bitte erstelle zuerst ein Notizbuch"
msgid "To-do title:"
msgstr "To-Do Titel:"
msgid "Notebook title:"
msgstr "Notizbuch Titel:"
@ -793,12 +855,11 @@ msgstr "Manche Objekte können nicht synchronisiert werden."
msgid "View them now"
msgstr "Zeige sie jetzt an"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Kann Synchronisierer nicht initialisieren."
msgstr "Einige Objekte können nicht entschlüsselt werden."
msgid "Set the password"
msgstr ""
msgstr "Setze ein Passwort"
msgid "Add or remove tags"
msgstr "Markierungen hinzufügen oder entfernen"
@ -837,7 +898,7 @@ msgid "Refresh"
msgstr "Aktualisieren"
msgid "Clear"
msgstr ""
msgstr "Leeren"
msgid "OneDrive Login"
msgstr "OneDrive Login"
@ -852,7 +913,7 @@ msgid "Synchronisation Status"
msgstr "Synchronisations Status"
msgid "Encryption Options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
msgid "Remove this tag from all the notes?"
msgstr "Diese Markierung von allen Notizen entfernen?"
@ -890,12 +951,18 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system"
msgstr "Dateisystem"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Nur für Tests)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Unbekanntes Log Level: %s"
@ -954,12 +1021,16 @@ msgstr "Lokale Objekte gelöscht: %d."
msgid "Deleted remote items: %d."
msgstr "Remote Objekte gelöscht: %d."
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Geladene Objekte: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Status: \"%s\"."
msgid "Cancelling..."
msgstr "Breche ab..."
msgstr "Abbrechen..."
#, javascript-format
msgid "Completed: %s"
@ -970,11 +1041,10 @@ msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid "Encrypted"
msgstr ""
msgstr "Verschlüsselt"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Manche Objekte können nicht synchronisiert werden."
msgstr "Verschlüsselte Objekte können nicht verändert werden."
msgid "Conflicts"
msgstr "Konflikte"
@ -1037,6 +1107,30 @@ msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Erstellt ein neues To-Do."
#, fuzzy
msgid "Focus title"
msgstr "Notiz Titel:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Erstellt eine neue Notiz."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Einstellen des Anwendungszooms"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Synchronisation interval"
msgstr "Synchronisationsinterval"
@ -1052,9 +1146,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"
@ -1062,12 +1153,12 @@ 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."
"Das Ziel, mit dem synchronisiert werden soll. Jedes Synchronisationsziel "
"kann zusätzliche Parameter haben, die als `sync.NUM.NAME` (alle unten "
"dokumentiert) bezeichnet werden."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@ -1076,8 +1167,29 @@ msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
"Der Pfad, mit dem synchronisiert werden soll, wenn die Dateisystem-"
"Synchronisation aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Nexcloud Benutzername"
msgid "Nexcloud password"
msgstr "Nexcloud Passwort"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Nexcloud Benutzername"
#, fuzzy
msgid "WebDAV password"
msgstr "Setze ein Passwort"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@ -1086,15 +1198,18 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s %s (%s)"
msgstr "%s (%s): %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)"
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
@ -1138,13 +1253,13 @@ msgid "Delete these notes?"
msgstr "Sollen diese Notizen gelöscht werden?"
msgid "Log"
msgstr "Log"
msgstr "Protokoll"
msgid "Export Debug Report"
msgstr "Fehlerbreicht exportieren"
msgstr "Fehlerbericht exportieren"
msgid "Encryption Config"
msgstr ""
msgstr "Verschlüsselungskonfiguration"
msgid "Configuration"
msgstr "Konfiguration"
@ -1157,7 +1272,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Tippe hier, um das Entschlüsselungspasswort festzulegen."
msgid "Select date"
msgstr "Datum auswählen"
@ -1170,22 +1285,20 @@ msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Master Key %s"
msgstr ""
msgstr "Hauptschlüssel %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Erstellt: %d."
msgstr "Erstellt: %s"
#, fuzzy
msgid "Password:"
msgstr "Passwort"
msgstr "Passwort:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Passwort darf nicht leer sein"
#, fuzzy
msgid "Enable"
msgstr "Deaktiviert"
msgstr "Aktivieren"
#, javascript-format
msgid "The notebook could not be saved: %s"
@ -1194,6 +1307,12 @@ msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
msgid "Edit notebook"
msgstr "Notizbuch bearbeiten"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Diese Notiz wurde verändert:"
@ -1250,6 +1369,17 @@ 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 "To-do title:"
#~ msgstr "To-Do Titel:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""

View File

@ -214,6 +214,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@ -413,6 +417,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@ -530,9 +538,6 @@ msgstr ""
msgid "File"
msgstr ""
msgid "Save"
msgstr ""
msgid "New note"
msgstr ""
@ -584,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@ -591,16 +599,52 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -650,6 +694,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@ -667,15 +720,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr ""
msgid "Note title:"
msgstr ""
msgid "Please create a notebook first"
msgstr ""
msgid "To-do title:"
msgstr ""
msgid "Notebook title:"
msgstr ""
@ -791,12 +838,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@ -847,6 +900,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@ -925,6 +982,27 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@ -940,9 +1018,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@ -950,8 +1025,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)"
@ -962,6 +1037,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@ -1073,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@ -224,6 +224,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@ -449,6 +453,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@ -462,10 +474,6 @@ msgstr ""
"curso, puedes eliminar el archivo de bloqueo en \"%s\" y reanudar la "
"operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@ -586,9 +594,6 @@ msgstr ""
msgid "File"
msgstr "Archivo"
msgid "Save"
msgstr ""
msgid "New note"
msgstr "Nueva nota"
@ -642,6 +647,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentacion"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@ -649,16 +657,53 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "Ok"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -710,6 +755,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Estatus"
@ -729,15 +783,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Por favor crea una libreta primero."
msgid "Note title:"
msgstr "Título de nota:"
msgid "Please create a notebook first"
msgstr "Por favor crea una libreta primero"
msgid "To-do title:"
msgstr "Títuto de lista de tareas:"
msgid "Notebook title:"
msgstr "Título de libreta:"
@ -860,6 +908,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@ -867,6 +918,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev(Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@ -927,6 +981,10 @@ msgstr "Artículos locales borrados: %d."
msgid "Deleted remote items: %d."
msgstr "Artículos remotos borrados: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Artículos locales creados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: \"%s\"."
@ -1011,6 +1069,30 @@ msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
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"
@ -1026,22 +1108,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 ""
@ -1053,6 +1129,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor inválido de opción: \"%s\". Los válores inválidos son: %s."
@ -1170,6 +1264,12 @@ msgstr "Esta libreta no pudo ser guardada: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@ -1227,6 +1327,17 @@ 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 "To-do title:"
#~ msgstr "Títuto de lista de tareas:"
#~ msgid "Delete notebook?"
#~ msgstr "Eliminar libreta?"

View File

@ -225,6 +225,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@ -458,6 +462,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@ -471,10 +483,6 @@ msgstr ""
"sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar "
"la operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@ -601,9 +609,6 @@ msgstr ""
msgid "File"
msgstr "Archivo"
msgid "Save"
msgstr "Guardar"
msgid "New note"
msgstr "Nota nueva"
@ -656,6 +661,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentación"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@ -663,16 +671,53 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Las notas y los ajustes se guardan en: %s"
msgid "Save"
msgstr "Guardar"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -722,6 +767,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Estado"
@ -739,15 +793,9 @@ msgstr "Se creará la nueva libreta «%s» y se importará en ella el archivo «
msgid "Please create a notebook first."
msgstr "Cree primero una libreta."
msgid "Note title:"
msgstr "Título de la nota:"
msgid "Please create a notebook first"
msgstr "Por favor crea una libreta primero"
msgid "To-do title:"
msgstr "Títuto de lista de tareas:"
msgid "Notebook title:"
msgstr "Título de libreta:"
@ -864,12 +912,18 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@ -928,6 +982,10 @@ msgstr "Elementos locales borrados: %d."
msgid "Deleted remote items: %d."
msgstr "Elementos remotos borrados: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Elementos locales creados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: «%s»."
@ -1010,6 +1068,30 @@ msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de la nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
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"
@ -1025,9 +1107,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"
@ -1035,11 +1114,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)"
@ -1051,6 +1128,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Opción inválida: «%s». Los valores posibles son: %s."
@ -1163,6 +1258,12 @@ msgstr "No se ha podido guardar esta libreta: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@ -1217,6 +1318,16 @@ 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 "To-do title:"
#~ msgstr "Títuto de lista de tareas:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "«%s»: «%s»"

View File

@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Last-Translator: Laurent Cozic\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
@ -178,7 +178,7 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Gérer la configuration E2EE (Encryption de bout à bout). Les commandes sont "
"Gérer la configuration E2EE (Cryptage de bout à bout). Les commandes sont "
"`enable`, `disable`, `decrypt` et `status` et `target-status`."
msgid "Enter master password:"
@ -205,7 +205,7 @@ msgstr "Désactivé"
#, javascript-format
msgid "Encryption is: %s"
msgstr "L'encryptage est : %s"
msgstr "Le cryptage est : %s"
msgid "Edit note."
msgstr "Éditer la note."
@ -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)"
@ -603,9 +613,6 @@ msgstr ""
msgid "File"
msgstr "Fichier"
msgid "Save"
msgstr "Enregistrer"
msgid "New note"
msgstr "Nouvelle note"
@ -646,7 +653,7 @@ msgid "Synchronisation status"
msgstr "État de la synchronisation"
msgid "Encryption options"
msgstr "Optons d'encryptage"
msgstr "Options de cryptage"
msgid "General Options"
msgstr "Options générales"
@ -657,6 +664,9 @@ msgstr "Aide"
msgid "Website and documentation"
msgstr "Documentation en ligne"
msgid "Check for updates..."
msgstr "Vérifier les mises à jour..."
msgid "About Joplin"
msgstr "A propos de Joplin"
@ -664,24 +674,63 @@ msgstr "A propos de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, javascript-format
msgid "Open %s"
msgstr "Ouvrir %s"
msgid "Exit"
msgstr "Quitter"
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annulation"
msgstr "Annuler"
msgid "Error"
msgstr "Erreur"
msgid "An update is available, do you want to update now?"
msgstr ""
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Impossible de télécharger la mise à jour : %s"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
msgid "New version downloaded - application will quit now and update..."
msgstr ""
"La nouvelle version a été téléchargée - le programme va se fermer et se "
"mettre à jour..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Impossible d'installer la mise à jour : %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Les notes et paramètres se trouve dans : %s"
msgid "Save"
msgstr "Enregistrer"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Désactiver l'encryptage signifie que *toutes* les notes et fichiers vont "
"être re-synchronisés et envoyés décryptés sur la cible de la "
"synchronisation. Souhaitez vous continuer ?"
"Désactiver le cryptage signifie que *toutes* les notes et fichiers vont être "
"re-synchronisés et envoyés décryptés sur la cible de la synchronisation. "
"Souhaitez vous continuer ?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@ -689,17 +738,17 @@ msgid ""
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
"Activer l'encryptage signifie que *toutes* les notes et fichiers vont être "
"re-synchronisés et envoyés encrypté vers la cible de la synchronisation. Ne "
"Activer le cryptage signifie que *toutes* les notes et fichiers vont être re-"
"synchronisés et envoyés cryptés vers la cible de la synchronisation. Ne "
"perdez pas votre mot de passe car, pour des raisons de sécurité, ce sera la "
"*seule* façon de décrypter les données ! Pour activer l'encryptage, veuillez "
"*seule* façon de décrypter les données ! Pour activer le cryptage, veuillez "
"entrer votre mot de passe ci-dessous."
msgid "Disable encryption"
msgstr "Désactiver l'encryptage"
msgstr "Désactiver le cryptage"
msgid "Enable encryption"
msgstr "Activer l'encryptage"
msgstr "Activer le cryptage"
msgid "Master Keys"
msgstr "Clefs maître"
@ -730,16 +779,28 @@ msgid ""
"as \"active\"). Any of the keys might be used for decryption, depending on "
"how the notes or notebooks were originally encrypted."
msgstr ""
"Note : seule une clef maître va être utilisée pour l'encryptage (celle "
"Note : seule une clef maître va être utilisée pour le cryptage (celle "
"marquée comme \"actif\" ci-dessus). N'importe quel clef peut-être utilisée "
"pour le décryptage, selon la façon dont les notes ou carnets étaient "
"encryptés à l'origine."
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
"à l'origine."
msgid "Missing Master Keys"
msgstr "Clefs maître manquantes"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
"probable qu'elle vont être prochainement disponible via la synchronisation."
msgid "Status"
msgstr "État"
msgid "Encryption is:"
msgstr "L'encryptage est :"
msgstr "Le cryptage est :"
msgid "Back"
msgstr "Retour"
@ -754,15 +815,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Veuillez d'abord sélectionner un carnet."
msgid "Note title:"
msgstr "Titre de la note :"
msgid "Please create a notebook first"
msgstr "Veuillez d'abord créer un carnet d'abord"
msgid "To-do title:"
msgstr "Titre de la tâche :"
msgid "Notebook title:"
msgstr "Titre du carnet :"
@ -776,7 +831,7 @@ msgid "Rename notebook:"
msgstr "Renommer le carnet :"
msgid "Set alarm:"
msgstr "Définir ou modifier alarme :"
msgstr "Régler alarme :"
msgid "Layout"
msgstr "Disposition"
@ -823,7 +878,7 @@ msgid "Attach file"
msgstr "Attacher un fichier"
msgid "Set alarm"
msgstr "Définir ou modifier alarme"
msgstr "Régler alarme"
msgid "Refresh"
msgstr "Rafraîchir"
@ -844,7 +899,7 @@ msgid "Synchronisation Status"
msgstr "État de la synchronisation"
msgid "Encryption Options"
msgstr "Options d'encryptage"
msgstr "Options de cryptage"
msgid "Remove this tag from all the notes?"
msgstr "Enlever cette étiquette de toutes les notes ?"
@ -882,12 +937,18 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
msgid "WebDAV (Beta)"
msgstr "WebDAV (Bêta)"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@ -946,6 +1007,10 @@ msgstr "Objets supprimés localement : %d."
msgid "Deleted remote items: %d."
msgstr "Objets distants supprimés : %d."
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Téléchargés : %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "État : \"%s\"."
@ -1026,6 +1091,27 @@ msgstr "Tâches non-terminées en haut des listes"
msgid "Save geo-location with notes"
msgstr "Enregistrer l'emplacement avec les notes"
msgid "When creating a new to-do:"
msgstr "Lors de la création d'une tâche :"
msgid "Focus title"
msgstr "Curseur sur le titre"
msgid "Focus body"
msgstr "Curseur sur corps du message"
msgid "When creating a new note:"
msgstr "Lors de la création d'une note :"
msgid "Show tray icon"
msgstr "Afficher icône dans la zone de notifications"
msgid "Set application zoom percentage"
msgstr "Niveau de zoom"
msgid "Automatically update the application"
msgstr "Mettre à jour le logiciel automatiquement"
msgid "Synchronisation interval"
msgstr "Intervalle de synchronisation"
@ -1041,9 +1127,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"
@ -1051,11 +1134,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)"
@ -1067,6 +1151,24 @@ 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"
msgid "WebDAV URL"
msgstr "WebDAV : URL"
msgid "WebDAV username"
msgstr "WebDAV : Nom utilisateur"
msgid "WebDAV password"
msgstr "WebDAV : Mot de passe"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
@ -1135,7 +1237,7 @@ msgid "Export Debug Report"
msgstr "Exporter rapport de débogage"
msgid "Encryption Config"
msgstr "Config encryptage"
msgstr "Config cryptage"
msgid "Configuration"
msgstr "Configuration"
@ -1183,6 +1285,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
msgid "Edit notebook"
msgstr "Éditer le carnet"
msgid "Show all"
msgstr "Afficher tous"
msgid "Errors only"
msgstr "Erreurs seulement"
msgid "This note has been modified:"
msgstr "Cette note a été modifiée :"
@ -1212,10 +1320,10 @@ msgid "Hide metadata"
msgstr "Cacher les métadonnées"
msgid "Show metadata"
msgstr "Afficher les métadonnées"
msgstr "Voir métadonnées"
msgid "View on map"
msgstr "Voir emplacement sur carte"
msgstr "Voir sur carte"
msgid "Delete notebook"
msgstr "Supprimer le carnet"
@ -1238,6 +1346,16 @@ 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 "To-do title:"
#~ msgstr "Titre de la tâche :"
#~ msgid "Delete notebook?"
#~ msgstr "Supprimer le carnet ?"
@ -1299,9 +1417,6 @@ msgstr "Bienvenue"
#~ msgid "Todo filter"
#~ msgstr "Filtre des tâches"
#~ msgid "Show all"
#~ msgstr "Afficher tous"
#~ msgid "Non-completed and recently completed ones"
#~ msgstr "Tâches non-complétées et récentes"

View File

@ -235,6 +235,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Počinjem uređivati bilješku. Za povratak u naredbeni redak, zatvori uređivač."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Bilješka je spremljena."
@ -466,6 +470,16 @@ msgstr "Sinkronizira sa udaljenom pohranom podataka."
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)"
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sinkronizacija je već u toku."
@ -477,12 +491,6 @@ msgid ""
msgstr ""
"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi..."
#, fuzzy
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Meta sinkronizacije: %s (%s)"
@ -609,9 +617,6 @@ msgstr ""
msgid "File"
msgstr "Datoteka"
msgid "Save"
msgstr "Spremi"
msgid "New note"
msgstr "Nova bilješka"
@ -664,6 +669,9 @@ msgstr "Pomoć"
msgid "Website and documentation"
msgstr "Website i dokumentacija"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "O Joplinu"
@ -671,16 +679,53 @@ msgstr "O Joplinu"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "On %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "U redu"
msgid "Cancel"
msgstr "Odustani"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Bilješke i postavke su pohranjene u: %s"
msgid "Save"
msgstr "Spremi"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -730,6 +775,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@ -749,15 +803,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Prvo stvori bilježnicu."
msgid "Note title:"
msgstr "Naslov bilješke:"
msgid "Please create a notebook first"
msgstr "Prvo stvori bilježnicu"
msgid "To-do title:"
msgstr "Naslov zadatka:"
msgid "Notebook title:"
msgstr "Naslov bilježnice:"
@ -874,12 +922,18 @@ msgstr "Nepoznata zastavica: %s"
msgid "File system"
msgstr "Datotečni sustav"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Samo za testiranje)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nepoznata razina logiranja: %s"
@ -936,6 +990,10 @@ msgstr "Obrisane lokalne stavke: %d."
msgid "Deleted remote items: %d."
msgstr "Obrisane udaljene stavke: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Stvorene lokalne stavke: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Stanje: \"%s\"."
@ -1017,6 +1075,30 @@ msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Save geo-location with notes"
msgstr "Spremi geolokacijske podatke sa bilješkama"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Stvara novi zadatak."
#, fuzzy
msgid "Focus title"
msgstr "Naslov bilješke:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Stvara novu bilješku."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Automatsko instaliranje nove verzije"
msgid "Synchronisation interval"
msgstr "Interval sinkronizacije"
@ -1032,9 +1114,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"
@ -1042,11 +1121,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)"
@ -1058,6 +1135,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
@ -1170,6 +1265,12 @@ msgstr "Bilježnicu nije moguće snimiti: %s"
msgid "Edit notebook"
msgstr "Uredi bilježnicu"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Bilješka je promijenjena:"
@ -1223,5 +1324,15 @@ 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 "To-do title:"
#~ msgstr "Naslov zadatka:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""

View File

@ -226,6 +226,10 @@ msgstr "Non esiste la nota: \"%s\". Desideri crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Comincia a modificare la nota. Chiudi l'editor per tornare al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota è stata salvata."
@ -449,6 +453,16 @@ msgstr ""
"Sincronizza con l'obiettivo fornito (come predefinito il valore di "
"configurazione sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "La sincronizzazione è in corso."
@ -462,12 +476,6 @@ msgstr ""
"sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e "
"riprendere l'operazione."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Autenticazione non completata (non è stato ricevuto alcun token di "
"autenticazione)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Posizione di sincronizzazione: %s (%s)"
@ -587,9 +595,6 @@ msgstr ""
msgid "File"
msgstr "File"
msgid "Save"
msgstr ""
msgid "New note"
msgstr "Nuova nota"
@ -642,6 +647,9 @@ msgstr "Aiuto"
msgid "Website and documentation"
msgstr "Sito web e documentazione"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Informazione si Joplin"
@ -649,16 +657,53 @@ msgstr "Informazione si Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Su %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancella"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -710,6 +755,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Stato"
@ -727,15 +781,9 @@ msgstr "Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato"
msgid "Please create a notebook first."
msgstr "Per favore prima crea un blocco note."
msgid "Note title:"
msgstr "Titolo della Nota:"
msgid "Please create a notebook first"
msgstr "Per favore prima crea un blocco note"
msgid "To-do title:"
msgstr "Titolo dell'attività:"
msgid "Notebook title:"
msgstr "Titolo del blocco note:"
@ -854,12 +902,18 @@ msgstr "Etichetta sconosciuta: %s"
msgid "File system"
msgstr "File system"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (solo per test)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Livello di log sconosciuto: %s"
@ -918,6 +972,10 @@ msgstr "Elementi locali eliminati: %d."
msgid "Deleted remote items: %d."
msgstr "Elementi remoti eliminati: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Elementi locali creati: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Stato: \"%s\"."
@ -999,6 +1057,30 @@ msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Save geo-location with notes"
msgstr "Salva geo-localizzazione con le note"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nuova attività."
#, fuzzy
msgid "Focus title"
msgstr "Titolo della Nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nuova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Aggiorna automaticamente l'applicazione"
msgid "Synchronisation interval"
msgstr "Intervallo di sincronizzazione"
@ -1014,9 +1096,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"
@ -1024,12 +1103,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 ""
@ -1041,6 +1117,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
@ -1153,6 +1247,12 @@ msgstr "Il blocco note non può essere salvato: %s"
msgid "Edit notebook"
msgstr "Modifica blocco note"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Questa note è stata modificata:"
@ -1208,6 +1308,17 @@ 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 "To-do title:"
#~ msgstr "Titolo dell'attività:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""

View File

@ -223,6 +223,10 @@ msgstr "\"%s\"というノートはありません。お作りいたしますか
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "ノートの編集の開始。エディタを閉じると元の画面に戻ることが出来ます。"
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "ノートは保存されました。"
@ -446,6 +450,14 @@ msgstr "リモート保存領域と同期します。"
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "指定のターゲットと同期します。(標準: sync.targetの設定値)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "認証は完了していません(認証トークンが得られませんでした)"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "同期はすでに実行中です。"
@ -458,10 +470,6 @@ msgstr ""
"ロックファイルがすでに保持されています。同期作業が行われていない場合は、\"%s"
"\"にあるロックファイルを削除して、作業を再度行ってください。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "認証は完了していません(認証トークンが得られませんでした)"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同期先: %s (%s)"
@ -586,9 +594,6 @@ msgstr ""
msgid "File"
msgstr "ファイル"
msgid "Save"
msgstr "保存"
msgid "New note"
msgstr "新しいノート"
@ -641,6 +646,9 @@ msgstr "ヘルプ"
msgid "Website and documentation"
msgstr "Webサイトとドキュメント"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Joplinについて"
@ -648,16 +656,52 @@ msgstr "Joplinについて"
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr "キャンセル"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "ノートと設定は、%sに保存されます。"
msgid "Save"
msgstr "保存"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -711,6 +755,15 @@ msgstr ""
"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に"
"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。"
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状態"
@ -730,15 +783,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "ますはノートブックを作成して下さい。"
msgid "Note title:"
msgstr "ノートの題名:"
msgid "Please create a notebook first"
msgstr "ますはノートブックを作成して下さい。"
msgid "To-do title:"
msgstr "ToDoの題名:"
msgid "Notebook title:"
msgstr "ノートブックの題名:"
@ -855,12 +902,18 @@ msgstr "不明なフラグ: %s"
msgid "File system"
msgstr "ファイルシステム"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@ -919,6 +972,10 @@ msgstr "ローカルアイテムの削除: %d."
msgid "Deleted remote items: %d."
msgstr "リモートアイテムの削除: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "ローカルアイテムの作成: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "状態: \"%s\"。"
@ -1002,6 +1059,30 @@ msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "新しいToDoを作成します。"
#, fuzzy
msgid "Focus title"
msgstr "ノートの題名:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "あたらしいノートを作成します。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Synchronisation interval"
msgstr "同期間隔"
@ -1017,9 +1098,6 @@ msgstr "%d 時間"
msgid "%d hours"
msgstr "%d 時間"
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Show advanced options"
msgstr "詳細な設定の表示"
@ -1027,11 +1105,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 "同期先のディレクトリ(絶対パス)"
@ -1043,6 +1119,24 @@ msgstr ""
"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく"
"ださい。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "無効な設定値: \"%s\"。有効な値は: %sです。"
@ -1155,6 +1249,12 @@ msgstr "ノートブックは保存できませんでした:%s"
msgid "Edit notebook"
msgstr "ノートブックの編集"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "ノートは変更されています:"
@ -1209,3 +1309,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 "To-do title:"
#~ msgstr "ToDoの題名:"

View File

@ -214,6 +214,10 @@ msgstr ""
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr ""
@ -413,6 +417,14 @@ msgstr ""
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr ""
@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@ -530,9 +538,6 @@ msgstr ""
msgid "File"
msgstr ""
msgid "Save"
msgstr ""
msgid "New note"
msgstr ""
@ -584,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@ -591,16 +599,52 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -650,6 +694,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr ""
@ -667,15 +720,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr ""
msgid "Note title:"
msgstr ""
msgid "Please create a notebook first"
msgstr ""
msgid "To-do title:"
msgstr ""
msgid "Notebook title:"
msgstr ""
@ -791,12 +838,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@ -847,6 +900,10 @@ msgstr ""
msgid "Deleted remote items: %d."
msgstr ""
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr ""
#, javascript-format
msgid "State: \"%s\"."
msgstr ""
@ -925,6 +982,27 @@ msgstr ""
msgid "Save geo-location with notes"
msgstr ""
msgid "When creating a new to-do:"
msgstr ""
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
msgid "When creating a new note:"
msgstr ""
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Synchronisation interval"
msgstr ""
@ -940,9 +1018,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@ -950,8 +1025,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)"
@ -962,6 +1037,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr ""
@ -1073,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

1350
CliClient/locales/nl_BE.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -223,6 +223,10 @@ msgstr "A nota não existe: \"%s\". Criar?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Começando a editar a nota. Feche o editor para voltar ao prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Nota gravada."
@ -445,6 +449,15 @@ msgstr ""
"Sincronizar para destino fornecido (p padrão é o valor de configuração sync."
"target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "A sincronização já está em andamento."
@ -458,11 +471,6 @@ msgstr ""
"está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar "
"a operação."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"A autenticação não foi concluída (não recebeu um token de autenticação)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Alvo de sincronização: %s (%s)"
@ -581,9 +589,6 @@ msgstr ""
msgid "File"
msgstr "Arquivo"
msgid "Save"
msgstr ""
msgid "New note"
msgstr "Nova nota"
@ -637,6 +642,9 @@ msgstr "Ajuda"
msgid "Website and documentation"
msgstr "Website e documentação"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Sobre o Joplin"
@ -644,16 +652,53 @@ msgstr "Sobre o Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Em %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -705,6 +750,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Status"
@ -723,15 +777,9 @@ msgstr ""
msgid "Please create a notebook first."
msgstr "Primeiro, crie um caderno."
msgid "Note title:"
msgstr "Título da nota:"
msgid "Please create a notebook first"
msgstr "Primeiro, crie um caderno"
msgid "To-do title:"
msgstr "Título da tarefa:"
msgid "Notebook title:"
msgstr "Título do caderno:"
@ -852,12 +900,18 @@ msgstr "Flag desconhecido: %s"
msgid "File system"
msgstr "Sistema de arquivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (apenas para testes)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nível de log desconhecido: %s"
@ -916,6 +970,10 @@ msgstr "Itens locais excluídos: %d."
msgid "Deleted remote items: %d."
msgstr "Itens remotos excluídos: %d."
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Itens locais criados: %d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Estado: \"%s\"."
@ -998,6 +1056,30 @@ msgstr "Mostrar tarefas incompletas no topo das listas"
msgid "Save geo-location with notes"
msgstr "Salvar geolocalização com notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Cria uma nova tarefa."
#, fuzzy
msgid "Focus title"
msgstr "Título da nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Cria uma nova nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Atualizar automaticamente o aplicativo"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronização"
@ -1013,9 +1095,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"
@ -1023,11 +1102,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 ""
@ -1039,6 +1116,24 @@ 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 ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
@ -1151,6 +1246,12 @@ msgstr "O caderno não pôde ser salvo: %s"
msgid "Edit notebook"
msgstr "Editar caderno"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota foi modificada:"
@ -1204,6 +1305,16 @@ 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 "To-do title:"
#~ msgstr "Título da tarefa:"
#~ msgid "Delete notebook?"
#~ msgstr "Excluir caderno?"

View File

@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: rtmkrlv <artyom.karlov@gmail.com>\n"
"Last-Translator: Artyom Karlov <artyom.karlov@gmail.com>\n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@ -112,7 +112,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Команда «%s» доступна только в режиме GUI"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Не удалось изменить зашифрованный элемент"
#, javascript-format
msgid "Missing required argument: %s"
@ -123,7 +123,7 @@ msgid "%s: %s"
msgstr "%s: %s"
msgid "Your choice: "
msgstr "Ваш выбор:"
msgstr "Ваш выбор: "
#, javascript-format
msgid "Invalid answer: %s"
@ -180,22 +180,24 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
"`status` и `target-status`."
#, fuzzy
msgid "Enter master password:"
msgstr "Установить пароль"
msgstr "Введите мастер-пароль:"
msgid "Operation cancelled"
msgstr ""
msgstr "Операция отменена"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Запуск расшифровки... Пожалуйста, ожидайте. Время расшифровки зависит от "
"объёма расшифровываемых данных."
#, fuzzy
msgid "Completed decryption."
msgstr "Включить шифрование"
msgstr "Расшифровка завершена."
msgid "Enabled"
msgstr "Включено"
@ -203,9 +205,9 @@ msgstr "Включено"
msgid "Disabled"
msgstr "Отключено"
#, fuzzy, javascript-format
#, javascript-format
msgid "Encryption is: %s"
msgstr "Шифрование:"
msgstr "Шифрование: %s"
msgid "Edit note."
msgstr "Редактировать заметку."
@ -228,6 +230,10 @@ msgstr ""
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
"командной строке."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Ошибка при открытии заметки в редакторе: %s"
msgid "Note has been saved."
msgstr "Заметка сохранена."
@ -277,7 +283,7 @@ msgstr ""
"элемент."
msgid "To move from one pane to another, press Tab or Shift+Tab."
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab"
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab."
msgid ""
"Use the arrows and page up/down to scroll the lists and text areas "
@ -299,7 +305,7 @@ msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
msgstr ""
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
"shortcuts`."
"shortcuts`"
msgid "Imports an Evernote notebook file (.enex file)."
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
@ -455,6 +461,15 @@ 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 ""
"Не аутентифицировано с %s. Пожалуйста, предоставьте все недостающие данные."
msgid "Synchronisation is already in progress."
msgstr "Синхронизация уже выполняется."
@ -468,10 +483,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)"
@ -593,13 +604,14 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
"расшифруются в фоновом режиме и вскоре станут доступны."
msgid "File"
msgstr "Файл"
msgid "Save"
msgstr "Сохранить"
msgid "New note"
msgstr "Новая заметка"
@ -639,13 +651,11 @@ msgstr "Инструменты"
msgid "Synchronisation status"
msgstr "Статус синхронизации"
#, fuzzy
msgid "Encryption options"
msgstr "Настройки шифрования"
#, fuzzy
msgid "General Options"
msgstr "Настройки"
msgstr "Основные настройки"
msgid "Help"
msgstr "Помощь"
@ -653,6 +663,9 @@ msgstr "Помощь"
msgid "Website and documentation"
msgstr "Сайт и документация"
msgid "Check for updates..."
msgstr "Проверить обновления..."
msgid "About Joplin"
msgstr "О Joplin"
@ -660,16 +673,54 @@ msgstr "О Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "В %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Отмена"
msgid "Error"
msgstr "Ошибка"
msgid "An update is available, do you want to update now?"
msgstr "Доступно обновление. Обновить сейчас?"
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Не удалось загрузить обновление: %s"
msgid "Current version is up-to-date."
msgstr "Вы используете самую свежую версию."
msgid "New version downloaded - application will quit now and update..."
msgstr ""
"Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Не удалось установить обновление: %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Заметки и настройки сохранены в: %s"
msgid "Save"
msgstr "Сохранить"
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -731,6 +782,16 @@ msgstr ""
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
"блокноты."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Мастер-ключи"
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "Статус"
@ -748,15 +809,9 @@ msgstr "Будет создан новый блокнот «%s» и в него
msgid "Please create a notebook first."
msgstr "Сначала создайте блокнот."
msgid "Note title:"
msgstr "Название заметки:"
msgid "Please create a notebook first"
msgstr "Сначала создайте блокнот"
msgid "To-do title:"
msgstr "Название задачи:"
msgid "Notebook title:"
msgstr "Название блокнота:"
@ -872,12 +927,18 @@ msgstr "Неизвестный флаг: %s"
msgid "File system"
msgstr "Файловая система"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (только для тестирования)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Неизвестный уровень лога: %s"
@ -936,6 +997,10 @@ msgstr "Удалено локальных элементов: %d."
msgid "Deleted remote items: %d."
msgstr "Удалено удалённых элементов: %d."
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Получено элементов: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Статус: «%s»."
@ -951,13 +1016,11 @@ msgstr "Завершено: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s"
#, fuzzy
msgid "Encrypted"
msgstr "Шифрование:"
msgstr "Зашифровано"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Некоторые элементы не могут быть синхронизированы."
msgstr "Зашифрованные элементы не могут быть изменены"
msgid "Conflicts"
msgstr "Конфликты"
@ -1018,6 +1081,27 @@ msgstr "Показывать незавершённые задачи вверх
msgid "Save geo-location with notes"
msgstr "Сохранять информацию о геолокации в заметках"
msgid "When creating a new to-do:"
msgstr "При создании новой задачи:"
msgid "Focus title"
msgstr "Фокус на названии"
msgid "Focus body"
msgstr "Фокус на содержимом"
msgid "When creating a new note:"
msgstr "При создании новой заметки:"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Масштаб приложения в процентах"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Synchronisation interval"
msgstr "Интервал синхронизации"
@ -1033,9 +1117,6 @@ msgstr "%d час"
msgid "%d hours"
msgstr "%d часов"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Show advanced options"
msgstr "Показывать расширенные настройки"
@ -1043,11 +1124,11 @@ 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` указывается целевой каталог."
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
"параметры, именованные как «sync.NUM.NAME» (все описаны ниже)."
msgid "Directory to synchronise with (absolute path)"
msgstr "Каталог синхронизации (абсолютный путь)"
@ -1059,6 +1140,27 @@ msgstr ""
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Имя пользователя Nexcloud"
msgid "Nexcloud password"
msgstr "Пароль Nexcloud"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Имя пользователя Nexcloud"
#, fuzzy
msgid "WebDAV password"
msgstr "Установить пароль"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
@ -1066,15 +1168,18 @@ msgstr "Неверное значение параметра: «%s». Досту
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%s %s (%s)"
msgstr "%s (%s): %s"
msgid ""
"These items will remain on the device but will not be uploaded to the sync "
"target. In order to find these items, either search for the title or the ID "
"(which is displayed in brackets above)."
msgstr ""
"Эти элементы будут оставаться на устройстве, но не будут загружены в целевой "
"объект синхронизации. Чтобы найти эти элементы, воспользуйтесь поиском по "
"названию или ID (который указывается в скобках выше)."
msgid "Sync status (synced items / total items)"
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
@ -1121,9 +1226,8 @@ msgstr "Лог"
msgid "Export Debug Report"
msgstr "Экспортировать отладочный отчёт"
#, fuzzy
msgid "Encryption Config"
msgstr "Шифрование:"
msgstr "Конфигурация шифрования"
msgid "Configuration"
msgstr "Конфигурация"
@ -1136,7 +1240,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "Переместить %d заметок в блокнот «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Нажмите, чтобы установить пароль для расшифровки."
msgid "Select date"
msgstr "Выбрать дату"
@ -1147,22 +1251,20 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию"
#, fuzzy, javascript-format
#, javascript-format
msgid "Master Key %s"
msgstr "Мастер-ключи"
msgstr "Мастер-ключ %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Создано: %d."
msgstr "Создано: %s"
#, fuzzy
msgid "Password:"
msgstr "Пароль"
msgstr "Пароль:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Пароль не может быть пустым"
#, fuzzy
msgid "Enable"
msgstr "Включено"
@ -1173,6 +1275,13 @@ msgstr "Не удалось сохранить блокнот: %s"
msgid "Edit notebook"
msgstr "Редактировать блокнот"
msgid "Show all"
msgstr ""
#, fuzzy
msgid "Errors only"
msgstr "Ошибка"
msgid "This note has been modified:"
msgstr "Эта заметка была изменена:"
@ -1226,5 +1335,15 @@ 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 "To-do title:"
#~ msgstr "Название задачи:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "«%s»: «%s»"

View File

@ -218,6 +218,10 @@ msgstr "此笔记不存在:\"%s\"。是否创建?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "开始编辑笔记。关闭编辑器则返回提示。"
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "笔记已被保存。"
@ -426,6 +430,14 @@ msgstr "与远程储存空间同步。"
msgid "Sync to provided target (defaults to sync.target config value)"
msgstr "同步至所提供的目标(默认为同步目标配置值)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "认证未完成(未收到认证令牌)。"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "同步正在进行中。"
@ -438,10 +450,6 @@ msgstr ""
"锁定文件已被保留。若当前没有任何正在进行的同步,您可以在\"%s\"删除锁定文件并"
"继续操作。"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "认证未完成(未收到认证令牌)。"
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "同步目标:%s (%s)"
@ -553,9 +561,6 @@ msgstr ""
msgid "File"
msgstr "文件"
msgid "Save"
msgstr ""
msgid "New note"
msgstr "新笔记"
@ -608,6 +613,9 @@ msgstr "帮助"
msgid "Website and documentation"
msgstr "网站与文档"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "关于Joplin"
@ -615,16 +623,53 @@ msgstr "关于Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "%s:%s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "确认"
msgid "Cancel"
msgstr "取消"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "否"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
msgid "Save"
msgstr ""
msgid ""
"Disabling encryption means *all* your notes and attachments are going to be "
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
@ -676,6 +721,15 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
msgid "Missing Master Keys"
msgstr ""
msgid ""
"The master keys with these IDs are used to encrypt some of your items, "
"however the application does not currently have access to them. It is likely "
"they will eventually be downloaded via synchronisation."
msgstr ""
msgid "Status"
msgstr "状态"
@ -693,15 +747,9 @@ msgstr "将创建新笔记本\"%s\"并将文件\"%s\"导入至其中"
msgid "Please create a notebook first."
msgstr "请先创建笔记本。"
msgid "Note title:"
msgstr "笔记标题:"
msgid "Please create a notebook first"
msgstr "请先创建笔记本"
msgid "To-do title:"
msgstr "待办事项标题:"
msgid "Notebook title:"
msgstr "笔记本标题:"
@ -820,12 +868,18 @@ msgstr "未知标记:%s"
msgid "File system"
msgstr "文件系统"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive开发员(仅测试用)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "未知日志level:%s"
@ -881,6 +935,10 @@ msgstr "已删除本地项目: %d。"
msgid "Deleted remote items: %d."
msgstr "已删除远程项目: %d。"
#, fuzzy, javascript-format
msgid "Fetched items: %d/%d."
msgstr "已新建本地项目: %d。"
#, javascript-format
msgid "State: \"%s\"."
msgstr "状态:\"%s\"。"
@ -960,6 +1018,30 @@ msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "创建新待办事项。"
#, fuzzy
msgid "Focus title"
msgstr "笔记标题:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "创建新笔记。"
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Synchronisation interval"
msgstr "同步间隔"
@ -975,9 +1057,6 @@ msgstr "%d小时"
msgid "%d hours"
msgstr "%d小时"
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Show advanced options"
msgstr "显示高级选项"
@ -985,9 +1064,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 ""
@ -997,6 +1076,24 @@ msgid ""
"See `sync.target`."
msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud password"
msgstr ""
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
msgid "WebDAV password"
msgstr ""
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "无效的选项值:\"%s\"。可用值为:%s。"
@ -1109,6 +1206,12 @@ msgstr "此笔记本无法保存:%s"
msgid "Edit notebook"
msgstr "编辑笔记本"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "此笔记已被修改:"
@ -1160,6 +1263,14 @@ 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 "To-do title:"
#~ msgstr "待办事项标题:"
#~ msgid "\"%s\": \"%s\""
#~ msgstr "\"%s\": \"%s\""

View File

@ -1,13 +1,13 @@
{
"name": "joplin",
"version": "0.10.87",
"version": "0.10.92",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"ajv": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz",
"integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"requires": {
"co": "4.6.0",
"fast-deep-equal": "1.0.0",
@ -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",
@ -442,7 +447,7 @@
"ndarray": "1.0.18",
"ndarray-pack": "1.2.1",
"node-bitmap": "0.0.1",
"omggif": "1.0.8",
"omggif": "1.0.9",
"parse-data-uri": "0.2.0",
"pngjs": "2.3.1",
"request": "2.83.0",
@ -494,7 +499,7 @@
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
"requires": {
"ajv": "5.3.0",
"ajv": "5.5.2",
"har-schema": "2.0.0"
}
},
@ -953,9 +958,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"omggif": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.8.tgz",
"integrity": "sha1-F483sqsLPXtG7ToORr0HkLWNNTA="
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.9.tgz",
"integrity": "sha1-3LcCTazVDFK00wPwSALJHAV8dl8="
},
"once": {
"version": "1.4.0",
@ -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",
@ -1920,9 +1935,9 @@
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
},
"string-kit": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.3.tgz",
"integrity": "sha512-G2T92klsuE+S9mqdKQyWurFweNQV5X+FRzSKTqYHRdaVUN/4dL6urbYJJ+xb9ep/4XWm+4RNT8j3acncNhFRBg==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.4.tgz",
"integrity": "sha512-imrOojdsXlL6xzfERCxvc/iA9Zwpzbfs+qeP6VB0s0rQVnMc3Nwkyhge0e8Uoayph7PVAwPNmLpohox27G3fgA==",
"requires": {
"xregexp": "3.2.0"
}
@ -2019,15 +2034,15 @@
}
},
"terminal-kit": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.0.tgz",
"integrity": "sha512-ir0I2QtcBDSg2w0UvohlqdDpGlS3S2UYBG4NnYKnK/4VywgnbfxgdpXN3el0uCH3OeH6fG38luW7RmDM96FqUw==",
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.3.tgz",
"integrity": "sha512-ZHtuElnBhK0IXOYNvQ7eYgaArwEoOv7saQc4Q0Z9p02JeC7iajC20/odV77BKB3jw/Qthvf9mpASf8gNDYv7xQ==",
"requires": {
"async-kit": "2.2.3",
"get-pixels": "3.3.0",
"ndarray": "1.0.18",
"nextgen-events": "0.10.2",
"string-kit": "0.6.3",
"string-kit": "0.6.4",
"tree-kit": "0.5.26"
}
},
@ -2037,16 +2052,16 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
},
"tkwidgets": {
"version": "0.5.20",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.20.tgz",
"integrity": "sha512-9wGsMrrFJvE/6TKUc0dEFFhwxvZLeNsYOxnpy1JCwyk/hYCEF70nuvk7VvJeG4TPaQBaGKPj6c7pCgdREvz4Jw==",
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.21.tgz",
"integrity": "sha512-gJfpYq3UM6AZ23ZM+D9BZ1PhsJLLHgjCOf487/lS9pO0uDdnkMcVXkkKEfRl00EyjPnGc88QZhEkVOvrtKsuPA==",
"requires": {
"chalk": "2.3.0",
"emphasize": "1.5.0",
"node-emoji": "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5",
"slice-ansi": "1.0.0",
"string-width": "2.1.1",
"terminal-kit": "1.14.0",
"terminal-kit": "1.14.3",
"wrap-ansi": "3.0.1"
},
"dependencies": {
@ -2100,6 +2115,15 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"url-parse": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
"requires": {
"querystringify": "1.0.0",
"requires-port": "1.0.0"
}
},
"url-to-options": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
@ -2144,6 +2168,20 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": "1.2.4",
"xmlbuilder": "9.0.4"
}
},
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
},
"xregexp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",

View File

@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.87",
"version": "0.10.92",
"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",
@ -56,9 +57,11 @@
"string-to-stream": "^1.1.0",
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.20",
"tkwidgets": "^0.5.21",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {

View File

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

View File

@ -8,7 +8,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('Encryption', function() {
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
@ -29,4 +29,19 @@ describe('Encryption', function() {
done();
});
it('should find items using binary search', async (done) => {
let items = ['aaa', 'ccc', 'bbb'];
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
items.sort();
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(1);
expect(ArrayUtils.binarySearch(items, 'ccc')).toBe(2);
expect(ArrayUtils.binarySearch(items, 'oops')).toBe(-1);
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(0);
items = [];
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
done();
});
});

View File

@ -19,7 +19,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
jasmine.DEFAULT_TIMEOUT_INTERVAL = 35000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@ -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);
@ -268,6 +268,24 @@ describe('Synchronizer', function() {
expect(deletedItems.length).toBe(0);
}));
it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
await synchronizer().start();
await switchClient(2);
await synchronizer().start();
await Folder.delete(folder1.id);
await synchronizer().start();
await switchClient(1);
await synchronizer().start();
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
it('should delete local notes', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
@ -286,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" });
@ -304,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" });
@ -327,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" });
@ -370,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
@ -441,7 +459,7 @@ describe('Synchronizer', function() {
let unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
}));
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@ -471,7 +489,7 @@ describe('Synchronizer', function() {
let items = await allItems();
expect(items.length).toBe(1);
}));
}));
it('should allow duplicate folder titles', asyncTest(async () => {
let localF1 = await Folder.save({ title: "folder" });
@ -557,10 +575,12 @@ describe('Synchronizer', function() {
}
it('should sync tags', asyncTest(async () => {
await shoudSyncTagTest(false); }));
await shoudSyncTagTest(false);
}));
it('should sync encrypted tags', asyncTest(async () => {
await shoudSyncTagTest(true); }));
await shoudSyncTagTest(true);
}));
it('should not sync notes with conflicts', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });
@ -665,12 +685,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);
@ -684,9 +704,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);
@ -830,7 +850,7 @@ describe('Synchronizer', function() {
}));
it('should sync resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
while (insideBeforeEach) await time.msleep(500);
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });

View File

@ -15,6 +15,7 @@ const { Synchronizer } = require('lib/synchronizer.js');
const { FileApi } = require('lib/file-api.js');
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const { time } = require('lib/time-utils.js');
const { shimInit } = require('lib/shim-init-node.js');
@ -22,8 +23,10 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const EncryptionService = require('lib/services/EncryptionService.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
const WebDavApi = require('lib/WebDavApi');
let databases_ = [];
let synchronizers_ = [];
@ -38,6 +41,7 @@ const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
const logDir = __dirname + '/../tests/logs';
fs.mkdirpSync(logDir, 0o755);
@ -45,12 +49,16 @@ fs.mkdirpSync(logDir, 0o755);
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
const syncDir = __dirname + '/../tests/sync';
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 10;//400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const logger = new Logger();
logger.addTarget('console');
@ -142,25 +150,6 @@ async function setupDatabase(id = null) {
BaseModel.db_ = databases_[id];
await Setting.load();
//return setupDatabase(id);
// return databases_[id].open({ name: filePath }).then(() => {
// BaseModel.db_ = databases_[id];
// return setupDatabase(id);
// });
// return fs.unlink(filePath).catch(() => {
// // Don't care if the file doesn't exist
// }).then(() => {
// databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
// return databases_[id].open({ name: filePath }).then(() => {
// BaseModel.db_ = databases_[id];
// return setupDatabase(id);
// });
// });
}
function resourceDir(id = null) {
@ -192,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) {
@ -248,7 +232,17 @@ function fileApi() {
fileApi_ = new FileApi(syncDir, new FileApiDriverLocal());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
fileApi_ = new FileApi('/root', new FileApiDriverMemory());
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('nextcloud')) {
const options = {
baseUrl: () => 'http://nextcloud.local/remote.php/dav/files/admin/JoplinTest',
username: () => 'admin',
password: () => '123456',
};
const api = new WebDavApi(options);
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
}
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
// let auth = require('./onedrive-auth.json');
// if (!auth) {

View File

@ -1,5 +1,5 @@
const { _ } = require('lib/locale.js');
const { BrowserWindow } = require('electron');
const { BrowserWindow, Menu, Tray } = require('electron');
const { shim } = require('lib/shim');
const url = require('url')
const path = require('path')
@ -12,6 +12,7 @@ class ElectronAppWrapper {
this.env_ = env;
this.win_ = null;
this.willQuitApp_ = false;
this.tray_ = null;
}
electronApp() {
@ -62,11 +63,27 @@ class ElectronAppWrapper {
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
if (this.willQuitApp_ || process.platform !== 'darwin') {
this.win_ = null;
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
// user clicks on the icon in the task bar).
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
// case the app must be explicitely closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
if (process.platform === 'darwin') {
if (this.willQuitApp_) {
this.win_ = null;
} else {
event.preventDefault();
this.win_.hide();
}
} else {
event.preventDefault();
this.win_.hide();
if (this.trayShown() && !this.willQuitApp_) {
event.preventDefault();
this.win_.hide();
} else {
this.win_ = null;
}
}
})
@ -93,6 +110,27 @@ class ElectronAppWrapper {
this.electronApp_.quit();
}
trayShown() {
return !!this.tray_;
}
// Note: this must be called only after the "ready" event of the app has been dispatched
createTray(contextMenu) {
this.tray_ = new Tray(__dirname + '/build/icons/16x16.png')
this.tray_.setToolTip(this.electronApp_.getName())
this.tray_.setContextMenu(contextMenu)
this.tray_.on('click', () => {
this.window().show();
});
}
destroyTray() {
if (!this.tray_) return;
this.tray_.destroy();
this.tray_ = null;
}
async start() {
// Since we are doing other async things before creating the window, we might miss
// the "ready" event. So we use the function below to make sure that the app is ready.

View File

@ -49,6 +49,10 @@ class Application extends BaseApplication {
return true;
}
checkForUpdateLoggerPath() {
return Setting.value('profileDir') + '/log-autoupdater.txt';
}
reducer(state = appDefaultState, action) {
let newState = state;
@ -136,6 +140,10 @@ class Application extends BaseApplication {
this.refreshMenu();
}
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
this.updateTray();
}
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
}
@ -294,6 +302,11 @@ class Application extends BaseApplication {
label: _('Website and documentation'),
accelerator: 'F1',
click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('Check for updates...'),
click: () => {
bridge().checkForUpdates(false, this.checkForUpdateLoggerPath());
}
}, {
label: _('About Joplin'),
click: () => {
@ -340,6 +353,23 @@ class Application extends BaseApplication {
this.lastMenuScreen_ = screen;
}
updateTray() {
const app = bridge().electronApp();
if (app.trayShown() === Setting.value('showTrayIcon')) return;
if (!Setting.value('showTrayIcon')) {
app.destroyTray();
} else {
const contextMenu = Menu.buildFromTemplate([
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
{ type: 'separator' },
{ label: _('Exit'), click: () => { app.exit() } },
])
app.createTray(contextMenu);
}
}
async start(argv) {
argv = await super.start(argv);
@ -385,9 +415,9 @@ class Application extends BaseApplication {
// Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {
const runAutoUpdateCheck = function() {
const runAutoUpdateCheck = () => {
if (Setting.value('autoUpdateEnabled')) {
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
bridge().checkForUpdates(true, this.checkForUpdateLoggerPath());
}
}
@ -396,6 +426,8 @@ class Application extends BaseApplication {
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
}
this.updateTray();
setTimeout(() => {
AlarmService.garbageCollect();
}, 1000 * 60 * 60);

View File

@ -125,6 +125,11 @@ class Bridge {
}
}
checkForUpdates(inBackground, logFilePath) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, logFilePath);
}
}
let bridge_ = null;

View File

@ -0,0 +1,72 @@
const { dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
let autoUpdateLogger_ = new Logger();
let checkInBackground_ = false;
autoUpdater.autoDownload = false;
autoUpdater.on('error', (error) => {
autoUpdateLogger_.error(error);
if (checkInBackground_) return;
dialog.showErrorBox(_('Error'), error == null ? "unknown" : (error.stack || error).toString())
})
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
message: _('An update is available, do you want to update now?'),
buttons: [_('Yes'), _('No')]
}, (buttonIndex) => {
if (buttonIndex === 0) {
try {
autoUpdater.downloadUpdate()
} catch (error) {
autoUpdateLogger_.error(error);
dialog.showErrorBox(_('Error'), _('Could not download the update: %s', error.message));
}
}
})
})
autoUpdater.on('update-not-available', () => {
if (checkInBackground_) return;
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
})
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({ message: _('New version downloaded - application will quit now and update...') }, () => {
setTimeout(() => {
try {
autoUpdater.quitAndInstall();
} catch (error) {
autoUpdateLogger_.error(error);
dialog.showErrorBox(_('Error'), _('Could not install the update: %s', error.message));
}
}, 100);
})
})
function checkForUpdates(inBackground, logFilePath) {
if (logFilePath && !autoUpdateLogger_.targets().length) {
autoUpdateLogger_ = new Logger();
autoUpdateLogger_.addTarget('file', { path: logFilePath });
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
autoUpdateLogger_.info('checkForUpdates: Initializing...');
autoUpdater.logger = autoUpdateLogger_;
}
checkInBackground_ = inBackground;
try {
autoUpdater.checkForUpdates()
} catch (error) {
autoUpdateLogger_.error(error);
if (!checkInBackground_) dialog.showErrorBox(_('Error'), error.message);
}
}
module.exports.checkForUpdates = checkForUpdates

View File

@ -0,0 +1,3 @@
owner: laurent22
repo: joplin
provider: github

View File

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

View File

@ -92,10 +92,14 @@ class EncryptionConfigScreenComponent extends React.Component {
};
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@ -121,7 +125,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;
@ -149,6 +153,31 @@ class EncryptionConfigScreenComponent extends React.Component {
);
}
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<tr key={id}><td style={theme.textStyle}>{id}</td></tr>);
}
nonExistingMasterKeySection = (
<div>
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
<table>
<tbody>
<tr>
<th style={theme.textStyle}>{_('ID')}</th>
</tr>
{ rows }
</tbody>
</table>
</div>
);
}
return (
<div>
<Header style={headerStyle} />
@ -169,6 +198,7 @@ class EncryptionConfigScreenComponent extends React.Component {
{decryptedItemsInfo}
{toggleButton}
{masterKeySection}
{nonExistingMasterKeySection}
</div>
</div>
);
@ -183,6 +213,7 @@ const mapStateToProps = (state) => {
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
};

View File

@ -44,16 +44,14 @@ class MainScreenComponent extends React.Component {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: title,
const newNote = {
parent_id: folderId,
is_todo: isTodo ? 1 : 0,
});
Note.updateGeolocation(note.id);
};
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
type: 'NOTE_SET_NEW_ONE',
item: newNote,
});
}
@ -65,30 +63,14 @@ class MainScreenComponent extends React.Component {
return;
}
this.setState({
promptOptions: {
label: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, false);
} else if (command.name === 'newTodo') {
if (!this.props.folders.length) {
bridge().showErrorMessageBox(_('Please create a notebook first'));
return;
}
this.setState({
promptOptions: {
label: _('To-do title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, true);
} else if (command.name === 'newNotebook') {
this.setState({
promptOptions: {
@ -292,20 +274,22 @@ class MainScreenComponent extends React.Component {
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
const theme = themeStyle(this.props.theme);
const selectedFolderId = this.props.selectedFolderId;
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
const headerButtons = [];
headerButtons.push({
title: _('New note'),
iconName: 'fa-file-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newNote' }) },
});
headerButtons.push({
title: _('New to-do'),
iconName: 'fa-check-square-o',
enabled: !!folders.length,
enabled: !!folders.length && !onConflictFolder,
onClick: () => { this.doCommand({ name: 'newTodo' }) },
});
@ -402,6 +386,7 @@ const mapStateToProps = (state) => {
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
selectedFolderId: state.selectedFolderId,
};
};

View File

@ -146,7 +146,10 @@ class NoteListComponent extends React.Component {
const hPadding = 10;
let style = Object.assign({ width: width }, this.style().listItem);
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected);
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) {
style = Object.assign(style, this.style().listItemSelected);
}
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
// but don't know how it will look in other OSes.
@ -182,8 +185,9 @@ class NoteListComponent extends React.Component {
render() {
const theme = themeStyle(this.props.theme);
const style = this.props.style;
let notes = this.props.notes.slice();
if (!this.props.notes.length) {
if (!notes.length) {
const padding = 10;
const emptyDivStyle = Object.assign({
padding: padding + 'px',
@ -202,7 +206,7 @@ class NoteListComponent extends React.Component {
itemHeight={this.style().listItem.height}
style={style}
className={"note-list"}
items={this.props.notes}
items={notes}
itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } }
></ItemList>
);

View File

@ -36,7 +36,13 @@ class NoteTextComponent extends React.Component {
isLoading: true,
webviewReady: false,
scrollHeight: null,
editorScrollTop: 0
editorScrollTop: 0,
newNote: null,
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
// to automatically set the title.
newAndNoTitleChangeNoteId: null,
};
this.lastLoadedNoteId_ = null;
@ -75,7 +81,10 @@ class NoteTextComponent extends React.Component {
async componentWillMount() {
let note = null;
if (this.props.noteId) {
if (this.props.newNote) {
note = Object.assign({}, this.props.newNote);
} else if (this.props.noteId) {
note = await Note.load(this.props.noteId);
}
@ -114,7 +123,14 @@ class NoteTextComponent extends React.Component {
}
async saveOneProperty(name, value) {
await shared.saveOneProperty(this, name, value);
if (this.state.note && !this.state.note.id) {
const note = Object.assign({}, this.state.note);
note[name] = value;
this.setState({ note: note });
this.scheduleSave();
} else {
await shared.saveOneProperty(this, name, value);
}
}
scheduleSave() {
@ -128,19 +144,32 @@ class NoteTextComponent extends React.Component {
if (!options) options = {};
if (!('noReloadIfLocalChanges' in options)) options.noReloadIfLocalChanges = false;
const stateNoteId = this.state.note ? this.state.note.id : null;
const noteId = props.noteId;
let loadingNewNote = stateNoteId !== noteId;
this.lastLoadedNoteId_ = noteId;
const note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
if (!options.noReloadIfLocalChanges && this.isModified()) return;
await this.saveIfNeeded();
// If the note hasn't been changed, exit now
if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note);
delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return;
const previousNote = this.state.note ? Object.assign({}, this.state.note) : null;
const stateNoteId = this.state.note ? this.state.note.id : null;
let noteId = null;
let note = null;
let loadingNewNote = true;
if (props.newNote) {
note = Object.assign({}, props.newNote);
this.lastLoadedNoteId_ = null;
} else {
noteId = props.noteId;
loadingNewNote = stateNoteId !== noteId;
this.lastLoadedNoteId_ = noteId;
note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
if (options.noReloadIfLocalChanges && this.isModified()) return;
// If the note hasn't been changed, exit now
if (this.state.note && note) {
let diff = Note.diffObjects(this.state.note, note);
delete diff.type_;
if (!Object.getOwnPropertyNames(diff).length) return;
}
}
this.mdToHtml_ = null;
@ -148,7 +177,7 @@ class NoteTextComponent extends React.Component {
// If we are loading nothing (noteId == null), make sure to
// set webviewReady to false too because the webview component
// is going to be removed in render().
const webviewReady = this.webview_ && this.state.webviewReady && noteId;
const webviewReady = this.webview_ && this.state.webviewReady && (noteId || props.newNote);
// Scroll back to top when loading new note
if (loadingNewNote) {
@ -160,26 +189,49 @@ class NoteTextComponent extends React.Component {
// https://github.com/ajaxorg/ace/issues/2195
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
}
this.setState({
note: note,
lastSavedNote: Object.assign({}, note),
webviewReady: webviewReady,
});
}
if (note) {
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
async componentWillReceiveProps(nextProps) {
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
await this.reloadNote(nextProps);
if (this.editor_){
if (Setting.value(focusSettingName) === 'title') {
if (this.titleField_) this.titleField_.focus();
} else {
if (this.editor_) this.editor_.editor.focus();
}
}
if (this.editor_) {
const session = this.editor_.editor.getSession();
const undoManager = session.getUndoManager();
undoManager.reset();
session.setUndoManager(undoManager);
this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0);
}
}
let newState = {
note: note,
lastSavedNote: Object.assign({}, note),
webviewReady: webviewReady,
};
if (!note) {
newState.newAndNoTitleChangeNoteId = null;
} else if (note.id !== this.state.newAndNoTitleChangeNoteId) {
newState.newAndNoTitleChangeNoteId = null;
}
this.setState(newState);
}
async componentWillReceiveProps(nextProps) {
if (nextProps.newNote) {
await this.reloadNote(nextProps);
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
await this.reloadNote(nextProps);
}
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
await this.reloadNote(nextProps, { noReloadIfLocalChanges: true });
}
@ -195,6 +247,7 @@ class NoteTextComponent extends React.Component {
title_changeText(event) {
shared.noteComponent_change(this, 'title', event.target.value);
this.setState({ newAndNoTitleChangeNoteId: null });
this.scheduleSave();
}
@ -402,20 +455,10 @@ class NoteTextComponent extends React.Component {
menu.popup(bridge().window());
}
// shouldComponentUpdate(nextProps, nextState) {
// //console.info('NEXT PROPS', JSON.stringify(nextProps));
// console.info('NEXT STATE ====================');
// for (var n in nextProps) {
// if (!nextProps.hasOwnProperty(n)) continue;
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
// }
// return true;
// }
render() {
const style = this.props.style;
const note = this.state.note;
const body = note ? note.body : '';
const body = note && note.body ? note.body : '';
const theme = themeStyle(this.props.theme);
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
@ -519,13 +562,6 @@ class NoteTextComponent extends React.Component {
const toolbarItems = [];
// toolbarItems.push({
// title: _('Save'),
// iconName: 'fa-save',
// enabled: this.isModified(),
// onClick: () => { },
// });
toolbarItems.push({
title: _('Attach file'),
iconName: 'fa-paperclip',
@ -548,8 +584,9 @@ class NoteTextComponent extends React.Component {
const titleEditor = <input
type="text"
ref={(elem) => { this.titleField_ = elem; } }
style={titleEditorStyle}
value={note ? note.title : ''}
value={note && note.title ? note.title : ''}
onChange={(event) => { this.title_changeText(event); }}
/>
@ -617,6 +654,7 @@ const mapStateToProps = (state) => {
theme: state.settings.theme,
showAdvancedOptions: state.settings.showAdvancedOptions,
syncStarted: state.syncStarted,
newNote: state.newNote,
};
};

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@ locales['fr_FR'] = require('./fr_FR.json');
locales['hr_HR'] = require('./hr_HR.json');
locales['it_IT'] = require('./it_IT.json');
locales['ja_JP'] = require('./ja_JP.json');
locales['nl_BE'] = require('./nl_BE.json');
locales['pt_BR'] = require('./pt_BR.json');
locales['ru_RU'] = require('./ru_RU.json');
locales['zh_CN'] = require('./zh_CN.json');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,15 @@
// Make it possible to require("/lib/...") without specifying full path
require('app-module-path').addPath(__dirname);
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
};
const { app } = require('./app.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');
@ -17,11 +26,13 @@ const { FsDriverNode } = require('lib/fs-driver-node.js');
const { shimInit } = require('lib/shim-init-node.js');
const EncryptionService = require('lib/services/EncryptionService');
const { bridge } = require('electron').remote.require('./bridge');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const fsDriver = new FsDriverNode();
Logger.fsDriver_ = fsDriver;
Resource.fsDriver_ = fsDriver;
EncryptionService.fsDriver_ = fsDriver;
FileApiDriverLocal.fsDriver_ = fsDriver;
// That's not good, but it's to avoid circular dependency issues
// in the BaseItem class.

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.43",
"version": "0.10.54",
"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",
@ -85,6 +86,8 @@
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tcp-port-used": "^0.1.2",
"uuid": "^3.1.0"
"url-parse": "^1.2.0",
"uuid": "^3.1.0",
"xml2js": "^0.4.19"
}
}

View File

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

20
LICENSE
View File

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

View File

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

View File

@ -4,7 +4,7 @@ Joplin is a free, open source note taking and to-do application, which can handl
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
The notes can be [synchronised](#synchronisation) with various targets including [Nextcloud](https://nextcloud.com/), the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
Joplin is still under development but is out of Beta and should be suitable for every day use. The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
@ -18,16 +18,16 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download
-----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-Setup-0.10.43.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-Setup-0.10.54.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-0.10.54.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-0.10.54-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## Mobile applications
Operating System | 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>
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>
Operating System | Download | Alt. Download
-----------------|----------|----------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.81/joplin-v0.10.81.apk)
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,16 +85,47 @@ 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.
## WebDAV synchronisation
**Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup the profile directory of the desktop or terminal application.**
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
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
# Encryption
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 [End-To-End Encryption Tutorial](http://joplin.cozic.net/help/e2ee) for more information about this feature and how to enable it.
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](http://joplin.cozic.net/help/spec).
# Attachments / Resources
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
@ -107,7 +138,7 @@ On the desktop and mobile apps, an alarm can be associated with any to-do. It wi
- **macOS**: >= 10.8 or Growl if earlier.
- **Linux**: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default). Growl otherwise
See [documentation and flow chart for reporter choice](./DECISION_FLOW.md)
See [documentation and flow chart for reporter choice](https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md)
On mobile, the alarms will be displayed using the built-in notification system.
@ -115,23 +146,43 @@ If for any reason the notifications do not work, please [open an issue](https://
# Localisation
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from there: https://github.com/laurent22/joplin/tree/master/CliClient/locales
This translation will apply to the three applications - desktop, mobile and terminal.
Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Code | Last translator | Percent done
---|---|---|---|---
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | hr_HR | Hrvoje Mandić <trbuhom@net.hr> | 72%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | de_DE | Tobias Strobel <git@strobeltobias.de> | 92%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | en_GB | | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | es_ES | Lucas Vieites | 80%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cr.png) | Español (Costa Rica) | es_CR | | 68%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | fr_FR | Laurent Cozic | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | it_IT | | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | nl_BE | | 90%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | pt_BR | | 74%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | ru_RU | Artyom Karlov <artyom.karlov@gmail.com> | 96%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | zh_CN | RCJacH <RCJacH@outlook.com> | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | ja_JP | | 74%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
# Coming features
- NextCloud support
- All: End to end encryption
- Mobile: manage tags
- Windows: Tray icon
- Desktop apps: Tag auto-complete
- Desktop apps: Dark theme

View File

@ -1,10 +1,10 @@
# About End-To-End Encryption (E2EE)
3. Now you need to synchronise all your notes so that thEnd-to-end encryption (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 developer of Joplin from being able to access the data.
End-to-end encryption (E2EE) is a system where only the owner of the data (i.e. notes, notebooks, tags or resources) can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data.
The systems is designed to defeat any attempts at surveillance or tampering because no third parties can decipher the data being communicated or stored.
The system is designed to defeat any attempts at surveillance or tampering because no third party can decipher the data being communicated or stored.
There is a small overhead to using E2EE since data constantly have to be encrypted and decrypted so consider whether you really need the feature.
There is a small overhead to using E2EE since data constantly has to be encrypted and decrypted so consider whether you really need the feature.
# Enabling E2EE
@ -13,8 +13,8 @@ Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on
To enable it, please follow these steps:
1. On your first device (eg. on the desktop application), go to the Encryption Config screen and click "Enable encryption"
2. Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you do not forget it since, for security reason, it cannot be recovered.
ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".
2. Input your password. This is the Master Key password which will be used to encrypt all your notes. Make sure you to not forget it since, for security reason, it cannot be recovered.
3. Now you need to synchronise all your notes so that they are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wait for any synchronisation that might be in progress and click on "Synchronise".
4. Wait for this synchronisation operation to complete. Since all the data needs to be re-sent (encrypted) to the sync target, it may take a long time, especially if you have many notes and resources. Note that even if synchronisation seems stuck, most likely it is still running - do not cancel it and simply let it run over night if needed.
5. Once this first synchronisation operation is done, open the next device you are synchronising with. Click "Synchronise" and wait for the sync operation to complete. The device will receive the master key, and you will need to provide the password for it. At this point E2EE will be automatically enabled on this device. Once done, click Synchronise again and wait for it to complete.
6. Repeat step 5 for each device.
@ -23,4 +23,8 @@ Once all the devices are in sync with E2EE enabled, the encryption/decryption sh
# Disabling E2EE
Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.
Follow the same procedure as above but instead disable E2EE on each device one by one. Again it might be simpler to do it one device at a time and to wait every time for the synchronisation to complete.
# Technical specification
For a more technical description, mostly relevant for development or to review the method being used, please see the [Encryption specification](http://joplin.cozic.net/help/spec).

4
README_faq.md Normal file
View File

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

View File

@ -19,11 +19,11 @@ Length | 6 chars (Hexa string)
Encryption method | 2 chars (Hexa string)
Master key ID | 32 chars (Hexa string)
See lib/services/EncryptionService.js for the list of available encryption methods.
See `lib/services/EncryptionService.js` for the list of available encryption methods.
### Data chunk
The data is encoded in one or more chuncks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).
The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).
Name | Size
--------|----------------------------
@ -42,11 +42,11 @@ Only one master key can be active for encryption purposes. For decryption, the a
## Encryption Service
The applications make use of the EncryptionService class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and marked as "active".
The applications make use of the `EncryptionService` class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".
## Encryption workflow
Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.
Items are encrypted only during synchronisation, when they are serialised (via `BaseItem.serializeForSync`), so before being sent to the sync target.
They are decrypted by DecryptionWorker in the background.

View File

@ -111,11 +111,30 @@ 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.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync

View File

@ -75,7 +75,7 @@ apply from: "../../node_modules/react-native/react.gradle"
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = true
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 86
versionName "0.10.71"
versionCode 2097259
versionName "0.10.81"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@ -137,11 +137,11 @@ android {
}
dependencies {
compile project(':react-native-securerandom')
compile project(':react-native-push-notification')
compile project(':react-native-fs')
compile project(':react-native-image-picker')
compile project(':react-native-vector-icons')
compile project(':react-native-securerandom')
compile project(':react-native-push-notification')
compile project(':react-native-fs')
compile project(':react-native-image-picker')
compile project(':react-native-vector-icons')
compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"

View File

@ -13,4 +13,28 @@ ArrayUtils.removeElement = function(array, element) {
return array;
}
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
module.exports = ArrayUtils;

View File

@ -26,12 +26,16 @@ 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 SyncTargetWebDAV = require('lib/SyncTargetWebDAV.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);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
class BaseApplication {

View File

@ -254,10 +254,25 @@ class BaseModel {
let n = fieldNames[i];
if (n in o) temp[n] = o[n];
}
// Remove fields that are not in the `fields` list, if provided.
// Note that things like update_time, user_update_time will still
// be part of the final list of fields if autoTimestamp is on.
// id also will stay.
if (!options.isNew && options.fields) {
const filtered = {};
for (let k in temp) {
if (!temp.hasOwnProperty(k)) continue;
if (k !== 'id' && options.fields.indexOf(k) < 0) continue;
filtered[k] = temp[k];
}
temp = filtered;
}
o = temp;
let modelId = temp.id;
let query = {};
let modelId = o.id;
const timeNow = time.unixMs();
@ -296,15 +311,6 @@ class BaseModel {
let temp = Object.assign({}, o);
delete temp.id;
if (options.fields) {
let filtered = {};
for (let i = 0; i < options.fields.length; i++) {
const f = options.fields[i];
filtered[f] = o[f];
}
temp = filtered;
}
query = Database.updateQuery(this.tableName(), temp, where);
}

View File

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

View File

@ -0,0 +1,36 @@
class Cache {
async getItem(name) {
let output = null;
try {
const storage = await Cache.storage();
output = await storage.getItem(name);
} catch (error) {
console.info(error);
// Defaults to returning null
}
return output;
}
async setItem(name, value, ttl = null) {
try {
const storage = await Cache.storage();
const options = {};
if (ttl !== null) options.ttl = ttl;
await storage.setItem(name, value, options);
} catch (error) {
// Defaults to not saving to cache
}
}
}
Cache.storage = async function() {
if (Cache.storage_) return Cache.storage_;
Cache.storage_ = require('node-persist');
const osTmpdir = require('os-tmpdir');
await Cache.storage_.init({ dir: osTmpdir() + '/joplin-cache', ttl: 1000 * 60 });
return Cache.storage_;
}
module.exports = Cache;

View File

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

View File

@ -28,7 +28,7 @@ class MdToHtml {
const r = resources[n];
k.push(r.id);
}
k.push(md5(body));
k.push(md5(escape(body))); // https://github.com/pvorb/node-md5/issues/41
k.push(md5(JSON.stringify(style)));
k.push(md5(JSON.stringify(options)));
return k.join('_');
@ -73,7 +73,7 @@ class MdToHtml {
renderImage_(attrs, options) {
const loadResource = async (id) => {
console.info('Loading resource: ' + id);
// console.info('Loading resource: ' + id);
// Initially set to to an empty object to make
// it clear that it is being loaded. Otherwise

View File

@ -24,9 +24,12 @@ class SyncTargetFilesystem extends BaseSyncTarget {
}
async initFileApi() {
const fileApi = new FileApi(Setting.value('sync.2.path'), new FileApiDriverLocal());
const syncPath = Setting.value('sync.2.path');
const driver = new FileApiDriverLocal();
const fileApi = new FileApi(syncPath, driver);
fileApi.setLogger(this.logger());
fileApi.setSyncTargetId(SyncTargetFilesystem.id());
await driver.mkdir(syncPath);
return fileApi;
}

View File

@ -0,0 +1,54 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetNextcloud extends BaseSyncTarget {
static id() {
return 5;
}
constructor(db, options = null) {
super(db, options);
// this.authenticated_ = false;
}
static targetName() {
return 'nextcloud';
}
static label() {
return _('Nextcloud (Beta)');
}
isAuthenticated() {
return true;
//return this.authenticated_;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.5.path'),
username: () => Setting.value('sync.5.username'),
password: () => Setting.value('sync.5.password'),
};
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetNextcloud.id());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetNextcloud;

View File

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

View File

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

View File

@ -0,0 +1,52 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetWebDAV extends BaseSyncTarget {
static id() {
return 6;
}
constructor(db, options = null) {
super(db, options);
}
static targetName() {
return 'webdav';
}
static label() {
return _('WebDAV (Beta)');
}
isAuthenticated() {
return true;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.6.path'),
username: () => Setting.value('sync.6.username'),
password: () => Setting.value('sync.6.password'),
};
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetWebDAV.id());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetWebDAV;

View File

@ -0,0 +1,245 @@
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) {
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
const nameProcessor = (name) => {
if (name.indexOf('xmlns:') !== 0) {
// Check if the current name is within the DAV namespace. If it is, normalise it
// by moving it to the "d:" namespace, which is what all the functions are using.
const p = name.split(':');
if (p.length == 2) {
const ns = p[0];
if (davNamespaces.indexOf(ns) >= 0) {
name = 'd:' + p[1];
}
}
}
return name.toLowerCase();
};
const attrValueProcessor = (value, name) => {
if (value.toLowerCase() === 'dav:') {
const p = name.split(':');
davNamespaces.push(p[p.length - 1]);
}
}
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
attrValueProcessors: [attrValueProcessor]
}
return new Promise((resolve, reject) => {
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 the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
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, options = 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 }, options);
}
// 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;
if (typeof body === 'string') headers['Content-Length'] = body.length;
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;
// console.info('WebDAV', method + ' ' + url, headers, options);
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(method + ' ' + path + ': ' + shortResponseText(), response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
// null responses are possible, for example for DELETE calls
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw new Error('Not a valid JSON response: ' + shortResponseText());
return output;
}
}
module.exports = WebDavApi;

View File

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

View File

@ -372,7 +372,7 @@ class ScreenHeaderComponent extends Component {
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
output.push({ label: Folder.displayTitle(f), value: f.id });
}
output.sort((a, b) => {
if (a.value === null) return -1;

View File

@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
const { _, setLocale } = require('lib/locale.js');
@ -17,6 +17,23 @@ class ConfigScreenComponent extends BaseScreenComponent {
constructor() {
super();
this.styles_ = {};
this.state = {
settings: {},
settingsChanged: false,
};
this.saveButton_press = () => {
for (let n in this.state.settings) {
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
this.setState({settingsChanged:false});
};
}
componentWillMount() {
this.setState({ settings: this.props.settings });
}
styles() {
@ -83,7 +100,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
let output = null;
const updateSettingValue = (key, value) => {
Setting.setValue(key, value);
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
this.setState({
settings: settings,
settingsChanged: true,
});
console.info(settings['sync.5.path']);
}
const md = Setting.settingMetadata(key);
@ -135,23 +159,33 @@ class ConfigScreenComponent extends BaseScreenComponent {
<Slider key="control" style={this.styles().settingControl} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
</View>
);
} else if (md.type == Setting.TYPE_STRING) {
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
<TextInput key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
);
} else {
//throw new Error('Unsupported setting type: ' + setting.type);
//throw new Error('Unsupported setting type: ' + md.type);
}
return output;
}
render() {
const settings = this.props.settings;
const settings = this.state.settings;
const keys = Setting.keys(true, 'mobile');
let settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
//if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(key)) continue;
const md = Setting.settingMetadata(key);
if (md.show && !md.show(settings)) continue;
const comp = this.settingToComponent(key, settings[key]);
if (!comp) continue;
settingComps.push(comp);
@ -173,11 +207,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
);
//style={this.styles().body}
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Configuration')}/>
<ScreenHeader
title={_('Configuration')}
showSaveButton={true}
saveButtonDisabled={!this.state.settingsChanged}
onSaveButtonPress={this.saveButton_press}
/>
<ScrollView >
{ settingComps }
</ScrollView>

View File

@ -159,9 +159,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@ -183,6 +189,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
<View style={{marginTop: 10}}>{rows}</View>
</View>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
@ -204,6 +228,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{flex:1, height: 20}}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
@ -221,6 +246,7 @@ const EncryptionConfigScreen = connect(
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
}
)(EncryptionConfigScreenComponent)

View File

@ -23,6 +23,7 @@ class LogScreenComponent extends BaseScreenComponent {
});
this.state = {
dataSource: ds,
showErrorsOnly: false,
};
this.styles_ = {};
}
@ -62,13 +63,24 @@ class LogScreenComponent extends BaseScreenComponent {
this.resfreshLogEntries();
}
resfreshLogEntries() {
reg.logger().lastEntries(1000).then((entries) => {
resfreshLogEntries(showErrorsOnly = null) {
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
}
toggleErrorsOnly() {
const showErrorsOnly = !this.state.showErrorsOnly;
this.setState({ showErrorsOnly: showErrorsOnly });
this.resfreshLogEntries(showErrorsOnly);
}
render() {
let renderRow = (item) => {
let textStyle = this.styles().rowText;
@ -91,7 +103,14 @@ class LogScreenComponent extends BaseScreenComponent {
renderRow={renderRow}
enableEmptySections={true}
/>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1, marginRight: 5 }}>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
</View>
<View style={{flex:1}}>
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
</View>
</View>
</View>
);
}

View File

@ -17,49 +17,37 @@ shared.saveNoteButton_press = async function(comp) {
// just save a new note by clearing the note ID.
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
// reg.logger().info('Saving note: ', note);
if (!note.parent_id) {
let folder = await Folder.defaultFolder();
if (!folder) {
//Log.warn('Cannot save note without a notebook');
return;
}
if (!folder) return;
note.parent_id = folder.id;
}
let isNew = !note.id;
let titleWasAutoAssigned = false;
if (isNew && !note.title) {
note.title = Note.defaultTitle(note);
titleWasAutoAssigned = true;
}
// Save only the properties that have changed
// let diff = null;
// if (!isNew) {
// diff = BaseModel.diffObjects(comp.state.lastSavedNote, note);
// diff.type_ = note.type_;
// diff.id = note.id;
// } else {
// diff = Object.assign({}, note);
// }
// const savedNote = await Note.save(diff);
let options = {};
let options = { userSideValidation: true };
if (!isNew) {
options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note);
}
const savedNote = ('fields' in options) && !options.fields.length ? Object.assign({}, note) : await Note.save(note, { userSideValidation: true });
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title);
if (hasAutoTitle) {
note.title = Note.defaultTitle(note);
if (options.fields && options.fields.indexOf('title') < 0) options.fields.push('title');
}
const savedNote = ('fields' in options) && !options.fields.length ? Object.assign({}, note) : await Note.save(note, options);
const stateNote = comp.state.note;
// Note was reloaded while being saved.
if (!isNew && (!stateNote || stateNote.id !== savedNote.id)) return;
// Re-assign any property that might have changed during saving (updated_time, etc.)
note = Object.assign(note, savedNote);
if (stateNote) {
if (stateNote.id === note.id) {
// But we preserve the current title and body because
// the user might have changed them between the time
// saveNoteButton_press was called and the note was
@ -67,17 +55,52 @@ shared.saveNoteButton_press = async function(comp) {
//
// If the title was auto-assigned above, we don't restore
// it from the state because it will be empty there.
if (!titleWasAutoAssigned) note.title = stateNote.title;
if (!hasAutoTitle) note.title = stateNote.title;
note.body = stateNote.body;
}
comp.setState({
let newState = {
lastSavedNote: Object.assign({}, note),
note: note,
});
};
if (isNew && hasAutoTitle) newState.newAndNoTitleChangeNoteId = note.id;
comp.setState(newState);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
// Geo-location for this note has been saved to the database however the properties
// are is not in the state so set them now.
const geoInfo = {
longitude: geoNote.longitude,
latitude: geoNote.latitude,
altitude: geoNote.altitude,
}
const modNote = Object.assign({}, stateNote, geoInfo);
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
comp.refreshNoteMetadata();
});
}
if (isNew) Note.updateGeolocation(note.id);
comp.refreshNoteMetadata();
if (isNew) {
// Clear the newNote item now that the note has been saved, and
// make sure that the note we're editing is selected.
comp.props.dispatch({
type: 'NOTE_SELECT',
id: savedNote.id,
});
}
}
shared.saveOneProperty = async function(comp, name, value) {
@ -106,9 +129,13 @@ shared.saveOneProperty = async function(comp, name, value) {
}
shared.noteComponent_change = function(comp, propName, propValue) {
let newState = {}
let note = Object.assign({}, comp.state.note);
note[propName] = propValue;
comp.setState({ note: note });
newState.note = note;
comp.setState(newState);
}
shared.refreshNoteMetadata = async function(comp, force = null) {
@ -120,7 +147,7 @@ shared.refreshNoteMetadata = async function(comp, force = null) {
shared.isModified = function(comp) {
if (!comp.state.note || !comp.state.lastSavedNote) return false;
let diff = BaseModel.diffObjects(comp.state.note, comp.state.lastSavedNote);
let diff = BaseModel.diffObjects(comp.state.lastSavedNote, comp.state.note);
delete diff.type_;
return !!Object.getOwnPropertyNames(diff).length;
}

View File

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

View File

@ -1,8 +1,5 @@
const fs = require('fs-extra');
const { promiseChain } = require('lib/promise-utils.js');
const moment = require('moment');
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,
@ -19,118 +16,73 @@ const { time } = require('lib/time-utils.js');
class FileApiDriverLocal {
fsErrorToJsError_(error) {
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;
}
stat(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (error, s) => {
if (error) {
if (error.code == 'ENOENT') {
resolve(null);
} else {
reject(this.fsErrorToJsError_(error));
}
return;
}
resolve(this.metadataFromStats_(path, s));
});
});
fsDriver() {
if (!FileApiDriverLocal.fsDriver_) throw new Error('FileApiDriverLocal.fsDriver_ not set!');
return FileApiDriverLocal.fsDriver_;
}
statTimeToTimestampMs_(time) {
let m = moment(time, 'YYYY-MM-DDTHH:mm:ss.SSSZ');
if (!m.isValid()) {
throw new Error('Invalid date: ' + time);
async stat(path) {
try {
const s = await this.fsDriver().stat(path);
if (!s) return null;
return this.metadataFromStat_(s);
} catch (error) {
throw this.fsErrorToJsError_(error);
}
return m.toDate().getTime();
}
metadataFromStats_(path, stats) {
metadataFromStat_(stat) {
return {
path: path,
created_time: this.statTimeToTimestampMs_(stats.birthtime),
updated_time: this.statTimeToTimestampMs_(stats.mtime),
created_time_orig: stats.birthtime,
updated_time_orig: stats.mtime,
isDir: stats.isDirectory(),
path: stat.path,
created_time: stat.birthtime.getTime(),
updated_time: stat.mtime.getTime(),
isDir: stat.isDirectory(),
};
}
setTimestamp(path, timestampMs) {
return new Promise((resolve, reject) => {
let t = Math.floor(timestampMs / 1000);
fs.utimes(path, t, t, (error) => {
if (error) {
reject(this.fsErrorToJsError_(error));
return;
}
resolve();
});
});
metadataFromStats_(stats) {
let output = [];
for (let i = 0; i < stats.length; i++) {
const mdStat = this.metadataFromStat_(stats[i]);
output.push(mdStat);
}
return output;
}
async setTimestamp(path, timestampMs) {
try {
await this.fsDriver().setTimestamp(path, new Date(timestampMs));
} catch (error) {
throw this.fsErrorToJsError_(error);
}
}
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 {
let items = await fs.readdir(path);
let output = [];
for (let i = 0; i < items.length; i++) {
let stat = await this.stat(path + '/' + items[i]);
if (!stat) continue; // Has been deleted between the readdir() call and now
stat.path = items[i];
output.push(stat);
}
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);
throw this.fsErrorToJsError_(error, path);
}
}
async list(path, options) {
try {
let items = await fs.readdir(path);
let output = [];
for (let i = 0; i < items.length; i++) {
let stat = await this.stat(path + '/' + items[i]);
if (!stat) continue; // Has been deleted between the readdir() call and now
stat.path = items[i];
output.push(stat);
}
const stats = await this.fsDriver().readDirStats(path);
const output = this.metadataFromStats_(stats);
return {
items: output,
@ -138,7 +90,7 @@ class FileApiDriverLocal {
context: null,
};
} catch(error) {
throw this.fsErrorToJsError_(error);
throw this.fsErrorToJsError_(error, path);
}
}
@ -147,96 +99,136 @@ class FileApiDriverLocal {
try {
if (options.target === 'file') {
output = await fs.copy(path, options.path, { overwrite: true });
//output = await fs.copy(path, options.path, { overwrite: true });
output = await this.fsDriver().copy(path, options.path);
} else {
output = await fs.readFile(path, options.encoding);
//output = await fs.readFile(path, options.encoding);
output = await this.fsDriver().readFile(path, options.encoding);
}
} catch (error) {
if (error.code == 'ENOENT') return null;
throw this.fsErrorToJsError_(error);
throw this.fsErrorToJsError_(error, path);
}
return output;
}
mkdir(path) {
return new Promise((resolve, reject) => {
fs.exists(path, (exists) => {
if (exists) {
resolve();
return;
}
async mkdir(path) {
if (await this.fsDriver().exists(path)) return;
try {
await this.fsDriver().mkdir(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
// return new Promise((resolve, reject) => {
// fs.exists(path, (exists) => {
// if (exists) {
// resolve();
// return;
// }
fs.mkdirp(path, (error) => {
if (error) {
reject(this.fsErrorToJsError_(error));
} else {
resolve();
}
});
});
});
// fs.mkdirp(path, (error) => {
// if (error) {
// reject(this.fsErrorToJsError_(error));
// } else {
// resolve();
// }
// });
// });
// });
}
async put(path, content, options = null) {
if (!options) options = {};
if (options.source === 'file') content = await fs.readFile(options.path);
try {
if (options.source === 'file') {
await this.fsDriver().copy(options.path, path);
return;
}
await this.fsDriver().writeFile(path, content, 'utf8');
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
return new Promise((resolve, reject) => {
fs.writeFile(path, content, function(error) {
if (error) {
reject(this.fsErrorToJsError_(error));
} else {
resolve();
}
});
});
// if (!options) options = {};
// if (options.source === 'file') content = await fs.readFile(options.path);
// return new Promise((resolve, reject) => {
// fs.writeFile(path, content, function(error) {
// if (error) {
// reject(this.fsErrorToJsError_(error));
// } else {
// resolve();
// }
// });
// });
}
delete(path) {
return new Promise((resolve, reject) => {
fs.unlink(path, function(error) {
if (error) {
if (error && error.code == 'ENOENT') {
// File doesn't exist - it's fine
resolve();
} else {
reject(this.fsErrorToJsError_(error));
}
} else {
resolve();
}
});
});
async delete(path) {
try {
await this.fsDriver().unlink(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
// return new Promise((resolve, reject) => {
// fs.unlink(path, function(error) {
// if (error) {
// if (error && error.code == 'ENOENT') {
// // File doesn't exist - it's fine
// resolve();
// } else {
// reject(this.fsErrorToJsError_(error));
// }
// } else {
// resolve();
// }
// });
// });
}
async move(oldPath, newPath) {
let lastError = null;
for (let i = 0; i < 5; i++) {
try {
let output = await fs.move(oldPath, newPath, { overwrite: true });
return output;
} catch (error) {
lastError = error;
// Normally cannot happen with the `overwrite` flag but sometime it still does.
// In this case, retry.
if (error.code == 'EEXIST') {
await time.sleep(1);
continue;
}
throw this.fsErrorToJsError_(error);
}
try {
await this.fsDriver().move(oldPath, newPath);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
throw lastError;
// let lastError = null;
// for (let i = 0; i < 5; i++) {
// try {
// let output = await fs.move(oldPath, newPath, { overwrite: true });
// return output;
// } catch (error) {
// lastError = error;
// // Normally cannot happen with the `overwrite` flag but sometime it still does.
// // In this case, retry.
// if (error.code == 'EEXIST') {
// await time.sleep(1);
// continue;
// }
// throw this.fsErrorToJsError_(error);
// }
// }
// throw lastError;
}
format() {
throw new Error('Not supported');
}
async clearRoot(baseDir) {
await this.fsDriver().remove(baseDir);
await this.fsDriver().mkdir(baseDir);
}
}
module.exports = { FileApiDriverLocal };

View File

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

View File

@ -189,6 +189,10 @@ class FileApiDriverOneDrive {
return this.pathCache_[path];
}
clearRoot() {
throw new Error('Not implemented');
}
async delta(path, options = null) {
let output = {
hasMore: false,
@ -196,17 +200,24 @@ class FileApiDriverOneDrive {
items: [],
};
const freshStartDelta = () => {
const url = this.makePath_(path) + ':/delta';
const query = this.itemFilter_();
query.select += ',deleted';
return { url: url, query: query };
}
const pathDetails = await this.pathDetails_(path);
const pathId = pathDetails.id;
const pathId = pathDetails.id;
let context = options ? options.context : null;
let url = context ? context.nextLink : null;
let query = null;
if (!url) {
url = this.makePath_(path) + ':/delta';
const query = this.itemFilter_();
query.select += ',deleted';
const info = freshStartDelta();
url = info.url;
query = info.query;
}
let response = null;
@ -218,18 +229,18 @@ class FileApiDriverOneDrive {
// Code: resyncRequired
// Request: GET https://graph.microsoft.com/v1.0/drive/root:/Apps/JoplinDev:/delta?select=...
// The delta token has expired or is invalid and so a full resync is required.
// It is an error that is hard to replicate and it's not entirely clear what
// URL is in the Location header. What might happen is that:
// - OneDrive will get all the latest changes (since delta is done at the
// end of the sync process)
// - Client will get all the new files and updates from OneDrive
// This is unknown:
// - Will the files that have been deleted on OneDrive be part of the this
// URL in the Location header?
//
// The delta token has expired or is invalid and so a full resync is required. This happens for example when all the items
// on the OneDrive App folder are manually deleted. In this case, instead of sending the list of deleted items in the delta
// call, OneDrive simply request the client to re-sync everything.
// OneDrive provides a URL to resume syncing from but it does not appear to work so below we simply start over from
// the beginning. The synchronizer will ensure that no duplicate are created and conflicts will be resolved.
// More info there: https://stackoverflow.com/q/46941371/561309
url = error.headers.get('location');
const info = freshStartDelta();
url = info.url;
query = info.query;
response = await this.api_.execJson('GET', url, query);
} else {
throw error;

View File

@ -0,0 +1,357 @@
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');
const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
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',
'd:getcontentlength', // Remove this once PUT call issue is sorted out
]);
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]);
const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
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 === '',
sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
};
}
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);
}
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
// works with paths relative to the base URL.
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
let output = '';
if (href.indexOf(baseUrl) === 0) {
output = href.substr(baseUrl.length);
} else if (href.indexOf(relativeBaseUrl) === 0) {
output = href.substr(relativeBaseUrl.length);
} else {
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
}
return rtrimSlashes(ltrimSlashes(output));
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
const baseUrl = this.api().baseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async list(path, options) {
// const relativeBaseUrl = this.api().relativeBaseUrl();
// function parsePropFindXml(xmlString) {
// return new Promise(async (resolve, reject) => {
// const saxOptions = {};
// const saxParser = require('sax').parser(false, { position: false });
// let stats = [];
// let currentStat = null;
// let currentText = '';
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // which makes parsing about 25% faster. However it's a bit of a hack so keep it as
// // an option so that it can be disabled if it causes problems.
// const optimizeXml = true;
// const tagResponse = optimizeXml ? 'd:r' : 'd:response';
// const tagGetLastModified = optimizeXml ? 'd:glm' : 'd:getlastmodified';
// const tagPropStat = optimizeXml ? 'd:ps' : 'd:propstat';
// const replaceUrls = optimizeXml;
// saxParser.onerror = function (error) {
// reject(new Error(e.toString()));
// };
// saxParser.ontext = function (t) {
// currentText += t;
// };
// saxParser.onopentag = function (node) {
// const tagName = node.name.toLowerCase();
// currentText = '';
// if (tagName === tagResponse) {
// currentStat = { isDir: false };
// }
// };
// saxParser.onclosetag = function(tagName) {
// tagName = tagName.toLowerCase();
// if (tagName === tagResponse) {
// if (currentStat.path) { // The list of resources includes the root dir too, which we don't want
// if (!currentStat.updated_time) throw new Error('Resource does not have a getlastmodified prop');
// stats.push(currentStat);
// }
// currentStat = null;
// }
// if (tagName === 'd:href') {
// const href = currentText;
// if (replaceUrls) {
// currentStat.path = rtrimSlashes(ltrimSlashes(href));
// } else {
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// currentStat.path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// }
// }
// if (tagName === tagGetLastModified) {
// const lastModifiedDate = new Date(currentText);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + currentText);
// currentStat.updated_time = lastModifiedDate.getTime();
// currentStat.created_time = currentStat.updated_time;
// }
// if (tagName === 'd:collection') {
// currentStat.isDir = true;
// }
// currentText = '';
// }
// saxParser.onend = function () {
// resolve(stats);
// };
// if (optimizeXml) {
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// }
// let idx = 0;
// let size = 1024 * 100;
// while (true) {
// sub = xmlString.substr(idx, size);
// if (!sub.length) break;
// saxParser.write(sub);
// idx += size;
// //await time.msleep(500);
// }
// saxParser.close();
// //saxParser.write(xmlString).close();
// });
// }
// For performance reasons, the response of the PROPFIND call is manually parsed with a regex below
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
// let output = [];
// let match = null;
// while (match = regex.exec(xmlString)) {
// const href = html_entity_decode(match[1]);
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
// const lastModifiedDate = new Date(match[2]);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
// output.push({
// path: path,
// updated_time: lastModifiedDate.getTime(),
// created_time: lastModifiedDate.getTime(),
// isDir: !BaseItem.isSystemPath(path),
// });
// }
// return output;
// }
// const resultXml = await this.api().execPropFind(path, 1, [
// 'd:getlastmodified',
// //'d:resourcetype', // Include this to use parsePropFindXml()
// ], { responseFormat: 'text' });
// const stats = await parsePropFindXml2(resultXml);
// return {
// items: stats,
// hasMore: false,
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources)
return {
items: stats,
hasMore: false,
context: null,
};
}
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
const response = await this.api().exec('GET', path, null, null, options);
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
// with a response body "The specified file doesn't exist." for non-existing files,
// so we need to check for this.
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
return response;
} catch (error) {
if (error.code !== 404) throw error;
}
}
async mkdir(path) {
try {
await this.api().exec('MKCOL', path);
} catch (error) {
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
// we get this code, verify that indeed the directory already exists and exit if it does.
if (error.code === 409) {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
async put(path, content, options = null) {
// In theory, if a client doesn't complete an upload, the file will not appear in the Nextcloud app. Likewise if
// the server interrupts the upload midway, the client should receive some kind of error and try uploading the
// file again next time. At the very least the file should not appear half-uploaded on the server. In practice
// however it seems some files might end up half uploaded on the server (at least on ocloud.de) so, for now,
// instead of doing a simple PUT, we do it to a temp file on Nextcloud, then check the file size and, if it
// matches, move it its actual place (hoping the server won't mess up and only copy half of the file).
// This is innefficient so once the bug is better understood it should hopefully be possible to go back to
// using a single PUT call.
let contentSize = 0;
if (content) contentSize = content.length;
if (options && options.path) {
const stat = await shim.fsDriver().stat(options.path);
contentSize = stat.size;
}
const tempPath = this.fileApi_.tempDirName() + '/' + basename(path) + '_' + Date.now();
await this.api().exec('PUT', tempPath, content, null, options);
const stat = await this.stat(tempPath);
if (stat.sizeDONOTUSE != contentSize) {
// await this.delete(tempPath);
throw new Error('WebDAV PUT - Size check failed for ' + tempPath + ' Expected: ' + contentSize + '. Found: ' + stat.sizeDONOTUSE);
}
await this.move(tempPath, path);
}
async delete(path) {
try {
await this.api().exec('DELETE', path);
} catch (error) {
if (error.code !== 404) throw error;
}
}
async move(oldPath, newPath) {
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
});
}
format() {
throw new Error('Not supported');
}
async clearRoot() {
await this.delete('');
await this.mkdir('');
}
}
module.exports = { FileApiDriverWebDav };

View File

@ -1,5 +1,9 @@
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');
const ArrayUtils = require('lib/ArrayUtils');
class FileApi {
@ -8,6 +12,21 @@ class FileApi {
this.driver_ = driver;
this.logger_ = new Logger();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
}
tempDirName() {
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
return this.tempDirName_;
}
setTempDirName(v) {
this.tempDirName_ = v;
}
fsDriver() {
return shim.fsDriver();
}
driver() {
@ -32,9 +51,10 @@ class FileApi {
}
fullPath_(path) {
let output = this.baseDir_;
if (path != '') output += '/' + path;
return output;
let output = [];
if (this.baseDir_) output.push(this.baseDir_);
if (path) output.push(path);
return output.join('/');
}
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
@ -57,6 +77,7 @@ class FileApi {
});
}
// Deprectated
setTimestamp(path, timestampMs) {
this.logger().debug('setTimestamp ' + this.fullPath_(path));
return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
@ -83,8 +104,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 +119,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 +141,120 @@ class FileApi {
}
module.exports = { FileApi };
function basicDeltaContextFromOptions_(options) {
let output = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
statIdsCache: null,
deletedItemsProcessed: false,
};
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;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && ('deletedItemsProcessed' in options.context) ? options.context.deletedItemsProcessed : false;
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,
statIdsCache: context.statIdsCache,
deletedItemsProcessed: context.deletedItemsProcessed,
};
// 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;
});
newContext.statIdsCache = newContext.statsCache.map((item) => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
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;
}
if (!newContext.deletedItemsProcessed) {
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
// Note that when deleted items are processed it might result in the output having
// more items than outputLimit. This is acceptable since delete operations are cheap.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
}
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) newContext.statsCache = null;
return {
hasMore: hasMore,
context: newContext,
items: output,
};
}
module.exports = { FileApi, basicDelta };

View File

@ -1,38 +1,135 @@
const fs = require('fs-extra');
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);
}
}
move(source, dest) {
return fs.move(source, dest, { overwrite: true });
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) {
let lastError = null;
for (let i = 0; i < 5; i++) {
try {
const output = await fs.move(source, dest, { overwrite: true });
return output;
} catch (error) {
lastError = error;
// Normally cannot happen with the `overwrite` flag but sometime it still does.
// In this case, retry.
if (error.code == 'EEXIST') {
await time.sleep(1);
continue;
}
throw this.fsErrorToJsError_(error);
}
}
throw lastError;
}
exists(path) {
return fs.pathExists(path);
}
open(path, mode) {
return fs.open(path, mode);
async mkdir(path) {
return fs.mkdirp(path);
}
close(handle) {
return fs.close(handle);
async stat(path) {
try {
const s = await fs.stat(path);
s.path = path;
return s;
} catch (error) {
if (error.code == 'ENOENT') return null;
throw error;
}
}
readFile(path) {
return fs.readFile(path);
async setTimestamp(path, timestampDate) {
return fs.utimes(path, timestampDate, timestampDate);
}
async readDirStats(path) {
let items = await fs.readdir(path);
let output = [];
for (let i = 0; i < items.length; i++) {
let stat = await this.stat(path + '/' + items[i]);
if (!stat) continue; // Has been deleted between the readdir() call and now
stat.path = stat.path.substr(path.length + 1);
output.push(stat);
}
return output;
}
async open(path, mode) {
try {
return await fs.open(path, mode);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async close(handle) {
try {
return await fs.close(handle);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
readFile(path, encoding = 'utf8') {
if (encoding === 'Buffer') return fs.readFile(path); // Returns the raw buffer
return fs.readFile(path, encoding);
}
// Always overwrite destination
async copy(source, dest) {
return fs.copy(source, dest, { overwrite: true });
}
async unlink(path) {

View File

@ -10,10 +10,41 @@ class FsDriverRN {
return RNFS.appendFile(path, string, encoding);
}
writeFile(path, string, encoding = 'base64') {
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');
}
// Returns a format compatible with Node.js format
rnfsStatToStd_(stat, path) {
return {
birthtime: stat.ctime ? stat.ctime : stat.mtime, // Confusingly, "ctime" normally means "change time" but here it's used as "creation time". Also sometimes it is null
mtime: stat.mtime,
isDirectory: () => stat.isDirectory(),
path: path,
size: stat.size,
};
}
async readDirStats(path) {
let items = await RNFS.readDir(path);
let output = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const relativePath = item.path.substr(path.length + 1);
output.push(this.rnfsStatToStd_(item, relativePath));
}
return output;
}
async move(source, dest) {
return RNFS.moveFile(source, dest);
}
@ -22,11 +53,38 @@ class FsDriverRN {
return RNFS.exists(path);
}
async mkdir(path) {
return RNFS.mkdir(path);
}
async stat(path) {
try {
const r = await RNFS.stat(path);
return this.rnfsStatToStd_(r, path);
} catch (error) {
if (error && error.message && error.message.indexOf('exist') >= 0) {
// Probably { [Error: File does not exist] framesToPop: 1, code: 'EUNSPECIFIED' }
// which unfortunately does not have a proper error code. Can be ignored.
return null;
} else {
throw error;
}
}
}
// NOTE: DOES NOT WORK - no error is thrown and the function is called with the right
// arguments but the function returns `false` and the timestamp is not set.
// Current setTimestamp is not really used so keep it that way, but careful if it
// becomes needed.
async setTimestamp(path, timestampDate) {
// return RNFS.touch(path, timestampDate, timestampDate);
}
async open(path, mode) {
// Note: RNFS.read() doesn't provide any way to know if the end of file has been reached.
// So instead we stat the file here and use stat.size to manually check for end of file.
// Bug: https://github.com/itinance/react-native-fs/issues/342
const stat = await RNFS.stat(path);
const stat = await this.stat(path);
return {
path: path,
offset: 0,
@ -39,8 +97,23 @@ class FsDriverRN {
return null;
}
readFile(path) {
throw new Error('Not implemented');
readFile(path, encoding = 'utf8') {
if (encoding === 'Buffer') throw new Error('Raw buffer output not supported for FsDriverRN.readFile');
return RNFS.readFile(path, encoding);
}
// Always overwrite destination
async copy(source, dest) {
let retry = false;
try {
await RNFS.copyFile(source, dest);
} catch (error) {
// On iOS it will throw an error if the file already exist
retry = true;
await this.unlink(dest);
}
if (retry) await RNFS.copyFile(source, dest);
}
async unlink(path) {

View File

@ -297,4 +297,4 @@ function _(s, ...args) {
return sprintf(result, ...args);
}
module.exports = { _, supportedLocales, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode };
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@ -25,6 +25,10 @@ class Logger {
return this.level_;
}
targets() {
return this.targets_;
}
clearTargets() {
this.targets_.clear();
}
@ -81,11 +85,15 @@ class Logger {
}
// Only for database at the moment
async lastEntries(limit = 100) {
async lastEntries(limit = 100, options = null) {
if (options === null) options = {};
if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (!options.levels.length) return [];
for (let i = 0; i < this.targets_.length; i++) {
const target = this.targets_[i];
if (target.type == 'database') {
let sql = 'SELECT * FROM logs ORDER BY timestamp DESC';
let sql = 'SELECT * FROM logs WHERE level IN (' + options.levels.join(',') + ') ORDER BY timestamp DESC';
if (limit !== null) sql += ' LIMIT ' + limit
return await target.database.selectAll(sql);
}

View File

@ -174,6 +174,15 @@ class BaseItem extends BaseModel {
}
}
// Note: Currently, once a deleted_items entry has been processed, it is removed from the database. In practice it means that
// the following case will not work as expected:
// - Client 1 creates a note and sync with target 1 and 2
// - Client 2 sync with target 1
// - Client 2 deletes note and sync with target 1
// - Client 1 syncs with target 1 only (note is deleted from local machine, as expected)
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
// that it was previously deleted exist (deleted_items entry has been deleted).
// The solution would be to permanently store the list of deleted items on each client.
static deletedItems(syncTarget) {
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
}

View File

@ -69,8 +69,6 @@ class Note extends BaseItem {
}
static defaultTitle(note) {
if (note.title && note.title.length) return note.title;
if (note.body && note.body.length) {
const lines = note.body.trim().split("\n");
return lines[0].trim().substr(0, 80).trim();

View File

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

View File

@ -60,9 +60,24 @@ class Setting extends BaseModel {
// })},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'newNoteFocus': { value: 'body', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new note:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'showTrayIcon': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon') },
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false },
'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 +90,27 @@ 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.6.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV URL') },
'sync.6.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV username') },
'sync.6.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV password'), secure: true },
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
@ -192,7 +222,8 @@ class Setting extends BaseModel {
if (c.value === value) return;
this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
// Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs
// this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
c.value = value;

View File

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

View File

@ -29,6 +29,7 @@ const defaultState = {
appState: 'starting',
//windowContentSize: { width: 0, height: 0 },
hasDisabledSyncItems: false,
newNote: null,
};
function arrayHasEncryptedItems(array) {
@ -144,12 +145,14 @@ function changeSelectedNotes(state, action) {
if (action.type === 'NOTE_SELECT') {
newState.selectedNoteIds = noteIds;
newState.newNote = null;
return newState;
}
if (action.type === 'NOTE_SELECT_ADD') {
if (!noteIds.length) return state;
newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
newState.newNote = null;
return newState;
}
@ -164,6 +167,7 @@ function changeSelectedNotes(state, action) {
newSelectedNoteIds.push(id);
}
newState.selectedNoteIds = newSelectedNoteIds;
newState.newNote = null;
return newState;
}
@ -177,6 +181,8 @@ function changeSelectedNotes(state, action) {
newState = changeSelectedNotes(state, { type: 'NOTE_SELECT_ADD', id: noteIds[0] });
}
newState.newNote = null;
return newState;
}
@ -455,6 +461,12 @@ const reducer = (state = defaultState, action) => {
newState.hasDisabledSyncItems = true;
break;
case 'NOTE_SET_NEW_ONE':
newState = Object.assign({}, state);
newState.newNote = action.item;
break;
}
} catch (error) {
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);

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