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

Compare commits

...

58 Commits

Author SHA1 Message Date
Laurent Cozic
c984c19fee Android release v0.10.78 2018-01-18 22:35:05 +00:00
Laurent Cozic
ac8e91e82e Started FAQ 2018-01-18 22:34:27 +00:00
Laurent Cozic
738ef2b0fa Android release v0.10.75 2018-01-18 17:29:57 +00:00
Laurent Cozic
9746a3964b All: Removed certain log statements so that sensitive info doesn't end up in logs 2018-01-17 21:17:40 +00:00
Laurent Cozic
9efbf74b6f All: Various changes to get filesystem target to work on mobile 2018-01-17 21:01:41 +00:00
Laurent Cozic
c16ea6b237 Typo 2018-01-17 20:19:45 +00:00
Laurent Cozic
b06a3b588f Add APK download link 2018-01-17 20:19:20 +00:00
Laurent Cozic
6ff67e0995 Automate building and deploying Android app 2018-01-17 20:16:13 +00:00
Laurent Cozic
1a5c8d126d All: Refactored filesystem sync driver to support mobile 2018-01-17 18:51:15 +00:00
Laurent Cozic
f632580eed CLI: Display error when cannot open note with editor 2018-01-17 18:10:07 +00:00
Laurent Cozic
1d73f0cdee Simplified and fixed caching issue 2018-01-17 17:59:33 +00:00
Laurent Cozic
99c7111f8c CLI: Fixes #168: Invalid code block would crash app 2018-01-17 17:52:55 +00:00
Laurent Cozic
ae9806561a Added tech spec info 2018-01-16 19:51:37 +00:00
Laurent Cozic
fffdf5b5b7 Fixed link 2018-01-17 00:47:45 +00:00
Laurent Cozic
3de19c3db7 All: Added Dutch translation. Thanks @tcassaert 2018-01-17 00:43:47 +00:00
Laurent Cozic
56e074b4ef Merge branch 'master' of github.com:laurent22/joplin 2018-01-17 00:41:04 +00:00
Laurent Cozic
1a79253780 Update doc 2018-01-17 00:40:35 +00:00
Laurent Cozic
b67908df11 Merge pull request #165 from tcassaert/master
Dutch translation
2018-01-16 17:32:33 +00:00
tcassaert
6a5089f71d Dutch translation
All EN keys are translated to nl_BE.
2018-01-15 22:33:05 +01:00
Laurent Cozic
f710463b67 Electron: Fixes #155: Caret alignment issue with Russian text 2018-01-15 19:01:00 +00:00
Laurent Cozic
6ae0c3aba0 Electron release v0.10.47 2018-01-15 18:41:19 +00:00
Laurent Cozic
07c6347014 Android v0.10.74 2018-01-15 18:40:44 +00:00
Laurent Cozic
b10999e83e All: Update French translation 2018-01-15 18:35:39 +00:00
Laurent Cozic
961b5bfd25 All: Fixes #85: Don't record deleted_items entries for folders deleted via sync 2018-01-15 18:10:14 +00:00
Laurent Cozic
d1f1d1068a Electron release v0.10.46 2018-01-15 12:29:58 +00:00
Laurent Cozic
faade0afe2 All: Fixed model ID issue 2018-01-14 17:11:44 +00:00
Laurent Cozic
a442a49e2f Electron release v0.10.45 2018-01-15 12:27:10 +00:00
Laurent Cozic
7d3fbbcaba Updated translations 2018-01-14 17:07:34 +00:00
Laurent Cozic
d9bb7c3271 Android v0.10.73 2018-01-14 17:05:59 +00:00
Laurent Cozic
4d1dd17fa2 All: Fixed issue with timestamp when saving notes 2018-01-14 17:01:22 +00:00
Laurent Cozic
c5c6c777be Electron release v0.10.44 2018-01-12 18:11:39 +01:00
Laurent Cozic
1fd1a73fda Electron: Improved the way new note are created, and automatically add a title. Made saving and loading notes more reliable. 2018-01-12 19:58:01 +00:00
Laurent Cozic
feeb498a79 All: Fixed OneDrive sync when resync is requested 2018-01-12 19:01:34 +00:00
Laurent Cozic
1d7f30d441 Electron: Fixed logic to save, and make sure scheduled save always happen even when changing note 2018-01-11 21:05:34 +01:00
Laurent Cozic
424443a2d8 CLI: Display arrays and objects in settings 2018-01-09 21:25:31 +01:00
Laurent Cozic
08b58f0e4c All: Fixed table font size and family 2018-01-09 21:09:49 +01:00
Laurent Cozic
c2a0d8600f Electron: Move prompt to top to avoid issue with date picker being hidden 2018-01-09 21:06:47 +01:00
Laurent Cozic
ede3c2ce2f Electron: Fixed display of too long notebook titles 2018-01-09 19:34:06 +01:00
Laurent Cozic
0b93515711 Electron: Display URL for links 2018-01-09 19:26:46 +01:00
Laurent Cozic
2f13e689b9 Electron: Don't scroll back to top when note is reloaded via sync 2018-01-09 20:26:20 +00:00
Laurent Cozic
ea135a0d28 Electron: Fixed logic of what note is used when right-clicking one or more notes 2018-01-09 20:16:09 +00:00
Laurent Cozic
f67e4a03e4 CLI: Detect installed Node version 2018-01-09 19:56:38 +00:00
Laurent Cozic
e9268edeff All: Fixes #150: Extra comma causes crash 2018-01-09 19:45:08 +00:00
Laurent Cozic
a8576a55d6 CLI v0.10.87 2018-01-09 09:31:03 +01:00
Laurent Cozic
eb500cdf9e All: Display sync items being fetched 2018-01-08 21:36:00 +01:00
Laurent Cozic
7b9dc66121 All: Schedule sync after enabling or disabling encryption 2018-01-08 21:25:38 +01:00
Laurent Cozic
bba2c68c6f All: Schedule sync only after 30 seconds 2018-01-08 21:05:08 +01:00
Laurent Cozic
c70d8bea78 All: Fixes #129: Tags are case insensitive 2018-01-08 21:04:44 +01:00
Laurent Cozic
176bda66ad Merge branch 'master' of github.com:laurent22/joplin 2018-01-08 20:09:12 +01:00
Laurent Cozic
78ce10ddf0 All: Fixed race condition when a note is being uploaded while it's being modified in the text editor 2018-01-08 20:09:01 +01:00
Laurent Cozic
29f14681a8 Mobile: Fixed mix of tabs and spaces 2018-01-08 19:31:04 +00:00
Laurent Cozic
aaf617e41c iOS v0.10.9 2018-01-08 18:26:30 +01:00
Laurent Cozic
b99146ed7f Merge pull request #111 from marcosvega91/fix_scroll_note_keyboard
Fix scroll note keyboard on IOS
2018-01-08 16:45:58 +00:00
Laurent Cozic
d136161650 Android v0.10.71 2018-01-07 19:29:57 +00:00
Laurent Cozic
39051a27a1 Update website 2018-01-08 11:12:22 +01:00
marcosvega91
277ad90f72 Indent with tab 2017-12-19 21:14:40 +01:00
marcosvega91
f2e3bedde6 Fix scroll
After fixing the issue on ios, it caused an issue on android that I solved with this commit
2017-12-19 10:28:52 +01:00
marcosvega91
98c0f2315a Fix scroll
Fixed the issue that not permit to view edited text when the keyboard is shown.
2017-12-19 10:08:22 +01:00
95 changed files with 2387 additions and 588 deletions

