Compare commits
60 Commits
android-v0
...
cli-v0.10.
Author | SHA1 | Date | |
---|---|---|---|
|
d56b247adf | ||
|
11bdfbde61 | ||
|
a59bf55c16 | ||
|
043be1916c | ||
|
42e34b5c3b | ||
|
931083b2e2 | ||
|
b9194e94aa | ||
|
0f343bccda | ||
|
a513f6f3f0 | ||
|
3b4809714e | ||
|
238b5ab9b9 | ||
|
08d9e9b6aa | ||
|
b22900eb3a | ||
|
89fc2c4779 | ||
|
353b79f5e5 | ||
|
b929b46281 | ||
|
5c4a536dad | ||
|
91e337307c | ||
|
2855b68ed4 | ||
|
45ca6284f9 | ||
|
145ee13356 | ||
|
6f97747199 | ||
|
f3751e4ba6 | ||
|
5cd55cada6 | ||
|
bad4b2ecb8 | ||
|
7aafd63ff3 | ||
|
3227a13035 | ||
|
027f96d100 | ||
|
f242a3c215 | ||
|
ad6c347180 | ||
|
4f0431da55 | ||
|
b873fdd029 | ||
|
c1ff820913 | ||
|
7008daf92a | ||
|
ed914c6907 | ||
|
37663bd110 | ||
|
f01c6aa8d1 | ||
|
8838017830 | ||
|
1d6fb8058f | ||
|
bb51729bea | ||
|
507e7e6014 | ||
|
f42908b11c | ||
|
03ec406627 | ||
|
c703521b6c | ||
|
cf97bf9a77 | ||
|
304b9a582f | ||
|
4d5c4b1743 | ||
|
4abe5d07c4 | ||
|
f6633e23f5 | ||
|
a6d6201ecb | ||
|
4314c392f6 | ||
|
73e81a54b4 | ||
|
ab8c66a361 | ||
|
4b55fefcb1 | ||
|
0eac8b25e1 | ||
|
aec556ff7d | ||
|
110dc29bd4 | ||
|
b1efea1bd9 | ||
|
8671467ed3 | ||
|
485ef1f2c2 |
BIN
Assets/All.psd
Before Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
Assets/Icon.psd
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,2 +0,0 @@
|
||||
#!/bin/bash
|
||||
iconutil --convert icns macOs.iconset
|
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg>
|
Before Width: | Height: | Size: 612 B |
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M813 1299l614-614q19-19 19-45t-19-45l-102-102q-19-19-45-19t-45 19l-467 467-211-211q-19-19-45-19t-45 19l-102 102q-19 19-19 45t19 45l358 358q19 19 45 19t45-19zm851-883v960q0 119-84.5 203.5t-203.5 84.5h-960q-119 0-203.5-84.5t-84.5-203.5v-960q0-119 84.5-203.5t203.5-84.5h960q119 0 203.5 84.5t84.5 203.5z"/></svg>
|
Before Width: | Height: | Size: 447 B |
@@ -1,2 +0,0 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg viewBox='0 0 1792 1792' xmlns='http://www.w3.org/2000/svg'><path d='M1312 256h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-832q0-66-47-113t-113-47zm288 160v832q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q119 0 203.5 84.5t84.5 203.5z'/></svg>
|
Before Width: | Height: | Size: 370 B |
26
BUILD.md
@@ -1,18 +1,21 @@
|
||||
# General information
|
||||
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when builing each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. For this reasons, it's generally better to get the CLI app to build first so that everything is setup correctly.
|
||||
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node xgettext
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
brew install yarn node
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
|
||||
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
## Linux and Windows dependencies
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
@@ -31,8 +34,17 @@ From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
|
||||
# Building the Mobile application
|
||||
|
||||
From `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
First you need to to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "Building Projects with Native Code" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
From `/CliClient`, run `npm install` then run `run.sh`. If you get an error about `xgettext`, comment out the command `node build-translation.js --silent` in build.sh
|
||||
```
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
@@ -177,22 +177,33 @@ class Application extends BaseApplication {
|
||||
await doExit();
|
||||
}
|
||||
|
||||
commands() {
|
||||
if (this.allCommandsLoaded_) return this.commands_;
|
||||
commands(uiType = null) {
|
||||
if (!this.allCommandsLoaded_) {
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
|
||||
fs.readdirSync(__dirname).forEach((path) => {
|
||||
if (path.indexOf('command-') !== 0) return;
|
||||
const ext = fileExtension(path)
|
||||
if (ext != 'js') return;
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
|
||||
let CommandClass = require('./' + path);
|
||||
let cmd = new CommandClass();
|
||||
if (!cmd.enabled()) return;
|
||||
cmd = this.setupCommand(cmd);
|
||||
this.commands_[cmd.name()] = cmd;
|
||||
});
|
||||
this.allCommandsLoaded_ = true;
|
||||
}
|
||||
|
||||
this.allCommandsLoaded_ = true;
|
||||
if (uiType !== null) {
|
||||
let temp = [];
|
||||
for (let n in this.commands_) {
|
||||
if (!this.commands_.hasOwnProperty(n)) continue;
|
||||
const c = this.commands_[n];
|
||||
if (!c.supportsUi(uiType)) continue;
|
||||
temp[n] = c;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
return this.commands_;
|
||||
}
|
||||
@@ -246,9 +257,13 @@ class Application extends BaseApplication {
|
||||
try {
|
||||
CommandClass = require(__dirname + '/command-' + name + '.js');
|
||||
} catch (error) {
|
||||
let e = new Error('No such command: ' + name);
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
let e = new Error(_('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = new CommandClass();
|
||||
@@ -306,6 +321,8 @@ class Application extends BaseApplication {
|
||||
if (argv.length) {
|
||||
this.gui_ = this.dummyGui();
|
||||
|
||||
this.currentFolder_ = await Folder.load(Setting.value('activeFolderId'));
|
||||
|
||||
try {
|
||||
await this.execCommand(argv);
|
||||
} catch (error) {
|
||||
|
@@ -12,6 +12,10 @@ class Command extends BaseCommand {
|
||||
return _('Exits the application.');
|
||||
}
|
||||
|
||||
compatibleUis() {
|
||||
return ['gui'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
await app().exit();
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
allCommands() {
|
||||
const commands = app().commands();
|
||||
const commands = app().commands(app().uiType());
|
||||
let output = [];
|
||||
for (let n in commands) {
|
||||
if (!commands.hasOwnProperty(n)) continue;
|
||||
@@ -65,7 +65,7 @@ class Command extends BaseCommand {
|
||||
} else {
|
||||
const commandNames = this.allCommands().map((a) => a.name());
|
||||
|
||||
this.stdout(_('Type `help [command]` for more information about a command.'));
|
||||
this.stdout(_('Type `help [command]` for more information about a command; or type `help all` for the complete usage information.'));
|
||||
this.stdout('');
|
||||
this.stdout(_('The possible commands are:'));
|
||||
this.stdout('');
|
||||
|
@@ -29,7 +29,7 @@ class Command extends BaseCommand {
|
||||
|
||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook "%s"?', folder.title), { booleanAnswerDefault: 'n' });
|
||||
const ok = force ? true : await this.prompt(_('Delete notebook? All notes within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
|
||||
if (!ok) return;
|
||||
|
||||
await Folder.delete(folder.id);
|
||||
|
@@ -2,6 +2,7 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
@@ -12,16 +13,16 @@ class Command extends BaseCommand {
|
||||
return 'set <note> <name> [value]';
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
description() {
|
||||
return _('Sets the property <name> of the given <note> to the given [value].');
|
||||
}
|
||||
const fields = Note.fields();
|
||||
const s = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
const f = fields[i];
|
||||
if (f.name === 'id') continue;
|
||||
s.push(f.name + ' (' + Database.enumName('fieldType', f.type) + ')');
|
||||
}
|
||||
|
||||
hidden() {
|
||||
return true;
|
||||
return _('Sets the property <name> of the given <note> to the given [value]. Possible properties are:\n\n%s', s.join(', '));
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
@@ -32,7 +32,6 @@ class Command extends BaseCommand {
|
||||
options() {
|
||||
return [
|
||||
['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
|
||||
['--random-failures', 'For debugging purposes. Do not use.'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -140,7 +139,6 @@ class Command extends BaseCommand {
|
||||
cliUtils.redrawDone();
|
||||
this.stdout(msg);
|
||||
},
|
||||
randomFailures: args.options['random-failures'] === true,
|
||||
};
|
||||
|
||||
this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
|
||||
|
@@ -18,8 +18,8 @@ class Command extends BaseCommand {
|
||||
return { data: autocompleteFolders };
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
compatibleUis() {
|
||||
return ['cli'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const Note = require('lib/models/note.js').Note;
|
||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class NoteWidget extends TextWidget {
|
||||
|
||||
@@ -32,8 +33,15 @@ class NoteWidget extends TextWidget {
|
||||
this.reloadNote();
|
||||
}
|
||||
|
||||
welcomeText() {
|
||||
return _('Welcome to Joplin!\n\nType `:help shortcuts` for the list of keyboard shortcuts, or just `:help` for usage information.\n\nFor example, to create a notebook press `mb`; to create a note press `mn`.');
|
||||
}
|
||||
|
||||
reloadNote() {
|
||||
if (this.noteId_) {
|
||||
if (!this.noteId_ && !this.notes.length) {
|
||||
this.text = this.welcomeText();
|
||||
this.scrollTop = 0;
|
||||
} else if (this.noteId_) {
|
||||
this.doAsync('loadNote', async () => {
|
||||
this.note_ = await Note.load(this.noteId_);
|
||||
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
||||
|
@@ -1,3 +0,0 @@
|
||||
#/bin/bash
|
||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
NODE_PATH="$CLIENT_DIR/build" node "$CLIENT_DIR/build/build-translation.js" --silent
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
"$ROOT_DIR/build.sh" && NODE_PATH="$ROOT_DIR/build" node "$ROOT_DIR/build/build-website.js"
|
@@ -6,7 +6,4 @@ BUILD_DIR="$ROOT_DIR/build"
|
||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
||||
|
||||
# cd "$BUILD_DIR"
|
||||
# node build-translation.js --silent
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
@@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
"$CLIENT_DIR/publish.sh"
|
||||
npm install -g joplin
|
1062
CliClient/locales/de_DE.po
Normal file
@@ -100,6 +100,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -508,6 +512,9 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +575,12 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -605,6 +618,9 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr ""
|
||||
|
||||
@@ -812,6 +828,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
|
1030
CliClient/locales/es_419.po
Normal file
@@ -106,6 +106,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Cancelando sincronización en segundo plano... Por favor espere."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Comando inválido: \"%s\""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "El comando \"%s\" solo está disponible en el modo gráfico (GUI)"
|
||||
@@ -553,6 +557,10 @@ msgstr "Buscar en todas la notas"
|
||||
msgid "Tools"
|
||||
msgstr "Herramientas"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Objetivo de sincronización"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Opciones"
|
||||
|
||||
@@ -616,6 +624,13 @@ msgstr "Establecer alarma"
|
||||
msgid "Layout"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "No se puede iniciar la sincronización."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Añadir o eliminar etiquetas"
|
||||
|
||||
@@ -654,6 +669,10 @@ msgstr "Inicio de sesión de OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importar"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Objetivo de sincronización"
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr "¿Eliminar cuaderno?"
|
||||
|
||||
@@ -869,6 +888,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Valor inválido: \"%s\". Posibles valores: %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
"Estado de sincronización (elementos sincronizados / total de elementos)"
|
||||
|
@@ -100,6 +100,10 @@ msgstr "o"
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Annulation de la synchronisation... Veuillez patienter."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Commande invalide : \"%s\""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -558,6 +562,10 @@ msgstr "Chercher dans toutes les notes"
|
||||
msgid "Tools"
|
||||
msgstr "Outils"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
|
||||
@@ -621,6 +629,13 @@ msgstr "Définir ou modifier alarme"
|
||||
msgid "Layout"
|
||||
msgstr "Disposition"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Impossible d'initialiser la synchronisation."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Gérer les étiquettes"
|
||||
|
||||
@@ -660,6 +675,10 @@ msgstr "Connexion OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importer"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr "Supprimer le carnet ?"
|
||||
|
||||
@@ -875,6 +894,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Status de la synchronisation (objets synchro. / total)"
|
||||
|
||||
@@ -1078,10 +1104,6 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Show/Hide the console"
|
||||
#~ msgstr "Quitter le logiciel."
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Last command: %s"
|
||||
#~ msgstr "Commande invalide : \"%s\""
|
||||
|
||||
#~ msgid "Done editing."
|
||||
#~ msgstr "Edition terminée."
|
||||
|
||||
|
@@ -100,6 +100,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -508,6 +512,9 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +575,12 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -605,6 +618,9 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr ""
|
||||
|
||||
@@ -812,6 +828,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
|
24
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.82",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -460,16 +460,6 @@
|
||||
"assert-plus": "1.0.0"
|
||||
}
|
||||
},
|
||||
"gettext-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-iloxjcw+uTPnQ8DrGICWtqkHNgk3mAiDI77pLmXQCnhM+BxFQXstzTA4zj3EpIYMysRQnnNzHyHzBUEazz80Sw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"encoding": "0.1.12",
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
@@ -797,12 +787,6 @@
|
||||
"highlight.js": "9.12.0"
|
||||
}
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
|
||||
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=",
|
||||
"dev": true
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
@@ -884,12 +868,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"mustache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz",
|
||||
"integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
|
||||
|
@@ -18,7 +18,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.82",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -60,10 +60,7 @@
|
||||
"yargs-parser": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gettext-parser": "^1.2.2",
|
||||
"jasmine": "^2.6.0",
|
||||
"marked": "^0.3.6",
|
||||
"mustache": "^2.3.0"
|
||||
"jasmine": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jasmine"
|
||||
|
@@ -7,4 +7,4 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||
mkdir -p "$BUILD_DIR/data"
|
||||
|
||||
npm test tests-build/synchronizer.js
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
|
@@ -15,7 +15,7 @@ process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 9000; // The first test is slow because the database needs to be built
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
|
||||
|
||||
async function allItems() {
|
||||
let folders = await Folder.all();
|
||||
@@ -100,6 +100,7 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
|
||||
done();
|
||||
@@ -627,5 +628,33 @@ describe('Synchronizer', function() {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('items should skip items that cannot be synced', async (done) => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
const noteId = note1.id;
|
||||
await synchronizer().start();
|
||||
let disabledItems = await BaseItem.syncDisabledItems();
|
||||
expect(disabledItems.length).toBe(0);
|
||||
await Note.save({ id: noteId, title: "un mod", });
|
||||
synchronizer().debugFlags_ = ['cannotSync'];
|
||||
await synchronizer().start();
|
||||
synchronizer().debugFlags_ = [];
|
||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].title).toBe('un');
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
disabledItems = await BaseItem.syncDisabledItems();
|
||||
expect(disabledItems.length).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
@@ -43,8 +43,9 @@ const syncDir = __dirname + '/../tests/sync';
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
logger.addTarget('file', { path: logDir + '/log.txt' });
|
||||
logger.setLevel(Logger.LEVEL_DEBUG);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
|
||||
BaseItem.loadClass('Note', Note);
|
||||
BaseItem.loadClass('Folder', Folder);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { shim } = require('lib/shim');
|
||||
const url = require('url')
|
||||
const path = require('path')
|
||||
const urlUtils = require('lib/urlUtils.js');
|
||||
@@ -38,12 +39,18 @@ class ElectronAppWrapper {
|
||||
defaultHeight: 600,
|
||||
});
|
||||
|
||||
this.win_ = new BrowserWindow({
|
||||
const windowOptions = {
|
||||
'x': windowState.x,
|
||||
'y': windowState.y,
|
||||
'width': windowState.width,
|
||||
'height': windowState.height
|
||||
})
|
||||
'height': windowState.height,
|
||||
};
|
||||
|
||||
// Linux icon workaround for bug https://github.com/electron-userland/electron-builder/issues/2098
|
||||
// Fix: https://github.com/electron-userland/electron-builder/issues/2269
|
||||
if (shim.isLinux()) windowOptions.icon = __dirname + '/build/icons/128x128.png';
|
||||
|
||||
this.win_ = new BrowserWindow(windowOptions)
|
||||
|
||||
this.win_.loadURL(url.format({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
|
@@ -259,6 +259,14 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Tools'),
|
||||
submenu: [{
|
||||
label: _('Synchronisation status'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Status',
|
||||
});
|
||||
}
|
||||
},{
|
||||
label: _('Options'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
|
@@ -39,10 +39,22 @@ class Bridge {
|
||||
return this.window().setSize(width, height);
|
||||
}
|
||||
|
||||
showSaveDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
const filePath = dialog.showSaveDialog(options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPath_ = filePath;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
showOpenDialog(options) {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const filePaths = dialog.showOpenDialog(options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPath_ = dirname(filePaths[0]);
|
||||
@@ -71,6 +83,15 @@ class Bridge {
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
showInfoMessageBox(message) {
|
||||
const result = this.showMessageBox({
|
||||
type: 'info',
|
||||
message: message,
|
||||
buttons: [_('OK')],
|
||||
});
|
||||
return result === 0;
|
||||
}
|
||||
|
||||
get Menu() {
|
||||
return require('electron').Menu;
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ fs.readdirSync(guiPath).forEach((filename) => {
|
||||
if (ext !== 'jsx') return;
|
||||
p.pop();
|
||||
|
||||
const basePath = p.join('/');
|
||||
const basePath = p.join('.');
|
||||
|
||||
const jsPath = basePath + '.min.js';
|
||||
|
||||
|
@@ -5,10 +5,23 @@ const { Setting } = require('lib/models/setting.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
settings: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
settingToComponent(key, value) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
@@ -28,7 +41,9 @@ class ConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
Setting.setValue(key, value);
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = value;
|
||||
this.setState({ settings: settings });
|
||||
}
|
||||
|
||||
// Component key needs to be key+value otherwise it doesn't update when the settings change.
|
||||
@@ -44,7 +59,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key+value} style={rowStyle}>
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<select value={value} style={controlStyle} onChange={(event) => { updateSettingValue(key, event.target.value) }}>
|
||||
{items}
|
||||
@@ -52,22 +67,53 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
const onCheckboxClick = (event) => {
|
||||
updateSettingValue(key, !value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key+value} style={rowStyle}>
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={controlStyle}>
|
||||
<label><input type="checkbox" defaultChecked={!!value} onChange={(event) => { updateSettingValue(key, !!event.target.checked) }}/><span style={labelStyle}> {md.label()}</span></label>
|
||||
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
const onTextChange = (event) => {
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = event.target.value;
|
||||
this.setState({ settings: settings });
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
console.warn('Type not implemented: ' + key);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
onSaveClick() {
|
||||
for (let n in this.state.settings) {
|
||||
if (!this.state.settings.hasOwnProperty(n)) continue;
|
||||
Setting.setValue(n, this.state.settings[n]);
|
||||
}
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
onCancelClick() {
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const settings = this.props.settings;
|
||||
const settings = this.state.settings;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
@@ -77,15 +123,21 @@ class ConfigScreenComponent extends React.Component {
|
||||
padding: 10,
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
display: this.state.settings === this.props.settings ? 'none' : 'inline-block',
|
||||
marginRight: 10,
|
||||
}
|
||||
|
||||
let settingComps = [];
|
||||
let keys = Setting.keys(true, 'desktop');
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key === 'sync.target') continue;
|
||||
if (!(key in settings)) {
|
||||
console.warn('Missing setting: ' + key);
|
||||
continue;
|
||||
}
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
const comp = this.settingToComponent(key, settings[key]);
|
||||
if (!comp) continue;
|
||||
settingComps.push(comp);
|
||||
@@ -95,7 +147,12 @@ class ConfigScreenComponent extends React.Component {
|
||||
<div style={style}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={containerStyle}>
|
||||
<div style={Object.assign({}, theme.textStyle, {marginBottom: 20})}>
|
||||
{_('Notes and settings are stored in: %s', pathUtils.toSystemSlashes(Setting.value('profileDir'), process.platform))}
|
||||
</div>
|
||||
{ settingComps }
|
||||
<button onClick={() => {this.onSaveClick()}} style={buttonStyle}>{_('Save')}</button>
|
||||
<button onClick={() => {this.onCancelClick()}} style={buttonStyle}>{_('Cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -41,19 +41,24 @@ class ImportScreenComponent extends React.Component {
|
||||
const messages = this.state.messages.slice();
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (messages[i].key === key) {
|
||||
messages[i].text = text;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) messages.push({ key: key, text: text });
|
||||
messages.push({ key: key, text: text });
|
||||
|
||||
this.setState({ messages: messages });
|
||||
}
|
||||
|
||||
uniqueMessages() {
|
||||
let output = [];
|
||||
const messages = this.state.messages.slice();
|
||||
let foundKeys = [];
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (foundKeys.indexOf(msg.key) >= 0) continue;
|
||||
foundKeys.push(msg.key);
|
||||
output.unshift(msg);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async doImport() {
|
||||
const filePath = this.props.filePath;
|
||||
const folderTitle = await Folder.findUniqueFolderTitle(filename(filePath));
|
||||
@@ -77,10 +82,9 @@ class ImportScreenComponent extends React.Component {
|
||||
this.addMessage('progress', lastProgress);
|
||||
},
|
||||
onError: (error) => {
|
||||
const messages = this.state.messages.slice();
|
||||
let s = error.trace ? error.trace : error.toString();
|
||||
messages.push({ key: 'error_' + (progressCount++), text: s });
|
||||
this.addMessage('error_' + (progressCount++), lastProgress);
|
||||
// Don't display the error directly because most of the time it doesn't matter
|
||||
// (eg. for weird broken HTML, but the note is still imported)
|
||||
console.warn('When importing ENEX file', error);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,7 +99,7 @@ class ImportScreenComponent extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const messages = this.state.messages;
|
||||
const messages = this.uniqueMessages();
|
||||
|
||||
const messagesStyle = {
|
||||
padding: 10,
|
||||
|
@@ -229,8 +229,8 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
styles(themeId, width, height) {
|
||||
const styleKey = themeId + '_' + width + '_' + height;
|
||||
styles(themeId, width, height, messageBoxVisible) {
|
||||
const styleKey = themeId + '_' + width + '_' + height + '_' + messageBoxVisible;
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
const theme = themeStyle(themeId);
|
||||
@@ -239,12 +239,21 @@ class MainScreenComponent extends React.Component {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const rowHeight = height - theme.headerHeight;
|
||||
|
||||
this.styles_.header = {
|
||||
width: width,
|
||||
};
|
||||
|
||||
this.styles_.messageBox = {
|
||||
width: width,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 10,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
}
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.sideBar = {
|
||||
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
|
||||
height: rowHeight,
|
||||
@@ -279,8 +288,10 @@ class MainScreenComponent extends React.Component {
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const folders = this.props.folders;
|
||||
const notes = this.props.notes;
|
||||
const messageBoxVisible = this.props.hasDisabledSyncItems;
|
||||
|
||||
const styles = this.styles(this.props.theme, style.width, style.height);
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerButtons = [];
|
||||
|
||||
@@ -325,6 +336,21 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const onViewDisabledItemsClick = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Status',
|
||||
});
|
||||
}
|
||||
|
||||
const messageComp = messageBoxVisible ? (
|
||||
<div style={styles.messageBox}>
|
||||
<span style={theme.textStyle}>
|
||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<PromptDialog
|
||||
@@ -339,6 +365,7 @@ class MainScreenComponent extends React.Component {
|
||||
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
|
||||
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
|
||||
<Header style={styles.header} showBackButton={false} buttons={headerButtons} />
|
||||
{messageComp}
|
||||
<SideBar style={styles.sideBar} />
|
||||
<NoteList style={styles.noteList} />
|
||||
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
|
||||
@@ -355,6 +382,7 @@ const mapStateToProps = (state) => {
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
folders: state.folders,
|
||||
notes: state.notes,
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -174,7 +174,7 @@ class NoteListComponent extends React.Component {
|
||||
}, style);
|
||||
emptyDivStyle.width = emptyDivStyle.width - padding * 2;
|
||||
emptyDivStyle.height = emptyDivStyle.height - padding * 2;
|
||||
return <div style={emptyDivStyle}>{_('No notes in here. Create one by clicking on "New note".')}</div>
|
||||
return <div style={emptyDivStyle}>{ this.props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -193,6 +193,7 @@ class NoteListComponent extends React.Component {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
notes: state.notes,
|
||||
folders: state.folders,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
theme: state.settings.theme,
|
||||
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
|
@@ -8,6 +8,7 @@ const { Setting } = require('lib/models/setting.js');
|
||||
|
||||
const { MainScreen } = require('./MainScreen.min.js');
|
||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
@@ -75,6 +76,7 @@ class RootComponent extends React.Component {
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -98,7 +98,7 @@ class SideBarComponent extends React.Component {
|
||||
|
||||
let deleteMessage = '';
|
||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
deleteMessage = _('Delete notebook?');
|
||||
deleteMessage = _('Delete notebook? All notes within this notebook will also be deleted.');
|
||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||
deleteMessage = _('Remove this tag from all the notes?');
|
||||
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
||||
|
135
ElectronClient/app/gui/StatusScreen.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class StatusScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
report: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resfreshScreen();
|
||||
}
|
||||
|
||||
async resfreshScreen() {
|
||||
const service = new ReportService();
|
||||
const report = await service.status(Setting.value('sync.target'));
|
||||
this.setState({ report: report });
|
||||
}
|
||||
|
||||
async exportDebugReportClick() {
|
||||
const filename = 'syncReport-' + (new Date()).getTime() + '.csv';
|
||||
|
||||
const filePath = bridge().showSaveDialog({
|
||||
title: _('Please select where the sync status should be exported to'),
|
||||
defaultPath: filename,
|
||||
});
|
||||
|
||||
if (!filePath) return;
|
||||
|
||||
const service = new ReportService();
|
||||
const csv = await service.basicItemList({ format: 'csv' });
|
||||
await fs.writeFileSync(filePath, csv);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
};
|
||||
|
||||
const containerPadding = 10;
|
||||
|
||||
const containerStyle = {
|
||||
padding: containerPadding,
|
||||
overflowY: 'auto',
|
||||
height: style.height - theme.headerHeight - containerPadding * 2,
|
||||
};
|
||||
|
||||
function renderSectionTitleHtml(key, title) {
|
||||
return <h2 key={'section_' + key} style={theme.h2Style}>{title}</h2>
|
||||
}
|
||||
|
||||
function renderSectionHtml(key, section) {
|
||||
let itemsHtml = [];
|
||||
|
||||
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
|
||||
|
||||
for (let n in section.body) {
|
||||
if (!section.body.hasOwnProperty(n)) continue;
|
||||
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
{itemsHtml}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBodyHtml(report) {
|
||||
let output = [];
|
||||
let baseStyle = {
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
flex: 0,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
let sectionsHtml = [];
|
||||
|
||||
for (let i = 0; i < report.length; i++) {
|
||||
let section = report[i];
|
||||
if (!section.body.length) continue;
|
||||
sectionsHtml.push(renderSectionHtml(i, section));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{sectionsHtml}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let body = renderBodyHtml(this.state.report);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={containerStyle}>
|
||||
<a style={theme.textStyle} onClick={() => this.exportDebugReportClick()}href="#">Export debug report</a>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
settings: state.settings,
|
||||
locale: state.settings.locale,
|
||||
};
|
||||
};
|
||||
|
||||
const StatusScreen = connect(mapStateToProps)(StatusScreenComponent);
|
||||
|
||||
module.exports = { StatusScreen };
|
1
ElectronClient/app/locales/de_DE.json
Normal file
@@ -1,5 +1,6 @@
|
||||
var locales = {};
|
||||
locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
module.exports = { locales: locales };
|
2
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.35",
|
||||
"version": "0.10.39",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "0.10.35",
|
||||
"version": "0.10.39",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -25,6 +25,10 @@
|
||||
"win": {
|
||||
"icon": "../../Assets/Joplin.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"mac": {
|
||||
"icon": "../../Assets/macOs.icns",
|
||||
"asar": false
|
||||
|
@@ -9,6 +9,19 @@ body, textarea {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
padding: .5em;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||
as red boxes, but since those are actually valid characters and common in imported
|
||||
Evernote data, we hide them here. */
|
||||
|
@@ -25,6 +25,8 @@ const globalStyle = {
|
||||
selectedColor2: "#5A4D70",
|
||||
colorError2: "#ff6c6c",
|
||||
|
||||
warningBackgroundColor: "#FFD08D",
|
||||
|
||||
headerHeight: 35,
|
||||
headerButtonHPadding: 6,
|
||||
|
||||
@@ -69,6 +71,9 @@ globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
||||
color: globalStyle.color2,
|
||||
});
|
||||
|
||||
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
||||
globalStyle.h2Style.fontSize *= 1.3;
|
||||
|
||||
let themeCache_ = {};
|
||||
|
||||
function themeStyle(theme) {
|
||||
|
@@ -3,4 +3,4 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$ROOT_DIR"
|
||||
./build.sh || exit 1
|
||||
cd "$ROOT_DIR/app"
|
||||
./node_modules/.bin/electron . --env dev --log-level debug --open-dev-tools "$@"
|
||||
./node_modules/.bin/electron . --env dev --log-level warn --open-dev-tools "$@"
|
14
README.md
@@ -18,9 +18,9 @@ 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.35/Joplin-Setup-0.10.35.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.35/Joplin-0.10.35.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.35/Joplin-0.10.35-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.38/Joplin-Setup-0.10.38.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.38/Joplin-0.10.38.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.38/Joplin-0.10.38-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
|
||||
|
||||
@@ -31,13 +31,13 @@ iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'
|
||||
|
||||
## Terminal application
|
||||
|
||||
**IMPORTANT: Node v8+ is required**
|
||||
|
||||
On macOS:
|
||||
|
||||
brew install joplin
|
||||
brew install node joplin
|
||||
|
||||
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
|
||||
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):
|
||||
|
||||
**Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.
|
||||
|
||||
NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
|
||||
sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
|
@@ -10,13 +10,13 @@ The notes can be [synchronised](#synchronisation) with various targets including
|
||||
|
||||
# Installation
|
||||
|
||||
**IMPORTANT: Node v8+ is required**
|
||||
|
||||
On macOS:
|
||||
|
||||
brew install joplin
|
||||
brew install node joplin
|
||||
|
||||
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)), type:
|
||||
On Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)):
|
||||
|
||||
**Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.
|
||||
|
||||
NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin
|
||||
sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin
|
||||
@@ -79,7 +79,7 @@ Rename the currently selected notebook ($b) to "Something":
|
||||
|
||||
Attach a local file to the currently selected note ($n):
|
||||
|
||||
ren $n /home/laurent/pictures/Vacation12.jpg
|
||||
attach $n /home/laurent/pictures/Vacation12.jpg
|
||||
|
||||
The configuration can also be changed from command-line mode. For example, to change the current editor to Sublime Text:
|
||||
|
||||
@@ -137,6 +137,25 @@ Since this is still an actual URL, the terminal will still make it clickable. An
|
||||
|
||||
In Markdown, links to resources are represented as a simple ID to the resource. In order to give access to these resources, they will be, like links, converted to local URLs. Clicking this link will then open a browser, which will handle the file - i.e. display the image, open the PDF file, etc.
|
||||
|
||||
# Shell mode
|
||||
|
||||
Commands can also be used directly from a shell. To view the list of available commands, type `joplin help all`. To reference a note, notebook or tag you can either use the ID (type `joplin ls -l` to view the ID) or by title.
|
||||
|
||||
For example, this will create a new note "My note" in the notebook "My notebook":
|
||||
|
||||
$ joplin mkbook "My notebook"
|
||||
$ joplin use "My notebook"
|
||||
$ joplin mknote "My note"
|
||||
|
||||
To view the newly created note:
|
||||
|
||||
$ joplin ls -l
|
||||
fe889 07/12/2017 17:57 My note
|
||||
|
||||
Give a new title to the note:
|
||||
|
||||
$ joplin set fe889 title "New title"
|
||||
|
||||
# Available shortcuts
|
||||
|
||||
There are two types of shortcuts: those that manipulate the user interface directly, such as `TAB` to move from one pane to another, and those that are simply shortcuts to actual commands. In a way similar to Vim, these shortcuts are generally a verb followed by an object. For example, typing `mn` ([m]ake [n]ote), is used to create a new note: it will switch the interface to command line mode and pre-fill it with `mknote ""` from where the title of the note can be entered. See below for the full list of shortcuts:
|
||||
|
@@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 77
|
||||
versionName "0.10.62"
|
||||
versionCode 80
|
||||
versionName "0.10.65"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@@ -179,6 +179,111 @@
|
||||
remoteGlobalIDString = E23D7B471ACEFE2A00C59171;
|
||||
remoteInfo = SQLite;
|
||||
};
|
||||
4D2AFF551FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28201D9B03D100D4039D;
|
||||
remoteInfo = "RCTAnimation-tvOS";
|
||||
};
|
||||
4D2AFF591FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = ADD01A681E09402E00F6D226;
|
||||
remoteInfo = "RCTBlob-tvOS";
|
||||
};
|
||||
4D2AFF5E1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A283A1D9B042B00D4039D;
|
||||
remoteInfo = "RCTImage-tvOS";
|
||||
};
|
||||
4D2AFF631FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28471D9B043800D4039D;
|
||||
remoteInfo = "RCTLinking-tvOS";
|
||||
};
|
||||
4D2AFF671FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28541D9B044C00D4039D;
|
||||
remoteInfo = "RCTNetwork-tvOS";
|
||||
};
|
||||
4D2AFF6D1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28611D9B046600D4039D;
|
||||
remoteInfo = "RCTSettings-tvOS";
|
||||
};
|
||||
4D2AFF711FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A287B1D9B048500D4039D;
|
||||
remoteInfo = "RCTText-tvOS";
|
||||
};
|
||||
4D2AFF781FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28881D9B049200D4039D;
|
||||
remoteInfo = "RCTWebSocket-tvOS";
|
||||
};
|
||||
4D2AFF7A1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3DBE0D0D1F3B181C0099AA32;
|
||||
remoteInfo = "fishhook-tvOS";
|
||||
};
|
||||
4D2AFF881FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 2D2A28131D9B038B00D4039D;
|
||||
remoteInfo = "React-tvOS";
|
||||
};
|
||||
4D2AFF8A1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3D3C06751DE3340C00C268FA;
|
||||
remoteInfo = "yoga-tvOS";
|
||||
};
|
||||
4D2AFF8C1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3D3CD9321DE5FBEE00167DC4;
|
||||
remoteInfo = "cxxreact-tvOS";
|
||||
};
|
||||
4D2AFF8E1FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3D3CD9181DE5FBD800167DC4;
|
||||
remoteInfo = "jschelpers-tvOS";
|
||||
};
|
||||
4D2AFF901FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3D383D3C1EBD27B6005632C8;
|
||||
remoteInfo = "third-party-tvOS";
|
||||
};
|
||||
4D2AFF921FDA002000599716 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 3D383D621EBD27B9005632C8;
|
||||
remoteInfo = "double-conversion-tvOS";
|
||||
};
|
||||
4D3A19261FBDDA9400457703 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
@@ -348,6 +453,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
00C302C01ABCB91800DB3ED1 /* libRCTImage.a */,
|
||||
4D2AFF5F1FDA002000599716 /* libRCTImage-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -356,6 +462,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */,
|
||||
4D2AFF681FDA002000599716 /* libRCTNetwork-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -380,6 +487,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
139105C11AF99BAD00B5F7CC /* libRCTSettings.a */,
|
||||
4D2AFF6E1FDA002000599716 /* libRCTSettings-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -388,7 +496,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
139FDEF41B06529B00C62182 /* libRCTWebSocket.a */,
|
||||
4D2AFF791FDA002000599716 /* libRCTWebSocket-tvOS.a */,
|
||||
4D2A85AA1FBCE3AC0028537D /* libfishhook.a */,
|
||||
4D2AFF7B1FDA002000599716 /* libfishhook-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -412,11 +522,17 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
146834041AC3E56700842450 /* libReact.a */,
|
||||
4D2AFF891FDA002000599716 /* libReact.a */,
|
||||
3DAD3EA51DF850E9000B6D8A /* libyoga.a */,
|
||||
4D2AFF8B1FDA002000599716 /* libyoga.a */,
|
||||
3DAD3EA91DF850E9000B6D8A /* libcxxreact.a */,
|
||||
4D2AFF8D1FDA002000599716 /* libcxxreact.a */,
|
||||
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */,
|
||||
4D2AFF8F1FDA002000599716 /* libjschelpers.a */,
|
||||
4D3A19271FBDDA9400457703 /* libthird-party.a */,
|
||||
4D2AFF911FDA002000599716 /* libthird-party.a */,
|
||||
4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */,
|
||||
4D2AFF931FDA002000599716 /* libdouble-conversion.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -505,6 +621,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */,
|
||||
4D2AFF561FDA002000599716 /* libRCTAnimation.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -513,6 +630,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78C398B91ACF4ADC00677621 /* libRCTLinking.a */,
|
||||
4D2AFF641FDA002000599716 /* libRCTLinking-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -566,6 +684,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
832341B51AAA6A8300B99B32 /* libRCTText.a */,
|
||||
4D2AFF721FDA002000599716 /* libRCTText-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -597,6 +716,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */,
|
||||
4D2AFF5A1FDA002000599716 /* libRCTBlob-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -879,6 +999,111 @@
|
||||
remoteRef = 4D2A85CC1FBCE3AD0028537D /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF561FDA002000599716 /* libRCTAnimation.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRCTAnimation.a;
|
||||
remoteRef = 4D2AFF551FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF5A1FDA002000599716 /* libRCTBlob-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTBlob-tvOS.a";
|
||||
remoteRef = 4D2AFF591FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF5F1FDA002000599716 /* libRCTImage-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTImage-tvOS.a";
|
||||
remoteRef = 4D2AFF5E1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF641FDA002000599716 /* libRCTLinking-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTLinking-tvOS.a";
|
||||
remoteRef = 4D2AFF631FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF681FDA002000599716 /* libRCTNetwork-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTNetwork-tvOS.a";
|
||||
remoteRef = 4D2AFF671FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF6E1FDA002000599716 /* libRCTSettings-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTSettings-tvOS.a";
|
||||
remoteRef = 4D2AFF6D1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF721FDA002000599716 /* libRCTText-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTText-tvOS.a";
|
||||
remoteRef = 4D2AFF711FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF791FDA002000599716 /* libRCTWebSocket-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libRCTWebSocket-tvOS.a";
|
||||
remoteRef = 4D2AFF781FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF7B1FDA002000599716 /* libfishhook-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libfishhook-tvOS.a";
|
||||
remoteRef = 4D2AFF7A1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF891FDA002000599716 /* libReact.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libReact.a;
|
||||
remoteRef = 4D2AFF881FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF8B1FDA002000599716 /* libyoga.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libyoga.a;
|
||||
remoteRef = 4D2AFF8A1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF8D1FDA002000599716 /* libcxxreact.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libcxxreact.a;
|
||||
remoteRef = 4D2AFF8C1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF8F1FDA002000599716 /* libjschelpers.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libjschelpers.a;
|
||||
remoteRef = 4D2AFF8E1FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF911FDA002000599716 /* libthird-party.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libthird-party.a";
|
||||
remoteRef = 4D2AFF901FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D2AFF931FDA002000599716 /* libdouble-conversion.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libdouble-conversion.a";
|
||||
remoteRef = 4D2AFF921FDA002000599716 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
4D3A19271FBDDA9400457703 /* libthird-party.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
|
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.10.3</string>
|
||||
<string>0.10.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>3</string>
|
||||
<string>6</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
|
@@ -66,10 +66,15 @@ class BaseApplication {
|
||||
}
|
||||
|
||||
switchCurrentFolder(folder) {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
if (!this.hasGui()) {
|
||||
this.currentFolder_ = Object.assign({}, folder);
|
||||
Setting.setValue('activeFolderId', folder ? folder.id : '');
|
||||
} else {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the initial flags passed to main script and
|
||||
@@ -227,6 +232,10 @@ class BaseApplication {
|
||||
return false;
|
||||
}
|
||||
|
||||
uiType() {
|
||||
return this.hasGui() ? 'gui' : 'cli';
|
||||
}
|
||||
|
||||
generalMiddlewareFn() {
|
||||
const middleware = store => next => (action) => {
|
||||
return this.generalMiddleware(store, next, action);
|
||||
@@ -271,6 +280,11 @@ class BaseApplication {
|
||||
}
|
||||
}
|
||||
|
||||
// if (action.type === 'NOTE_DELETE') {
|
||||
// // Update folders if a note is deleted in case the deleted note was a conflict
|
||||
// await FoldersScreenUtils.refreshFolders();
|
||||
// }
|
||||
|
||||
if (this.hasGui() && action.type == 'SETTING_UPDATE_ONE' && action.key == 'sync.interval' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
reg.setupRecurrentSync();
|
||||
}
|
||||
|
@@ -100,7 +100,7 @@ class MdToHtml {
|
||||
const href = this.getAttr_(attrs, 'src');
|
||||
|
||||
if (!Resource.isResourceUrl(href)) {
|
||||
return '<span>' + href + '</span><img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||
return '<img title="' + htmlentities(title) + '" src="' + href + '"/>';
|
||||
}
|
||||
|
||||
const resourceId = Resource.urlToId(href);
|
||||
|
@@ -62,11 +62,12 @@ class BaseModel {
|
||||
return temp;
|
||||
}
|
||||
|
||||
static fieldType(name) {
|
||||
static fieldType(name, defaultValue = null) {
|
||||
let fields = this.fields();
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (fields[i].name == name) return fields[i].type;
|
||||
}
|
||||
if (defaultValue !== null) return defaultValue;
|
||||
throw new Error('Unknown field: ' + name);
|
||||
}
|
||||
|
||||
@@ -201,11 +202,11 @@ class BaseModel {
|
||||
let output = {};
|
||||
let type = null;
|
||||
for (let n in newModel) {
|
||||
if (!newModel.hasOwnProperty(n)) continue;
|
||||
if (n == 'type_') {
|
||||
type = n;
|
||||
type = newModel[n];
|
||||
continue;
|
||||
}
|
||||
if (!newModel.hasOwnProperty(n)) continue;
|
||||
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
|
||||
output[n] = newModel[n];
|
||||
}
|
||||
@@ -214,6 +215,12 @@ class BaseModel {
|
||||
return output;
|
||||
}
|
||||
|
||||
static modelsAreSame(oldModel, newModel) {
|
||||
const diff = this.diffObjects(oldModel, newModel);
|
||||
delete diff.type_;
|
||||
return !Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
static saveQuery(o, options) {
|
||||
let temp = {}
|
||||
let fieldNames = this.fieldNames();
|
||||
@@ -232,6 +239,9 @@ class BaseModel {
|
||||
o.updated_time = timeNow;
|
||||
}
|
||||
|
||||
// The purpose of user_updated_time is to allow the user to manually set the time of a note (in which case
|
||||
// options.autoTimestamp will be `false`). However note that if the item is later changed, this timestamp
|
||||
// will be set again to the current time.
|
||||
if (options.autoTimestamp && this.hasField('user_updated_time')) {
|
||||
o.user_updated_time = timeNow;
|
||||
}
|
||||
@@ -272,6 +282,18 @@ class BaseModel {
|
||||
options = this.modOptions(options);
|
||||
options.isNew = this.isNew(o, options);
|
||||
|
||||
// Diff saving is an optimisation which takes a new version of the item and an old one,
|
||||
// do a diff and save only this diff. IMPORTANT: When using this make sure that both
|
||||
// models have been normalised using ItemClass.filter()
|
||||
const isDiffSaving = options && options.oldItem && !options.isNew;
|
||||
|
||||
if (isDiffSaving) {
|
||||
const newObject = BaseModel.diffObjects(options.oldItem, o);
|
||||
newObject.type_ = o.type_;
|
||||
newObject.id = o.id;
|
||||
o = newObject;
|
||||
}
|
||||
|
||||
o = this.filter(o);
|
||||
|
||||
let queries = [];
|
||||
@@ -292,6 +314,15 @@ class BaseModel {
|
||||
if ('user_updated_time' in saveQuery.modObject) o.user_updated_time = saveQuery.modObject.user_updated_time;
|
||||
if ('user_created_time' in saveQuery.modObject) o.user_created_time = saveQuery.modObject.user_created_time;
|
||||
o = this.addModelMd(o);
|
||||
|
||||
if (isDiffSaving) {
|
||||
for (let n in options.oldItem) {
|
||||
if (!options.oldItem.hasOwnProperty(n)) continue;
|
||||
if (n in o) continue;
|
||||
o[n] = options.oldItem[n];
|
||||
}
|
||||
}
|
||||
|
||||
return this.filter(o);
|
||||
}).catch((error) => {
|
||||
Log.error('Cannot save model', error);
|
||||
@@ -322,9 +353,18 @@ class BaseModel {
|
||||
let output = Object.assign({}, model);
|
||||
for (let n in output) {
|
||||
if (!output.hasOwnProperty(n)) continue;
|
||||
|
||||
// The SQLite database doesn't have booleans so cast everything to int
|
||||
if (output[n] === true) output[n] = 1;
|
||||
if (output[n] === false) output[n] = 0;
|
||||
if (output[n] === true) {
|
||||
output[n] = 1;
|
||||
} else if (output[n] === false) {
|
||||
output[n] = 0;
|
||||
} else {
|
||||
const t = this.fieldType(n, Database.TYPE_UNKNOWN);
|
||||
if (t === Database.TYPE_INT) {
|
||||
output[n] = !n ? 0 : parseInt(output[n], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@@ -17,15 +17,13 @@ class Dropdown extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
updateHeaderCoordinates() {
|
||||
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
|
||||
setTimeout(() => {
|
||||
this.headerRef_.measure((fx, fy, width, height, px, py) => {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height }
|
||||
});
|
||||
this.headerRef_.measure((fx, fy, width, height, px, py) => {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height }
|
||||
});
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -107,7 +105,10 @@ class Dropdown extends React.Component {
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'column' }}>
|
||||
<TouchableOpacity style={headerWrapperStyle} ref={(ref) => this.headerRef_ = ref} onPress={() => { this.setState({ listVisible: true }) }}>
|
||||
<TouchableOpacity style={headerWrapperStyle} ref={(ref) => this.headerRef_ = ref} onPress={() => {
|
||||
this.updateHeaderCoordinates();
|
||||
this.setState({ listVisible: true });
|
||||
}}>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>{headerLabel}</Text>
|
||||
<Text style={headerArrowStyle}>{'▼'}</Text>
|
||||
</TouchableOpacity>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image } = require('react-native');
|
||||
const { Platform, View, Text, Button, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { Log } = require('lib/log.js');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
@@ -218,7 +218,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = RNFS.ExternalDirectoryPath + '/syncReport-' + (new Date()).getTime() + '.txt';
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n--------------------------------------------------------------------------------");
|
||||
const finalText = [logItemCsv, itemListCsv].join("\n================================================================================\n");
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
alert('Debug report exported to ' + filePath);
|
||||
@@ -410,6 +410,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const backButtonComp = backButton(this.styles(), () => this.backButton_press(), !this.props.historyCanGoBack);
|
||||
const searchButtonComp = this.props.noteSelectionEnabled ? null : searchButton(this.styles(), () => this.searchButton_press());
|
||||
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press()) : null;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
const menuComp = (
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
@@ -417,7 +418,9 @@ class ScreenHeaderComponent extends Component {
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
{ menuOptionComponents }
|
||||
<ScrollView style={{ maxHeight: windowHeight }}>
|
||||
{ menuOptionComponents }
|
||||
</ScrollView>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
|
@@ -69,7 +69,7 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
|
||||
deleteFolder_onPress(folderId) {
|
||||
dialogs.confirm(this, _('Delete notebook?')).then((ok) => {
|
||||
dialogs.confirm(this, _('Delete notebook? All notes within this notebook will also be deleted.')).then((ok) => {
|
||||
if (!ok) return;
|
||||
|
||||
Folder.delete(folderId).then(() => {
|
||||
|
@@ -29,9 +29,11 @@ shared.saveNoteButton_press = async function(comp) {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -54,8 +56,11 @@ shared.saveNoteButton_press = async function(comp) {
|
||||
// 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
|
||||
// saved (it's done asynchronously)
|
||||
note.title = stateNote.title;
|
||||
// saved (it's done asynchronously).
|
||||
//
|
||||
// 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;
|
||||
note.body = stateNote.body;
|
||||
}
|
||||
|
||||
|
@@ -165,6 +165,16 @@ class Database {
|
||||
throw new Error('Unknown enum type or value: ' + type + ', ' + s);
|
||||
}
|
||||
|
||||
static enumName(type, id) {
|
||||
if (type === 'fieldType') {
|
||||
if (id === Database.TYPE_UNKNOWN) return 'unknown';
|
||||
if (id === Database.TYPE_INT) return 'int';
|
||||
if (id === Database.TYPE_TEXT) return 'text';
|
||||
if (id === Database.TYPE_NUMERIC) return 'numeric';
|
||||
throw new Error('Invalid type id: ' + id);
|
||||
}
|
||||
}
|
||||
|
||||
static formatValue(type, value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (type == this.TYPE_INT) return Number(value);
|
||||
@@ -308,6 +318,7 @@ class Database {
|
||||
|
||||
}
|
||||
|
||||
Database.TYPE_UNKNOWN = 0;
|
||||
Database.TYPE_INT = 1;
|
||||
Database.TYPE_TEXT = 2;
|
||||
Database.TYPE_NUMERIC = 3;
|
||||
|
@@ -123,15 +123,27 @@ class FileApiDriverOneDrive {
|
||||
return this.makeItem_(item);
|
||||
}
|
||||
|
||||
put(path, content, options = null) {
|
||||
async put(path, content, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.source == 'file') {
|
||||
return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options);
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'text/plain' };
|
||||
return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options);
|
||||
let response = null;
|
||||
|
||||
try {
|
||||
if (options.source == 'file') {
|
||||
response = await this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options);
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'text/plain' };
|
||||
response = await this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === 'BadRequest' && error.message === 'Maximum request length exceeded.') {
|
||||
error.code = 'cannotSync';
|
||||
error.message = 'Resource exceeds OneDrive max file size (4MB)';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
|
@@ -194,11 +194,15 @@ function addResourceTag(lines, resource, alt = "") {
|
||||
|
||||
|
||||
function isBlockTag(n) {
|
||||
return n=="div" || n=="p" || n=="dl" || n=="dd" || n=="center";
|
||||
return ["div", "p", "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isStrongTag(n) {
|
||||
return n == "strong" || n == "b";
|
||||
return n == "strong" || n == "b" || n == 'big';
|
||||
}
|
||||
|
||||
function isStrikeTag(n) {
|
||||
return n == "strike" || n == "s" || n == 'del';
|
||||
}
|
||||
|
||||
function isEmTag(n) {
|
||||
@@ -210,7 +214,7 @@ function isAnchor(n) {
|
||||
}
|
||||
|
||||
function isIgnoredEndTag(n) {
|
||||
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody' || n == 'sup';
|
||||
return ["en-note", "en-todo", "span", "body", "html", "font", "br", 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isListTag(n) {
|
||||
@@ -219,7 +223,7 @@ function isListTag(n) {
|
||||
|
||||
// Elements that don't require any special treatment beside adding a newline character
|
||||
function isNewLineOnlyEndTag(n) {
|
||||
return n=="div" || n=="p" || n=="li" || n=="h1" || n=="h2" || n=="h3" || n=="h4" || n=="h5" || n=="dl" || n=="dd" || n=="center";
|
||||
return ["div", "p", "li", "h1", "h2", "h3", "h4", "h5", 'h6', "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isCodeTag(n) {
|
||||
@@ -244,6 +248,10 @@ function isCodeTag(n) {
|
||||
return n == "pre" || n == "code";
|
||||
}
|
||||
|
||||
function isInlineCodeTag(n) {
|
||||
return ['samp', 'kbd'].indexOf(n) >= 0;
|
||||
}
|
||||
|
||||
function isNewLineBlock(s) {
|
||||
return s == BLOCK_OPEN || s == BLOCK_CLOSE;
|
||||
}
|
||||
@@ -253,8 +261,27 @@ function xmlNodeText(xmlNode) {
|
||||
return xmlNode[0];
|
||||
}
|
||||
|
||||
function attributeToLowerCase(node) {
|
||||
if (!node.attributes) return {};
|
||||
let output = {};
|
||||
for (let n in node.attributes) {
|
||||
if (!node.attributes.hasOwnProperty(n)) continue;
|
||||
output[n.toLowerCase()] = node.attributes[n];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function enexXmlToMdArray(stream, resources) {
|
||||
resources = resources.slice();
|
||||
let remainingResources = resources.slice();
|
||||
|
||||
const removeRemainingResource = (id) => {
|
||||
for (let i = 0; i < remainingResources.length; i++) {
|
||||
const r = remainingResources[i];
|
||||
if (r.id === id) {
|
||||
remainingResources.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let state = {
|
||||
@@ -265,7 +292,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
|
||||
let options = {};
|
||||
let strict = true;
|
||||
let strict = false;
|
||||
var saxStream = require('sax').createStream(strict, options)
|
||||
|
||||
let section = {
|
||||
@@ -275,14 +302,18 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
|
||||
saxStream.on('error', function(e) {
|
||||
reject(e);
|
||||
console.warn(e);
|
||||
//reject(e);
|
||||
})
|
||||
|
||||
saxStream.on('text', function(text) {
|
||||
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
|
||||
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
|
||||
})
|
||||
|
||||
saxStream.on('opentag', function(node) {
|
||||
const nodeAttributes = attributeToLowerCase(node);
|
||||
|
||||
let n = node.name.toLowerCase();
|
||||
if (n == 'en-note') {
|
||||
// Start of note
|
||||
@@ -296,10 +327,13 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (n == 'tbody') {
|
||||
} else if (n == 'tbody' || n == 'thead') {
|
||||
// Ignore it
|
||||
} else if (n == 'tr') {
|
||||
if (section.type != 'table') throw new Error('Found a <tr> tag outside of a table');
|
||||
if (section.type != 'table') {
|
||||
console.warn('Found a <tr> tag outside of a table');
|
||||
return;
|
||||
}
|
||||
|
||||
let newSection = {
|
||||
type: 'tr',
|
||||
@@ -311,7 +345,10 @@ function enexXmlToMdArray(stream, resources) {
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (n == 'td' || n == 'th') {
|
||||
if (section.type != 'tr') throw new Error('Found a <td> tag outside of a <tr>');
|
||||
if (section.type != 'tr') {
|
||||
console.warn('Found a <td> tag outside of a <tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (n == 'th') section.isHeader = true;
|
||||
|
||||
@@ -342,17 +379,27 @@ function enexXmlToMdArray(stream, resources) {
|
||||
}
|
||||
} else if (isStrongTag(n)) {
|
||||
section.lines.push("**");
|
||||
} else if (n == 's') {
|
||||
// Not supported
|
||||
} else if (isStrikeTag(n)) {
|
||||
section.lines.push('(');
|
||||
} else if (isInlineCodeTag(n)) {
|
||||
section.lines.push('`');
|
||||
} else if (n == 'q') {
|
||||
section.lines.push('"');
|
||||
} else if (n == 'img') {
|
||||
// TODO: TEST IMAGE
|
||||
if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML
|
||||
let s = '';
|
||||
section.lines.push(s);
|
||||
}
|
||||
} else if (isAnchor(n)) {
|
||||
state.anchorAttributes.push(node.attributes);
|
||||
state.anchorAttributes.push(nodeAttributes);
|
||||
section.lines.push('[');
|
||||
} else if (isEmTag(n)) {
|
||||
section.lines.push("*");
|
||||
} else if (n == "en-todo") {
|
||||
let x = node.attributes && node.attributes.checked && node.attributes.checked.toLowerCase() == 'true' ? 'X' : ' ';
|
||||
let x = nodeAttributes && nodeAttributes.checked && nodeAttributes.checked.toLowerCase() == 'true' ? 'X' : ' ';
|
||||
section.lines.push('- [' + x + '] ');
|
||||
} else if (n == "hr") {
|
||||
// Needs to be surrounded by new lines so that it's properly rendered as a line when converting to HTML
|
||||
@@ -375,20 +422,20 @@ function enexXmlToMdArray(stream, resources) {
|
||||
} else if (n == 'blockquote') {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
state.inQuote = true;
|
||||
} else if (isCodeTag(n, node.attributes)) {
|
||||
} else if (isCodeTag(n, nodeAttributes)) {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
state.inCode = true;
|
||||
} else if (n == "br") {
|
||||
section.lines.push(NEWLINE);
|
||||
} else if (n == "en-media") {
|
||||
const hash = node.attributes.hash;
|
||||
const hash = nodeAttributes.hash;
|
||||
|
||||
let resource = null;
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
let r = resources[i];
|
||||
if (r.id == hash) {
|
||||
resource = r;
|
||||
resources.splice(i, 1);
|
||||
removeRemainingResource(r.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -430,11 +477,11 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// </en-export>
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
let r = resources[i];
|
||||
for (let i = 0; i < remainingResources.length; i++) {
|
||||
let r = remainingResources[i];
|
||||
if (!r.id) {
|
||||
r.id = hash;
|
||||
resources[i] = r;
|
||||
remainingResources[i] = r;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -448,27 +495,29 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// means it's an attachement. It will be appended along with the
|
||||
// other remaining resources at the bottom of the markdown text.
|
||||
if (!!resource.id) {
|
||||
section.lines = addResourceTag(section.lines, resource, node.attributes.alt);
|
||||
section.lines = addResourceTag(section.lines, resource, nodeAttributes.alt);
|
||||
}
|
||||
}
|
||||
} else if (n == "span" || n == "font" || n == 'sup') {
|
||||
// Ignore
|
||||
} else if (["span", "font", 'sup', 'cite', 'abbr', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0) {
|
||||
// Inline tags that can be ignored in Markdown
|
||||
} else {
|
||||
console.warn("Unsupported start tag: " + n);
|
||||
}
|
||||
})
|
||||
|
||||
saxStream.on('closetag', function(n) {
|
||||
n = n ? n.toLowerCase() : n;
|
||||
|
||||
if (n == 'en-note') {
|
||||
// End of note
|
||||
} else if (isNewLineOnlyEndTag(n)) {
|
||||
section.lines.push(BLOCK_CLOSE);
|
||||
} else if (n == 'td' || n == 'th') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (n == 'tr') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (n == 'table') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (isIgnoredEndTag(n)) {
|
||||
// Skip
|
||||
} else if (isListTag(n)) {
|
||||
@@ -476,6 +525,10 @@ function enexXmlToMdArray(stream, resources) {
|
||||
state.lists.pop();
|
||||
} else if (isStrongTag(n)) {
|
||||
section.lines.push("**");
|
||||
} else if (isStrikeTag(n)) {
|
||||
section.lines.push(')');
|
||||
} else if (isInlineCodeTag(n)) {
|
||||
section.lines.push('`');
|
||||
} else if (isEmTag(n)) {
|
||||
section.lines.push("*");
|
||||
} else if (n == 'q') {
|
||||
@@ -492,6 +545,10 @@ function enexXmlToMdArray(stream, resources) {
|
||||
|
||||
if (section.lines.length < 1) throw new Error('Invalid anchor tag closing'); // Sanity check, but normally not possible
|
||||
|
||||
const pushEmptyAnchor = (url) => {
|
||||
section.lines.push('[link](' + url + ')');
|
||||
}
|
||||
|
||||
// When closing the anchor tag, check if there's is any text content. If not
|
||||
// put the URL as is (don't wrap it in [](url)). The markdown parser, using
|
||||
// GitHub flavour, will turn this URL into a link. This is to generate slightly
|
||||
@@ -499,13 +556,38 @@ function enexXmlToMdArray(stream, resources) {
|
||||
let previous = section.lines[section.lines.length - 1];
|
||||
if (previous == '[') {
|
||||
section.lines.pop();
|
||||
section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else if (!previous || previous == url) {
|
||||
section.lines.pop();
|
||||
section.lines.pop();
|
||||
section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else {
|
||||
section.lines.push('](' + url + ')');
|
||||
// Need to remove any new line character between the current ']' and the previous '['
|
||||
// otherwise it won't render properly.
|
||||
let allSpaces = true;
|
||||
for (let i = section.lines.length - 1; i >= 0; i--) {
|
||||
const c = section.lines[i];
|
||||
if (c === '[') {
|
||||
break;
|
||||
} else {
|
||||
if (c === BLOCK_CLOSE || c === BLOCK_OPEN || c === NEWLINE) {
|
||||
section.lines[i] = SPACE;
|
||||
} else {
|
||||
if (!isWhiteSpace(c)) allSpaces = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allSpaces) {
|
||||
for (let i = section.lines.length - 1; i >= 0; i--) {
|
||||
const c = section.lines.pop();
|
||||
if (c === '[') break;
|
||||
}
|
||||
//section.lines.push(url);
|
||||
pushEmptyAnchor(url);
|
||||
} else {
|
||||
section.lines.push('](' + url + ')');
|
||||
}
|
||||
}
|
||||
} else if (isListTag(n)) {
|
||||
section.lines.push(BLOCK_CLOSE);
|
||||
@@ -527,7 +609,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
saxStream.on('end', function() {
|
||||
resolve({
|
||||
content: section,
|
||||
resources: resources,
|
||||
resources: remainingResources,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -535,50 +617,28 @@ function enexXmlToMdArray(stream, resources) {
|
||||
});
|
||||
}
|
||||
|
||||
function setTableCellContent(table) {
|
||||
if (!table.type == 'table') throw new Error('Only for tables');
|
||||
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
let td = tr.lines[tdIndex];
|
||||
td.content = processMdArrayNewLines(td.lines);
|
||||
td.content = td.content.replace(/\n\n\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n\n/g, ' ');
|
||||
td.content = td.content.replace(/\n/g, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
function removeTableCellNewLines(cellText) {
|
||||
return cellText.replace(/\n+/g, " ");
|
||||
}
|
||||
|
||||
function cellWidth(cellText) {
|
||||
const lines = cellText.split("\n");
|
||||
let maxWidth = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.length > maxWidth) maxWidth = line.length;
|
||||
}
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
function colWidths(table) {
|
||||
let output = [];
|
||||
function tableHasSubTables(table) {
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
const w = cellWidth(td.content);
|
||||
if (output.length <= tdIndex) output.push(0);
|
||||
if (w > output[tdIndex]) output[tdIndex] = w;
|
||||
for (let i = 0; i < td.lines.length; i++) {
|
||||
if (typeof td.lines[i] === 'object') return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawTable(table, colWidths) {
|
||||
// Markdown tables don't support tables within tables, which is common in notes that are complete web pages, for example when imported
|
||||
// via Web Clipper. So to handle this, we render all the outer tables as regular text (as if replacing all the <table>, <tr> and <td>
|
||||
// elements by <div>) and only the inner ones, those that don't contain any other tables, are rendered as actual tables. This is generally
|
||||
// the required behaviour since the outer tables are usually for layout and the inner ones are the content.
|
||||
function drawTable(table) {
|
||||
// | First Header | Second Header |
|
||||
// | ------------- | ------------- |
|
||||
// | Content Cell | Content Cell |
|
||||
@@ -586,8 +646,11 @@ function drawTable(table, colWidths) {
|
||||
|
||||
// There must be at least 3 dashes separating each header cell.
|
||||
// https://gist.github.com/IanWang/28965e13cdafdef4e11dc91f578d160d#tables
|
||||
|
||||
const flatRender = tableHasSubTables(table); // Render the table has regular text
|
||||
const minColWidth = 3;
|
||||
let lines = [];
|
||||
lines.push(BLOCK_OPEN);
|
||||
let headerDone = false;
|
||||
for (let trIndex = 0; trIndex < table.lines.length; trIndex++) {
|
||||
const tr = table.lines[trIndex];
|
||||
@@ -595,37 +658,79 @@ function drawTable(table, colWidths) {
|
||||
let line = [];
|
||||
let headerLine = [];
|
||||
let emptyHeader = null;
|
||||
for (let tdIndex = 0; tdIndex < colWidths.length; tdIndex++) {
|
||||
const width = Math.max(minColWidth, colWidths[tdIndex]);
|
||||
const cell = tr.lines[tdIndex] ? tr.lines[tdIndex].content : '';
|
||||
line.push(stringPadding(cell, width, ' ', stringPadding.RIGHT));
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
|
||||
if (!headerDone) {
|
||||
if (!isHeader) {
|
||||
if (!emptyHeader) emptyHeader = [];
|
||||
let h = stringPadding(' ', width, ' ', stringPadding.RIGHT);
|
||||
if (!width) h = '';
|
||||
emptyHeader.push(h);
|
||||
if (flatRender) {
|
||||
line.push(BLOCK_OPEN);
|
||||
|
||||
let currentCells = [];
|
||||
|
||||
const renderCurrentCells = () => {
|
||||
if (!currentCells.length) return;
|
||||
const cellText = processMdArrayNewLines(currentCells);
|
||||
line.push(cellText);
|
||||
currentCells = [];
|
||||
}
|
||||
headerLine.push('-'.repeat(width));
|
||||
|
||||
// In here, recursively render the tables
|
||||
for (let i = 0; i < td.lines.length; i++) {
|
||||
const c = td.lines[i];
|
||||
if (typeof c === 'object') { // This is a table
|
||||
renderCurrentCells();
|
||||
currentCells = currentCells.concat(drawTable(c));
|
||||
} else { // This is plain text
|
||||
currentCells.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
renderCurrentCells();
|
||||
|
||||
line.push(BLOCK_CLOSE);
|
||||
} else { // Regular table rendering
|
||||
|
||||
// A cell in a Markdown table cannot have new lines so remove them
|
||||
const cellText = removeTableCellNewLines(processMdArrayNewLines(td.lines));
|
||||
|
||||
const width = Math.max(cellText.length, 3);
|
||||
line.push(stringPadding(cellText, width, ' ', stringPadding.RIGHT));
|
||||
|
||||
if (!headerDone) {
|
||||
if (!isHeader) {
|
||||
if (!emptyHeader) emptyHeader = [];
|
||||
let h = stringPadding(' ', width, ' ', stringPadding.RIGHT);
|
||||
emptyHeader.push(h);
|
||||
}
|
||||
headerLine.push('-'.repeat(width));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyHeader) {
|
||||
lines.push('| ' + emptyHeader.join(' | ') + ' |');
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
if (flatRender) {
|
||||
headerDone = true;
|
||||
}
|
||||
lines.push(BLOCK_OPEN);
|
||||
lines = lines.concat(line);
|
||||
lines.push(BLOCK_CLOSE);
|
||||
} else {
|
||||
if (emptyHeader) {
|
||||
lines.push('| ' + emptyHeader.join(' | ') + ' |');
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
}
|
||||
|
||||
lines.push('| ' + line.join(' | ') + ' |');
|
||||
lines.push('| ' + line.join(' | ') + ' |');
|
||||
|
||||
if (!headerDone) {
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
if (!headerDone) {
|
||||
lines.push('| ' + headerLine.join(' | ') + ' |');
|
||||
headerDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('<<<<:D>>>>' + NEWLINE + '<<<<:D>>>>').split('<<<<:D>>>>');
|
||||
lines.push(BLOCK_CLOSE);
|
||||
|
||||
return flatRender ? lines : lines.join('<<<<:D>>>>' + NEWLINE + '<<<<:D>>>>').split('<<<<:D>>>>');
|
||||
}
|
||||
|
||||
async function enexXmlToMd(stream, resources) {
|
||||
@@ -636,13 +741,9 @@ async function enexXmlToMd(stream, resources) {
|
||||
for (let i = 0; i < result.content.lines.length; i++) {
|
||||
let line = result.content.lines[i];
|
||||
if (typeof line === 'object') { // A table
|
||||
let table = setTableCellContent(line);
|
||||
//console.log(require('util').inspect(table, false, null))
|
||||
const cw = colWidths(table);
|
||||
const tableLines = drawTable(table, cw);
|
||||
mdLines.push(BLOCK_OPEN);
|
||||
const table = line;
|
||||
const tableLines = drawTable(table);
|
||||
mdLines = mdLines.concat(tableLines);
|
||||
mdLines.push(BLOCK_CLOSE);
|
||||
} else { // an actual line
|
||||
mdLines.push(line);
|
||||
}
|
||||
|
@@ -212,51 +212,92 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
async function processNotes() {
|
||||
if (processingNotes) return false;
|
||||
|
||||
processingNotes = true;
|
||||
stream.pause();
|
||||
try {
|
||||
processingNotes = true;
|
||||
stream.pause();
|
||||
|
||||
let chain = [];
|
||||
while (notes.length) {
|
||||
let note = notes.shift();
|
||||
const contentStream = stringToStream(note.bodyXml);
|
||||
chain.push(() => {
|
||||
return enexXmlToMd(contentStream, note.resources).then((body) => {
|
||||
delete note.bodyXml;
|
||||
while (notes.length) {
|
||||
let note = notes.shift();
|
||||
const contentStream = stringToStream(note.bodyXml);
|
||||
const body = await enexXmlToMd(contentStream, note.resources);
|
||||
delete note.bodyXml;
|
||||
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info(body);
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info('*************************************************************************');
|
||||
// console.info(body);
|
||||
// console.info('*************************************************************************');
|
||||
|
||||
note.id = uuid.create();
|
||||
note.parent_id = parentFolderId;
|
||||
note.body = body;
|
||||
note.id = uuid.create();
|
||||
note.parent_id = parentFolderId;
|
||||
note.body = body;
|
||||
|
||||
// Notes in enex files always have a created timestamp but not always an
|
||||
// updated timestamp (it the note has never been modified). For sync
|
||||
// we require an updated_time property, so set it to create_time in that case
|
||||
if (!note.updated_time) note.updated_time = note.created_time;
|
||||
// Notes in enex files always have a created timestamp but not always an
|
||||
// updated timestamp (it the note has never been modified). For sync
|
||||
// we require an updated_time property, so set it to create_time in that case
|
||||
if (!note.updated_time) note.updated_time = note.created_time;
|
||||
|
||||
return saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
}).then((result) => {
|
||||
if (result.noteUpdated) {
|
||||
progressState.updated++;
|
||||
} else if (result.noteCreated) {
|
||||
progressState.created++;
|
||||
} else if (result.noteSkipped) {
|
||||
progressState.skipped++;
|
||||
}
|
||||
progressState.resourcesCreated += result.resourcesCreated;
|
||||
progressState.notesTagged += result.notesTagged;
|
||||
importOptions.onProgress(progressState);
|
||||
});
|
||||
});
|
||||
const result = await saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
|
||||
if (result.noteUpdated) {
|
||||
progressState.updated++;
|
||||
} else if (result.noteCreated) {
|
||||
progressState.created++;
|
||||
} else if (result.noteSkipped) {
|
||||
progressState.skipped++;
|
||||
}
|
||||
progressState.resourcesCreated += result.resourcesCreated;
|
||||
progressState.notesTagged += result.notesTagged;
|
||||
importOptions.onProgress(progressState);
|
||||
}
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return promiseChain(chain).then(() => {
|
||||
stream.resume();
|
||||
processingNotes = false;
|
||||
return true;
|
||||
});
|
||||
stream.resume();
|
||||
processingNotes = false;
|
||||
return true;
|
||||
|
||||
// let chain = [];
|
||||
// while (notes.length) {
|
||||
// let note = notes.shift();
|
||||
// const contentStream = stringToStream(note.bodyXml);
|
||||
// chain.push(() => {
|
||||
// return enexXmlToMd(contentStream, note.resources).then((body) => {
|
||||
// delete note.bodyXml;
|
||||
|
||||
// // console.info('-----------------------------------------------------------');
|
||||
// // console.info(body);
|
||||
// // console.info('-----------------------------------------------------------');
|
||||
|
||||
// note.id = uuid.create();
|
||||
// note.parent_id = parentFolderId;
|
||||
// note.body = body;
|
||||
|
||||
// // Notes in enex files always have a created timestamp but not always an
|
||||
// // updated timestamp (it the note has never been modified). For sync
|
||||
// // we require an updated_time property, so set it to create_time in that case
|
||||
// if (!note.updated_time) note.updated_time = note.created_time;
|
||||
|
||||
// return saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
// }).then((result) => {
|
||||
// if (result.noteUpdated) {
|
||||
// progressState.updated++;
|
||||
// } else if (result.noteCreated) {
|
||||
// progressState.created++;
|
||||
// } else if (result.noteSkipped) {
|
||||
// progressState.skipped++;
|
||||
// }
|
||||
// progressState.resourcesCreated += result.resourcesCreated;
|
||||
// progressState.notesTagged += result.notesTagged;
|
||||
// importOptions.onProgress(progressState);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// return promiseChain(chain).then(() => {
|
||||
// stream.resume();
|
||||
// processingNotes = false;
|
||||
// return true;
|
||||
// });
|
||||
}
|
||||
|
||||
saxStream.on('error', (error) => {
|
||||
@@ -323,7 +364,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
noteResourceRecognition.objID = extractRecognitionObjId(data);
|
||||
} else if (note) {
|
||||
if (n == 'content') {
|
||||
note.bodyXml = data;
|
||||
if ('bodyXml' in note) {
|
||||
note.bodyXml += data;
|
||||
} else {
|
||||
note.bodyXml = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -202,7 +202,7 @@ class JoplinDatabase extends Database {
|
||||
// default value and thus might cause problems. In that case, the default value
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
// currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer
|
||||
@@ -265,6 +265,11 @@ class JoplinDatabase extends Database {
|
||||
queries.push('ALTER TABLE resources ADD COLUMN file_extension TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
if (targetVersion == 8) {
|
||||
queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled INT NOT NULL DEFAULT "0"');
|
||||
queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled_reason TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
|
||||
await this.transactionExecBatch(queries);
|
||||
|
||||
|
@@ -339,6 +339,7 @@ class BaseItem extends BaseModel {
|
||||
JOIN sync_items s ON s.item_id = items.id
|
||||
WHERE sync_target = %d
|
||||
AND s.sync_time < items.updated_time
|
||||
AND s.sync_disabled = 0
|
||||
%s
|
||||
LIMIT %d
|
||||
`,
|
||||
@@ -382,7 +383,21 @@ class BaseItem extends BaseModel {
|
||||
throw new Error('Invalid type: ' + type);
|
||||
}
|
||||
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime) {
|
||||
static async syncDisabledItems(syncTargetId) {
|
||||
const rows = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const item = await this.loadItem(rows[i].item_type, rows[i].item_id);
|
||||
if (!item) continue; // The referenced item no longer exist
|
||||
output.push({
|
||||
syncInfo: rows[i],
|
||||
item: item,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = '') {
|
||||
const itemType = item.type_;
|
||||
const itemId = item.id;
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error('Invalid parameters in updateSyncTimeQueries()');
|
||||
@@ -393,8 +408,8 @@ class BaseItem extends BaseModel {
|
||||
params: [syncTarget, itemType, itemId],
|
||||
},
|
||||
{
|
||||
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time) VALUES (?, ?, ?, ?)',
|
||||
params: [syncTarget, itemType, itemId, syncTime],
|
||||
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time, sync_disabled, sync_disabled_reason) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
params: [syncTarget, itemType, itemId, syncTime, syncDisabled ? 1 : 0, syncDisabledReason + ''],
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -404,6 +419,12 @@ class BaseItem extends BaseModel {
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static async saveSyncDisabled(syncTargetId, item, syncDisabledReason) {
|
||||
const syncTime = 'sync_time' in item ? item.sync_time : 0;
|
||||
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
// When an item is deleted, its associated sync_items data is not immediately deleted for
|
||||
// performance reason. So this function is used to look for these remaining sync_items and
|
||||
// delete them.
|
||||
|
@@ -126,7 +126,11 @@ class Note extends BaseItem {
|
||||
let r = null;
|
||||
r = noteFieldComp(a.user_updated_time, b.user_updated_time); if (r) return r;
|
||||
r = noteFieldComp(a.user_created_time, b.user_created_time); if (r) return r;
|
||||
r = noteFieldComp(a.title.toLowerCase(), b.title.toLowerCase()); if (r) return r;
|
||||
|
||||
const titleA = a.title ? a.title.toLowerCase() : '';
|
||||
const titleB = b.title ? b.title.toLowerCase() : '';
|
||||
r = noteFieldComp(titleA, titleB); if (r) return r;
|
||||
|
||||
return noteFieldComp(a.id, b.id);
|
||||
}
|
||||
|
||||
|
@@ -22,15 +22,6 @@ class Setting extends BaseModel {
|
||||
this.metadata_ = {
|
||||
'activeFolderId': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'firstStart': { value: true, type: Setting.TYPE_BOOL, public: false },
|
||||
'sync.2.path': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['cli'], label: () => _('File system synchronisation target directory'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
|
||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.2.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.3.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.5.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'editor': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['cli'], label: () => _('Text editor'), description: () => _('The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||
'locale': { value: defaultLocale(), type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Language'), options: () => {
|
||||
return supportedLocalesToLanguages();
|
||||
@@ -86,6 +77,15 @@ class Setting extends BaseModel {
|
||||
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.'), options: () => {
|
||||
return SyncTargetRegistry.idAndLabelPlainObject();
|
||||
}},
|
||||
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem') }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
|
||||
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.2.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.3.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.4.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.5.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
};
|
||||
|
||||
return this.metadata_;
|
||||
|
@@ -212,6 +212,18 @@ class OneDriveApi {
|
||||
// type: 'system',
|
||||
// errno: 'EAGAIN',
|
||||
// code: 'EAGAIN' }
|
||||
this.logger().info('Got error below - retrying (' + i + ')...');
|
||||
this.logger().info(error);
|
||||
await time.sleep((i + 1) * 3);
|
||||
continue;
|
||||
} else if (error && error.error && error.error.code === 'resourceModified') {
|
||||
// NOTE: not tested, very hard to reproduce and non-informative error message, but can be repeated
|
||||
|
||||
// Error: ETag does not match current item's value
|
||||
// Code: resourceModified
|
||||
// Header: {"_headers":{"cache-control":["private"],"transfer-encoding":["chunked"],"content-type":["application/json"],"request-id":["d...ea47"],"client-request-id":["d99...ea47"],"x-ms-ags-diagnostic":["{\"ServerInfo\":{\"DataCenter\":\"North Europe\",\"Slice\":\"SliceA\",\"Ring\":\"2\",\"ScaleUnit\":\"000\",\"Host\":\"AGSFE_IN_13\",\"ADSiteName\":\"DUB\"}}"],"duration":["96.9464"],"date":[],"connection":["close"]}}
|
||||
// Request: PATCH https://graph.microsoft.com/v1.0/drive/root:/Apps/JoplinDev/f56c5601fee94b8085524513bf3e352f.md null "{\"fileSystemInfo\":{\"lastModifiedDateTime\":\"....\"}}" {"headers":{"Content-Type":"application/json","Authorization":"bearer ...
|
||||
|
||||
this.logger().info('Got error below - retrying (' + i + ')...');
|
||||
this.logger().info(error);
|
||||
await time.sleep((i + 1) * 3);
|
||||
|
@@ -40,4 +40,9 @@ function safeFileExtension(e) {
|
||||
return e.replace(/[^a-zA-Z0-9]/g, '')
|
||||
}
|
||||
|
||||
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension };
|
||||
function toSystemSlashes(path, os) {
|
||||
if (os === 'win32') return path.replace(/\//g, "\\");
|
||||
return path.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes };
|
@@ -25,7 +25,8 @@ const defaultState = {
|
||||
searchQuery: '',
|
||||
settings: {},
|
||||
appState: 'starting',
|
||||
windowContentSize: { width: 0, height: 0 },
|
||||
//windowContentSize: { width: 0, height: 0 },
|
||||
hasDisabledSyncItems: false,
|
||||
};
|
||||
|
||||
// When deleting a note, tag or folder
|
||||
@@ -395,6 +396,12 @@ const reducer = (state = defaultState, action) => {
|
||||
newState.appState = action.state;
|
||||
break;
|
||||
|
||||
case 'SYNC_HAS_DISABLED_SYNC_ITEMS':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.hasDisabledSyncItems = true;
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
|
||||
|
@@ -109,8 +109,21 @@ class ReportService {
|
||||
async status(syncTarget) {
|
||||
let r = await this.syncStatus(syncTarget);
|
||||
let sections = [];
|
||||
let section = null;
|
||||
|
||||
let section = { title: _('Sync status (synced items / total items)'), body: [] };
|
||||
const disabledItems = await BaseItem.syncDisabledItems(syncTarget);
|
||||
|
||||
if (disabledItems.length) {
|
||||
section = { title: _('Items that cannot be synchronised'), body: [] };
|
||||
|
||||
for (let i = 0; i < disabledItems.length; i++) {
|
||||
const row = disabledItems[i];
|
||||
section.body.push(_('"%s": "%s"', row.item.title, row.syncInfo.sync_disabled_reason));
|
||||
}
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
section = { title: _('Sync status (synced items / total items)'), body: [] };
|
||||
|
||||
for (let n in r.items) {
|
||||
if (!r.items.hasOwnProperty(n)) continue;
|
||||
@@ -138,16 +151,19 @@ class ReportService {
|
||||
|
||||
sections.push(section);
|
||||
|
||||
section = { title: _('Coming alarms'), body: [] };
|
||||
|
||||
const alarms = await Alarm.allDue();
|
||||
for (let i = 0; i < alarms.length; i++) {
|
||||
const alarm = alarms[i];
|
||||
const note = await Note.load(alarm.note_id);
|
||||
section.body.push(_('On %s: %s', time.formatMsToLocal(alarm.trigger_time), note.title));
|
||||
}
|
||||
|
||||
sections.push(section);
|
||||
if (alarms.length) {
|
||||
section = { title: _('Coming alarms'), body: [] };
|
||||
|
||||
for (let i = 0; i < alarms.length; i++) {
|
||||
const alarm = alarms[i];
|
||||
const note = await Note.load(alarm.note_id);
|
||||
section.body.push(_('On %s: %s', time.formatMsToLocal(alarm.trigger_time), note.title));
|
||||
}
|
||||
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
@@ -208,6 +208,7 @@ class Synchronizer {
|
||||
let action = null;
|
||||
let updateSyncTimeOnly = true;
|
||||
let reason = '';
|
||||
let remoteContent = null;
|
||||
|
||||
if (!remote) {
|
||||
if (!local.sync_time) {
|
||||
@@ -220,7 +221,25 @@ class Synchronizer {
|
||||
reason = 'remote has been deleted, but local has changes';
|
||||
}
|
||||
} else {
|
||||
if (remote.updated_time > local.sync_time) {
|
||||
// Note: in order to know the real updated_time value, we need to load the content. In theory we could
|
||||
// rely on the file timestamp (in remote.updated_time) but in practice it's not accurate enough and
|
||||
// can lead to conflicts (for example when the file timestamp is slightly ahead of it's real
|
||||
// updated_time). updated_time is set and managed by clients so it's always accurate.
|
||||
// Same situation below for updateLocal.
|
||||
//
|
||||
// This is a bit inefficient because if the resulting action is "updateRemote" we don't need the whole
|
||||
// content, but for now that will do since being reliable is the priority.
|
||||
//
|
||||
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
|
||||
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
|
||||
// could be done using the file timestamp and the potentially unecessary content loading could be skipped.
|
||||
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
|
||||
// a few seconds ahead of what it was set with setTimestamp()
|
||||
remoteContent = await this.api().get(path);
|
||||
if (!remoteContent) throw new Error('Got metadata for path but could not fetch content: ' + path);
|
||||
remoteContent = await BaseItem.unserialize(remoteContent);
|
||||
|
||||
if (remoteContent.updated_time > local.sync_time) {
|
||||
// Since, in this loop, we are only dealing with items that require sync, if the
|
||||
// remote has been modified after the sync time, it means both items have been
|
||||
// modified and so there's a conflict.
|
||||
@@ -234,22 +253,36 @@ class Synchronizer {
|
||||
|
||||
this.logSyncOperation(action, local, remote, reason);
|
||||
|
||||
const handleCannotSyncItem = async (syncTargetId, item, cannotSyncReason) => {
|
||||
await ItemClass.saveSyncDisabled(syncTargetId, item, cannotSyncReason);
|
||||
this.dispatch({ type: 'SYNC_HAS_DISABLED_SYNC_ITEMS' });
|
||||
}
|
||||
|
||||
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) {
|
||||
let remoteContentPath = this.resourceDirName_ + '/' + local.id;
|
||||
// TODO: handle node and mobile in the same way
|
||||
if (shim.isNode()) {
|
||||
let resourceContent = '';
|
||||
try {
|
||||
resourceContent = await Resource.content(local);
|
||||
} catch (error) {
|
||||
error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message;
|
||||
this.logger().error(error);
|
||||
this.progressReport_.errors.push(error);
|
||||
try {
|
||||
// TODO: handle node and mobile in the same way
|
||||
if (shim.isNode()) {
|
||||
let resourceContent = '';
|
||||
try {
|
||||
resourceContent = await Resource.content(local);
|
||||
} catch (error) {
|
||||
error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message;
|
||||
this.logger().error(error);
|
||||
this.progressReport_.errors.push(error);
|
||||
}
|
||||
await this.api().put(remoteContentPath, resourceContent);
|
||||
} else {
|
||||
const localResourceContentPath = Resource.fullPath(local);
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
action = null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
await this.api().put(remoteContentPath, resourceContent);
|
||||
} else {
|
||||
const localResourceContentPath = Resource.fullPath(local);
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,15 +299,32 @@ class Synchronizer {
|
||||
// await this.api().setTimestamp(tempPath, local.updated_time);
|
||||
// await this.api().move(tempPath, path);
|
||||
|
||||
await this.api().put(path, content);
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||
let canSync = true;
|
||||
try {
|
||||
if (this.debugFlags_.indexOf('cannotSync') >= 0) {
|
||||
const error = new Error('Testing cannotSync');
|
||||
error.code = 'cannotSync';
|
||||
throw error;
|
||||
}
|
||||
await this.api().put(path, content);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
canSync = false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (canSync) {
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||
}
|
||||
|
||||
} else if (action == 'itemConflict') {
|
||||
|
||||
if (remote) {
|
||||
let remoteContent = await this.api().get(path);
|
||||
local = await BaseItem.unserialize(remoteContent);
|
||||
local = remoteContent;
|
||||
|
||||
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||
@@ -290,12 +340,9 @@ class Synchronizer {
|
||||
// so in this case we just take the remote content.
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
let loadedRemote = null;
|
||||
let mustHandleConflict = true;
|
||||
if (remote) {
|
||||
const remoteContent = await this.api().get(path);
|
||||
loadedRemote = await BaseItem.unserialize(remoteContent);
|
||||
mustHandleConflict = Note.mustHandleConflict(local, loadedRemote);
|
||||
if (remoteContent) {
|
||||
mustHandleConflict = Note.mustHandleConflict(local, remoteContent);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
@@ -316,7 +363,7 @@ class Synchronizer {
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
if (remote) {
|
||||
local = loadedRemote;
|
||||
local = remoteContent;
|
||||
const syncTimeQueries = BaseItem.updateSyncTimeQueries(syncTargetId, local, time.unixMs());
|
||||
await ItemClass.save(local, { autoTimestamp: false, nextQueries: syncTimeQueries });
|
||||
} else {
|
||||
@@ -385,21 +432,34 @@ class Synchronizer {
|
||||
let remote = remotes[i];
|
||||
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
|
||||
|
||||
const loadContent = async () => {
|
||||
content = await this.api().get(path);
|
||||
if (!content) return null;
|
||||
return await BaseItem.unserialize(content);
|
||||
}
|
||||
|
||||
let path = remote.path;
|
||||
let action = null;
|
||||
let reason = '';
|
||||
let local = await BaseItem.loadItemByPath(path);
|
||||
let ItemClass = null;
|
||||
let content = null;
|
||||
if (!local) {
|
||||
if (!remote.isDeleted) {
|
||||
if (remote.isDeleted !== true) {
|
||||
action = 'createLocal';
|
||||
reason = 'remote exists but local does not';
|
||||
content = await loadContent();
|
||||
ItemClass = content ? BaseItem.itemClass(content) : null;
|
||||
}
|
||||
} else {
|
||||
ItemClass = BaseItem.itemClass(local);
|
||||
local = ItemClass.filter(local);
|
||||
if (remote.isDeleted) {
|
||||
action = 'deleteLocal';
|
||||
reason = 'remote has been deleted';
|
||||
} else {
|
||||
if (remote.updated_time > local.updated_time) {
|
||||
content = await loadContent();
|
||||
if (content && content.updated_time > local.updated_time) {
|
||||
action = 'updateLocal';
|
||||
reason = 'remote is more recent than local';
|
||||
}
|
||||
@@ -412,43 +472,34 @@ class Synchronizer {
|
||||
|
||||
if (action == 'createLocal' || action == 'updateLocal') {
|
||||
|
||||
let content = await this.api().get(path);
|
||||
if (content === null) {
|
||||
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
|
||||
continue;
|
||||
}
|
||||
content = await BaseItem.unserialize(content);
|
||||
let ItemClass = BaseItem.itemClass(content);
|
||||
content = ItemClass.filter(content);
|
||||
|
||||
// 2017-12-03: This was added because the new user_updated_time and user_created_time properties were added
|
||||
// to the items. However changing the database is not enough since remote items that haven't been synced yet
|
||||
// will not have these properties and, since they are required, it would cause a problem. So this check
|
||||
// if they are present and, if not, set them to a reasonable default.
|
||||
// Let's leave these two lines for 6 months, by which time all the clients should have been synced.
|
||||
if (!content.user_updated_time) content.user_updated_time = content.updated_time;
|
||||
if (!content.user_created_time) content.user_created_time = content.created_time;
|
||||
|
||||
let newContent = null;
|
||||
|
||||
if (action === 'createLocal') {
|
||||
newContent = Object.assign({}, content);
|
||||
} else if (action === 'updateLocal') {
|
||||
newContent = BaseModel.diffObjects(local, content);
|
||||
newContent.type_ = content.type_;
|
||||
newContent.id = content.id;
|
||||
} else {
|
||||
throw new Error('Unknown action: ' + action);
|
||||
}
|
||||
|
||||
let options = {
|
||||
autoTimestamp: false,
|
||||
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, newContent, time.unixMs()),
|
||||
nextQueries: BaseItem.updateSyncTimeQueries(syncTargetId, content, time.unixMs()),
|
||||
};
|
||||
if (action == 'createLocal') options.isNew = true;
|
||||
if (action == 'updateLocal') options.oldItem = local;
|
||||
|
||||
if (newContent.type_ == BaseModel.TYPE_RESOURCE && action == 'createLocal') {
|
||||
let localResourceContentPath = Resource.fullPath(newContent);
|
||||
let remoteResourceContentPath = this.resourceDirName_ + '/' + newContent.id;
|
||||
if (content.type_ == BaseModel.TYPE_RESOURCE && action == 'createLocal') {
|
||||
let localResourceContentPath = Resource.fullPath(content);
|
||||
let remoteResourceContentPath = this.resourceDirName_ + '/' + content.id;
|
||||
await this.api().get(remoteResourceContentPath, { path: localResourceContentPath, target: 'file' });
|
||||
}
|
||||
|
||||
await ItemClass.save(newContent, options);
|
||||
await ItemClass.save(content, options);
|
||||
|
||||
} else if (action == 'deleteLocal') {
|
||||
|
||||
|
1
ReactNativeClient/locales/de_DE.json
Normal file
@@ -1,5 +1,6 @@
|
||||
var locales = {};
|
||||
locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
module.exports = { locales: locales };
|
90
ReactNativeClient/package-lock.json
generated
@@ -549,6 +549,15 @@
|
||||
"babel-template": "6.26.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-transform-define": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-define/-/babel-plugin-transform-define-1.3.0.tgz",
|
||||
"integrity": "sha1-lMX5RZyBDHOMx8UMvUSjGCnW8xk=",
|
||||
"requires": {
|
||||
"lodash": "4.17.4",
|
||||
"traverse": "0.6.6"
|
||||
}
|
||||
},
|
||||
"babel-plugin-transform-es2015-arrow-functions": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz",
|
||||
@@ -1192,6 +1201,11 @@
|
||||
"integrity": "sha512-vHDDF/bP9RYpTWtUhpJRhCFdvvp3iDWvEbuDbWgvjUrNGV1MXJrE0MPcwGtEled04m61iwdBLUIHZtDgzWS4ZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"clamp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
|
||||
"integrity": "sha1-ZqDmQBGBbjcZaCj9yMjBRzEshjQ="
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
|
||||
@@ -5045,6 +5059,21 @@
|
||||
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=",
|
||||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
|
||||
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
|
||||
"requires": {
|
||||
"isarray": "0.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
|
||||
}
|
||||
}
|
||||
},
|
||||
"path-type": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
|
||||
@@ -5402,11 +5431,32 @@
|
||||
"prop-types": "15.6.0"
|
||||
}
|
||||
},
|
||||
"react-native-dismiss-keyboard": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz",
|
||||
"integrity": "sha1-MohiQrPyMX4SHzrrmwpYXiuHm0k="
|
||||
},
|
||||
"react-native-document-picker": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-2.1.0.tgz",
|
||||
"integrity": "sha512-BFCBXwz8xuLvHLVFVeQM+RhaY8yZ38PEWt9WSbq5VIoZ/VssP6uu51XxOfdwaMALOrAHIojK0SiYnd155upZAg=="
|
||||
},
|
||||
"react-native-drawer-layout": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-1.3.2.tgz",
|
||||
"integrity": "sha512-fjO0scqbJUfNu2wuEpvywL7DYLXuCXJ2W/zYhWz986rdLytidbys1QGVvkaszHrb4Y7OqO96mTkgpOcP8KWevw==",
|
||||
"requires": {
|
||||
"react-native-dismiss-keyboard": "1.0.0"
|
||||
}
|
||||
},
|
||||
"react-native-drawer-layout-polyfill": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout-polyfill/-/react-native-drawer-layout-polyfill-1.3.2.tgz",
|
||||
"integrity": "sha512-XzPhfLDJrYHru+e8+dFwhf0FtTeAp7JXPpFYezYV6P1nTeA1Tia/kDpFT+O2DWTrBKBEI8FGhZnThrroZmHIxg==",
|
||||
"requires": {
|
||||
"react-native-drawer-layout": "1.3.2"
|
||||
}
|
||||
},
|
||||
"react-native-dropdownalert": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-dropdownalert/-/react-native-dropdownalert-3.1.2.tgz",
|
||||
@@ -5515,9 +5565,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-popup-menu": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-popup-menu/-/react-native-popup-menu-0.8.3.tgz",
|
||||
"integrity": "sha1-HbsLT4iclBC2myKKidV7Vq6lzC4="
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-popup-menu/-/react-native-popup-menu-0.10.0.tgz",
|
||||
"integrity": "sha1-zhU2eo1WKIfVypB+IyMB1BLd5+c="
|
||||
},
|
||||
"react-native-push-notification": {
|
||||
"version": "3.0.1",
|
||||
@@ -5590,6 +5640,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-tab-view": {
|
||||
"version": "0.0.70",
|
||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-0.0.70.tgz",
|
||||
"integrity": "sha512-VOTZs2UCamYrTyBtcxpn4Ko9RkWrq6rZGOY7AagHF/gcCUQBMSWADoplsBopSc5vXnHHeCSR458BkceEuSEaQA==",
|
||||
"requires": {
|
||||
"prop-types": "15.6.0"
|
||||
}
|
||||
},
|
||||
"react-native-vector-icons": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-4.4.2.tgz",
|
||||
@@ -5731,6 +5789,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-navigation": {
|
||||
"version": "1.0.0-beta.21",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-1.0.0-beta.21.tgz",
|
||||
"integrity": "sha512-bNAiNBtxrumZQmIj6uH2l1jHIkH8mILoGse5BMHOeFWACADl8LnbE91mzAkJ2EbAzAQ5LNpkSjGSIGxbvfsRCw==",
|
||||
"requires": {
|
||||
"babel-plugin-transform-define": "1.3.0",
|
||||
"clamp": "1.0.1",
|
||||
"hoist-non-react-statics": "2.3.1",
|
||||
"path-to-regexp": "1.7.0",
|
||||
"prop-types": "15.6.0",
|
||||
"react-native-drawer-layout-polyfill": "1.3.2",
|
||||
"react-native-tab-view": "0.0.70"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz",
|
||||
"integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-proxy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz",
|
||||
@@ -6549,6 +6628,11 @@
|
||||
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
|
||||
"dev": true
|
||||
},
|
||||
"traverse": {
|
||||
"version": "0.6.6",
|
||||
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
|
||||
"integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc="
|
||||
},
|
||||
"trim-right": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
||||
|
@@ -29,11 +29,12 @@
|
||||
"react-native-image-resizer": "^1.0.0",
|
||||
"react-native-material-dropdown": "^0.5.2",
|
||||
"react-native-popup-dialog": "^0.9.35",
|
||||
"react-native-popup-menu": "^0.8.3",
|
||||
"react-native-popup-menu": "^0.10.0",
|
||||
"react-native-push-notification": "^3.0.1",
|
||||
"react-native-side-menu": "^1.1.3",
|
||||
"react-native-sqlite-storage": "3.3.*",
|
||||
"react-native-vector-icons": "^4.4.2",
|
||||
"react-navigation": "^1.0.0-beta.21",
|
||||
"react-redux": "4.4.8",
|
||||
"redux": "3.6.0",
|
||||
"uuid": "^3.0.1"
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Keyboard, NativeModules, BackHandler } = require('react-native');
|
||||
const { SafeAreaView } = require('react-navigation');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
const AlarmService = require('lib/services/AlarmService.js');
|
||||
@@ -461,7 +462,7 @@ class AppComponent extends React.Component {
|
||||
render() {
|
||||
if (this.props.appState != 'ready') return null;
|
||||
|
||||
const sideMenuContent = <SideMenuContent/>;
|
||||
const sideMenuContent = <SafeAreaView style={{flex:1}}><SideMenuContent/></SafeAreaView>;
|
||||
|
||||
const appNavInit = {
|
||||
Welcome: { screen: WelcomeScreen },
|
||||
@@ -487,7 +488,9 @@ class AppComponent extends React.Component {
|
||||
}}
|
||||
>
|
||||
<MenuContext style={{ flex: 1 }}>
|
||||
<AppNav screens={appNavInit} />
|
||||
<SafeAreaView style={{flex:1}}>
|
||||
<AppNav screens={appNavInit} />
|
||||
</SafeAreaView>
|
||||
<DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
</MenuContext>
|
||||
</SideMenu>
|
||||
|