You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
58 Commits
v0.10.43
...
android-v0
Author | SHA1 | Date | |
---|---|---|---|
|
c984c19fee | ||
|
ac8e91e82e | ||
|
738ef2b0fa | ||
|
9746a3964b | ||
|
9efbf74b6f | ||
|
c16ea6b237 | ||
|
b06a3b588f | ||
|
6ff67e0995 | ||
|
1a5c8d126d | ||
|
f632580eed | ||
|
1d73f0cdee | ||
|
99c7111f8c | ||
|
ae9806561a | ||
|
fffdf5b5b7 | ||
|
3de19c3db7 | ||
|
56e074b4ef | ||
|
1a79253780 | ||
|
b67908df11 | ||
|
6a5089f71d | ||
|
f710463b67 | ||
|
6ae0c3aba0 | ||
|
07c6347014 | ||
|
b10999e83e | ||
|
961b5bfd25 | ||
|
d1f1d1068a | ||
|
faade0afe2 | ||
|
a442a49e2f | ||
|
7d3fbbcaba | ||
|
d9bb7c3271 | ||
|
4d1dd17fa2 | ||
|
c5c6c777be | ||
|
1fd1a73fda | ||
|
feeb498a79 | ||
|
1d7f30d441 | ||
|
424443a2d8 | ||
|
08b58f0e4c | ||
|
c2a0d8600f | ||
|
ede3c2ce2f | ||
|
0b93515711 | ||
|
2f13e689b9 | ||
|
ea135a0d28 | ||
|
f67e4a03e4 | ||
|
e9268edeff | ||
|
a8576a55d6 | ||
|
eb500cdf9e | ||
|
7b9dc66121 | ||
|
bba2c68c6f | ||
|
c70d8bea78 | ||
|
176bda66ad | ||
|
78ce10ddf0 | ||
|
29f14681a8 | ||
|
aaf617e41c | ||
|
b99146ed7f | ||
|
d136161650 | ||
|
39051a27a1 | ||
|
277ad90f72 | ||
|
f2e3bedde6 | ||
|
98c0f2315a |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -35,4 +35,6 @@ _vieux/
|
||||
_mydocs
|
||||
.DS_Store
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
2
BUILD.md
2
BUILD.md
@@ -28,6 +28,8 @@ yarn dist
|
||||
|
||||
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
|
||||
|
||||
For node-gyp to work, you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
|
||||
|
||||
That will create the executable file in the `dist` directory.
|
||||
|
||||
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
|
@@ -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_);
|
||||
|
@@ -23,7 +23,8 @@ class Command extends BaseCommand {
|
||||
const verbose = args.options.verbose;
|
||||
|
||||
const renderKeyValue = (name) => {
|
||||
const value = Setting.value(name);
|
||||
let value = Setting.value(name);
|
||||
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
|
||||
if (Setting.isEnum(name)) {
|
||||
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
|
||||
} else {
|
||||
|
@@ -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();
|
||||
|
@@ -3,6 +3,13 @@
|
||||
// Make it possible to require("/lib/...") without specifying full path
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const compareVersion = require('compare-version');
|
||||
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
|
||||
if (compareVersion(nodeVersion, '8.0.0') < 0) {
|
||||
console.error('Joplin requires Node 8+. Detected version ' + nodeVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { app } = require('./app.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
@@ -16,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.
|
||||
|
@@ -616,9 +616,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Datei"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Neue Notiz"
|
||||
|
||||
@@ -688,6 +685,9 @@ msgstr "Abbrechen"
|
||||
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 "
|
||||
@@ -760,15 +760,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:"
|
||||
|
||||
@@ -954,6 +948,10 @@ msgstr "Lokale Objekte gelöscht: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Remote Objekte gelöscht: %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Lokale Objekte erstellt: %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "Status: \"%s\"."
|
||||
@@ -1250,6 +1248,12 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Willkommen"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Notizen Titel:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "To-Do Titel:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
||||
|
@@ -530,9 +530,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr ""
|
||||
|
||||
@@ -601,6 +598,9 @@ msgstr ""
|
||||
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 "
|
||||
@@ -667,15 +667,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 ""
|
||||
|
||||
@@ -847,6 +841,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
|
@@ -586,9 +586,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Archivo"
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nueva nota"
|
||||
|
||||
@@ -659,6 +656,9 @@ msgstr "Cancelar"
|
||||
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 "
|
||||
@@ -729,15 +729,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:"
|
||||
|
||||
@@ -927,6 +921,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\"."
|
||||
@@ -1227,6 +1225,12 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenido"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Título de nota:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Títuto de lista de tareas:"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Eliminar libreta?"
|
||||
|
||||
|
@@ -601,9 +601,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Archivo"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Guardar"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nota nueva"
|
||||
|
||||
@@ -673,6 +670,9 @@ msgstr "Cancelar"
|
||||
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 "
|
||||
@@ -739,15 +739,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:"
|
||||
|
||||
@@ -928,6 +922,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»."
|
||||
@@ -1217,6 +1215,12 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenido"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Título de la nota:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Títuto de lista de tareas:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "«%s»: «%s»"
|
||||
|
||||
|
@@ -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."
|
||||
@@ -603,9 +603,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nouvelle note"
|
||||
|
||||
@@ -646,7 +643,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"
|
||||
@@ -674,14 +671,17 @@ msgstr "Annulation"
|
||||
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 +689,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 +730,16 @@ 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 "Status"
|
||||
msgstr "État"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr "L'encryptage est :"
|
||||
msgstr "Le cryptage est :"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
@@ -754,15 +754,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 +770,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 +817,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 +838,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 ?"
|
||||
@@ -946,6 +940,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\"."
|
||||
@@ -1135,7 +1133,7 @@ msgid "Export Debug Report"
|
||||
msgstr "Exporter rapport de débogage"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr "Config encryptage"
|
||||
msgstr "Config cryptage"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Configuration"
|
||||
@@ -1212,10 +1210,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 +1236,12 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenue"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Titre de la note :"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Titre de la tâche :"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Supprimer le carnet ?"
|
||||
|
||||
|
@@ -609,9 +609,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Datoteka"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Spremi"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nova bilješka"
|
||||
|
||||
@@ -681,6 +678,9 @@ msgstr "Odustani"
|
||||
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 "
|
||||
@@ -749,15 +749,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:"
|
||||
|
||||
@@ -936,6 +930,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\"."
|
||||
@@ -1223,5 +1221,11 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
|
||||
msgid "Welcome"
|
||||
msgstr "Dobro došli"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Naslov bilješke:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Naslov zadatka:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
@@ -587,9 +587,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nuova nota"
|
||||
|
||||
@@ -659,6 +656,9 @@ msgstr "Cancella"
|
||||
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 "
|
||||
@@ -727,15 +727,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:"
|
||||
|
||||
@@ -918,6 +912,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\"."
|
||||
@@ -1208,6 +1206,12 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Benvenuto"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Titolo della Nota:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Titolo dell'attività:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
||||
|
@@ -586,9 +586,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "ファイル"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "新しいノート"
|
||||
|
||||
@@ -658,6 +655,9 @@ msgstr "キャンセル"
|
||||
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 "
|
||||
@@ -730,15 +730,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 "ノートブックの題名:"
|
||||
|
||||
@@ -919,6 +913,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\"。"
|
||||
@@ -1209,3 +1207,9 @@ msgstr ""
|
||||
|
||||
msgid "Welcome"
|
||||
msgstr "ようこそ"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "ノートの題名:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "ToDoの題名:"
|
||||
|
@@ -530,9 +530,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr ""
|
||||
|
||||
@@ -601,6 +598,9 @@ msgstr ""
|
||||
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 "
|
||||
@@ -667,15 +667,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 ""
|
||||
|
||||
@@ -847,6 +841,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
|
1238
CliClient/locales/nl_BE.po
Normal file
1238
CliClient/locales/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -581,9 +581,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Arquivo"
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Nova nota"
|
||||
|
||||
@@ -654,6 +651,9 @@ msgstr "Cancelar"
|
||||
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 "
|
||||
@@ -723,15 +723,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:"
|
||||
|
||||
@@ -916,6 +910,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\"."
|
||||
@@ -1204,6 +1202,12 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
|
||||
msgid "Welcome"
|
||||
msgstr "Bem-vindo"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Título da nota:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Título da tarefa:"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Excluir caderno?"
|
||||
|
||||
|
@@ -597,9 +597,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
msgid "New note"
|
||||
msgstr "Новая заметка"
|
||||
|
||||
@@ -670,6 +667,9 @@ msgstr "Отмена"
|
||||
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 "
|
||||
@@ -748,15 +748,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 "Название блокнота:"
|
||||
|
||||
@@ -936,6 +930,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»."
|
||||
@@ -1226,5 +1224,11 @@ msgstr "У вас сейчас нет блокнота. Создайте его
|
||||
msgid "Welcome"
|
||||
msgstr "Добро пожаловать"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "Название заметки:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Название задачи:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "«%s»: «%s»"
|
||||
|
@@ -553,9 +553,6 @@ msgstr ""
|
||||
msgid "File"
|
||||
msgstr "文件"
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "New note"
|
||||
msgstr "新笔记"
|
||||
|
||||
@@ -625,6 +622,9 @@ msgstr "取消"
|
||||
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 "
|
||||
@@ -693,15 +693,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 "笔记本标题:"
|
||||
|
||||
@@ -881,6 +875,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\"。"
|
||||
@@ -1160,6 +1158,12 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
|
||||
msgid "Welcome"
|
||||
msgstr "欢迎"
|
||||
|
||||
#~ msgid "Note title:"
|
||||
#~ msgstr "笔记标题:"
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "待办事项标题:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
||||
|
45
CliClient/package-lock.json
generated
45
CliClient/package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.86",
|
||||
"version": "0.10.88",
|
||||
"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",
|
||||
@@ -197,6 +197,11 @@
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"compare-version": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
||||
"integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -437,7 +442,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",
|
||||
@@ -489,7 +494,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"
|
||||
}
|
||||
},
|
||||
@@ -948,9 +953,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",
|
||||
@@ -1915,9 +1920,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"
|
||||
}
|
||||
@@ -2014,15 +2019,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"
|
||||
}
|
||||
},
|
||||
@@ -2032,16 +2037,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": {
|
||||
|
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.86",
|
||||
"version": "0.10.88",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"compare-version": "^0.1.2",
|
||||
"follow-redirects": "^1.2.4",
|
||||
"form-data": "^2.1.4",
|
||||
"fs-extra": "^5.0.0",
|
||||
@@ -55,7 +56,7 @@
|
||||
"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",
|
||||
"uuid": "^3.0.1",
|
||||
"word-wrap": "^1.2.3",
|
||||
"yargs-parser": "^7.0.0"
|
||||
|
@@ -9,4 +9,10 @@ bash $SCRIPT_DIR/build.sh
|
||||
cp "$SCRIPT_DIR/package.json" build/
|
||||
cp "$SCRIPT_DIR/../README.md" build/
|
||||
cd "$SCRIPT_DIR/build"
|
||||
npm publish
|
||||
npm publish
|
||||
|
||||
NEW_VERSION=$("cat package.json | jq -r .version")
|
||||
git add -A
|
||||
git commit -m "CLI v$NEW_VERSION"
|
||||
git tag "cli-v$NEW_VERSION"
|
||||
git push && git push --tags
|
@@ -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 });
|
||||
@@ -830,6 +848,8 @@ describe('Synchronizer', function() {
|
||||
}));
|
||||
|
||||
it('should sync resources', asyncTest(async () => {
|
||||
while (insideBeforeEach) await time.msleep(100);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||
|
@@ -38,6 +38,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);
|
||||
@@ -142,25 +143,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) {
|
||||
|
@@ -171,16 +171,6 @@ class Application extends BaseApplication {
|
||||
{
|
||||
label: _('File'),
|
||||
submenu: [{
|
||||
// label: _('Save'),
|
||||
// accelerator: 'CommandOrControl+S',
|
||||
// screens: ['Main'],
|
||||
// click: () => {
|
||||
// this.dispatch({
|
||||
// type: 'WINDOW_COMMAND',
|
||||
// name: 'save_ntoe',
|
||||
// });
|
||||
// }
|
||||
// }, {
|
||||
label: _('New note'),
|
||||
accelerator: 'CommandOrControl+N',
|
||||
screens: ['Main'],
|
||||
|
@@ -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: {
|
||||
|
@@ -54,7 +54,16 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
const currentItemId = event.currentTarget.getAttribute('data-id');
|
||||
if (!currentItemId) return;
|
||||
|
||||
let noteIds = [];
|
||||
if (this.props.selectedNoteIds.indexOf(currentItemId) < 0) {
|
||||
noteIds = [currentItemId];
|
||||
} else {
|
||||
noteIds = this.props.selectedNoteIds;
|
||||
}
|
||||
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id));
|
||||
@@ -137,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.
|
||||
@@ -163,6 +175,7 @@ class NoteListComponent extends React.Component {
|
||||
style={listItemTitleStyle}
|
||||
onClick={(event) => { onTitleClick(event, item) }}
|
||||
onDragStart={(event) => onDragStart(event) }
|
||||
data-id={item.id}
|
||||
>
|
||||
{Note.displayTitle(item)}
|
||||
</a>
|
||||
@@ -172,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',
|
||||
@@ -192,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>
|
||||
);
|
||||
|
@@ -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,17 +144,32 @@ class NoteTextComponent extends React.Component {
|
||||
if (!options) options = {};
|
||||
if (!('noReloadIfLocalChanges' in options)) options.noReloadIfLocalChanges = false;
|
||||
|
||||
const noteId = props.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;
|
||||
@@ -146,35 +177,53 @@ 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);
|
||||
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
// Scroll back to top when loading new note
|
||||
if (loadingNewNote) {
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
|
||||
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// and then (in the renderer callback) to the value we actually need. The first
|
||||
// operation helps clear the scroll position cache. See:
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
this.editorSetScrollTop(1);
|
||||
this.restoreScrollTop_ = 0;
|
||||
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// and then (in the renderer callback) to the value we actually need. The first
|
||||
// operation helps clear the scroll position cache. See:
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
this.editorSetScrollTop(1);
|
||||
this.restoreScrollTop_ = 0;
|
||||
|
||||
this.setState({
|
||||
note: note,
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
webviewReady: webviewReady,
|
||||
});
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
await this.reloadNote(nextProps);
|
||||
if(this.editor_){
|
||||
if (this.editor_) {
|
||||
const session = this.editor_.editor.getSession();
|
||||
const undoManager = session.getUndoManager();
|
||||
undoManager.reset();
|
||||
session.setUndoManager(undoManager);
|
||||
|
||||
this.editor_.editor.focus();
|
||||
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 });
|
||||
}
|
||||
@@ -190,6 +239,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
title_changeText(event) {
|
||||
shared.noteComponent_change(this, 'title', event.target.value);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
@@ -397,20 +447,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'];
|
||||
|
||||
@@ -514,13 +554,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',
|
||||
@@ -544,7 +577,7 @@ class NoteTextComponent extends React.Component {
|
||||
const titleEditor = <input
|
||||
type="text"
|
||||
style={titleEditorStyle}
|
||||
value={note ? note.title : ''}
|
||||
value={note && note.title ? note.title : ''}
|
||||
onChange={(event) => { this.title_changeText(event); }}
|
||||
/>
|
||||
|
||||
@@ -612,6 +645,7 @@ const mapStateToProps = (state) => {
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
syncStarted: state.syncStarted,
|
||||
newNote: state.newNote,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -42,17 +42,20 @@ class PromptDialog extends React.Component {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const paddingTop = 20;
|
||||
|
||||
this.styles_.modalLayer = {
|
||||
zIndex: 9999,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
height: height - paddingTop,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
display: visible ? 'flex' : 'none',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
paddingTop: paddingTop + 'px',
|
||||
};
|
||||
|
||||
this.styles_.promptDialog = {
|
||||
@@ -88,24 +91,6 @@ class PromptDialog extends React.Component {
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
// shouldComponentUpdate(nextProps, nextState) {
|
||||
// console.info(JSON.stringify(nextProps)+JSON.stringify(nextState));
|
||||
|
||||
// console.info('NEXT PROPS ====================');
|
||||
// for (var n in nextProps) {
|
||||
// if (!nextProps.hasOwnProperty(n)) continue;
|
||||
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
|
||||
// }
|
||||
|
||||
// console.info('NEXT STATE ====================');
|
||||
// for (var n in nextState) {
|
||||
// if (!nextState.hasOwnProperty(n)) continue;
|
||||
// console.info(n + ' = ' + (nextState[n] === this.state[n]));
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
@@ -35,6 +35,7 @@ class SideBarComponent extends React.Component {
|
||||
alignItems: 'center',
|
||||
cursor: 'default',
|
||||
opacity: 0.8,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
listItemSelected: {
|
||||
backgroundColor: theme.selectedColor2,
|
||||
|
@@ -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
@@ -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
1
ElectronClient/app/locales/nl_BE.json
Normal file
1
ElectronClient/app/locales/nl_BE.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,11 +17,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.
|
||||
|
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.47",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.43",
|
||||
"version": "0.10.47",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
24
README.md
24
README.md
@@ -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.41/Joplin-Setup-0.10.41.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.41/Joplin-0.10.41.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.41/Joplin-0.10.41-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.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>
|
||||
|
||||
## 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.78/joplin-v0.10.78.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
|
||||
|
||||
@@ -95,6 +95,12 @@ On the **terminal application**, to initiate the synchronisation process, type `
|
||||
|
||||
*/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 +113,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,7 +121,7 @@ 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 English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
|
||||
|
||||
- [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).
|
||||
|
@@ -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
4
README_faq.md
Normal 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.
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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 85
|
||||
versionName "0.10.70"
|
||||
versionCode 2097256
|
||||
versionName "0.10.78"
|
||||
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"
|
||||
|
@@ -5,6 +5,7 @@
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
|
||||
@@ -41,8 +42,8 @@
|
||||
E8DD8252C0DD4CF1B53590E9 /* SimpleLineIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 69B8EE98BFBC4AABA4885BB0 /* SimpleLineIcons.ttf */; };
|
||||
EA501DCDCF4745E9B63ECE98 /* Octicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7D46CBDF8846409890AD7A84 /* Octicons.ttf */; };
|
||||
EC11356C90E9419799A2626F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */; };
|
||||
FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB8BCAEA9AA41CAAE460443 /* libsqlite3.0.tbd */; };
|
||||
F3D0BB525E6C490294D73075 /* libRNSecureRandom.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */; };
|
||||
FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB8BCAEA9AA41CAAE460443 /* libsqlite3.0.tbd */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -123,6 +124,13 @@
|
||||
remoteGlobalIDString = 3D3CD90B1DE5FBD600167DC4;
|
||||
remoteInfo = jschelpers;
|
||||
};
|
||||
4D2A44E7200015A2001CA388 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||
remoteInfo = RNSecureRandom;
|
||||
};
|
||||
4D2A85A91FBCE3AC0028537D /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
@@ -370,6 +378,8 @@
|
||||
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
|
||||
15FD7D2C8F0A445BBA807A9D /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = "<group>"; };
|
||||
1F79F2CD7CED446B986A6252 /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = "<group>"; };
|
||||
22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSecureRandom.a; sourceTree = "<group>"; };
|
||||
252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSecureRandom.xcodeproj; path = "../node_modules/react-native-securerandom/ios/RNSecureRandom.xcodeproj"; sourceTree = "<group>"; };
|
||||
381C047F2739439CB3E6452A /* libRNVectorIcons.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNVectorIcons.a; sourceTree = "<group>"; };
|
||||
3FFC0F5EFDC54862B1F998DD /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; };
|
||||
44A39642217548C8ADA91CBA /* libRNImagePicker.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNImagePicker.a; sourceTree = "<group>"; };
|
||||
@@ -398,8 +408,6 @@
|
||||
F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = "<group>"; };
|
||||
FD370E24D76E461D960DD85D /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = "<group>"; };
|
||||
FF411B45E68B4A8CBCC35777 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = "<group>"; };
|
||||
252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */ = {isa = PBXFileReference; name = "RNSecureRandom.xcodeproj"; path = "../node_modules/react-native-securerandom/ios/RNSecureRandom.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
|
||||
22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */ = {isa = PBXFileReference; name = "libRNSecureRandom.a"; path = "libRNSecureRandom.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -540,6 +548,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4D2A44E4200015A2001CA388 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4D2A44E8200015A2001CA388 /* libRNSecureRandom.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4D2A85911FBCE3950028537D /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -550,6 +566,7 @@
|
||||
87BABCF4ED0A406B9546CCE9 /* libSQLite.a */,
|
||||
381C047F2739439CB3E6452A /* libRNVectorIcons.a */,
|
||||
44A39642217548C8ADA91CBA /* libRNImagePicker.a */,
|
||||
22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
@@ -853,6 +870,10 @@
|
||||
ProductGroup = 4DA7F8091FC1DA9C00353191 /* Products */;
|
||||
ProjectRef = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 4D2A44E4200015A2001CA388 /* Products */;
|
||||
ProjectRef = 252BD7B86BF7435B960DA901 /* RNSecureRandom.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 4D2A85B71FBCE3AC0028537D /* Products */;
|
||||
ProjectRef = 711CBD21F0894B83A2D8E234 /* RNVectorIcons.xcodeproj */;
|
||||
@@ -947,6 +968,13 @@
|
||||
remoteRef = 3DAD3EAC1DF850E9000B6D8A /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2A44E8200015A2001CA388 /* libRNSecureRandom.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRNSecureRandom.a;
|
||||
remoteRef = 4D2A44E7200015A2001CA388 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2A85AA1FBCE3AC0028537D /* libfishhook.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
@@ -1257,10 +1285,14 @@
|
||||
"$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
"$(SRCROOT)..\node_modules\neact-native-image-pickerios",
|
||||
"$(SRCROOT)\..\node_modules\react-native-securerandom\ios",
|
||||
"$(SRCROOT)..\node_modules\neact-native-securerandomios",
|
||||
);
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/Joplin\"",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1272,10 +1304,6 @@
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/Joplin\"",
|
||||
);
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1297,10 +1325,14 @@
|
||||
"$(SRCROOT)/../node_modules/react-native-sqlite-storage/src/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
"$(SRCROOT)..\node_modules\neact-native-image-pickerios",
|
||||
"$(SRCROOT)\..\node_modules\react-native-securerandom\ios",
|
||||
"$(SRCROOT)..\node_modules\neact-native-securerandomios",
|
||||
);
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/Joplin\"",
|
||||
);
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1312,10 +1344,6 @@
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\"$(SRCROOT)/Joplin\"",
|
||||
);
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.10.6</string>
|
||||
<string>0.10.9</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>6</string>
|
||||
<string>9</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
@@ -277,6 +277,10 @@ class BaseApplication {
|
||||
type: 'MASTERKEY_REMOVE_NOT_LOADED',
|
||||
ids: loadedMasterKeyIds,
|
||||
});
|
||||
|
||||
// Schedule a sync operation so that items that need to be encrypted
|
||||
// are sent to sync target.
|
||||
reg.scheduleSync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -193,8 +193,12 @@ class BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
static loadByField(fieldName, fieldValue) {
|
||||
return this.modelSelectOne('SELECT * FROM `' + this.tableName() + '` WHERE `' + fieldName + '` = ?', [fieldValue]);
|
||||
static loadByField(fieldName, fieldValue, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!('caseInsensitive' in options)) options.caseInsensitive = false;
|
||||
let sql = 'SELECT * FROM `' + this.tableName() + '` WHERE `' + fieldName + '` = ?';
|
||||
if (options.caseInsensitive) sql += ' COLLATE NOCASE';
|
||||
return this.modelSelectOne(sql, [fieldValue]);
|
||||
}
|
||||
|
||||
static loadByTitle(fieldValue) {
|
||||
@@ -250,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();
|
||||
|
||||
@@ -292,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);
|
||||
}
|
||||
|
||||
|
36
ReactNativeClient/lib/Cache.js
Normal file
36
ReactNativeClient/lib/Cache.js
Normal 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;
|
@@ -125,9 +125,9 @@ class MdToHtml {
|
||||
|
||||
renderOpenLink_(attrs, options) {
|
||||
let href = this.getAttr_(attrs, 'href');
|
||||
const title = this.getAttr_(attrs, 'title');
|
||||
const text = this.getAttr_(attrs, 'text');
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
|
||||
|
||||
if (isResourceUrl && !this.supportsResourceLinks_) {
|
||||
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
|
||||
@@ -305,13 +305,15 @@ class MdToHtml {
|
||||
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
||||
`;
|
||||
|
||||
const fontFamily = 'sans-serif';
|
||||
|
||||
const css = `
|
||||
body {
|
||||
font-size: ` + style.htmlFontSize + `;
|
||||
color: ` + style.htmlColor + `;
|
||||
line-height: ` + style.htmlLineHeight + `;
|
||||
background-color: ` + style.htmlBackgroundColor + `;
|
||||
font-family: sans-serif;
|
||||
font-family: ` + fontFamily + `;
|
||||
padding-bottom: ` + options.paddingBottom + `;
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6, ul, table {
|
||||
@@ -359,6 +361,10 @@ class MdToHtml {
|
||||
td, th {
|
||||
border: 1px solid silver;
|
||||
padding: .5em 1em .5em 1em;
|
||||
font-size: ` + style.htmlFontSize + `;
|
||||
color: ` + style.htmlColor + `;
|
||||
background-color: ` + style.htmlBackgroundColor + `;
|
||||
font-family: ` + fontFamily + `;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, KeyboardAvoidingView } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { Log } = require('lib/log.js');
|
||||
@@ -34,7 +34,7 @@ const AlarmService = require('lib/services/AlarmService.js');
|
||||
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
|
||||
static navigationOptions(options) {
|
||||
return { header: null };
|
||||
}
|
||||
@@ -51,11 +51,12 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
isLoading: true,
|
||||
titleTextInputHeight: 20,
|
||||
alarmDialogShown: false,
|
||||
heightBumpView:0
|
||||
};
|
||||
|
||||
// iOS doesn't support multiline text fields properly so disable it
|
||||
this.enableMultilineTitle_ = Platform.OS !== 'ios';
|
||||
|
||||
|
||||
this.saveButtonHasBeenShown_ = false;
|
||||
|
||||
this.styles_ = {};
|
||||
@@ -148,6 +149,12 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
await shared.initState(this);
|
||||
|
||||
this.refreshNoteMetadata();
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this._keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this._keyboardDidHide.bind(this));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshNoteMetadata(force = null) {
|
||||
@@ -156,6 +163,19 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.backHandler);
|
||||
|
||||
if (Platform.OS === 'ios'){
|
||||
this.keyboardDidShowListener.remove();
|
||||
this.keyboardDidHideListener.remove();
|
||||
}
|
||||
}
|
||||
|
||||
_keyboardDidShow () {
|
||||
this.setState({ heightBumpView:30 })
|
||||
}
|
||||
|
||||
_keyboardDidHide () {
|
||||
this.setState({ heightBumpView:0 })
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
@@ -241,13 +261,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG';
|
||||
reg.logger().info('Resizing image ' + localFilePath);
|
||||
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); //, 0, targetPath);
|
||||
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
reg.logger().info('Moving ' + resizedImagePath + ' => ' + targetPath);
|
||||
|
||||
|
||||
await RNFS.copyFile(resizedImagePath, targetPath);
|
||||
|
||||
|
||||
try {
|
||||
await RNFS.unlink(resizedImagePath);
|
||||
} catch (error) {
|
||||
@@ -522,7 +542,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<KeyboardAvoidingView behavior= {(Platform.OS === 'ios')? "padding" : null} style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader
|
||||
folderPickerOptions={{
|
||||
enabled: true,
|
||||
@@ -558,7 +578,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
/>
|
||||
|
||||
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
</View>
|
||||
<View style={{ height: this.state.heightBumpView }} />
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -17,49 +17,36 @@ 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 +54,30 @@ 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) newState.newAndNoTitleChangeNoteId = note.id;
|
||||
|
||||
comp.setState(newState);
|
||||
|
||||
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 +106,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 +124,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;
|
||||
}
|
||||
|
@@ -1,6 +1,3 @@
|
||||
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');
|
||||
|
||||
@@ -19,73 +16,63 @@ 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(),
|
||||
created_time_orig: stat.birthtime,
|
||||
updated_time_orig: stat.mtime,
|
||||
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();
|
||||
|
||||
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);
|
||||
let output = this.metadataFromStats_(stats);
|
||||
|
||||
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
|
||||
|
||||
@@ -117,20 +104,14 @@ class FileApiDriverLocal {
|
||||
items: 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 +119,7 @@ class FileApiDriverLocal {
|
||||
context: null,
|
||||
};
|
||||
} catch(error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,90 +128,125 @@ 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() {
|
||||
|
@@ -196,17 +196,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 +225,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;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const fs = require('fs-extra');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class FsDriverNode {
|
||||
|
||||
@@ -15,14 +16,67 @@ class FsDriverNode {
|
||||
return fs.writeFile(path, buffer);
|
||||
}
|
||||
|
||||
move(source, dest) {
|
||||
return fs.move(source, dest, { overwrite: true });
|
||||
writeFile(path, string, encoding = 'base64') {
|
||||
return fs.writeFile(path, string, { encoding: encoding });
|
||||
}
|
||||
|
||||
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 error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
exists(path) {
|
||||
return fs.pathExists(path);
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
return fs.mkdirp(path);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
open(path, mode) {
|
||||
return fs.open(path, mode);
|
||||
}
|
||||
@@ -31,8 +85,14 @@ class FsDriverNode {
|
||||
return fs.close(handle);
|
||||
}
|
||||
|
||||
readFile(path) {
|
||||
return fs.readFile(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) {
|
||||
|
@@ -10,10 +10,36 @@ class FsDriverRN {
|
||||
return RNFS.appendFile(path, string, encoding);
|
||||
}
|
||||
|
||||
writeFile(path, string, encoding = 'base64') {
|
||||
return RNFS.writeFile(path, string, encoding);
|
||||
}
|
||||
|
||||
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 +48,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 +92,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) {
|
||||
|
@@ -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]);
|
||||
}
|
||||
@@ -611,7 +620,7 @@ class BaseItem extends BaseModel {
|
||||
SELECT id
|
||||
FROM %s
|
||||
WHERE encryption_applied = 0`,
|
||||
this.db().escapeField(ItemClass.tableName()),
|
||||
this.db().escapeField(ItemClass.tableName())
|
||||
);
|
||||
|
||||
const items = await ItemClass.modelSelectAll(sql);
|
||||
|
@@ -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();
|
||||
|
@@ -120,7 +120,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) {
|
||||
|
@@ -192,7 +192,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;
|
||||
|
||||
@@ -250,7 +251,7 @@ class Setting extends BaseModel {
|
||||
static formatValue(key, value) {
|
||||
const md = this.settingMetadata(key);
|
||||
|
||||
if (md.type == Setting.TYPE_INT) return Math.floor(Number(value));
|
||||
if (md.type == Setting.TYPE_INT) return !value ? 0 : Math.floor(Number(value));
|
||||
|
||||
if (md.type == Setting.TYPE_BOOL) {
|
||||
if (typeof value === 'string') {
|
||||
|
@@ -109,7 +109,7 @@ class Tag extends BaseItem {
|
||||
for (let i = 0; i < tagTitles.length; i++) {
|
||||
const title = tagTitles[i].trim().toLowerCase();
|
||||
if (!title) continue;
|
||||
let tag = await this.loadByField('title', title);
|
||||
let tag = await this.loadByField('title', title, { caseInsensitive: true });
|
||||
if (!tag) tag = await Tag.save({ title: title }, { userSideValidation: true });
|
||||
await this.addNote(tag.id, noteId);
|
||||
addedTitles.push(title);
|
||||
|
@@ -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);
|
||||
|
@@ -44,7 +44,7 @@ reg.syncTarget = (syncTargetId = null) => {
|
||||
}
|
||||
|
||||
reg.scheduleSync = async (delay = null) => {
|
||||
if (delay === null) delay = 1000 * 3;
|
||||
if (delay === null) delay = 1000 * 30;
|
||||
|
||||
let promiseResolve = null;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
|
@@ -83,6 +83,7 @@ class DecryptionWorker {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ class Synchronizer {
|
||||
this.state_ = 'idle';
|
||||
this.db_ = db;
|
||||
this.api_ = api;
|
||||
this.syncDirName_ = '.sync';
|
||||
//this.syncDirName_ = '.sync';
|
||||
this.resourceDirName_ = '.resource';
|
||||
this.logger_ = new Logger();
|
||||
this.appType_ = appType;
|
||||
@@ -71,6 +71,7 @@ class Synchronizer {
|
||||
if (report.updateRemote) lines.push(_('Updated remote items: %d.', report.updateRemote));
|
||||
if (report.deleteLocal) lines.push(_('Deleted local items: %d.', report.deleteLocal));
|
||||
if (report.deleteRemote) lines.push(_('Deleted remote items: %d.', report.deleteRemote));
|
||||
if (report.fetchingTotal && report.fetchingProcessed) lines.push(_('Fetched items: %d/%d.', report.fetchingProcessed, report.fetchingTotal));
|
||||
if (!report.completedTime && report.state) lines.push(_('State: "%s".', report.state));
|
||||
if (report.cancelling && !report.completedTime) lines.push(_('Cancelling...'));
|
||||
if (report.completedTime) lines.push(_('Completed: %s', time.unixMsToLocalDateTime(report.completedTime)));
|
||||
@@ -78,7 +79,7 @@ class Synchronizer {
|
||||
return lines;
|
||||
}
|
||||
|
||||
logSyncOperation(action, local = null, remote = null, message = null) {
|
||||
logSyncOperation(action, local = null, remote = null, message = null, actionCount = 1) {
|
||||
let line = ['Sync'];
|
||||
line.push(action);
|
||||
if (message) line.push(message);
|
||||
@@ -91,21 +92,19 @@ class Synchronizer {
|
||||
if (local) {
|
||||
let s = [];
|
||||
s.push(local.id);
|
||||
if ('title' in local) s.push('"' + local.title + '"');
|
||||
line.push('(Local ' + s.join(', ') + ')');
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
let s = [];
|
||||
s.push(remote.id ? remote.id : remote.path);
|
||||
if ('title' in remote) s.push('"' + remote.title + '"');
|
||||
line.push('(Remote ' + s.join(', ') + ')');
|
||||
}
|
||||
|
||||
this.logger().debug(line.join(': '));
|
||||
|
||||
if (!this.progressReport_[action]) this.progressReport_[action] = 0;
|
||||
this.progressReport_[action]++;
|
||||
this.progressReport_[action] += actionCount;
|
||||
this.progressReport_.state = this.state();
|
||||
this.onProgress_(this.progressReport_);
|
||||
|
||||
@@ -196,7 +195,7 @@ class Synchronizer {
|
||||
this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']');
|
||||
|
||||
try {
|
||||
await this.api().mkdir(this.syncDirName_);
|
||||
//await this.api().mkdir(this.syncDirName_);
|
||||
await this.api().mkdir(this.resourceDirName_);
|
||||
|
||||
let donePaths = [];
|
||||
@@ -320,9 +319,23 @@ class Synchronizer {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Currently, we set sync_time to update_time, which should work fine given that the resolution is the millisecond.
|
||||
// In theory though, this could happen:
|
||||
//
|
||||
// 1. t0: Editor: Note is modified
|
||||
// 2. t0: Sync: Found that note was modified so start uploading it
|
||||
// 3. t0: Editor: Note is modified again
|
||||
// 4. t1: Sync: Note has finished uploading, set sync_time to t0
|
||||
//
|
||||
// Later any attempt to sync will not detect that note was modified in (3) (within the same millisecond as it was being uploaded)
|
||||
// because sync_time will be t0 too.
|
||||
//
|
||||
// The solution would be to use something like an etag (a simple counter incremented on every change) to make sure each
|
||||
// change is uniquely identified. Leaving it like this for now.
|
||||
|
||||
if (canSync) {
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
|
||||
}
|
||||
|
||||
} else if (action == 'itemConflict') {
|
||||
@@ -432,12 +445,17 @@ class Synchronizer {
|
||||
});
|
||||
|
||||
let remotes = listResult.items;
|
||||
|
||||
this.logSyncOperation('fetchingTotal', null, null, 'Fetching delta items from sync target', remotes.length);
|
||||
|
||||
for (let i = 0; i < remotes.length; i++) {
|
||||
if (this.cancelling() || this.debugFlags_.indexOf('cancelDeltaLoop2') >= 0) {
|
||||
hasCancelled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.logSyncOperation('fetchingProcessed', null, null, 'Processing fetched item');
|
||||
|
||||
let remote = remotes[i];
|
||||
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
|
||||
|
||||
@@ -565,7 +583,7 @@ class Synchronizer {
|
||||
if (noteIds.length) { // CONFLICT
|
||||
await Folder.markNotesAsConflict(item.id);
|
||||
}
|
||||
await Folder.delete(item.id, { deleteChildren: false });
|
||||
await Folder.delete(item.id, { deleteChildren: false, trackDeleted: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
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
@@ -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
1
ReactNativeClient/locales/nl_BE.json
Normal file
1
ReactNativeClient/locales/nl_BE.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -44,14 +44,19 @@ const { _, setLocale, closestSupportedLocale, defaultLocale } = require('lib/loc
|
||||
const RNFetchBlob = require('react-native-fetch-blob').default;
|
||||
const { PoorManIntervals } = require('lib/poor-man-intervals.js');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const DropdownAlert = require('react-native-dropdownalert').default;
|
||||
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
|
||||
// Disabled because not fully working
|
||||
//SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
|
||||
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
@@ -97,6 +102,10 @@ const generalMiddleware = store => next => async (action) => {
|
||||
type: 'MASTERKEY_REMOVE_NOT_LOADED',
|
||||
ids: loadedMasterKeyIds,
|
||||
});
|
||||
|
||||
// Schedule a sync operation so that items that need to be encrypted
|
||||
// are sent to sync target.
|
||||
reg.scheduleSync();
|
||||
}
|
||||
|
||||
if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
|
||||
@@ -337,6 +346,7 @@ async function initialize(dispatch) {
|
||||
const fsDriver = new FsDriverRN();
|
||||
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
AlarmService.setDriver(new AlarmServiceDriver());
|
||||
AlarmService.setLogger(mainLogger);
|
||||
|
13
Tools/package-lock.json
generated
13
Tools/package-lock.json
generated
@@ -73,6 +73,11 @@
|
||||
"is-stream": "1.1.0"
|
||||
}
|
||||
},
|
||||
"pct-encode": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pct-encode/-/pct-encode-1.0.2.tgz",
|
||||
"integrity": "sha1-uZt7BE1r18OeSDmnqAEirXUVyqU="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
|
||||
@@ -82,6 +87,14 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"uri-template": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-template/-/uri-template-1.0.1.tgz",
|
||||
"integrity": "sha1-FKklo35Nk/diVDKqEWsF5Qyuga0=",
|
||||
"requires": {
|
||||
"pct-encode": "1.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
"gettext-parser": "^1.3.0",
|
||||
"marked": "^0.3.7",
|
||||
"mustache": "^2.3.0",
|
||||
"node-fetch": "^1.7.3"
|
||||
"node-fetch": "^1.7.3",
|
||||
"uri-template": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
132
Tools/release-android.js
Normal file
132
Tools/release-android.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const fs = require('fs-extra');
|
||||
const { execCommand } = require('./tool-utils.js');
|
||||
const path = require('path');
|
||||
const fetch = require('node-fetch');
|
||||
const uriTemplate = require('uri-template');
|
||||
|
||||
const rnDir = __dirname + '/../ReactNativeClient';
|
||||
const rootDir = path.dirname(__dirname);
|
||||
const releaseDir = rootDir + '/_releases';
|
||||
|
||||
function increaseGradleVersionCode(content) {
|
||||
const newContent = content.replace(/versionCode\s+(\d+)/, function(a, versionCode, c) {
|
||||
const n = Number(versionCode);
|
||||
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
|
||||
return 'versionCode ' + (n + 1);
|
||||
});
|
||||
|
||||
if (newContent === content) throw new Error('Could not update version code');
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function increaseGradleVersionName(content) {
|
||||
const newContent = content.replace(/(versionName\s+"\d+?\.\d+?\.)(\d+)"/, function(match, prefix, buildNum) {
|
||||
const n = Number(buildNum);
|
||||
if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode);
|
||||
return prefix + (n + 1) + '"';
|
||||
});
|
||||
|
||||
if (newContent === content) throw new Error('Could not update version name');
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
function updateGradleConfig() {
|
||||
let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8');
|
||||
content = increaseGradleVersionCode(content);
|
||||
content = increaseGradleVersionName(content);
|
||||
fs.writeFileSync(rnDir + '/android/app/build.gradle', content);
|
||||
return content;
|
||||
}
|
||||
|
||||
function gradleVersionName(content) {
|
||||
const matches = content.match(/versionName\s+"(\d+?\.\d+?\.\d+)"/);
|
||||
if (!matches || matches.length < 1) throw new Error('Cannot get gradle version name');
|
||||
return matches[1];
|
||||
}
|
||||
|
||||
async function githubOauthToken() {
|
||||
const r = await fs.readFile(rootDir + '/Tools/github_oauth_token.txt');
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const oauthToken = await githubOauthToken();
|
||||
|
||||
const newContent = updateGradleConfig();
|
||||
const version = gradleVersionName(newContent);
|
||||
const tagName = 'android-v' + version;
|
||||
const apkFilename = 'joplin-v' + version + '.apk';
|
||||
const apkFilePath = releaseDir + '/' + apkFilename;
|
||||
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/' + tagName + '/' + apkFilename;
|
||||
|
||||
process.chdir(rootDir);
|
||||
|
||||
console.info('Running from: ' + process.cwd());
|
||||
|
||||
console.info('Building APK file...');
|
||||
const output = await execCommand('/mnt/c/Windows/System32/cmd.exe /c "cd ReactNativeClient\\android && gradlew.bat assembleRelease -PbuildDir=build --console plain"');
|
||||
console.info(output);
|
||||
|
||||
await fs.mkdirp(releaseDir);
|
||||
|
||||
console.info('Copying APK to ' + apkFilePath);
|
||||
await fs.copy('ReactNativeClient/android/app/build/outputs/apk/app-release.apk', apkFilePath);
|
||||
|
||||
console.info('Updating Readme URL...');
|
||||
|
||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.apk)/, downloadUrl);
|
||||
await fs.writeFile('README.md', readmeContent);
|
||||
|
||||
await execCommand('git add -A');
|
||||
await execCommand('git commit -m "Android release v' + version + '"');
|
||||
await execCommand('git tag ' + tagName);
|
||||
await execCommand('git push');
|
||||
await execCommand('git push --tags');
|
||||
|
||||
console.info('Creating GitHub release ' + tagName + '...');
|
||||
|
||||
const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tag_name: tagName,
|
||||
name: tagName,
|
||||
draft: true,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'token ' + oauthToken,
|
||||
},
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
const responseJson = JSON.parse(responseText);
|
||||
if (!responseJson.upload_url) throw new Error('No upload URL for release: ' + responseText);
|
||||
|
||||
const uploadUrlTemplate = uriTemplate.parse(responseJson.upload_url);
|
||||
const uploadUrl = uploadUrlTemplate.expand({ name: apkFilename });
|
||||
|
||||
const binaryBody = await fs.readFile(apkFilePath);
|
||||
|
||||
console.info('Uploading ' + apkFilename + ' to ' + uploadUrl);
|
||||
|
||||
const uploadResponse = await fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: binaryBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.android.package-archive',
|
||||
'Authorization': 'token ' + oauthToken,
|
||||
'Content-Length': binaryBody.length,
|
||||
},
|
||||
});
|
||||
|
||||
const uploadResponseText = await uploadResponse.text();
|
||||
console.info(uploadResponseText);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error');
|
||||
console.error(error);
|
||||
});
|
@@ -28,7 +28,7 @@ toolUtils.downloadFile = function(url, targetPath) {
|
||||
if (response.statusCode !== 200) reject(new Error('HTTP error ' + response.statusCode));
|
||||
response.pipe(file);
|
||||
file.on('finish', function() {
|
||||
file.close();
|
||||
//file.close();
|
||||
resolve();
|
||||
});
|
||||
}).on('error', (error) => {
|
||||
|
@@ -200,18 +200,16 @@
|
||||
|
||||
<div class="content">
|
||||
<h1 id="about-end-to-end-encryption-e2ee-">About End-To-End Encryption (E2EE)</h1>
|
||||
<ol>
|
||||
<li>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.</li>
|
||||
</ol>
|
||||
<p>The systems is designed to defeat any attempts at surveillance or tampering because no third parties can decipher the data being communicated or stored.</p>
|
||||
<p>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.</p>
|
||||
<p>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.</p>
|
||||
<p>The system is designed to defeat any attempts at surveillance or tampering because no third party can decipher the data being communicated or stored.</p>
|
||||
<p>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.</p>
|
||||
<h1 id="enabling-e2ee">Enabling E2EE</h1>
|
||||
<p>Due to the decentralised nature of Joplin, E2EE needs to be manually enabled on all the applications that you synchronise with. It is recommended to first enable it on the desktop or terminal application since they generally run on more powerful devices (unlike the mobile application), and so they can encrypt the initial data faster.</p>
|
||||
<p>To enable it, please follow these steps:</p>
|
||||
<ol>
|
||||
<li>On your first device (eg. on the desktop application), go to the Encryption Config screen and click "Enable encryption"</li>
|
||||
<li>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".</li>
|
||||
<li>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.</li>
|
||||
<li>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".</li>
|
||||
<li>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.</li>
|
||||
<li>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.</li>
|
||||
<li>Repeat step 5 for each device.</li>
|
||||
@@ -219,6 +217,8 @@ ey are sent encrypted to the sync target (eg. to OneDrive, Nextcloud, etc.). Wai
|
||||
<p>Once all the devices are in sync with E2EE enabled, the encryption/decryption should be mostly transparent. Occasionally you may see encrypted items but they will get decrypted in the background eventually.</p>
|
||||
<h1 id="disabling-e2ee">Disabling E2EE</h1>
|
||||
<p>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.</p>
|
||||
<h1 id="technical-specification">Technical specification</h1>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="http://joplin.cozic.net/help/spec">Encryption specification</a>.</p>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@@ -244,9 +244,9 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>See lib/services/EncryptionService.js for the list of available encryption methods.</p>
|
||||
<p>See <code>lib/services/EncryptionService.js</code> for the list of available encryption methods.</p>
|
||||
<h3 id="data-chunk">Data chunk</h3>
|
||||
<p>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).</p>
|
||||
<p>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).</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -271,9 +271,9 @@
|
||||
<p>The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.</p>
|
||||
<p>Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.</p>
|
||||
<h2 id="encryption-service">Encryption Service</h2>
|
||||
<p>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".</p>
|
||||
<p>The applications make use of the <code>EncryptionService</code> 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".</p>
|
||||
<h2 id="encryption-workflow">Encryption workflow</h2>
|
||||
<p>Items are encrypted only during synchronisation, when they are serialised (via BaseItem.serializeForSync), so before being sent to the sync target.</p>
|
||||
<p>Items are encrypted only during synchronisation, when they are serialised (via <code>BaseItem.serializeForSync</code>), so before being sent to the sync target.</p>
|
||||
<p>They are decrypted by DecryptionWorker in the background.</p>
|
||||
<p>The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.</p>
|
||||
<h2 id="enabling-and-disabling-encryption">Enabling and disabling encryption</h2>
|
||||
|
@@ -218,15 +218,15 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.41/Joplin-Setup-0.10.41.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a></td>
|
||||
<td><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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.41/Joplin-0.10.41.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a></td>
|
||||
<td><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></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v0.10.41/Joplin-0.10.41-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a></td>
|
||||
<td><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></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -236,16 +236,19 @@
|
||||
<tr>
|
||||
<th>Operating System</th>
|
||||
<th>Download</th>
|
||||
<th>Alt. Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin/releases/download/android-v0.10.78/joplin-v0.10.78.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
<td><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></td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -296,7 +299,10 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<p>On the <strong>desktop application</strong>, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.</p>
|
||||
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
|
||||
<pre><code>*/30 * * * * /path/to/joplin sync
|
||||
</code></pre><h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
</code></pre><h1 id="encryption">Encryption</h1>
|
||||
<p>Joplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the notes, notebooks, tags or resources can read them. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the <a href="http://joplin.cozic.net/help/e2ee">End-To-End Encryption Tutorial</a> for more information about this feature and how to enable it.</p>
|
||||
<p>For a more technical description, mostly relevant for development or to review the method being used, please see the <a href="http://joplin.cozic.net/help/spec">Encryption specification</a>.</p>
|
||||
<h1 id="attachments-resources">Attachments / Resources</h1>
|
||||
<p>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.</p>
|
||||
<h1 id="notifications">Notifications</h1>
|
||||
<p>On the desktop and mobile apps, an alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. How the notification will be displayed depends on the operating system since each has a different way to handle this. Please see below for the requirements for the desktop applications:</p>
|
||||
@@ -305,11 +311,11 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<li><strong>macOS</strong>: >= 10.8 or Growl if earlier.</li>
|
||||
<li><strong>Linux</strong>: <code>notify-osd</code> or <code>libnotify-bin</code> installed (Ubuntu should have this by default). Growl otherwise</li>
|
||||
</ul>
|
||||
<p>See <a href="./DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>See <a href="https://github.com/mikaelbr/node-notifier/blob/master/DECISION_FLOW.md">documentation and flow chart for reporter choice</a></p>
|
||||
<p>On mobile, the alarms will be displayed using the built-in notification system.</p>
|
||||
<p>If for any reason the notifications do not work, please <a href="https://github.com/laurent22/joplin/issues">open an issue</a>.</p>
|
||||
<h1 id="localisation">Localisation</h1>
|
||||
<p>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:</p>
|
||||
<p>Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:</p>
|
||||
<ul>
|
||||
<li><a href="https://poedit.net/">Download Poedit</a>, the translation editor, and install it.</li>
|
||||
<li><a href="https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot">Download the file to be translated</a>.</li>
|
||||
@@ -317,6 +323,8 @@ sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
<li>From then you can translate the file. Once it is done, please either <a href="https://github.com/laurent22/joplin/pulls">open a pull request</a> or send the file to <a href="https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png">this address</a>.</li>
|
||||
</ul>
|
||||
<p>This translation will apply to the three applications - desktop, mobile and terminal.</p>
|
||||
<h1 id="contributing">Contributing</h1>
|
||||
<p>Please see the guide for information on how to contribute to the development of Joplin: <a href="https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md">https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md</a></p>
|
||||
<h1 id="coming-features">Coming features</h1>
|
||||
<ul>
|
||||
<li>NextCloud support</li>
|
||||
|
Reference in New Issue
Block a user