4
.gitignore vendored
View File

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

View File

@@ -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.

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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.

View File

@@ -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\""

View File

@@ -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 ""

View File

@@ -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?"

View File

@@ -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»"

View File

@@ -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 ?"

View File

@@ -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\""

View File

@@ -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\""

View File

@@ -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の題名:"

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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?"

View File

@@ -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»"

View File

@@ -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\""

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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

View File

@@ -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');

View File

@@ -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) {

View File

@@ -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'],

View File

@@ -44,16 +44,14 @@ class MainScreenComponent extends React.Component {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: title,
const newNote = {
parent_id: folderId,
is_todo: isTodo ? 1 : 0,
});
Note.updateGeolocation(note.id);
};
this.props.dispatch({
type: 'NOTE_SELECT',
id: note.id,
type: 'NOTE_SET_NEW_ONE',
item: newNote,
});
}
@@ -65,30 +63,14 @@ class MainScreenComponent extends React.Component {
return;
}
this.setState({
promptOptions: {
label: _('Note title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, false);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, false);
} else if (command.name === 'newTodo') {
if (!this.props.folders.length) {
bridge().showErrorMessageBox(_('Please create a notebook first'));
return;
}
this.setState({
promptOptions: {
label: _('To-do title:'),
onClose: async (answer) => {
if (answer) await createNewNote(answer, true);
this.setState({ promptOptions: null });
}
},
});
await createNewNote(null, true);
} else if (command.name === 'newNotebook') {
this.setState({
promptOptions: {

View File

@@ -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>
);

View File

@@ -36,7 +36,13 @@ class NoteTextComponent extends React.Component {
isLoading: true,
webviewReady: false,
scrollHeight: null,
editorScrollTop: 0
editorScrollTop: 0,
newNote: null,
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
// to automatically set the title.
newAndNoTitleChangeNoteId: null,
};
this.lastLoadedNoteId_ = null;
@@ -75,7 +81,10 @@ class NoteTextComponent extends React.Component {
async componentWillMount() {
let note = null;
if (this.props.noteId) {
if (this.props.newNote) {
note = Object.assign({}, this.props.newNote);
} else if (this.props.noteId) {
note = await Note.load(this.props.noteId);
}
@@ -114,7 +123,14 @@ class NoteTextComponent extends React.Component {
}
async saveOneProperty(name, value) {
await shared.saveOneProperty(this, name, value);
if (this.state.note && !this.state.note.id) {
const note = Object.assign({}, this.state.note);
note[name] = value;
this.setState({ note: note });
this.scheduleSave();
} else {
await shared.saveOneProperty(this, name, value);
}
}
scheduleSave() {
@@ -128,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,
};
};

View File

@@ -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);

View File

@@ -35,6 +35,7 @@ class SideBarComponent extends React.Component {
alignItems: 'center',
cursor: 'default',
opacity: 0.8,
whiteSpace: 'nowrap',
},
listItemSelected: {
backgroundColor: theme.selectedColor2,

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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.

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.43",
"version": "0.10.47",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.43",
"version": "0.10.47",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {

View File

@@ -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).

View File

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

4
README_faq.md Normal file
View File

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

View File

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

View File

@@ -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"

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

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

View File

@@ -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;

View File

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

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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') {

View File

@@ -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);

View File

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

View File

@@ -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) => {

View File

@@ -83,6 +83,7 @@ class DecryptionWorker {
});
continue;
}
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')');
throw error;
}
}

View File

@@ -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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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);

View File

@@ -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"
}
}
}
}

View File

@@ -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
View 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);
});

View File

@@ -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) => {

View File

@@ -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 &quot;Enable encryption&quot;</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 &quot;Synchronise&quot;.</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 &quot;Synchronise&quot;.</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 &quot;Synchronise&quot; 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() {

View File

@@ -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&#39;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 &quot;active&quot;.</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 &quot;active&quot;.</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>

View File

@@ -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 &quot;Synchronise&quot; button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on &quot;Synchronise&quot; to start a synchronisation manually.</p>
<p>On the <strong>terminal application</strong>, to initiate the synchronisation process, type <code>:sync</code>. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing <code>joplin sync</code> from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:</p>
<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>: &gt;= 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>