mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-15 09:04:04 +02:00
commit
a6cecc103c
@ -44,6 +44,6 @@ before_install:
|
|||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
cd ElectronClient/app
|
cd ElectronClient/app
|
||||||
rsync -aP ../../ReactNativeClient/lib/ lib/
|
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||||
npm install
|
npm install
|
||||||
yarn dist
|
yarn dist
|
||||||
|
4
BUILD.md
4
BUILD.md
@ -21,7 +21,7 @@ If you get a node-gyp related error you might need to manually install it: `npm
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd ElectronClient/app
|
cd ElectronClient/app
|
||||||
rsync -a ../../ReactNativeClient/lib/ lib/
|
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||||
npm install
|
npm install
|
||||||
yarn dist
|
yarn dist
|
||||||
```
|
```
|
||||||
@ -44,7 +44,7 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
|
|||||||
cd CliClient
|
cd CliClient
|
||||||
npm install
|
npm install
|
||||||
./build.sh
|
./build.sh
|
||||||
rsync -aP ../ReactNativeClient/locales/ build/locales/
|
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||||
```
|
```
|
||||||
|
|
||||||
Run `run.sh` to start the application for testing.
|
Run `run.sh` to start the application for testing.
|
6
CONTRIBUTING.md
Normal file
6
CONTRIBUTING.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Adding new features
|
||||||
|
If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
|
||||||
|
|
||||||
|
# Style
|
||||||
|
- Only use tabs for indentation, not spaces.
|
||||||
|
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.
|
@ -1,6 +1,6 @@
|
|||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { netUtils } = require('lib/net-utils.js');
|
const { netUtils } = require('lib/net-utils.js');
|
||||||
|
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
const { reducer, defaultState } = require('lib/reducer.js');
|
const { reducer, defaultState } = require('lib/reducer.js');
|
||||||
const { splitCommandString } = require('lib/string-utils.js');
|
const { splitCommandString } = require('lib/string-utils.js');
|
||||||
@ -14,6 +14,7 @@ const chalk = require('chalk');
|
|||||||
const tk = require('terminal-kit');
|
const tk = require('terminal-kit');
|
||||||
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
|
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
|
||||||
const Renderer = require('tkwidgets/framework/Renderer.js');
|
const Renderer = require('tkwidgets/framework/Renderer.js');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
|
|
||||||
const BaseWidget = require('tkwidgets/BaseWidget.js');
|
const BaseWidget = require('tkwidgets/BaseWidget.js');
|
||||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||||
@ -65,6 +66,7 @@ class AppGui {
|
|||||||
// a regular command it's not necessary since the process
|
// a regular command it's not necessary since the process
|
||||||
// exits right away.
|
// exits right away.
|
||||||
reg.setupRecurrentSync();
|
reg.setupRecurrentSync();
|
||||||
|
DecryptionWorker.instance().scheduleStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
store() {
|
store() {
|
||||||
@ -80,8 +82,16 @@ class AppGui {
|
|||||||
await this.renderer_.renderRoot();
|
await this.renderer_.renderRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt(initialText = '', promptString = ':') {
|
termSaveState() {
|
||||||
return this.widget('statusBar').prompt(initialText, promptString);
|
return this.term().saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
termRestoreState(state) {
|
||||||
|
return this.term().restoreState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt(initialText = '', promptString = ':', options = null) {
|
||||||
|
return this.widget('statusBar').prompt(initialText, promptString, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
stdoutMaxWidth() {
|
stdoutMaxWidth() {
|
||||||
@ -548,6 +558,10 @@ class AppGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.widget('console').scrollBottom();
|
this.widget('console').scrollBottom();
|
||||||
|
|
||||||
|
// Invalidate so that the screen is redrawn in case inputting a command has moved
|
||||||
|
// the GUI up (in particular due to autocompletion), it's moved back to the right position.
|
||||||
|
this.widget('root').invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFolderList() {
|
async updateFolderList() {
|
||||||
@ -826,4 +840,4 @@ class AppGui {
|
|||||||
AppGui.INPUT_MODE_NORMAL = 1;
|
AppGui.INPUT_MODE_NORMAL = 1;
|
||||||
AppGui.INPUT_MODE_META = 2;
|
AppGui.INPUT_MODE_META = 2;
|
||||||
|
|
||||||
module.exports = AppGui;
|
module.exports = AppGui;
|
||||||
|
@ -5,12 +5,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
|
|||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
@ -144,13 +144,15 @@ class Application extends BaseApplication {
|
|||||||
message += ' (' + options.answers.join('/') + ')';
|
message += ' (' + options.answers.join('/') + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
let answer = await this.gui().prompt('', message + ' ');
|
let answer = await this.gui().prompt('', message + ' ', options);
|
||||||
|
|
||||||
if (options.type === 'boolean') {
|
if (options.type === 'boolean') {
|
||||||
if (answer === null) return false; // Pressed ESCAPE
|
if (answer === null) return false; // Pressed ESCAPE
|
||||||
if (!answer) answer = options.answers[0];
|
if (!answer) answer = options.answers[0];
|
||||||
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
|
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
|
||||||
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
|
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
|
||||||
|
} else {
|
||||||
|
return answer;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -275,7 +277,7 @@ class Application extends BaseApplication {
|
|||||||
dummyGui() {
|
dummyGui() {
|
||||||
return {
|
return {
|
||||||
isDummy: () => { return true; },
|
isDummy: () => { return true; },
|
||||||
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
|
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
|
||||||
showConsole: () => {},
|
showConsole: () => {},
|
||||||
maximizeConsole: () => {},
|
maximizeConsole: () => {},
|
||||||
stdout: (text) => { console.info(text); },
|
stdout: (text) => { console.info(text); },
|
||||||
@ -283,7 +285,10 @@ class Application extends BaseApplication {
|
|||||||
exit: () => {},
|
exit: () => {},
|
||||||
showModalOverlay: (text) => {},
|
showModalOverlay: (text) => {},
|
||||||
hideModalOverlay: () => {},
|
hideModalOverlay: () => {},
|
||||||
stdoutMaxWidth: () => { return 78; }
|
stdoutMaxWidth: () => { return 78; },
|
||||||
|
forceRender: () => {},
|
||||||
|
termSaveState: () => {},
|
||||||
|
termRestoreState: (state) => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +356,7 @@ class Application extends BaseApplication {
|
|||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'TAG_UPDATE_ALL',
|
type: 'TAG_UPDATE_ALL',
|
||||||
tags: tags,
|
items: tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.store().dispatch({
|
this.store().dispatch({
|
||||||
|
185
CliClient/app/autocompletion.js
Normal file
185
CliClient/app/autocompletion.js
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
var { app } = require('./app.js');
|
||||||
|
var Note = require('lib/models/Note.js');
|
||||||
|
var Folder = require('lib/models/Folder.js');
|
||||||
|
var Tag = require('lib/models/Tag.js');
|
||||||
|
var { cliUtils } = require('./cli-utils.js');
|
||||||
|
var yargParser = require('yargs-parser');
|
||||||
|
|
||||||
|
async function handleAutocompletionPromise(line) {
|
||||||
|
// Auto-complete the command name
|
||||||
|
const names = await app().commandNames();
|
||||||
|
let words = getArguments(line);
|
||||||
|
//If there is only one word and it is not already a command name then you
|
||||||
|
//should look for commmands it could be
|
||||||
|
if (words.length == 1) {
|
||||||
|
if (names.indexOf(words[0]) === -1) {
|
||||||
|
let x = names.filter((n) => n.indexOf(words[0]) === 0);
|
||||||
|
if (x.length === 1) {
|
||||||
|
return x[0] + ' ';
|
||||||
|
}
|
||||||
|
return x.length > 0 ? x.map((a) => a + ' ') : line;
|
||||||
|
} else {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//There is more than one word and it is a command
|
||||||
|
const metadata = (await app().commandMetadata())[words[0]];
|
||||||
|
//If for some reason this command does not have any associated metadata
|
||||||
|
//just don't autocomplete. However, this should not happen.
|
||||||
|
if (metadata === undefined) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
//complete an option
|
||||||
|
let next = words.length > 1 ? words[words.length - 1] : '';
|
||||||
|
let l = [];
|
||||||
|
if (next[0] === '-') {
|
||||||
|
for (let i = 0; i<metadata.options.length; i++) {
|
||||||
|
const options = metadata.options[i][0].split(' ');
|
||||||
|
//if there are multiple options then they will be seperated by comma and
|
||||||
|
//space. The comma should be removed
|
||||||
|
if (options[0][options[0].length - 1] === ',') {
|
||||||
|
options[0] = options[0].slice(0, -1);
|
||||||
|
}
|
||||||
|
if (words.includes(options[0]) || words.includes(options[1])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
//First two elements are the flag and the third is the description
|
||||||
|
//Only autocomplete long
|
||||||
|
if (options.length > 1 && options[1].indexOf(next) === 0) {
|
||||||
|
l.push(options[1]);
|
||||||
|
} else if (options[0].indexOf(next) === 0) {
|
||||||
|
l.push(options[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l.length === 0) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
let ret = l.map(a=>toCommandLine(a));
|
||||||
|
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
//Complete an argument
|
||||||
|
//Determine the number of positional arguments by counting the number of
|
||||||
|
//words that don't start with a - less one for the command name
|
||||||
|
const positionalArgs = words.filter((a)=>a.indexOf('-') !== 0).length - 1;
|
||||||
|
|
||||||
|
let cmdUsage = yargParser(metadata.usage)['_'];
|
||||||
|
cmdUsage.splice(0, 1);
|
||||||
|
|
||||||
|
if (cmdUsage.length >= positionalArgs) {
|
||||||
|
|
||||||
|
let argName = cmdUsage[positionalArgs - 1];
|
||||||
|
argName = cliUtils.parseCommandArg(argName).name;
|
||||||
|
|
||||||
|
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
|
||||||
|
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
|
||||||
|
l.push(...notes.map((n) => n.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argName == 'notebook') {
|
||||||
|
const folders = await Folder.search({ titlePattern: next + '*' });
|
||||||
|
l.push(...folders.map((n) => n.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argName == 'tag') {
|
||||||
|
let tags = await Tag.search({ titlePattern: next + '*' });
|
||||||
|
l.push(...tags.map((n) => n.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argName == 'tag-command') {
|
||||||
|
let c = filterList(['add', 'remove', 'list'], next);
|
||||||
|
l.push(...c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argName == 'todo-command') {
|
||||||
|
let c = filterList(['toggle', 'clear'], next);
|
||||||
|
l.push(...c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (l.length === 1) {
|
||||||
|
return toCommandLine([...words.slice(0, -1), l[0]]);
|
||||||
|
} else if (l.length > 1) {
|
||||||
|
let ret = l.map(a=>toCommandLine(a));
|
||||||
|
ret.prefix = toCommandLine(words.slice(0, -1)) + ' ';
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
|
||||||
|
}
|
||||||
|
function handleAutocompletion(str, callback) {
|
||||||
|
handleAutocompletionPromise(str).then(function(res) {
|
||||||
|
callback(undefined, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function toCommandLine(args) {
|
||||||
|
if (Array.isArray(args)) {
|
||||||
|
return args.map(function(a) {
|
||||||
|
if(a.indexOf('"') !== -1 || a.indexOf(' ') !== -1) {
|
||||||
|
return "'" + a + "'";
|
||||||
|
} else if (a.indexOf("'") !== -1) {
|
||||||
|
return '"' + a + '"';
|
||||||
|
} else {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}).join(' ');
|
||||||
|
} else {
|
||||||
|
if(args.indexOf('"') !== -1 || args.indexOf(' ') !== -1) {
|
||||||
|
return "'" + args + "' ";
|
||||||
|
} else if (args.indexOf("'") !== -1) {
|
||||||
|
return '"' + args + '" ';
|
||||||
|
} else {
|
||||||
|
return args + ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getArguments(line) {
|
||||||
|
let inSingleQuotes = false;
|
||||||
|
let inDoubleQuotes = false;
|
||||||
|
let currentWord = '';
|
||||||
|
let parsed = [];
|
||||||
|
for(let i = 0; i<line.length; i++) {
|
||||||
|
if(line[i] === '"') {
|
||||||
|
if(inDoubleQuotes) {
|
||||||
|
inDoubleQuotes = false;
|
||||||
|
//maybe push word to parsed?
|
||||||
|
//currentWord += '"';
|
||||||
|
} else {
|
||||||
|
inDoubleQuotes = true;
|
||||||
|
//currentWord += '"';
|
||||||
|
}
|
||||||
|
} else if(line[i] === "'") {
|
||||||
|
if(inSingleQuotes) {
|
||||||
|
inSingleQuotes = false;
|
||||||
|
//maybe push word to parsed?
|
||||||
|
//currentWord += "'";
|
||||||
|
} else {
|
||||||
|
inSingleQuotes = true;
|
||||||
|
//currentWord += "'";
|
||||||
|
}
|
||||||
|
} else if (/\s/.test(line[i]) &&
|
||||||
|
!(inDoubleQuotes || inSingleQuotes)) {
|
||||||
|
if (currentWord !== '') {
|
||||||
|
parsed.push(currentWord);
|
||||||
|
currentWord = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentWord += line[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(inSingleQuotes || inDoubleQuotes) && /\s/.test(line[line.length - 1])) {
|
||||||
|
parsed.push('');
|
||||||
|
} else {
|
||||||
|
parsed.push(currentWord);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
function filterList(list, next) {
|
||||||
|
let output = [];
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
if (list[i].indexOf(next) !== 0) continue;
|
||||||
|
output.push(list[i]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { handleAutocompletion };
|
@ -12,6 +12,10 @@ class BaseCommand {
|
|||||||
throw new Error('Usage not defined');
|
throw new Error('Usage not defined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encryptionCheck(item) {
|
||||||
|
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
|
||||||
|
}
|
||||||
|
|
||||||
description() {
|
description() {
|
||||||
throw new Error('Description not defined');
|
throw new Error('Description not defined');
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@ const { Logger } = require('lib/logger.js');
|
|||||||
const { dirname } = require('lib/path-utils.js');
|
const { dirname } = require('lib/path-utils.js');
|
||||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const exec = require('child_process').exec
|
const exec = require('child_process').exec
|
||||||
|
|
||||||
|
@ -178,38 +178,39 @@ cliUtils.promptConfirm = function(message, answers = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
cliUtils.promptInput = function(message) {
|
|
||||||
const readline = require('readline');
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
rl.question(message + ' ', (answer) => {
|
|
||||||
rl.close();
|
|
||||||
resolve(answer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: initialText is there to have the same signature as statusBar.prompt() so that
|
// Note: initialText is there to have the same signature as statusBar.prompt() so that
|
||||||
// it can be a drop-in replacement, however initialText is not used (and cannot be
|
// it can be a drop-in replacement, however initialText is not used (and cannot be
|
||||||
// with readline.question?).
|
// with readline.question?).
|
||||||
cliUtils.prompt = function(initialText = '', promptString = ':') {
|
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
|
||||||
|
if (!options) options = {};
|
||||||
|
|
||||||
const readline = require('readline');
|
const readline = require('readline');
|
||||||
|
const Writable = require('stream').Writable;
|
||||||
|
|
||||||
|
const mutableStdout = new Writable({
|
||||||
|
write: function(chunk, encoding, callback) {
|
||||||
|
if (!this.muted)
|
||||||
|
process.stdout.write(chunk, encoding);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
const rl = readline.createInterface({
|
||||||
input: process.stdin,
|
input: process.stdin,
|
||||||
output: process.stdout
|
output: mutableStdout,
|
||||||
|
terminal: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
mutableStdout.muted = false;
|
||||||
|
|
||||||
rl.question(promptString, (answer) => {
|
rl.question(promptString, (answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
if (!!options.secure) this.stdout_('');
|
||||||
resolve(answer);
|
resolve(answer);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mutableStdout.muted = !!options.secure;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ class Command extends BaseCommand {
|
|||||||
let title = args['note'];
|
let title = args['note'];
|
||||||
|
|
||||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||||
|
this.encryptionCheck(note);
|
||||||
if (!note) throw new Error(_('Cannot find "%s".', title));
|
if (!note) throw new Error(_('Cannot find "%s".', title));
|
||||||
|
|
||||||
const localFilePath = args['file'];
|
const localFilePath = args['file'];
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
@ -21,10 +21,6 @@ class Command extends BaseCommand {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async action(args) {
|
async action(args) {
|
||||||
let title = args['note'];
|
let title = args['note'];
|
||||||
|
|
||||||
@ -33,6 +29,9 @@ class Command extends BaseCommand {
|
|||||||
|
|
||||||
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
|
const content = args.options.verbose ? await Note.serialize(item) : await Note.serializeForEdit(item);
|
||||||
this.stdout(content);
|
this.stdout(content);
|
||||||
|
|
||||||
|
app().gui().showConsole();
|
||||||
|
app().gui().maximizeConsole();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { _, setLocale } = require('lib/locale.js');
|
const { _, setLocale } = require('lib/locale.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
@ -16,8 +16,9 @@ class Command extends BaseCommand {
|
|||||||
return _('Marks a to-do as done.');
|
return _('Marks a to-do as done.');
|
||||||
}
|
}
|
||||||
|
|
||||||
static async handleAction(args, isCompleted) {
|
static async handleAction(commandInstance, args, isCompleted) {
|
||||||
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
|
const note = await app().loadItem(BaseModel.TYPE_NOTE, args.note);
|
||||||
|
commandInstance.encryptionCheck(note);
|
||||||
if (!note) throw new Error(_('Cannot find "%s".', args.note));
|
if (!note) throw new Error(_('Cannot find "%s".', args.note));
|
||||||
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
|
if (!note.is_todo) throw new Error(_('Note is not a to-do: "%s"', args.note));
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async action(args) {
|
async action(args) {
|
||||||
await Command.handleAction(args, true);
|
await Command.handleAction(this, args, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
183
CliClient/app/command-e2ee.js
Normal file
183
CliClient/app/command-e2ee.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
const { BaseCommand } = require('./base-command.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
|
||||||
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
return 'e2ee <command> [path]';
|
||||||
|
}
|
||||||
|
|
||||||
|
description() {
|
||||||
|
return _('Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, `status` and `target-status`.');
|
||||||
|
}
|
||||||
|
|
||||||
|
options() {
|
||||||
|
return [
|
||||||
|
// This is here mostly for testing - shouldn't be used
|
||||||
|
['-p, --password <password>', 'Use this password as master password (For security reasons, it is not recommended to use this option).'],
|
||||||
|
['-v, --verbose', 'More verbose output for the `target-status` command'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async action(args) {
|
||||||
|
// change-password
|
||||||
|
|
||||||
|
const options = args.options;
|
||||||
|
|
||||||
|
if (args.command === 'enable') {
|
||||||
|
const password = options.password ? options.password.toString() : await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||||
|
if (!password) {
|
||||||
|
this.stdout(_('Operation cancelled'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'disable') {
|
||||||
|
await EncryptionService.instance().disableEncryption();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'decrypt') {
|
||||||
|
this.stdout(_('Starting decryption... Please wait as it may take several minutes depending on how much there is to decrypt.'));
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await DecryptionWorker.instance().start();
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'masterKeyNotLoaded') {
|
||||||
|
const masterKeyId = error.masterKeyId;
|
||||||
|
const password = await this.prompt(_('Enter master password:'), { type: 'string', secure: true });
|
||||||
|
if (!password) {
|
||||||
|
this.stdout(_('Operation cancelled'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Setting.setObjectKey('encryption.passwordCache', masterKeyId, password);
|
||||||
|
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stdout(_('Completed decryption.'));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'status') {
|
||||||
|
this.stdout(_('Encryption is: %s', Setting.value('encryption.enabled') ? _('Enabled') : _('Disabled')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'target-status') {
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const pathUtils = require('lib/path-utils.js');
|
||||||
|
const fsDriver = new (require('lib/fs-driver-node.js').FsDriverNode)();
|
||||||
|
|
||||||
|
const targetPath = args.path;
|
||||||
|
if (!targetPath) throw new Error('Please specify the sync target path.');
|
||||||
|
|
||||||
|
const dirPaths = function(targetPath) {
|
||||||
|
let paths = [];
|
||||||
|
fs.readdirSync(targetPath).forEach((path) => {
|
||||||
|
paths.push(path);
|
||||||
|
});
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemCount = 0;
|
||||||
|
let resourceCount = 0;
|
||||||
|
let encryptedItemCount = 0;
|
||||||
|
let encryptedResourceCount = 0;
|
||||||
|
let otherItemCount = 0;
|
||||||
|
|
||||||
|
let encryptedPaths = [];
|
||||||
|
let decryptedPaths = [];
|
||||||
|
|
||||||
|
let paths = dirPaths(targetPath);
|
||||||
|
|
||||||
|
for (let i = 0; i < paths.length; i++) {
|
||||||
|
const path = paths[i];
|
||||||
|
const fullPath = targetPath + '/' + path;
|
||||||
|
const stat = await fs.stat(fullPath);
|
||||||
|
|
||||||
|
// this.stdout(fullPath);
|
||||||
|
|
||||||
|
if (path === '.resource') {
|
||||||
|
let resourcePaths = dirPaths(fullPath);
|
||||||
|
for (let j = 0; j < resourcePaths.length; j++) {
|
||||||
|
const resourcePath = resourcePaths[j];
|
||||||
|
resourceCount++;
|
||||||
|
const fullResourcePath = fullPath + '/' + resourcePath;
|
||||||
|
const isEncrypted = await EncryptionService.instance().fileIsEncrypted(fullResourcePath);
|
||||||
|
if (isEncrypted) {
|
||||||
|
encryptedResourceCount++;
|
||||||
|
encryptedPaths.push(fullResourcePath);
|
||||||
|
} else {
|
||||||
|
decryptedPaths.push(fullResourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
itemCount++;
|
||||||
|
const content = await fs.readFile(fullPath, 'utf8');
|
||||||
|
const item = await BaseItem.unserialize(content);
|
||||||
|
const ItemClass = BaseItem.itemClass(item);
|
||||||
|
|
||||||
|
if (!ItemClass.encryptionSupported()) {
|
||||||
|
otherItemCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
|
||||||
|
|
||||||
|
if (isEncrypted) {
|
||||||
|
encryptedItemCount++;
|
||||||
|
encryptedPaths.push(fullPath);
|
||||||
|
} else {
|
||||||
|
decryptedPaths.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stdout('Encrypted items: ' + encryptedItemCount + '/' + itemCount);
|
||||||
|
this.stdout('Encrypted resources: ' + encryptedResourceCount + '/' + resourceCount);
|
||||||
|
this.stdout('Other items (never encrypted): ' + otherItemCount);
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
this.stdout('');
|
||||||
|
this.stdout('# Encrypted paths');
|
||||||
|
this.stdout('');
|
||||||
|
for (let i = 0; i < encryptedPaths.length; i++) {
|
||||||
|
const path = encryptedPaths[i];
|
||||||
|
this.stdout(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stdout('');
|
||||||
|
this.stdout('# Decrypted paths');
|
||||||
|
this.stdout('');
|
||||||
|
for (let i = 0; i < decryptedPaths.length; i++) {
|
||||||
|
const path = decryptedPaths[i];
|
||||||
|
this.stdout(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Command;
|
@ -3,10 +3,10 @@ const { BaseCommand } = require('./base-command.js');
|
|||||||
const { uuid } = require('lib/uuid.js');
|
const { uuid } = require('lib/uuid.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
@ -44,6 +44,8 @@ class Command extends BaseCommand {
|
|||||||
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
|
if (!app().currentFolder()) throw new Error(_('No active notebook.'));
|
||||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
||||||
|
|
||||||
|
this.encryptionCheck(note);
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
|
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@ -76,12 +78,12 @@ class Command extends BaseCommand {
|
|||||||
|
|
||||||
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
|
app().gui().showModalOverlay(_('Starting to edit note. Close the editor to get back to the prompt.'));
|
||||||
await app().gui().forceRender();
|
await app().gui().forceRender();
|
||||||
const termState = app().gui().term().saveState();
|
const termState = app().gui().termSaveState();
|
||||||
|
|
||||||
const spawnSync = require('child_process').spawnSync;
|
const spawnSync = require('child_process').spawnSync;
|
||||||
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||||
|
|
||||||
app().gui().term().restoreState(termState);
|
app().gui().termRestoreState(termState);
|
||||||
app().gui().hideModalOverlay();
|
app().gui().hideModalOverlay();
|
||||||
app().gui().forceRender();
|
app().gui().forceRender();
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { ReportService } = require('lib/services/report.js');
|
const { ReportService } = require('lib/services/report.js');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { Exporter } = require('lib/services/exporter.js');
|
const { Exporter } = require('lib/services/exporter.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
|
|||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { renderCommandHelp } = require('./help-utils.js');
|
const { renderCommandHelp } = require('./help-utils.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { wrap } = require('lib/string-utils.js');
|
const { wrap } = require('lib/string-utils.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { importEnex } = require('lib/import-enex');
|
const { importEnex } = require('lib/import-enex');
|
||||||
const { filename, basename } = require('lib/path-utils.js');
|
const { filename, basename } = require('lib/path-utils.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ class Command extends BaseCommand {
|
|||||||
const name = args['name'];
|
const name = args['name'];
|
||||||
|
|
||||||
const item = await app().loadItem('folderOrNote', pattern);
|
const item = await app().loadItem('folderOrNote', pattern);
|
||||||
|
this.encryptionCheck(item);
|
||||||
if (!item) throw new Error(_('Cannot find "%s".', pattern));
|
if (!item) throw new Error(_('Cannot find "%s".', pattern));
|
||||||
|
|
||||||
const newItem = {
|
const newItem = {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { uuid } = require('lib/uuid.js');
|
const { uuid } = require('lib/uuid.js');
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
@ -35,6 +35,8 @@ class Command extends BaseCommand {
|
|||||||
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
||||||
|
|
||||||
for (let i = 0; i < notes.length; i++) {
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
this.encryptionCheck(notes[i]);
|
||||||
|
|
||||||
let newNote = {
|
let newNote = {
|
||||||
id: notes[i].id,
|
id: notes[i].id,
|
||||||
type_: notes[i].type_,
|
type_: notes[i].type_,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { ReportService } = require('lib/services/report.js');
|
const { ReportService } = require('lib/services/report.js');
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ const { BaseCommand } = require('./base-command.js');
|
|||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Synchronizer } = require('lib/synchronizer.js');
|
const { Synchronizer } = require('lib/synchronizer.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
@ -25,6 +25,8 @@ class Command extends BaseCommand {
|
|||||||
for (let i = 0; i < notes.length; i++) {
|
for (let i = 0; i < notes.length; i++) {
|
||||||
const note = notes[i];
|
const note = notes[i];
|
||||||
|
|
||||||
|
this.encryptionCheck(note);
|
||||||
|
|
||||||
let toSave = {
|
let toSave = {
|
||||||
id: note.id,
|
id: note.id,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
|
||||||
const CommandDone = require('./command-done.js');
|
const CommandDone = require('./command-done.js');
|
||||||
@ -19,7 +19,7 @@ class Command extends BaseCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async action(args) {
|
async action(args) {
|
||||||
await CommandDone.handleAction(args, false);
|
await CommandDone.handleAction(this, args, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const { BaseCommand } = require('./base-command.js');
|
const { BaseCommand } = require('./base-command.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
|
|
||||||
class Command extends BaseCommand {
|
class Command extends BaseCommand {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { dirname } = require('lib/path-utils.js');
|
const { dirname } = require('lib/path-utils.js');
|
||||||
const { FsDriverNode } = require('./fs-driver-node.js');
|
const { FsDriverNode } = require('./fs-driver-node.js');
|
||||||
const lodash = require('lodash');
|
const lodash = require('lodash');
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const Folder = require('lib/models/folder.js').Folder;
|
const Folder = require('lib/models/Folder.js');
|
||||||
const Tag = require('lib/models/tag.js').Tag;
|
const Tag = require('lib/models/Tag.js');
|
||||||
const BaseModel = require('lib/base-model.js').BaseModel;
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||||
const _ = require('lib/locale.js')._;
|
const _ = require('lib/locale.js')._;
|
||||||
|
|
||||||
@ -24,9 +24,9 @@ class FolderListWidget extends ListWidget {
|
|||||||
if (item === '-') {
|
if (item === '-') {
|
||||||
output.push('-'.repeat(this.innerWidth));
|
output.push('-'.repeat(this.innerWidth));
|
||||||
} else if (item.type_ === Folder.modelType()) {
|
} else if (item.type_ === Folder.modelType()) {
|
||||||
output.push(item.title);
|
output.push(Folder.displayTitle(item));
|
||||||
} else if (item.type_ === Tag.modelType()) {
|
} else if (item.type_ === Tag.modelType()) {
|
||||||
output.push('[' + item.title + ']');
|
output.push('[' + Folder.displayTitle(item) + ']');
|
||||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||||
output.push(_('Search:'));
|
output.push(_('Search:'));
|
||||||
output.push(item.title);
|
output.push(item.title);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||||
|
|
||||||
class NoteListWidget extends ListWidget {
|
class NoteListWidget extends ListWidget {
|
||||||
@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget {
|
|||||||
this.updateIndexFromSelectedNoteId_ = false;
|
this.updateIndexFromSelectedNoteId_ = false;
|
||||||
|
|
||||||
this.itemRenderer = (note) => {
|
this.itemRenderer = (note) => {
|
||||||
let label = note.title; // + ' ' + note.id;
|
let label = Note.displayTitle(note); // + ' ' + note.id;
|
||||||
if (note.is_todo) {
|
if (note.is_todo) {
|
||||||
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
|
label = '[' + (note.todo_completed ? 'X' : ' ') + '] ' + label;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||||
|
|
||||||
class NoteMetadataWidget extends TextWidget {
|
class NoteMetadataWidget extends TextWidget {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const Note = require('lib/models/note.js').Note;
|
const Note = require('lib/models/Note.js');
|
||||||
const TextWidget = require('tkwidgets/TextWidget.js');
|
const TextWidget = require('tkwidgets/TextWidget.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
|
|
||||||
@ -44,7 +44,13 @@ class NoteWidget extends TextWidget {
|
|||||||
} else if (this.noteId_) {
|
} else if (this.noteId_) {
|
||||||
this.doAsync('loadNote', async () => {
|
this.doAsync('loadNote', async () => {
|
||||||
this.note_ = await Note.load(this.noteId_);
|
this.note_ = await Note.load(this.noteId_);
|
||||||
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
|
||||||
|
if (this.note_ && this.note_.encryption_applied) {
|
||||||
|
this.text = _('One or more items are currently encrypted and you may need to supply a master password. To do so please type `e2ee decrypt`. If you have already supplied the password, the encrypted items are being decrypted in the background and will be available soon.');
|
||||||
|
} else {
|
||||||
|
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
|
if (this.lastLoadedNoteId_ !== this.noteId_) this.scrollTop = 0;
|
||||||
this.lastLoadedNoteId_ = this.noteId_;
|
this.lastLoadedNoteId_ = this.noteId_;
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ const BaseWidget = require('tkwidgets/BaseWidget.js');
|
|||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const termutils = require('tkwidgets/framework/termutils.js');
|
const termutils = require('tkwidgets/framework/termutils.js');
|
||||||
const stripAnsi = require('strip-ansi');
|
const stripAnsi = require('strip-ansi');
|
||||||
|
const { handleAutocompletion } = require('../autocompletion.js');
|
||||||
|
|
||||||
class StatusBarWidget extends BaseWidget {
|
class StatusBarWidget extends BaseWidget {
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ class StatusBarWidget extends BaseWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
|
if ('cursorPosition' in options) this.promptState_.cursorPosition = options.cursorPosition;
|
||||||
|
if ('secure' in options) this.promptState_.secure = options.secure;
|
||||||
|
|
||||||
this.promptState_.promise = new Promise((resolve, reject) => {
|
this.promptState_.promise = new Promise((resolve, reject) => {
|
||||||
this.promptState_.resolve = resolve;
|
this.promptState_.resolve = resolve;
|
||||||
@ -104,13 +106,19 @@ class StatusBarWidget extends BaseWidget {
|
|||||||
|
|
||||||
this.term.showCursor(true);
|
this.term.showCursor(true);
|
||||||
|
|
||||||
|
const isSecurePrompt = !!this.promptState_.secure;
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
history: this.history,
|
history: this.history,
|
||||||
default: this.promptState_.initialText,
|
default: this.promptState_.initialText,
|
||||||
|
autoComplete: handleAutocompletion,
|
||||||
|
autoCompleteHint : true,
|
||||||
|
autoCompleteMenu : true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
|
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
|
||||||
|
if (isSecurePrompt) options.echoChar = true;
|
||||||
|
|
||||||
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
|
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
|
||||||
let resolveResult = null;
|
let resolveResult = null;
|
||||||
@ -125,7 +133,7 @@ class StatusBarWidget extends BaseWidget {
|
|||||||
resolveResult = input ? input.trim() : input;
|
resolveResult = input ? input.trim() : input;
|
||||||
// Add the command to history but only if it's longer than one character.
|
// Add the command to history but only if it's longer than one character.
|
||||||
// Below that it's usually an answer like "y"/"n", etc.
|
// Below that it's usually an answer like "y"/"n", etc.
|
||||||
if (input && input.length > 1) this.history_.push(input);
|
if (!isSecurePrompt && input && input.length > 1) this.history_.push(input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,4 +167,4 @@ class StatusBarWidget extends BaseWidget {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StatusBarWidget;
|
module.exports = StatusBarWidget;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { wrap } = require('lib/string-utils.js');
|
const { wrap } = require('lib/string-utils.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
|
const { fileExtension, basename, dirname } = require('lib/path-utils.js');
|
||||||
const { _, setLocale, languageCode } = require('lib/locale.js');
|
const { _, setLocale, languageCode } = require('lib/locale.js');
|
||||||
|
|
||||||
|
@ -1,26 +1,27 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
// Loading time: 20170803: 1.5s with no commands
|
// Make it possible to require("/lib/...") without specifying full path
|
||||||
|
|
||||||
require('app-module-path').addPath(__dirname);
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const NoteTag = require('lib/models/NoteTag.js');
|
||||||
const { NoteTag } = require('lib/models/note-tag.js');
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||||
const { shimInit } = require('lib/shim-init-node.js');
|
const { shimInit } = require('lib/shim-init-node.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
|
|
||||||
const fsDriver = new FsDriverNode();
|
const fsDriver = new FsDriverNode();
|
||||||
Logger.fsDriver_ = fsDriver;
|
Logger.fsDriver_ = fsDriver;
|
||||||
Resource.fsDriver_ = fsDriver;
|
Resource.fsDriver_ = fsDriver;
|
||||||
|
EncryptionService.fsDriver_ = fsDriver;
|
||||||
|
|
||||||
// That's not good, but it's to avoid circular dependency issues
|
// That's not good, but it's to avoid circular dependency issues
|
||||||
// in the BaseItem class.
|
// in the BaseItem class.
|
||||||
@ -29,6 +30,7 @@ BaseItem.loadClass('Folder', Folder);
|
|||||||
BaseItem.loadClass('Resource', Resource);
|
BaseItem.loadClass('Resource', Resource);
|
||||||
BaseItem.loadClass('Tag', Tag);
|
BaseItem.loadClass('Tag', Tag);
|
||||||
BaseItem.loadClass('NoteTag', NoteTag);
|
BaseItem.loadClass('NoteTag', NoteTag);
|
||||||
|
BaseItem.loadClass('MasterKey', MasterKey);
|
||||||
|
|
||||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||||
Setting.setConstant('appType', 'cli');
|
Setting.setConstant('appType', 'cli');
|
||||||
|
@ -4,6 +4,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|||||||
BUILD_DIR="$ROOT_DIR/build"
|
BUILD_DIR="$ROOT_DIR/build"
|
||||||
|
|
||||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||||
chmod 755 "$BUILD_DIR/main.js"
|
chmod 755 "$BUILD_DIR/main.js"
|
@ -2,18 +2,18 @@
|
|||||||
# Copyright (C) YEAR Laurent Cozic
|
# Copyright (C) YEAR Laurent Cozic
|
||||||
# This file is distributed under the same license as the Joplin-CLI package.
|
# This file is distributed under the same license as the Joplin-CLI package.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
#
|
#
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: Samuel Blickle <blickle.samuel@gmail.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: Poedit 2.0.4\n"
|
"X-Generator: Poedit 2.0.5\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
msgid "Give focus to next pane"
|
msgid "Give focus to next pane"
|
||||||
@ -38,7 +38,7 @@ msgid "Exit the application."
|
|||||||
msgstr "Das Programm verlassen."
|
msgstr "Das Programm verlassen."
|
||||||
|
|
||||||
msgid "Delete the currently selected note or notebook."
|
msgid "Delete the currently selected note or notebook."
|
||||||
msgstr "Die momentan ausgewählte Notiz(-buch) löschen."
|
msgstr "Die/das momentan ausgewählte Notiz(-buch) löschen."
|
||||||
|
|
||||||
msgid "To delete a tag, untag the associated notes."
|
msgid "To delete a tag, untag the associated notes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -79,7 +79,7 @@ msgid "Move the note to a notebook."
|
|||||||
msgstr "Die Notiz zu einem Notizbuch verschieben."
|
msgstr "Die Notiz zu einem Notizbuch verschieben."
|
||||||
|
|
||||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||||
msgstr "Drücke Strg+D oder schreibe \"exit\", um das Programm zu verlassen"
|
msgstr "Drücke Strg+D oder tippe \"exit\", um das Programm zu verlassen"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "More than one item match \"%s\". Please narrow down your query."
|
msgid "More than one item match \"%s\". Please narrow down your query."
|
||||||
@ -106,11 +106,11 @@ msgid "y"
|
|||||||
msgstr "j"
|
msgstr "j"
|
||||||
|
|
||||||
msgid "Cancelling background synchronisation... Please wait."
|
msgid "Cancelling background synchronisation... Please wait."
|
||||||
msgstr "Breche Hintergrund-Synchronisations ab....Bitte warten."
|
msgstr "Breche Hintergrund-Synchronisation ab... Bitte warten."
|
||||||
|
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "No such command: %s"
|
msgid "No such command: %s"
|
||||||
msgstr "Ungültiger Befehl: \"%s\""
|
msgstr "Ungültiger Befehl: %s"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "The command \"%s\" is only available in GUI mode"
|
msgid "The command \"%s\" is only available in GUI mode"
|
||||||
@ -150,12 +150,12 @@ msgid ""
|
|||||||
"current configuration."
|
"current configuration."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Zeigt an oder stellt einen Optionswert. Wenn kein [Wert] angegeben ist, wird "
|
"Zeigt an oder stellt einen Optionswert. Wenn kein [Wert] angegeben ist, wird "
|
||||||
"der Wert vom gegebenenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] "
|
"der Wert vom gegebenen [Namen] angezeigt. Wenn weder [Name] noch [Wert] "
|
||||||
"gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt."
|
"gegeben sind, wird eine Liste der momentanen Konfiguration angezeigt."
|
||||||
|
|
||||||
msgid "Also displays unset and hidden config variables."
|
msgid "Also displays unset and hidden config variables."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Zeige auch nicht angegebene oder versteckte Konfigurationsvariablen an."
|
"Zeigt auch nicht angegebene oder versteckte Konfigurationsvariablen an."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%s = %s (%s)"
|
msgid "%s = %s (%s)"
|
||||||
@ -169,8 +169,8 @@ msgid ""
|
|||||||
"Duplicates the notes matching <note> to [notebook]. If no notebook is "
|
"Duplicates the notes matching <note> to [notebook]. If no notebook is "
|
||||||
"specified the note is duplicated in the current notebook."
|
"specified the note is duplicated in the current notebook."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Vervielfältigt die Notizen die mit <note> übereinstimmen zu [Notizbuch]. "
|
"Dupliziert die Notizen die mit <note> übereinstimmen zu [Notizbuch]. Wenn "
|
||||||
"Wenn kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch "
|
"kein Notizbuch angegeben ist, wird die Notiz in das momentane Notizbuch "
|
||||||
"kopiert."
|
"kopiert."
|
||||||
|
|
||||||
msgid "Marks a to-do as done."
|
msgid "Marks a to-do as done."
|
||||||
@ -186,8 +186,8 @@ msgstr "Notiz bearbeiten."
|
|||||||
msgid ""
|
msgid ""
|
||||||
"No text editor is defined. Please set it using `config editor <editor-path>`"
|
"No text editor is defined. Please set it using `config editor <editor-path>`"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kein Textbearbeitungsprogramm angegeben. Bitte lege eines mit `config editor "
|
"Kein Textverarbeitungsprogramm angegeben. Bitte lege eines mit `config "
|
||||||
"<Pfad-Zum-Textbearbeitungsprogramm>` fest"
|
"editor <Pfad-Zum-Textverarbeitungsprogramm>` fest"
|
||||||
|
|
||||||
msgid "No active notebook."
|
msgid "No active notebook."
|
||||||
msgstr "Kein aktives Notizbuch."
|
msgstr "Kein aktives Notizbuch."
|
||||||
@ -198,8 +198,8 @@ msgstr "Notiz \"%s\" existiert nicht. Soll sie erstellt werden?"
|
|||||||
|
|
||||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Beginne die Notiz zu bearbeiten. Schließ das Textbearbeitungsprogramm, um "
|
"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um "
|
||||||
"zurück zum Terminal zu kommen."
|
"zurück zum Terminal zu gelangen."
|
||||||
|
|
||||||
msgid "Note has been saved."
|
msgid "Note has been saved."
|
||||||
msgstr "Die Notiz wurde gespeichert."
|
msgstr "Die Notiz wurde gespeichert."
|
||||||
@ -211,9 +211,9 @@ msgid ""
|
|||||||
"Exports Joplin data to the given directory. By default, it will export the "
|
"Exports Joplin data to the given directory. By default, it will export the "
|
||||||
"complete database including notebooks, notes, tags and resources."
|
"complete database including notebooks, notes, tags and resources."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Exportiert Joplins Datein zu dem angegebenen Pfad. Standardmäßig wird die "
|
"Exportiert Joplins Dateien zu dem angegebenen Pfad. Standardmäßig wird die "
|
||||||
"komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen usw. "
|
"komplette Datenbank inklusive Notizbüchern, Notizen, Markierungen und "
|
||||||
"exportiert."
|
"Anhängen exportiert."
|
||||||
|
|
||||||
msgid "Exports only the given note."
|
msgid "Exports only the given note."
|
||||||
msgstr "Exportiert nur die angegebene Notiz."
|
msgstr "Exportiert nur die angegebene Notiz."
|
||||||
@ -225,10 +225,10 @@ msgid "Displays a geolocation URL for the note."
|
|||||||
msgstr "Zeigt die Standort-URL der Notiz an."
|
msgstr "Zeigt die Standort-URL der Notiz an."
|
||||||
|
|
||||||
msgid "Displays usage information."
|
msgid "Displays usage information."
|
||||||
msgstr "Zeigt die Benutzungsstatistik an."
|
msgstr "Zeigt die Nutzungsstatistik an."
|
||||||
|
|
||||||
msgid "Shortcuts are not available in CLI mode."
|
msgid "Shortcuts are not available in CLI mode."
|
||||||
msgstr ""
|
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -248,8 +248,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre "
|
"In jedem Befehl können Notizen oder Notizbücher durch ihren Titel oder ihre "
|
||||||
"ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder "
|
"ID spezifiziert werden, oder durch die Abkürzung `$n` oder `$b` um entweder "
|
||||||
"das momentan augewählte Notizbuch oder die momentan ausgewählte Notiz zu "
|
"das momentan ausgewählte Notizbuch oder die momentan ausgewählte Notiz zu "
|
||||||
"wählen. `$c` kann benutzt werden, um die momentane Auswahl zu verweisen."
|
"wählen. `$c` kann benutzt werden, um auf die momentane Auswahl zu verweisen."
|
||||||
|
|
||||||
msgid "To move from one pane to another, press Tab or Shift+Tab."
|
msgid "To move from one pane to another, press Tab or Shift+Tab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -261,16 +261,16 @@ msgid ""
|
|||||||
"(including this console)."
|
"(including this console)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu "
|
"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu "
|
||||||
"scrollen ( inklusive diesem Terminal )."
|
"scrollen (inklusive diesem Terminal)."
|
||||||
|
|
||||||
msgid "To maximise/minimise the console, press \"TC\"."
|
msgid "To maximise/minimise the console, press \"TC\"."
|
||||||
msgstr "Um das Terminal zu maximieren/minimieren, drücke \"TC\"."
|
msgstr "Um das Terminal zu maximieren/minimieren, drücke \"TC\"."
|
||||||
|
|
||||||
msgid "To enter command line mode, press \":\""
|
msgid "To enter command line mode, press \":\""
|
||||||
msgstr ""
|
msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\""
|
||||||
|
|
||||||
msgid "To exit command line mode, press ESCAPE"
|
msgid "To exit command line mode, press ESCAPE"
|
||||||
msgstr ""
|
msgstr "Um den Kommandozeilen Modus zu beenden, drücke ESCAPE"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||||
@ -287,7 +287,7 @@ msgstr "Nicht nach einer Bestätigung fragen."
|
|||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?"
|
msgid "File \"%s\" will be imported into existing notebook \"%s\". Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Datei \"%s\" wird importiert in das existierende Notizbuch \"%s\". "
|
"Datei \"%s\" wird in das existierende Notizbuch \"%s\" importiert. "
|
||||||
"Fortfahren?"
|
"Fortfahren?"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@ -295,7 +295,7 @@ msgid ""
|
|||||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into "
|
"New notebook \"%s\" will be created and file \"%s\" will be imported into "
|
||||||
"it. Continue?"
|
"it. Continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es "
|
"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein "
|
||||||
"importiert. Fortfahren?"
|
"importiert. Fortfahren?"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@ -316,7 +316,7 @@ msgstr "Übersprungen: %d."
|
|||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Resources: %d."
|
msgid "Resources: %d."
|
||||||
msgstr ""
|
msgstr "Anhänge: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Tagged: %d."
|
msgid "Tagged: %d."
|
||||||
@ -337,11 +337,12 @@ msgstr ""
|
|||||||
"aller Notizbücher anzuzeigen."
|
"aller Notizbücher anzuzeigen."
|
||||||
|
|
||||||
msgid "Displays only the first top <num> notes."
|
msgid "Displays only the first top <num> notes."
|
||||||
msgstr "Zeigt nur die Top-<num> Notizen an."
|
msgstr "Zeigt nur die ersten <num> Notizen an."
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Sorts the item by <field> (eg. title, updated_time, created_time)."
|
msgid "Sorts the item by <field> (eg. title, updated_time, created_time)."
|
||||||
msgstr "Sortiert nach <field> ( z.B. Titel,"
|
msgstr ""
|
||||||
|
"Sortiert nach <field> ( z.B. Titel, Bearbeitungszeitpunkt, "
|
||||||
|
"Erstellungszeitpunkt)"
|
||||||
|
|
||||||
msgid "Reverses the sorting order."
|
msgid "Reverses the sorting order."
|
||||||
msgstr "Dreht die Sortierreihenfolge um."
|
msgstr "Dreht die Sortierreihenfolge um."
|
||||||
@ -351,9 +352,9 @@ msgid ""
|
|||||||
"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the "
|
"for to-dos, or `nt` for notes and to-dos (eg. `-tt` would display only the "
|
||||||
"to-dos, while `-ttd` would display notes and to-dos."
|
"to-dos, while `-ttd` would display notes and to-dos."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Zeige nur bestimmt Typen an. Kann `n` für Notizen sein, `t` für To-Dos, oder "
|
"Zeigt nur bestimmte Item Typen an. Kann `n` für Notizen sein, `t` für To-"
|
||||||
"`nt` für Notizen und To-Dos ( z.B. würde `-tt` nur To-Dos anzeigen, während "
|
"Dos, oder `nt` für Notizen und To-Dos ( z.B. zeigt `-tt` nur To-Dos an, "
|
||||||
"`-ttd` Notizen und To-Dos anzeigen würde )."
|
"während `-ttd` Notizen und To-Dos anzeigt)."
|
||||||
|
|
||||||
msgid "Either \"text\" or \"json\""
|
msgid "Either \"text\" or \"json\""
|
||||||
msgstr "Entweder \"text\" oder \"json\""
|
msgstr "Entweder \"text\" oder \"json\""
|
||||||
@ -362,6 +363,8 @@ msgid ""
|
|||||||
"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, "
|
"Use long list format. Format is ID, NOTE_COUNT (for notebook), DATE, "
|
||||||
"TODO_CHECKED (for to-dos), TITLE"
|
"TODO_CHECKED (for to-dos), TITLE"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Verwende ausführliches Listen Format. Das Format lautet: ID, NOTIZEN_ANZAHL "
|
||||||
|
"(für Notizbuch), DATUM, TODO_BEARBEITET (für To-Dos), TITEL"
|
||||||
|
|
||||||
msgid "Please select a notebook first."
|
msgid "Please select a notebook first."
|
||||||
msgstr "Bitte wähle erst ein Notizbuch aus."
|
msgstr "Bitte wähle erst ein Notizbuch aus."
|
||||||
@ -382,16 +385,17 @@ msgid "Moves the notes matching <note> to [notebook]."
|
|||||||
msgstr "Verschiebt die Notizen, die mit <note> übereinstimmen, zu [Notizbuch]"
|
msgstr "Verschiebt die Notizen, die mit <note> übereinstimmen, zu [Notizbuch]"
|
||||||
|
|
||||||
msgid "Renames the given <item> (note or notebook) to <name>."
|
msgid "Renames the given <item> (note or notebook) to <name>."
|
||||||
msgstr "Benennt das gegebene <item> ( Notiz oder Notizbuch ) zu <name> um."
|
msgstr "Benennt das angegebene <item> ( Notiz oder Notizbuch ) zu <name> um."
|
||||||
|
|
||||||
msgid "Deletes the given notebook."
|
msgid "Deletes the given notebook."
|
||||||
msgstr "Löscht das gegebene Notizbuch."
|
msgstr "Löscht das ausgewählte Notizbuch."
|
||||||
|
|
||||||
msgid "Deletes the notebook without asking for confirmation."
|
msgid "Deletes the notebook without asking for confirmation."
|
||||||
msgstr "Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen."
|
msgstr "Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen."
|
||||||
|
|
||||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht."
|
||||||
|
|
||||||
msgid "Deletes the notes matching <note-pattern>."
|
msgid "Deletes the notes matching <note-pattern>."
|
||||||
msgstr "Löscht die Notizen, die mit <note-pattern> übereinstimmen."
|
msgstr "Löscht die Notizen, die mit <note-pattern> übereinstimmen."
|
||||||
@ -408,27 +412,29 @@ msgid "Delete note?"
|
|||||||
msgstr "Notiz löschen?"
|
msgstr "Notiz löschen?"
|
||||||
|
|
||||||
msgid "Searches for the given <pattern> in all the notes."
|
msgid "Searches for the given <pattern> in all the notes."
|
||||||
msgstr "Sucht nach dem gegebenen <pattern> in allen Notizen."
|
msgstr "Sucht nach dem angegebenen <pattern> in allen Notizen."
|
||||||
|
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||||
"properties are:\n"
|
"properties are:\n"
|
||||||
"\n"
|
"\n"
|
||||||
"%s"
|
"%s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Setzt die Eigenschaft <name> der gegebenen <note> zu dem gegebenen [Wert]."
|
"Setzt die Eigenschaft <name> der gegebenen <note> auf den gegebenen [Wert]. "
|
||||||
|
"Mögliche Werte sind:\n"
|
||||||
|
"\n"
|
||||||
|
"%s"
|
||||||
|
|
||||||
msgid "Displays summary about the notes and notebooks."
|
msgid "Displays summary about the notes and notebooks."
|
||||||
msgstr "Zeigt eine Zusammenfassung über die Notizen und Notizbücher an."
|
msgstr "Zeigt eine Zusammenfassung der Notizen und Notizbücher an."
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Synchronises with remote storage."
|
msgid "Synchronises with remote storage."
|
||||||
msgstr "Synchronisiert mit "
|
msgstr "Synchronisiert mit Remotespeicher."
|
||||||
|
|
||||||
msgid "Sync to provided target (defaults to sync.target config value)"
|
msgid "Sync to provided target (defaults to sync.target config value)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Mit dem gegebenen Ziel synchronisieren ( voreingestellt auf den sync.target "
|
"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target "
|
||||||
"Optionswert)"
|
"Optionswert)"
|
||||||
|
|
||||||
msgid "Synchronisation is already in progress."
|
msgid "Synchronisation is already in progress."
|
||||||
@ -442,12 +448,12 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine "
|
"Eine Sperrdatei ist vorhanden. Wenn du dir sicher bist, dass keine "
|
||||||
"Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und "
|
"Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und "
|
||||||
"vortfahren."
|
"fortfahren."
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Authentication was not completed (did not receive an authentication token)."
|
"Authentication was not completed (did not receive an authentication token)."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Authentikation wurde nicht abgeschlossen (keinen Authentikations-Token "
|
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
|
||||||
"erhalten)."
|
"erhalten)."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
@ -478,7 +484,6 @@ msgstr ""
|
|||||||
msgid "Invalid command: \"%s\""
|
msgid "Invalid command: \"%s\""
|
||||||
msgstr "Ungültiger Befehl: \"%s\""
|
msgstr "Ungültiger Befehl: \"%s\""
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to "
|
"<todo-command> can either be \"toggle\" or \"clear\". Use \"toggle\" to "
|
||||||
"toggle the given to-do between completed and uncompleted state (If the "
|
"toggle the given to-do between completed and uncompleted state (If the "
|
||||||
@ -487,8 +492,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"<todo-command> kann entweder \"toggle\" oder \"clear\" sein. Benutze \"toggle"
|
"<todo-command> kann entweder \"toggle\" oder \"clear\" sein. Benutze \"toggle"
|
||||||
"\", um ein To-Do abzuschließen, oder es zu beginnen (Wenn das Ziel eine "
|
"\", um ein To-Do abzuschließen, oder es zu beginnen (Wenn das Ziel eine "
|
||||||
"normale Notiz ist, wird es zu einem To-Do umgewandelt). Benutze \"clear\", "
|
"normale Notiz ist, wird diese in ein To-Do umgewandelt). Benutze \"clear\", "
|
||||||
"um es zurück zu einem To-Do zu verwandeln."
|
"um es zurück in ein To-Do zu verwandeln."
|
||||||
|
|
||||||
msgid "Marks a to-do as non-completed."
|
msgid "Marks a to-do as non-completed."
|
||||||
msgstr "Makiert ein To-Do als nicht-abgeschlossen."
|
msgstr "Makiert ein To-Do als nicht-abgeschlossen."
|
||||||
@ -497,8 +502,8 @@ msgid ""
|
|||||||
"Switches to [notebook] - all further operations will happen within this "
|
"Switches to [notebook] - all further operations will happen within this "
|
||||||
"notebook."
|
"notebook."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wechselt zu [Notizbuch] - alle weiteren Tätigkeiten werden in diesem "
|
"Wechselt zu [Notizbuch] - alle weiteren Aktionen werden in diesem Notizbuch "
|
||||||
"Notizbuch verrichtet."
|
"ausgeführt."
|
||||||
|
|
||||||
msgid "Displays version information"
|
msgid "Displays version information"
|
||||||
msgstr "Zeigt die Versionsnummer an"
|
msgstr "Zeigt die Versionsnummer an"
|
||||||
@ -531,10 +536,10 @@ msgstr "Schwerwiegender Fehler:"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"The application has been authorised - you may now close this browser tab."
|
"The application has been authorised - you may now close this browser tab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Das Programm wurde authorisiert - Du kannst nun diesen Browsertab schließen."
|
"Das Programm wurde autorisiert - Du kannst diesen Browsertab nun schließen."
|
||||||
|
|
||||||
msgid "The application has been successfully authorised."
|
msgid "The application has been successfully authorised."
|
||||||
msgstr "Das Programm wurde erfolgreich authorisiert."
|
msgstr "Das Programm wurde erfolgreich autorisiert."
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Please open the following URL in your browser to authenticate the "
|
"Please open the following URL in your browser to authenticate the "
|
||||||
@ -546,8 +551,8 @@ msgstr ""
|
|||||||
"Bitte öffne die folgende URL in deinem Browser, um das Programm zu "
|
"Bitte öffne die folgende URL in deinem Browser, um das Programm zu "
|
||||||
"authentifizieren. Das Programm wird einen Ordner in \"Apps/Joplin\" "
|
"authentifizieren. Das Programm wird einen Ordner in \"Apps/Joplin\" "
|
||||||
"erstellen und wird nur in diesem Ordner schreiben und lesen. Es wird weder "
|
"erstellen und wird nur in diesem Ordner schreiben und lesen. Es wird weder "
|
||||||
"Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf persönliche "
|
"Zugriff auf Dateien außerhalb dieses Ordners haben, noch auf andere "
|
||||||
"Daten. Es werden keine Daten mit Dritten geteilt."
|
"persönliche Daten. Es werden keine Daten mit Dritten geteilt."
|
||||||
|
|
||||||
msgid "Search:"
|
msgid "Search:"
|
||||||
msgstr "Suchen:"
|
msgstr "Suchen:"
|
||||||
@ -560,6 +565,13 @@ msgid ""
|
|||||||
"\n"
|
"\n"
|
||||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Willkommen bei Joplin!\n"
|
||||||
|
"\n"
|
||||||
|
"Tippe `:help shortcuts` für eine Liste der Shortcuts oder `:help` für "
|
||||||
|
"Nutzungsinformationen ein.\n"
|
||||||
|
"\n"
|
||||||
|
"Um zum Beispiel ein Notizbuch zu erstellen, drücke `mb`; um eine Notiz zu "
|
||||||
|
"erstellen drücke `mn`."
|
||||||
|
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Datei"
|
msgstr "Datei"
|
||||||
@ -577,7 +589,7 @@ msgid "Import Evernote notes"
|
|||||||
msgstr "Evernote Notizen importieren"
|
msgstr "Evernote Notizen importieren"
|
||||||
|
|
||||||
msgid "Evernote Export Files"
|
msgid "Evernote Export Files"
|
||||||
msgstr ""
|
msgstr "Evernote Export Dateien"
|
||||||
|
|
||||||
msgid "Quit"
|
msgid "Quit"
|
||||||
msgstr "Verlassen"
|
msgstr "Verlassen"
|
||||||
@ -600,9 +612,8 @@ msgstr "Alle Notizen durchsuchen"
|
|||||||
msgid "Tools"
|
msgid "Tools"
|
||||||
msgstr "Werkzeuge"
|
msgstr "Werkzeuge"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Synchronisation status"
|
msgid "Synchronisation status"
|
||||||
msgstr "Synchronisationsziel"
|
msgstr "Status der Synchronisation"
|
||||||
|
|
||||||
msgid "Options"
|
msgid "Options"
|
||||||
msgstr "Optionen"
|
msgstr "Optionen"
|
||||||
@ -628,11 +639,77 @@ msgstr "Abbrechen"
|
|||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Notes and settings are stored in: %s"
|
msgid "Notes and settings are stored in: %s"
|
||||||
msgstr ""
|
msgstr "Notizen und Einstellungen gespeichert in: %s"
|
||||||
|
|
||||||
msgid "Save"
|
msgid "Save"
|
||||||
|
msgstr "Speichern"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "Aktiv"
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "ID"
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr "Quelle"
|
||||||
|
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Erstellt"
|
||||||
|
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "Aktualisiert"
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr "Passwort"
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr "Passwort OK"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
"Hinweis: Nur ein Hauptschlüssel wird für die Verschlüsselung verwendet (der "
|
||||||
|
"als \"aktiv\" markierte). Jeder der Schlüssel kann für die Entschlüsselung "
|
||||||
|
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
|
||||||
|
"Notizbücher ursprünglich verschlüsselt wurden."
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Deaktiviert"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Deaktiviert"
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "Zurück"
|
msgstr "Zurück"
|
||||||
|
|
||||||
@ -640,14 +717,14 @@ msgstr "Zurück"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ein neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird in es "
|
"Neues Notizbuch \"%s\" wird erstellt und die Datei \"%s\" wird hinein "
|
||||||
"importiert"
|
"importiert"
|
||||||
|
|
||||||
msgid "Please create a notebook first."
|
msgid "Please create a notebook first."
|
||||||
msgstr "Bitte erstelle zuerst ein Notizbuch."
|
msgstr "Bitte erstelle zuerst ein Notizbuch."
|
||||||
|
|
||||||
msgid "Note title:"
|
msgid "Note title:"
|
||||||
msgstr "Notiz Titel:"
|
msgstr "Notizen Titel:"
|
||||||
|
|
||||||
msgid "Please create a notebook first"
|
msgid "Please create a notebook first"
|
||||||
msgstr "Bitte erstelle zuerst ein Notizbuch"
|
msgstr "Bitte erstelle zuerst ein Notizbuch"
|
||||||
@ -673,26 +750,18 @@ msgstr "Alarm erstellen:"
|
|||||||
msgid "Layout"
|
msgid "Layout"
|
||||||
msgstr "Layout"
|
msgstr "Layout"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Some items cannot be synchronised."
|
msgid "Some items cannot be synchronised."
|
||||||
msgstr "Kann Synchronisierer nicht initialisieren."
|
msgstr "Manche Objekte können nicht synchronisiert werden."
|
||||||
|
|
||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr "Zeige sie jetzt an"
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Created"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr "Erstellt: %d."
|
msgstr "Kann Synchronisierer nicht initialisieren."
|
||||||
|
|
||||||
#, fuzzy
|
msgid "Set the password"
|
||||||
msgid "Updated"
|
msgstr ""
|
||||||
msgstr "Aktualisiert: %d."
|
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
msgstr "Markierungen hinzufügen oder entfernen"
|
msgstr "Markierungen hinzufügen oder entfernen"
|
||||||
@ -711,12 +780,11 @@ msgstr ""
|
|||||||
"Hier sind noch keine Notizen. Erstelle eine, indem du auf \"Neue Notiz\" "
|
"Hier sind noch keine Notizen. Erstelle eine, indem du auf \"Neue Notiz\" "
|
||||||
"drückst."
|
"drückst."
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Momentan existieren noch keine Notizen. Erstelle eine, indem du auf den (+) "
|
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den "
|
||||||
"Knopf drückst."
|
"(+) Knopf drückst."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Unsupported link or message: %s"
|
msgid "Unsupported link or message: %s"
|
||||||
@ -735,17 +803,19 @@ msgid "Clear"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "OneDrive Login"
|
msgid "OneDrive Login"
|
||||||
msgstr "OneDrive login"
|
msgstr "OneDrive Login"
|
||||||
|
|
||||||
msgid "Import"
|
msgid "Import"
|
||||||
msgstr "Importieren"
|
msgstr "Importieren"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr "Synchronisationsziel"
|
msgstr "Synchronisations Status"
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr "Diese Markierung von allen Notizen löschen?"
|
msgstr "Diese Markierung von allen Notizen entfernen?"
|
||||||
|
|
||||||
msgid "Remove this search from the sidebar?"
|
msgid "Remove this search from the sidebar?"
|
||||||
msgstr "Diese Suche von der Seitenleiste entfernen?"
|
msgstr "Diese Suche von der Seitenleiste entfernen?"
|
||||||
@ -765,15 +835,13 @@ msgstr "Markierungen"
|
|||||||
msgid "Searches"
|
msgid "Searches"
|
||||||
msgstr "Suchen"
|
msgstr "Suchen"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Please select where the sync status should be exported to"
|
msgid "Please select where the sync status should be exported to"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wähle bitte zuerst eine Notiz oder ein Notizbuch aus, das gelöscht werden "
|
"Bitte wähle aus, wohin der Synchronisations Status exportiert werden soll"
|
||||||
"soll."
|
|
||||||
|
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "Usage: %s"
|
msgid "Usage: %s"
|
||||||
msgstr "Benutzung: %s"
|
msgstr "Nutzung: %s"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Unknown flag: %s"
|
msgid "Unknown flag: %s"
|
||||||
@ -786,22 +854,22 @@ msgid "OneDrive"
|
|||||||
msgstr "OneDrive"
|
msgstr "OneDrive"
|
||||||
|
|
||||||
msgid "OneDrive Dev (For testing only)"
|
msgid "OneDrive Dev (For testing only)"
|
||||||
msgstr "OneDrive Dev ( Nur für Tests )"
|
msgstr "OneDrive Dev (Nur für Tests)"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Unknown log level: %s"
|
msgid "Unknown log level: %s"
|
||||||
msgstr "Unbekanntes Loglevel: %s"
|
msgstr "Unbekanntes Log Level: %s"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Unknown level ID: %s"
|
msgid "Unknown level ID: %s"
|
||||||
msgstr "Unbekannte Level-ID: %s"
|
msgstr "Unbekannte Level ID: %s"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Cannot refresh token: authentication data is missing. Starting the "
|
"Cannot refresh token: authentication data is missing. Starting the "
|
||||||
"synchronisation again may fix the problem."
|
"synchronisation again may fix the problem."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Kann Token nicht erneuern: Authentikationsdaten nicht vorhanden. Ein "
|
"Kann Token nicht erneuern: Authentifikationsdaten nicht vorhanden. Ein "
|
||||||
"Neustart der Synchronisation behebt das Problem vielleicht."
|
"Neustart der Synchronisation könnte das Problem beheben."
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Could not synchronize with OneDrive.\n"
|
"Could not synchronize with OneDrive.\n"
|
||||||
@ -816,7 +884,7 @@ msgstr ""
|
|||||||
"Dieser Fehler kommt oft vor, wenn OneDrive Business benutzt wird, das leider "
|
"Dieser Fehler kommt oft vor, wenn OneDrive Business benutzt wird, das leider "
|
||||||
"nicht unterstützt wird.\n"
|
"nicht unterstützt wird.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Bitte benutze stattdessen einen normalen OneDrive account."
|
"Bitte benutze stattdessen einen normalen OneDrive Account."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Cannot access %s"
|
msgid "Cannot access %s"
|
||||||
@ -824,27 +892,27 @@ msgstr "Kann nicht auf %s zugreifen"
|
|||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Created local items: %d."
|
msgid "Created local items: %d."
|
||||||
msgstr ""
|
msgstr "Lokale Objekte erstellt: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Updated local items: %d."
|
msgid "Updated local items: %d."
|
||||||
msgstr ""
|
msgstr "Lokale Objekte aktualisiert: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Created remote items: %d."
|
msgid "Created remote items: %d."
|
||||||
msgstr ""
|
msgstr "Remote Objekte erstellt: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Updated remote items: %d."
|
msgid "Updated remote items: %d."
|
||||||
msgstr ""
|
msgstr "Remote Objekte aktualisiert: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Deleted local items: %d."
|
msgid "Deleted local items: %d."
|
||||||
msgstr ""
|
msgstr "Lokale Objekte gelöscht: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Deleted remote items: %d."
|
msgid "Deleted remote items: %d."
|
||||||
msgstr ""
|
msgstr "Remote Objekte gelöscht: %d."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "State: \"%s\"."
|
msgid "State: \"%s\"."
|
||||||
@ -888,15 +956,15 @@ msgid "Cannot move note to \"%s\" notebook"
|
|||||||
msgstr "Kann Notiz nicht zu Notizbuch \"%s\" verschieben"
|
msgstr "Kann Notiz nicht zu Notizbuch \"%s\" verschieben"
|
||||||
|
|
||||||
msgid "Text editor"
|
msgid "Text editor"
|
||||||
msgstr "Textbearbeitungsprogramm"
|
msgstr "Textverarbeitungsprogramm"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The editor that will be used to open a note. If none is provided it will try "
|
"The editor that will be used to open a note. If none is provided it will try "
|
||||||
"to auto-detect the default editor."
|
"to auto-detect the default editor."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Das Textbearbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines "
|
"Das Textverarbeitungsprogramm, mit dem Notizen geöffnet werden. Wenn keines "
|
||||||
"ausgewählt wurde, wird Joplin versuchen das standard-"
|
"ausgewählt wurde, wird Joplin versuchen das standard-"
|
||||||
"Textbearbeitungsprogramm zu erkennen."
|
"Textverarbeitungsprogramm zu erkennen."
|
||||||
|
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Sprache"
|
msgstr "Sprache"
|
||||||
@ -917,7 +985,7 @@ msgid "Dark"
|
|||||||
msgstr "Dunkel"
|
msgstr "Dunkel"
|
||||||
|
|
||||||
msgid "Show uncompleted todos on top of the lists"
|
msgid "Show uncompleted todos on top of the lists"
|
||||||
msgstr "Unvollständige To-Dos oben in der Liste anzeigen"
|
msgstr "Zeige unvollständige To-Dos oben in der Liste"
|
||||||
|
|
||||||
msgid "Save geo-location with notes"
|
msgid "Save geo-location with notes"
|
||||||
msgstr "Momentanen Standort zusammen mit Notizen speichern"
|
msgstr "Momentanen Standort zusammen mit Notizen speichern"
|
||||||
@ -925,9 +993,6 @@ msgstr "Momentanen Standort zusammen mit Notizen speichern"
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr "Synchronisationsinterval"
|
msgstr "Synchronisationsinterval"
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "Deaktiviert"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr "%d Minuten"
|
msgstr "%d Minuten"
|
||||||
@ -954,11 +1019,11 @@ msgid ""
|
|||||||
"`sync.2.path` to specify the target directory."
|
"`sync.2.path` to specify the target directory."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem "
|
"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem "
|
||||||
"Dateisystem synchronisiert werden soll, setz den Wert zu `sync.2.path`, um "
|
"Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um "
|
||||||
"den Zielpfad zu spezifizieren."
|
"den Zielpfad zu spezifizieren."
|
||||||
|
|
||||||
msgid "Directory to synchronise with (absolute path)"
|
msgid "Directory to synchronise with (absolute path)"
|
||||||
msgstr ""
|
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The path to synchronise with when file system synchronisation is enabled. "
|
"The path to synchronise with when file system synchronisation is enabled. "
|
||||||
@ -972,15 +1037,14 @@ msgid "Invalid option value: \"%s\". Possible values are: %s."
|
|||||||
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
|
msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
|
||||||
|
|
||||||
msgid "Items that cannot be synchronised"
|
msgid "Items that cannot be synchronised"
|
||||||
msgstr ""
|
msgstr "Objekte können nicht synchronisiert werden"
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "\"%s\": \"%s\""
|
msgid "\"%s\": \"%s\""
|
||||||
msgstr "\"%s\": \"%s\""
|
msgstr "\"%s\": \"%s\""
|
||||||
|
|
||||||
msgid "Sync status (synced items / total items)"
|
msgid "Sync status (synced items / total items)"
|
||||||
msgstr ""
|
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
|
||||||
"Synchronisationsstatus (synchronisierte Notizen / vorhandenen Notizen )"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%s: %d/%d"
|
msgid "%s: %d/%d"
|
||||||
@ -992,11 +1056,11 @@ msgstr "Insgesamt: %d/%d"
|
|||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Conflicted: %d"
|
msgid "Conflicted: %d"
|
||||||
msgstr ""
|
msgstr "In Konflikt %d"
|
||||||
|
|
||||||
#, fuzzy, javascript-format
|
#, javascript-format
|
||||||
msgid "To delete: %d"
|
msgid "To delete: %d"
|
||||||
msgstr "Zu löschende Notizen: %d"
|
msgstr "Zu löschen: %d"
|
||||||
|
|
||||||
msgid "Folders"
|
msgid "Folders"
|
||||||
msgstr "Ordner"
|
msgstr "Ordner"
|
||||||
@ -1023,9 +1087,6 @@ msgstr "Sollen diese Notizen gelöscht werden?"
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "Log"
|
msgstr "Log"
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Status"
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr "Fehlerbreicht exportieren"
|
msgstr "Fehlerbreicht exportieren"
|
||||||
|
|
||||||
@ -1033,14 +1094,14 @@ msgid "Configuration"
|
|||||||
msgstr "Konfiguration"
|
msgstr "Konfiguration"
|
||||||
|
|
||||||
msgid "Move to notebook..."
|
msgid "Move to notebook..."
|
||||||
msgstr "Zu Notizbuch verschieben..."
|
msgstr "In Notizbuch verschieben..."
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "Move %d notes to notebook \"%s\"?"
|
msgid "Move %d notes to notebook \"%s\"?"
|
||||||
msgstr "%d Notizen zu dem Notizbuch \"%s\" verschieben?"
|
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
|
||||||
|
|
||||||
msgid "Select date"
|
msgid "Select date"
|
||||||
msgstr "Datum ausswählen"
|
msgstr "Datum auswählen"
|
||||||
|
|
||||||
msgid "Confirm"
|
msgid "Confirm"
|
||||||
msgstr "Bestätigen"
|
msgstr "Bestätigen"
|
||||||
@ -1075,10 +1136,10 @@ msgid "Attach any file"
|
|||||||
msgstr "Beliebige Datei anhängen"
|
msgstr "Beliebige Datei anhängen"
|
||||||
|
|
||||||
msgid "Convert to note"
|
msgid "Convert to note"
|
||||||
msgstr "Zu einer Notiz umwandeln"
|
msgstr "In eine Notiz umwandeln"
|
||||||
|
|
||||||
msgid "Convert to todo"
|
msgid "Convert to todo"
|
||||||
msgstr "Zu einem To-Do umwandeln"
|
msgstr "In ein To-Do umwandeln"
|
||||||
|
|
||||||
msgid "Hide metadata"
|
msgid "Hide metadata"
|
||||||
msgstr "Metadaten verstecken"
|
msgstr "Metadaten verstecken"
|
||||||
@ -1100,19 +1161,16 @@ msgid ""
|
|||||||
"menu to access your existing notebooks."
|
"menu to access your existing notebooks."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Drücke auf den (+) Knopf, um eine neue Notiz oder ein neues Notizbuch zu "
|
"Drücke auf den (+) Knopf, um eine neue Notiz oder ein neues Notizbuch zu "
|
||||||
"erstellen."
|
"erstellen. Tippe auf die Seitenleiste, um auf deine existierenden "
|
||||||
|
"Notizbücher zuzugreifen."
|
||||||
|
|
||||||
msgid "You currently have no notebook. Create one by clicking on (+) button."
|
msgid "You currently have no notebook. Create one by clicking on (+) button."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Du hast noch kein Notizbuch. Du kannst eines erstellen, indem du auf den (+) "
|
"Du hast noch kein Notizbuch. Erstelle eines, indem du auf den (+) Knopf "
|
||||||
"Knopf drückst."
|
"drückst."
|
||||||
|
|
||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
msgstr "Wilkommen"
|
msgstr "Willkommen"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
#~ msgid "Some items cannot be decrypted."
|
|
||||||
#~ msgstr "Kann Synchronisierer nicht initialisieren."
|
|
||||||
|
|
||||||
#~ msgid "Delete notebook?"
|
#~ msgid "Delete notebook?"
|
||||||
#~ msgstr "Notizbuch löschen?"
|
#~ msgstr "Notizbuch löschen?"
|
||||||
|
@ -559,6 +559,67 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -603,16 +664,10 @@ msgstr ""
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ID"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Source"
|
msgid "Set the password"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Created"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Updated"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
@ -659,6 +714,9 @@ msgstr ""
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -825,9 +883,6 @@ msgstr ""
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -915,9 +970,6 @@ msgstr ""
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -615,6 +615,70 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Creado: %d."
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "Actualizado: %d."
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Estatus"
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Deshabilitado"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Deshabilitado"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "Retroceder"
|
msgstr "Retroceder"
|
||||||
@ -662,19 +726,12 @@ msgstr "No se puede inicializar sincronizador."
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Created"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr "Creado: %d."
|
msgstr "No se puede inicializar sincronizador."
|
||||||
|
|
||||||
#, fuzzy
|
msgid "Set the password"
|
||||||
msgid "Updated"
|
msgstr ""
|
||||||
msgstr "Actualizado: %d."
|
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
msgstr "Agregar o borrar etiquetas"
|
msgstr "Agregar o borrar etiquetas"
|
||||||
@ -724,6 +781,9 @@ msgstr "Importar"
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr "Sincronización de objetivo"
|
msgstr "Sincronización de objetivo"
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr "Remover esta etiqueta de todas las notas?"
|
msgstr "Remover esta etiqueta de todas las notas?"
|
||||||
|
|
||||||
@ -907,9 +967,6 @@ msgstr "Guardar notas con geo-licalización"
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr "Intervalo de sincronización"
|
msgstr "Intervalo de sincronización"
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "Deshabilitado"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr "%d minutos"
|
msgstr "%d minutos"
|
||||||
@ -1006,9 +1063,6 @@ msgstr "Borrar estas notas?"
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "Log"
|
msgstr "Log"
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Estatus"
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr "Exportar reporte depuracion"
|
msgstr "Exportar reporte depuracion"
|
||||||
|
1151
CliClient/locales/es_ES.po
Normal file
1151
CliClient/locales/es_ES.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -611,6 +611,70 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Créés : %d."
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "Mis à jour : %d."
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "État"
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Désactivé"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Désactivé"
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "Retour"
|
msgstr "Retour"
|
||||||
|
|
||||||
@ -659,19 +723,12 @@ msgstr "Impossible d'initialiser la synchronisation."
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Created"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr "Créés : %d."
|
msgstr "Impossible d'initialiser la synchronisation."
|
||||||
|
|
||||||
#, fuzzy
|
msgid "Set the password"
|
||||||
msgid "Updated"
|
msgstr ""
|
||||||
msgstr "Mis à jour : %d."
|
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
msgstr "Gérer les étiquettes"
|
msgstr "Gérer les étiquettes"
|
||||||
@ -723,6 +780,9 @@ msgstr "Importer"
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr "Cible de la synchronisation"
|
msgstr "Cible de la synchronisation"
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr "Enlever cette étiquette de toutes les notes ?"
|
msgstr "Enlever cette étiquette de toutes les notes ?"
|
||||||
|
|
||||||
@ -894,9 +954,6 @@ msgstr "Enregistrer l'emplacement avec les notes"
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr "Intervalle de synchronisation"
|
msgstr "Intervalle de synchronisation"
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "Désactivé"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr "%d minutes"
|
msgstr "%d minutes"
|
||||||
@ -990,9 +1047,6 @@ msgstr "Supprimer ces notes ?"
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "Journal"
|
msgstr "Journal"
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "État"
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr "Exporter rapport de débogage"
|
msgstr "Exporter rapport de débogage"
|
||||||
|
|
||||||
@ -1077,10 +1131,6 @@ msgstr ""
|
|||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
msgstr "Bienvenue"
|
msgstr "Bienvenue"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
#~ msgid "Some items cannot be decrypted."
|
|
||||||
#~ msgstr "Impossible d'initialiser la synchronisation."
|
|
||||||
|
|
||||||
#~ msgid "Delete notebook?"
|
#~ msgid "Delete notebook?"
|
||||||
#~ msgstr "Supprimer le carnet ?"
|
#~ msgstr "Supprimer le carnet ?"
|
||||||
|
|
||||||
|
1147
CliClient/locales/hr_HR.po
Normal file
1147
CliClient/locales/hr_HR.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -615,6 +615,70 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Creato: %d."
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "Aggiornato: %d."
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Stato"
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Disabilitato"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Disabilitato"
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "Indietro"
|
msgstr "Indietro"
|
||||||
|
|
||||||
@ -659,19 +723,12 @@ msgstr "Alcuni elementi non possono essere sincronizzati."
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr "Mostrali ora"
|
msgstr "Mostrali ora"
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Created"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr "Creato: %d."
|
msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||||
|
|
||||||
#, fuzzy
|
msgid "Set the password"
|
||||||
msgid "Updated"
|
msgstr ""
|
||||||
msgstr "Aggiornato: %d."
|
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
msgstr "Aggiungi o rimuovi etichetta"
|
msgstr "Aggiungi o rimuovi etichetta"
|
||||||
@ -718,6 +775,9 @@ msgstr "Importa"
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr "Stato della Sincronizzazione"
|
msgstr "Stato della Sincronizzazione"
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr "Rimuovere questa etichetta da tutte le note?"
|
msgstr "Rimuovere questa etichetta da tutte le note?"
|
||||||
|
|
||||||
@ -895,9 +955,6 @@ msgstr "Salva geo-localizzazione con le note"
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr "Intervallo di sincronizzazione"
|
msgstr "Intervallo di sincronizzazione"
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "Disabilitato"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr "%d minuti"
|
msgstr "%d minuti"
|
||||||
@ -990,9 +1047,6 @@ msgstr "Cancellare queste note?"
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "Log"
|
msgstr "Log"
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Stato"
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr "Esporta il Report di Debug"
|
msgstr "Esporta il Report di Debug"
|
||||||
|
|
||||||
@ -1077,10 +1131,6 @@ msgstr ""
|
|||||||
msgid "Welcome"
|
msgid "Welcome"
|
||||||
msgstr "Benvenuto"
|
msgstr "Benvenuto"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
#~ msgid "Some items cannot be decrypted."
|
|
||||||
#~ msgstr "Alcuni elementi non possono essere sincronizzati."
|
|
||||||
|
|
||||||
#~ msgid "Delete notebook?"
|
#~ msgid "Delete notebook?"
|
||||||
#~ msgstr "Eliminare il blocco note?"
|
#~ msgstr "Eliminare il blocco note?"
|
||||||
|
|
||||||
|
1134
CliClient/locales/ja_JP.po
Normal file
1134
CliClient/locales/ja_JP.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -559,6 +559,67 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Created"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -603,16 +664,10 @@ msgstr ""
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ID"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Source"
|
msgid "Set the password"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Created"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Updated"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
@ -659,6 +714,9 @@ msgstr ""
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -825,9 +883,6 @@ msgstr ""
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -915,9 +970,6 @@ msgstr ""
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -610,6 +610,70 @@ msgstr ""
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||||
|
"continue?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||||
|
"re-synchronised and sent encrypted to the sync target. Do not lose the "
|
||||||
|
"password as, for security purposes, this will be the *only* way to decrypt "
|
||||||
|
"the data! To enable encryption, please enter your password below."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Disable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Enable encryption"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Master Keys"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Source"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Created"
|
||||||
|
msgstr "Criado: %d."
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Updated"
|
||||||
|
msgstr "Atualizado: %d."
|
||||||
|
|
||||||
|
msgid "Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Password OK"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Note: Only one master key is going to be used for encryption (the one marked "
|
||||||
|
"as \"active\"). Any of the keys might be used for decryption, depending on "
|
||||||
|
"how the notes or notebooks were originally encrypted."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Status"
|
||||||
|
|
||||||
|
msgid "Encryption is:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, fuzzy
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr "Desabilitado"
|
||||||
|
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr "Desabilitado"
|
||||||
|
|
||||||
msgid "Back"
|
msgid "Back"
|
||||||
msgstr "Voltar"
|
msgstr "Voltar"
|
||||||
|
|
||||||
@ -656,19 +720,12 @@ msgstr "Não é possível inicializar o sincronizador."
|
|||||||
msgid "View them now"
|
msgid "View them now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Source"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Created"
|
msgid "Some items cannot be decrypted."
|
||||||
msgstr "Criado: %d."
|
msgstr "Não é possível inicializar o sincronizador."
|
||||||
|
|
||||||
#, fuzzy
|
msgid "Set the password"
|
||||||
msgid "Updated"
|
msgstr ""
|
||||||
msgstr "Atualizado: %d."
|
|
||||||
|
|
||||||
msgid "Add or remove tags"
|
msgid "Add or remove tags"
|
||||||
msgstr "Adicionar ou remover tags"
|
msgstr "Adicionar ou remover tags"
|
||||||
@ -716,6 +773,9 @@ msgstr "Importar"
|
|||||||
msgid "Synchronisation Status"
|
msgid "Synchronisation Status"
|
||||||
msgstr "Alvo de sincronização"
|
msgstr "Alvo de sincronização"
|
||||||
|
|
||||||
|
msgid "Encryption Options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove this tag from all the notes?"
|
msgid "Remove this tag from all the notes?"
|
||||||
msgstr "Remover esta tag de todas as notas?"
|
msgstr "Remover esta tag de todas as notas?"
|
||||||
|
|
||||||
@ -894,9 +954,6 @@ msgstr "Salvar geolocalização com notas"
|
|||||||
msgid "Synchronisation interval"
|
msgid "Synchronisation interval"
|
||||||
msgstr "Intervalo de sincronização"
|
msgstr "Intervalo de sincronização"
|
||||||
|
|
||||||
msgid "Disabled"
|
|
||||||
msgstr "Desabilitado"
|
|
||||||
|
|
||||||
#, javascript-format
|
#, javascript-format
|
||||||
msgid "%d minutes"
|
msgid "%d minutes"
|
||||||
msgstr "%d minutos"
|
msgstr "%d minutos"
|
||||||
@ -988,9 +1045,6 @@ msgstr "Excluir estas notas?"
|
|||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "Log"
|
msgstr "Log"
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Status"
|
|
||||||
|
|
||||||
msgid "Export Debug Report"
|
msgid "Export Debug Report"
|
||||||
msgstr "Exportar Relatório de Debug"
|
msgstr "Exportar Relatório de Debug"
|
||||||
|
|
||||||
|
1134
CliClient/locales/ru_RU.po
Normal file
1134
CliClient/locales/ru_RU.po
Normal file
File diff suppressed because it is too large
Load Diff
1093
CliClient/locales/zh_CN.po
Normal file
1093
CliClient/locales/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
16
CliClient/package-lock.json
generated
16
CliClient/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "joplin",
|
"name": "joplin",
|
||||||
"version": "0.10.83",
|
"version": "0.10.86",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -411,12 +411,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fs-extra": {
|
"fs-extra": {
|
||||||
"version": "3.0.1",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||||
"integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
|
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"graceful-fs": "4.1.11",
|
"graceful-fs": "4.1.11",
|
||||||
"jsonfile": "3.0.1",
|
"jsonfile": "4.0.0",
|
||||||
"universalify": "0.1.1"
|
"universalify": "0.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -728,9 +728,9 @@
|
|||||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
"version": "3.0.1",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
"integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
|
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"graceful-fs": "4.1.11"
|
"graceful-fs": "4.1.11"
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,12 @@
|
|||||||
"title": "Joplin CLI",
|
"title": "Joplin CLI",
|
||||||
"years": [
|
"years": [
|
||||||
2016,
|
2016,
|
||||||
2017
|
2017,
|
||||||
|
2018
|
||||||
],
|
],
|
||||||
"owner": "Laurent Cozic"
|
"owner": "Laurent Cozic"
|
||||||
},
|
},
|
||||||
"version": "0.10.83",
|
"version": "0.10.86",
|
||||||
"bin": {
|
"bin": {
|
||||||
"joplin": "./main.js"
|
"joplin": "./main.js"
|
||||||
},
|
},
|
||||||
@ -29,7 +30,7 @@
|
|||||||
"app-module-path": "^2.2.0",
|
"app-module-path": "^2.2.0",
|
||||||
"follow-redirects": "^1.2.4",
|
"follow-redirects": "^1.2.4",
|
||||||
"form-data": "^2.1.4",
|
"form-data": "^2.1.4",
|
||||||
"fs-extra": "^3.0.1",
|
"fs-extra": "^5.0.0",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.2.1",
|
||||||
"jssha": "^2.3.0",
|
"jssha": "^2.3.0",
|
||||||
"levenshtein": "^1.0.5",
|
"levenshtein": "^1.0.5",
|
||||||
|
@ -4,4 +4,4 @@ CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|||||||
|
|
||||||
bash "$CLIENT_DIR/build.sh" && node "$CLIENT_DIR/build/main.js" --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
bash "$CLIENT_DIR/build.sh" && node "$CLIENT_DIR/build/main.js" --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
||||||
|
|
||||||
#bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/Temp/TestNotes2 --stack-trace-enabled --log-level debug --env dev "$@"
|
# bash $CLIENT_DIR/build.sh && NODE_PATH="$CLIENT_DIR/build/" node build/main.js --profile ~/.config/joplin --stack-trace-enabled --log-level debug "$@"
|
@ -1,10 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
BUILD_DIR="$ROOT_DIR/tests-build"
|
BUILD_DIR="$ROOT_DIR/tests-build"
|
||||||
|
TEST_FILE="$1"
|
||||||
|
|
||||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
|
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
|
||||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||||
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||||
mkdir -p "$BUILD_DIR/data"
|
mkdir -p "$BUILD_DIR/data"
|
||||||
|
|
||||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
|
if [[ $TEST_FILE == "" ]]; then
|
||||||
|
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
|
||||||
|
(cd "$ROOT_DIR" && npm test tests-build/encryption.js)
|
||||||
|
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
|
||||||
|
else
|
||||||
|
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
|
||||||
|
fi
|
32
CliClient/tests/ArrayUtils.js
Normal file
32
CliClient/tests/ArrayUtils.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||||
|
const ArrayUtils = require('lib/ArrayUtils.js');
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Encryption', function() {
|
||||||
|
|
||||||
|
beforeEach(async (done) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove array elements', async (done) => {
|
||||||
|
let a = ['un', 'deux', 'trois'];
|
||||||
|
a = ArrayUtils.removeElement(a, 'deux');
|
||||||
|
|
||||||
|
expect(a[0]).toBe('un');
|
||||||
|
expect(a[1]).toBe('trois');
|
||||||
|
expect(a.length).toBe(2);
|
||||||
|
|
||||||
|
a = ['un', 'deux', 'trois'];
|
||||||
|
a = ArrayUtils.removeElement(a, 'not in there');
|
||||||
|
expect(a.length).toBe(3);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
180
CliClient/tests/encryption.js
Normal file
180
CliClient/tests/encryption.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||||
|
const Folder = require('lib/models/Folder.js');
|
||||||
|
const Note = require('lib/models/Note.js');
|
||||||
|
const Tag = require('lib/models/Tag.js');
|
||||||
|
const { Database } = require('lib/database.js');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
|
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; // The first test is slow because the database needs to be built
|
||||||
|
|
||||||
|
let service = null;
|
||||||
|
|
||||||
|
describe('Encryption', function() {
|
||||||
|
|
||||||
|
beforeEach(async (done) => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
//await setupDatabaseAndSynchronizer(2);
|
||||||
|
//await switchClient(1);
|
||||||
|
service = new EncryptionService();
|
||||||
|
BaseItem.encryptionService_ = service;
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encode and decode header', async (done) => {
|
||||||
|
const header = {
|
||||||
|
encryptionMethod: EncryptionService.METHOD_SJCL,
|
||||||
|
masterKeyId: '01234568abcdefgh01234568abcdefgh',
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedHeader = service.encodeHeader_(header);
|
||||||
|
const decodedHeader = service.decodeHeader_(encodedHeader);
|
||||||
|
delete decodedHeader.length;
|
||||||
|
|
||||||
|
expect(objectsEqual(header, decodedHeader)).toBe(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate and decrypt a master key', async (done) => {
|
||||||
|
const masterKey = await service.generateMasterKey('123456');
|
||||||
|
expect(!!masterKey.checksum).toBe(true);
|
||||||
|
expect(!!masterKey.content).toBe(true);
|
||||||
|
|
||||||
|
let hasThrown = false;
|
||||||
|
try {
|
||||||
|
await service.decryptMasterKey(masterKey, 'wrongpassword');
|
||||||
|
} catch (error) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(hasThrown).toBe(true);
|
||||||
|
|
||||||
|
const decryptedMasterKey = await service.decryptMasterKey(masterKey, '123456');
|
||||||
|
expect(decryptedMasterKey.length).toBe(512);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt and decrypt with a master key', async (done) => {
|
||||||
|
let masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
const cipherText = await service.encryptString('some secret');
|
||||||
|
const plainText = await service.decryptString(cipherText);
|
||||||
|
|
||||||
|
expect(plainText).toBe('some secret');
|
||||||
|
|
||||||
|
// Test that a long string, that is going to be split into multiple chunks, encrypt
|
||||||
|
// and decrypt properly too.
|
||||||
|
let veryLongSecret = '';
|
||||||
|
for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9);
|
||||||
|
|
||||||
|
const cipherText2 = await service.encryptString(veryLongSecret);
|
||||||
|
const plainText2 = await service.decryptString(cipherText2);
|
||||||
|
|
||||||
|
expect(plainText2 === veryLongSecret).toBe(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to decrypt if master key not present', async (done) => {
|
||||||
|
let masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
const cipherText = await service.encryptString('some secret');
|
||||||
|
|
||||||
|
await service.unloadMasterKey(masterKey);
|
||||||
|
|
||||||
|
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
|
||||||
|
|
||||||
|
expect(hasThrown).toBe(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail to decrypt if data tampered with', async (done) => {
|
||||||
|
let masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
let cipherText = await service.encryptString('some secret');
|
||||||
|
cipherText += "ABCDEFGHIJ";
|
||||||
|
|
||||||
|
let hasThrown = await checkThrowAsync(async () => await service.decryptString(cipherText));
|
||||||
|
|
||||||
|
expect(hasThrown).toBe(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt and decrypt notes and folders', async (done) => {
|
||||||
|
let masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
let folder = await Folder.save({ title: 'folder' });
|
||||||
|
let note = await Note.save({ title: 'encrypted note', body: 'something', parent_id: folder.id });
|
||||||
|
let serialized = await Note.serializeForSync(note);
|
||||||
|
let deserialized = Note.filter(await Note.unserialize(serialized));
|
||||||
|
|
||||||
|
// Check that required properties are not encrypted
|
||||||
|
expect(deserialized.id).toBe(note.id);
|
||||||
|
expect(deserialized.parent_id).toBe(note.parent_id);
|
||||||
|
expect(deserialized.updated_time).toBe(note.updated_time);
|
||||||
|
|
||||||
|
// Check that at least title and body are encrypted
|
||||||
|
expect(!deserialized.title).toBe(true);
|
||||||
|
expect(!deserialized.body).toBe(true);
|
||||||
|
|
||||||
|
// Check that encrypted data is there
|
||||||
|
expect(!!deserialized.encryption_cipher_text).toBe(true);
|
||||||
|
|
||||||
|
encryptedNote = await Note.save(deserialized);
|
||||||
|
decryptedNote = await Note.decrypt(encryptedNote);
|
||||||
|
|
||||||
|
expect(decryptedNote.title).toBe(note.title);
|
||||||
|
expect(decryptedNote.body).toBe(note.body);
|
||||||
|
expect(decryptedNote.id).toBe(note.id);
|
||||||
|
expect(decryptedNote.parent_id).toBe(note.parent_id);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should encrypt and decrypt files', async (done) => {
|
||||||
|
let masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
const sourcePath = __dirname + '/../tests/support/photo.jpg';
|
||||||
|
const encryptedPath = __dirname + '/data/photo.crypted';
|
||||||
|
const decryptedPath = __dirname + '/data/photo.jpg';
|
||||||
|
|
||||||
|
await service.encryptFile(sourcePath, encryptedPath);
|
||||||
|
await service.decryptFile(encryptedPath, decryptedPath);
|
||||||
|
|
||||||
|
expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false);
|
||||||
|
expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
BIN
CliClient/tests/support/photo.jpg
Normal file
BIN
CliClient/tests/support/photo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
@ -1,14 +1,18 @@
|
|||||||
require('app-module-path').addPath(__dirname);
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = require('test-utils.js');
|
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const fs = require('fs-extra');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
|
const Note = require('lib/models/Note.js');
|
||||||
|
const Resource = require('lib/models/Resource.js');
|
||||||
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { Database } = require('lib/database.js');
|
const { Database } = require('lib/database.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
@ -23,6 +27,38 @@ async function allItems() {
|
|||||||
return folders.concat(notes);
|
return folders.concat(notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function allSyncTargetItemsEncrypted() {
|
||||||
|
const list = await fileApi().list();
|
||||||
|
const files = list.items;
|
||||||
|
|
||||||
|
//console.info(Setting.value('resourceDir'));
|
||||||
|
|
||||||
|
let totalCount = 0;
|
||||||
|
let encryptedCount = 0;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const remoteContentString = await fileApi().get(file.path);
|
||||||
|
const remoteContent = await BaseItem.unserialize(remoteContentString);
|
||||||
|
const ItemClass = BaseItem.itemClass(remoteContent);
|
||||||
|
|
||||||
|
if (!ItemClass.encryptionSupported()) continue;
|
||||||
|
|
||||||
|
totalCount++;
|
||||||
|
|
||||||
|
if (remoteContent.type_ === BaseModel.TYPE_RESOURCE) {
|
||||||
|
const content = await fileApi().get('.resource/' + remoteContent.id);
|
||||||
|
totalCount++;
|
||||||
|
if (content.substr(0, 5) === 'JED01') output = encryptedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!remoteContent.encryption_applied) encryptedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!totalCount) throw new Error('No encryptable item on sync target');
|
||||||
|
|
||||||
|
return totalCount === encryptedCount;
|
||||||
|
}
|
||||||
|
|
||||||
async function localItemsSameAsRemote(locals, expect) {
|
async function localItemsSameAsRemote(locals, expect) {
|
||||||
try {
|
try {
|
||||||
let files = await fileApi().list();
|
let files = await fileApi().list();
|
||||||
@ -53,16 +89,22 @@ async function localItemsSameAsRemote(locals, expect) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let insideBeforeEach = false;
|
||||||
|
|
||||||
describe('Synchronizer', function() {
|
describe('Synchronizer', function() {
|
||||||
|
|
||||||
beforeEach( async (done) => {
|
beforeEach(async (done) => {
|
||||||
|
insideBeforeEach = true;
|
||||||
|
|
||||||
await setupDatabaseAndSynchronizer(1);
|
await setupDatabaseAndSynchronizer(1);
|
||||||
await setupDatabaseAndSynchronizer(2);
|
await setupDatabaseAndSynchronizer(2);
|
||||||
await switchClient(1);
|
await switchClient(1);
|
||||||
done();
|
done();
|
||||||
|
|
||||||
|
insideBeforeEach = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create remote items', async (done) => {
|
it('should create remote items', asyncTest(async () => {
|
||||||
let folder = await Folder.save({ title: "folder1" });
|
let folder = await Folder.save({ title: "folder1" });
|
||||||
await Note.save({ title: "un", parent_id: folder.id });
|
await Note.save({ title: "un", parent_id: folder.id });
|
||||||
|
|
||||||
@ -71,11 +113,9 @@ describe('Synchronizer', function() {
|
|||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
|
||||||
await localItemsSameAsRemote(all, expect);
|
await localItemsSameAsRemote(all, expect);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should update remote item', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should update remote item', async (done) => {
|
|
||||||
let folder = await Folder.save({ title: "folder1" });
|
let folder = await Folder.save({ title: "folder1" });
|
||||||
let note = await Note.save({ title: "un", parent_id: folder.id });
|
let note = await Note.save({ title: "un", parent_id: folder.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -86,11 +126,9 @@ describe('Synchronizer', function() {
|
|||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
|
||||||
await localItemsSameAsRemote(all, expect);
|
await localItemsSameAsRemote(all, expect);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should create local items', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should create local items', async (done) => {
|
|
||||||
let folder = await Folder.save({ title: "folder1" });
|
let folder = await Folder.save({ title: "folder1" });
|
||||||
await Note.save({ title: "un", parent_id: folder.id });
|
await Note.save({ title: "un", parent_id: folder.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -102,11 +140,9 @@ describe('Synchronizer', function() {
|
|||||||
let all = await allItems();
|
let all = await allItems();
|
||||||
|
|
||||||
await localItemsSameAsRemote(all, expect);
|
await localItemsSameAsRemote(all, expect);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should update local items', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should update local items', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -131,11 +167,9 @@ describe('Synchronizer', function() {
|
|||||||
let all = await allItems();
|
let all = await allItems();
|
||||||
|
|
||||||
await localItemsSameAsRemote(all, expect);
|
await localItemsSameAsRemote(all, expect);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should resolve note conflicts', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve note conflicts', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -174,11 +208,9 @@ describe('Synchronizer', function() {
|
|||||||
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
||||||
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should resolve folders conflicts', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve folders conflicts', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -209,11 +241,9 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
let folder1_final = await Folder.load(folder1.id);
|
let folder1_final = await Folder.load(folder1.id);
|
||||||
expect(folder1_final.title).toBe(folder1_modRemote.title);
|
expect(folder1_final.title).toBe(folder1_modRemote.title);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should delete remote notes', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete remote notes', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -236,11 +266,9 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||||
expect(deletedItems.length).toBe(0);
|
expect(deletedItems.length).toBe(0);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should delete local notes', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete local notes', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -258,11 +286,9 @@ describe('Synchronizer', function() {
|
|||||||
expect(items.length).toBe(1);
|
expect(items.length).toBe(1);
|
||||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||||
expect(deletedItems.length).toBe(0);
|
expect(deletedItems.length).toBe(0);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete remote folder', async (done) => {
|
it('should delete remote folder', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let folder2 = await Folder.save({ title: "folder2" });
|
let folder2 = await Folder.save({ title: "folder2" });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -279,11 +305,9 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
let all = await allItems();
|
let all = await allItems();
|
||||||
localItemsSameAsRemote(all, expect);
|
localItemsSameAsRemote(all, expect);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete local folder', async (done) => {
|
it('should delete local folder', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let folder2 = await Folder.save({ title: "folder2" });
|
let folder2 = await Folder.save({ title: "folder2" });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -304,11 +328,9 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
let items = await allItems();
|
let items = await allItems();
|
||||||
localItemsSameAsRemote(items, expect);
|
localItemsSameAsRemote(items, expect);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', async (done) => {
|
it('should resolve conflict if remote folder has been deleted, but note has been added to folder locally', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
|
||||||
@ -326,11 +348,9 @@ describe('Synchronizer', function() {
|
|||||||
expect(items.length).toBe(1);
|
expect(items.length).toBe(1);
|
||||||
expect(items[0].title).toBe('note1');
|
expect(items[0].title).toBe('note1');
|
||||||
expect(items[0].is_conflict).toBe(1);
|
expect(items[0].is_conflict).toBe(1);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should resolve conflict if note has been deleted remotely and locally', async (done) => {
|
it('should resolve conflict if note has been deleted remotely and locally', asyncTest(async () => {
|
||||||
let folder = await Folder.save({ title: "folder" });
|
let folder = await Folder.save({ title: "folder" });
|
||||||
let note = await Note.save({ title: "note", parent_id: folder.title });
|
let note = await Note.save({ title: "note", parent_id: folder.title });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -351,11 +371,9 @@ describe('Synchronizer', function() {
|
|||||||
expect(items[0].title).toBe('folder');
|
expect(items[0].title).toBe('folder');
|
||||||
|
|
||||||
localItemsSameAsRemote(items, expect);
|
localItemsSameAsRemote(items, expect);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cross delete all folders', async (done) => {
|
it('should cross delete all folders', asyncTest(async () => {
|
||||||
// If client1 and 2 have two folders, client 1 deletes item 1 and client
|
// If client1 and 2 have two folders, client 1 deletes item 1 and client
|
||||||
// 2 deletes item 2, they should both end up with no items after sync.
|
// 2 deletes item 2, they should both end up with no items after sync.
|
||||||
|
|
||||||
@ -391,11 +409,9 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
expect(items1.length).toBe(0);
|
expect(items1.length).toBe(0);
|
||||||
expect(items1.length).toBe(items2.length);
|
expect(items1.length).toBe(items2.length);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle conflict when remote note is deleted then local note is modified', async (done) => {
|
it('should handle conflict when remote note is deleted then local note is modified', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -425,11 +441,9 @@ describe('Synchronizer', function() {
|
|||||||
let unconflictedNotes = await Note.unconflictedNotes();
|
let unconflictedNotes = await Note.unconflictedNotes();
|
||||||
|
|
||||||
expect(unconflictedNotes.length).toBe(0);
|
expect(unconflictedNotes.length).toBe(0);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle conflict when remote folder is deleted then local folder is renamed', async (done) => {
|
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let folder2 = await Folder.save({ title: "folder2" });
|
let folder2 = await Folder.save({ title: "folder2" });
|
||||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||||
@ -457,11 +471,9 @@ describe('Synchronizer', function() {
|
|||||||
let items = await allItems();
|
let items = await allItems();
|
||||||
|
|
||||||
expect(items.length).toBe(1);
|
expect(items.length).toBe(1);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should allow duplicate folder titles', async (done) => {
|
it('should allow duplicate folder titles', asyncTest(async () => {
|
||||||
let localF1 = await Folder.save({ title: "folder" });
|
let localF1 = await Folder.save({ title: "folder" });
|
||||||
|
|
||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
@ -493,11 +505,15 @@ describe('Synchronizer', function() {
|
|||||||
remoteF2 = await Folder.load(remoteF2.id);
|
remoteF2 = await Folder.load(remoteF2.id);
|
||||||
|
|
||||||
expect(remoteF2.title == localF2.title).toBe(true);
|
expect(remoteF2.title == localF2.title).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
async function shoudSyncTagTest(withEncryption) {
|
||||||
});
|
let masterKey = null;
|
||||||
|
if (withEncryption) {
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
masterKey = await loadEncryptionMasterKey();
|
||||||
|
}
|
||||||
|
|
||||||
it('should sync tags', async (done) => {
|
|
||||||
let f1 = await Folder.save({ title: "folder" });
|
let f1 = await Folder.save({ title: "folder" });
|
||||||
let n1 = await Note.save({ title: "mynote" });
|
let n1 = await Note.save({ title: "mynote" });
|
||||||
let n2 = await Note.save({ title: "mynote2" });
|
let n2 = await Note.save({ title: "mynote2" });
|
||||||
@ -507,6 +523,12 @@ describe('Synchronizer', function() {
|
|||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
|
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
if (withEncryption) {
|
||||||
|
const masterKey_2 = await MasterKey.load(masterKey.id);
|
||||||
|
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||||
|
let t = await Tag.load(tag.id);
|
||||||
|
await Tag.decrypt(t);
|
||||||
|
}
|
||||||
let remoteTag = await Tag.loadByTitle(tag.title);
|
let remoteTag = await Tag.loadByTitle(tag.title);
|
||||||
expect(!!remoteTag).toBe(true);
|
expect(!!remoteTag).toBe(true);
|
||||||
expect(remoteTag.id).toBe(tag.id);
|
expect(remoteTag.id).toBe(tag.id);
|
||||||
@ -532,11 +554,15 @@ describe('Synchronizer', function() {
|
|||||||
noteIds = await Tag.noteIds(tag.id);
|
noteIds = await Tag.noteIds(tag.id);
|
||||||
expect(noteIds.length).toBe(1);
|
expect(noteIds.length).toBe(1);
|
||||||
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
expect(remoteNoteIds[0]).toBe(noteIds[0]);
|
||||||
|
}
|
||||||
|
|
||||||
done();
|
it('should sync tags', asyncTest(async () => {
|
||||||
});
|
await shoudSyncTagTest(false); }));
|
||||||
|
|
||||||
it('should not sync notes with conflicts', async (done) => {
|
it('should sync encrypted tags', asyncTest(async () => {
|
||||||
|
await shoudSyncTagTest(true); }));
|
||||||
|
|
||||||
|
it('should not sync notes with conflicts', asyncTest(async () => {
|
||||||
let f1 = await Folder.save({ title: "folder" });
|
let f1 = await Folder.save({ title: "folder" });
|
||||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
|
let n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -548,11 +574,9 @@ describe('Synchronizer', function() {
|
|||||||
let folders = await Folder.all()
|
let folders = await Folder.all()
|
||||||
expect(notes.length).toBe(0);
|
expect(notes.length).toBe(0);
|
||||||
expect(folders.length).toBe(1);
|
expect(folders.length).toBe(1);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('should not try to delete on remote conflicted notes that have been deleted', async (done) => {
|
|
||||||
let f1 = await Folder.save({ title: "folder" });
|
let f1 = await Folder.save({ title: "folder" });
|
||||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
|
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -565,17 +589,13 @@ describe('Synchronizer', function() {
|
|||||||
const deletedItems = await BaseItem.deletedItems(syncTargetId());
|
const deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||||
|
|
||||||
expect(deletedItems.length).toBe(0);
|
expect(deletedItems.length).toBe(0);
|
||||||
|
}));
|
||||||
done();
|
|
||||||
});
|
async function ignorableNoteConflictTest(withEncryption) {
|
||||||
|
if (withEncryption) {
|
||||||
it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => {
|
Setting.setValue('encryption.enabled', true);
|
||||||
// That was previously a common conflict:
|
await loadEncryptionMasterKey();
|
||||||
// - Client 1 mark todo as "done", and sync
|
}
|
||||||
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
|
|
||||||
// In theory it is a conflict because the todo_completed dates are different
|
|
||||||
// but in practice it doesn't matter, we can just take the date when the
|
|
||||||
// todo was marked as "done" the first time.
|
|
||||||
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||||
@ -584,6 +604,10 @@ describe('Synchronizer', function() {
|
|||||||
await switchClient(2);
|
await switchClient(2);
|
||||||
|
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
if (withEncryption) {
|
||||||
|
await loadEncryptionMasterKey(null, true);
|
||||||
|
await decryptionWorker().start();
|
||||||
|
}
|
||||||
let note2 = await Note.load(note1.id);
|
let note2 = await Note.load(note1.id);
|
||||||
note2.todo_completed = time.unixMs()-1;
|
note2.todo_completed = time.unixMs()-1;
|
||||||
await Note.save(note2);
|
await Note.save(note2);
|
||||||
@ -598,18 +622,43 @@ describe('Synchronizer', function() {
|
|||||||
note2conf = await Note.load(note1.id);
|
note2conf = await Note.load(note1.id);
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
|
|
||||||
let conflictedNotes = await Note.conflictedNotes();
|
if (!withEncryption) {
|
||||||
expect(conflictedNotes.length).toBe(0);
|
// That was previously a common conflict:
|
||||||
|
// - Client 1 mark todo as "done", and sync
|
||||||
|
// - Client 2 doesn't sync, mark todo as "done" todo. Then sync.
|
||||||
|
// In theory it is a conflict because the todo_completed dates are different
|
||||||
|
// but in practice it doesn't matter, we can just take the date when the
|
||||||
|
// todo was marked as "done" the first time.
|
||||||
|
|
||||||
let notes = await Note.all();
|
let conflictedNotes = await Note.conflictedNotes();
|
||||||
expect(notes.length).toBe(1);
|
expect(conflictedNotes.length).toBe(0);
|
||||||
expect(notes[0].id).toBe(note1.id);
|
|
||||||
expect(notes[0].todo_completed).toBe(note2.todo_completed);
|
|
||||||
|
|
||||||
done();
|
let notes = await Note.all();
|
||||||
});
|
expect(notes.length).toBe(1);
|
||||||
|
expect(notes[0].id).toBe(note1.id);
|
||||||
|
expect(notes[0].todo_completed).toBe(note2.todo_completed);
|
||||||
|
} else {
|
||||||
|
// If the notes are encrypted however it's not possible to do this kind of
|
||||||
|
// smart conflict resolving since we don't know the content, so in that
|
||||||
|
// case it's handled as a regular conflict.
|
||||||
|
|
||||||
it('items should be downloaded again when user cancels in the middle of delta operation', async (done) => {
|
let conflictedNotes = await Note.conflictedNotes();
|
||||||
|
expect(conflictedNotes.length).toBe(1);
|
||||||
|
|
||||||
|
let notes = await Note.all();
|
||||||
|
expect(notes.length).toBe(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not consider it is a conflict if neither the title nor body of the note have changed', asyncTest(async () => {
|
||||||
|
await ignorableNoteConflictTest(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should always handle conflict if local or remote are encrypted', asyncTest(async () => {
|
||||||
|
await ignorableNoteConflictTest(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('items should be downloaded again when user cancels in the middle of delta operation', asyncTest(async () => {
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
@ -625,19 +674,17 @@ describe('Synchronizer', function() {
|
|||||||
await synchronizer().start({ context: context });
|
await synchronizer().start({ context: context });
|
||||||
notes = await Note.all();
|
notes = await Note.all();
|
||||||
expect(notes.length).toBe(1);
|
expect(notes.length).toBe(1);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('should skip items that cannot be synced', asyncTest(async () => {
|
||||||
});
|
|
||||||
|
|
||||||
it('items should skip items that cannot be synced', async (done) => {
|
|
||||||
let folder1 = await Folder.save({ title: "folder1" });
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||||
const noteId = note1.id;
|
const noteId = note1.id;
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
let disabledItems = await BaseItem.syncDisabledItems();
|
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||||
expect(disabledItems.length).toBe(0);
|
expect(disabledItems.length).toBe(0);
|
||||||
await Note.save({ id: noteId, title: "un mod", });
|
await Note.save({ id: noteId, title: "un mod", });
|
||||||
synchronizer().debugFlags_ = ['cannotSync'];
|
synchronizer().debugFlags_ = ['rejectedByTarget'];
|
||||||
await synchronizer().start();
|
await synchronizer().start();
|
||||||
synchronizer().debugFlags_ = [];
|
synchronizer().debugFlags_ = [];
|
||||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||||
@ -651,10 +698,269 @@ describe('Synchronizer', function() {
|
|||||||
|
|
||||||
await switchClient(1);
|
await switchClient(1);
|
||||||
|
|
||||||
disabledItems = await BaseItem.syncDisabledItems();
|
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||||
expect(disabledItems.length).toBe(1);
|
expect(disabledItems.length).toBe(1);
|
||||||
|
}));
|
||||||
|
|
||||||
done();
|
it('notes and folders should get encrypted when encryption is enabled', asyncTest(async () => {
|
||||||
});
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: "un", body: 'to be encrypted', parent_id: folder1.id });
|
||||||
|
await synchronizer().start();
|
||||||
|
// After synchronisation, remote items should be encrypted but local ones remain plain text
|
||||||
|
note1 = await Note.load(note1.id);
|
||||||
|
expect(note1.title).toBe('un');
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
let folder1_2 = await Folder.load(folder1.id);
|
||||||
|
let note1_2 = await Note.load(note1.id);
|
||||||
|
let masterKey_2 = await MasterKey.load(masterKey.id);
|
||||||
|
// On this side however it should be received encrypted
|
||||||
|
expect(!note1_2.title).toBe(true);
|
||||||
|
expect(!folder1_2.title).toBe(true);
|
||||||
|
expect(!!note1_2.encryption_cipher_text).toBe(true);
|
||||||
|
expect(!!folder1_2.encryption_cipher_text).toBe(true);
|
||||||
|
// Master key is already encrypted so it does not get re-encrypted during sync
|
||||||
|
expect(masterKey_2.content).toBe(masterKey.content);
|
||||||
|
expect(masterKey_2.checksum).toBe(masterKey.checksum);
|
||||||
|
// Now load the master key we got from client 1 and try to decrypt
|
||||||
|
await encryptionService().loadMasterKey(masterKey_2, '123456', true);
|
||||||
|
// Get the decrypted items back
|
||||||
|
await Folder.decrypt(folder1_2);
|
||||||
|
await Note.decrypt(note1_2);
|
||||||
|
folder1_2 = await Folder.load(folder1.id);
|
||||||
|
note1_2 = await Note.load(note1.id);
|
||||||
|
// Check that properties match the original items. Also check
|
||||||
|
// the encryption did not affect the updated_time timestamp.
|
||||||
|
expect(note1_2.title).toBe(note1.title);
|
||||||
|
expect(note1_2.body).toBe(note1.body);
|
||||||
|
expect(note1_2.updated_time).toBe(note1.updated_time);
|
||||||
|
expect(!note1_2.encryption_cipher_text).toBe(true);
|
||||||
|
expect(folder1_2.title).toBe(folder1.title);
|
||||||
|
expect(folder1_2.updated_time).toBe(folder1.updated_time);
|
||||||
|
expect(!folder1_2.encryption_cipher_text).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should enable encryption automatically when downloading new master key (and none was previously available)',asyncTest(async () => {
|
||||||
|
// Enable encryption on client 1 and sync an item
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
await loadEncryptionMasterKey();
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
// Synchronising should enable encryption since we're going to get a master key
|
||||||
|
expect(Setting.value('encryption.enabled')).toBe(false);
|
||||||
|
await synchronizer().start();
|
||||||
|
expect(Setting.value('encryption.enabled')).toBe(true);
|
||||||
|
|
||||||
|
// Check that we got the master key from client 1
|
||||||
|
const masterKey = (await MasterKey.all())[0];
|
||||||
|
expect(!!masterKey).toBe(true);
|
||||||
|
|
||||||
|
// Since client 2 hasn't supplied a password yet, no master key is currently loaded
|
||||||
|
expect(encryptionService().loadedMasterKeyIds().length).toBe(0);
|
||||||
|
|
||||||
|
// If we sync now, nothing should be sent to target since we don't have a password.
|
||||||
|
// Technically it's incorrect to set the property of an encrypted variable but it allows confirming
|
||||||
|
// that encryption doesn't work if user hasn't supplied a password.
|
||||||
|
await BaseItem.forceSync(folder1.id);
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(1);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
folder1 = await Folder.load(folder1.id);
|
||||||
|
expect(folder1.title).toBe('folder1'); // Still at old value
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
// Now client 2 set the master key password
|
||||||
|
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
|
||||||
|
// Now that master key should be loaded
|
||||||
|
expect(encryptionService().loadedMasterKeyIds()[0]).toBe(masterKey.id);
|
||||||
|
|
||||||
|
// Decrypt all the data. Now change the title and sync again - this time the changes should be transmitted
|
||||||
|
await decryptionWorker().start();
|
||||||
|
folder1_2 = await Folder.save({ id: folder1.id, title: "change test" });
|
||||||
|
|
||||||
|
// If we sync now, this time client 1 should get the changes we did earlier
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(1);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
// Decrypt the data we just got
|
||||||
|
await decryptionWorker().start();
|
||||||
|
folder1 = await Folder.load(folder1.id);
|
||||||
|
expect(folder1.title).toBe('change test'); // Got title from client 2
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should encrypt existing notes too when enabling E2EE', asyncTest(async () => {
|
||||||
|
// First create a folder, without encryption enabled, and sync it
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
await synchronizer().start();
|
||||||
|
let files = await fileApi().list()
|
||||||
|
let content = await fileApi().get(files.items[0].path);
|
||||||
|
expect(content.indexOf('folder1') >= 0).toBe(true)
|
||||||
|
|
||||||
|
// Then enable encryption and sync again
|
||||||
|
let masterKey = await encryptionService().generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
await encryptionService().enableEncryption(masterKey, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
// Even though the folder has not been changed it should have been synced again so that
|
||||||
|
// an encrypted version of it replaces the decrypted version.
|
||||||
|
files = await fileApi().list()
|
||||||
|
expect(files.items.length).toBe(2);
|
||||||
|
// By checking that the folder title is not present, we can confirm that the item has indeed been encrypted
|
||||||
|
// One of the two items is the master key
|
||||||
|
content = await fileApi().get(files.items[0].path);
|
||||||
|
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||||
|
content = await fileApi().get(files.items[1].path);
|
||||||
|
expect(content.indexOf('folder1') < 0).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should sync resources', asyncTest(async () => {
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||||
|
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||||
|
let resource1 = (await Resource.all())[0];
|
||||||
|
let resourcePath1 = Resource.fullPath(resource1);
|
||||||
|
await synchronizer().start();
|
||||||
|
expect((await fileApi().list()).items.length).toBe(3);
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
let allResources = await Resource.all();
|
||||||
|
expect(allResources.length).toBe(1);
|
||||||
|
let resource1_2 = allResources[0];
|
||||||
|
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||||
|
|
||||||
|
expect(resource1_2.id).toBe(resource1.id);
|
||||||
|
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should encryt resources', asyncTest(async () => {
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||||
|
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||||
|
let resource1 = (await Resource.all())[0];
|
||||||
|
let resourcePath1 = Resource.fullPath(resource1);
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
|
||||||
|
let resource1_2 = (await Resource.all())[0];
|
||||||
|
resource1_2 = await Resource.decrypt(resource1_2);
|
||||||
|
let resourcePath1_2 = Resource.fullPath(resource1_2);
|
||||||
|
|
||||||
|
expect(fileContentEqual(resourcePath1, resourcePath1_2)).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should upload decrypted items to sync target after encryption disabled', asyncTest(async () => {
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
let allEncrypted = await allSyncTargetItemsEncrypted();
|
||||||
|
expect(allEncrypted).toBe(true);
|
||||||
|
|
||||||
|
await encryptionService().disableEncryption();
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||||
|
expect(allEncrypted).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not upload any item if encryption was enabled, and items have not been decrypted, and then encryption disabled', asyncTest(async () => {
|
||||||
|
// For some reason I can't explain, this test is sometimes executed before beforeEach is finished
|
||||||
|
// which means it's going to fail in unexpected way. So the loop below wait for beforeEach to be done.
|
||||||
|
while (insideBeforeEach) await time.msleep(100);
|
||||||
|
|
||||||
|
Setting.setValue('encryption.enabled', true);
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
await switchClient(2);
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
expect(Setting.value('encryption.enabled')).toBe(true);
|
||||||
|
|
||||||
|
// If we try to disable encryption now, it should throw an error because some items are
|
||||||
|
// currently encrypted. They must be decrypted first so that they can be sent as
|
||||||
|
// plain text to the sync target.
|
||||||
|
//let hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
|
||||||
|
//expect(hasThrown).toBe(true);
|
||||||
|
|
||||||
|
// Now supply the password, and decrypt the items
|
||||||
|
Setting.setObjectKey('encryption.passwordCache', masterKey.id, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
await decryptionWorker().start();
|
||||||
|
|
||||||
|
// Try to disable encryption again
|
||||||
|
hasThrown = await checkThrowAsync(async () => await encryptionService().disableEncryption());
|
||||||
|
expect(hasThrown).toBe(false);
|
||||||
|
|
||||||
|
// If we sync now the target should receive the decrypted items
|
||||||
|
await synchronizer().start();
|
||||||
|
allEncrypted = await allSyncTargetItemsEncrypted();
|
||||||
|
expect(allEncrypted).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should encrypt remote resources after encryption has been enabled', asyncTest(async () => {
|
||||||
|
while (insideBeforeEach) await time.msleep(100);
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||||
|
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||||
|
let resource1 = (await Resource.all())[0];
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
expect(await allSyncTargetItemsEncrypted()).toBe(false);
|
||||||
|
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
await encryptionService().enableEncryption(masterKey, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
expect(await allSyncTargetItemsEncrypted()).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should upload encrypted resource, but it should not mark the blob as encrypted locally', asyncTest(async () => {
|
||||||
|
while (insideBeforeEach) await time.msleep(100);
|
||||||
|
|
||||||
|
let folder1 = await Folder.save({ title: "folder1" });
|
||||||
|
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||||
|
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
|
||||||
|
const masterKey = await loadEncryptionMasterKey();
|
||||||
|
await encryptionService().enableEncryption(masterKey, '123456');
|
||||||
|
await encryptionService().loadMasterKeysFromSettings();
|
||||||
|
await synchronizer().start();
|
||||||
|
|
||||||
|
let resource1 = (await Resource.all())[0];
|
||||||
|
expect(resource1.encryption_blob_encrypted).toBe(0);
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
@ -1,34 +1,43 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { NoteTag } = require('lib/models/note-tag.js');
|
const NoteTag = require('lib/models/NoteTag.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Synchronizer } = require('lib/synchronizer.js');
|
const { Synchronizer } = require('lib/synchronizer.js');
|
||||||
const { FileApi } = require('lib/file-api.js');
|
const { FileApi } = require('lib/file-api.js');
|
||||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const { shimInit } = require('lib/shim-init-node.js');
|
||||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||||
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
||||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||||
|
|
||||||
let databases_ = [];
|
let databases_ = [];
|
||||||
let synchronizers_ = [];
|
let synchronizers_ = [];
|
||||||
|
let encryptionServices_ = [];
|
||||||
|
let decryptionWorkers_ = [];
|
||||||
let fileApi_ = null;
|
let fileApi_ = null;
|
||||||
let currentClient_ = 1;
|
let currentClient_ = 1;
|
||||||
|
|
||||||
|
shimInit();
|
||||||
|
|
||||||
const fsDriver = new FsDriverNode();
|
const fsDriver = new FsDriverNode();
|
||||||
Logger.fsDriver_ = fsDriver;
|
Logger.fsDriver_ = fsDriver;
|
||||||
Resource.fsDriver_ = fsDriver;
|
Resource.fsDriver_ = fsDriver;
|
||||||
|
EncryptionService.fsDriver_ = fsDriver;
|
||||||
|
|
||||||
const logDir = __dirname + '/../tests/logs';
|
const logDir = __dirname + '/../tests/logs';
|
||||||
fs.mkdirpSync(logDir, 0o755);
|
fs.mkdirpSync(logDir, 0o755);
|
||||||
@ -37,7 +46,8 @@ SyncTargetRegistry.addClass(SyncTargetMemory);
|
|||||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
|
|
||||||
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
||||||
|
const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
||||||
const syncDir = __dirname + '/../tests/sync';
|
const syncDir = __dirname + '/../tests/sync';
|
||||||
|
|
||||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
||||||
@ -45,13 +55,14 @@ const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1
|
|||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
logger.addTarget('console');
|
logger.addTarget('console');
|
||||||
logger.addTarget('file', { path: logDir + '/log.txt' });
|
logger.addTarget('file', { path: logDir + '/log.txt' });
|
||||||
logger.setLevel(Logger.LEVEL_WARN);
|
logger.setLevel(Logger.LEVEL_WARN); // Set to INFO to display sync process in console
|
||||||
|
|
||||||
BaseItem.loadClass('Note', Note);
|
BaseItem.loadClass('Note', Note);
|
||||||
BaseItem.loadClass('Folder', Folder);
|
BaseItem.loadClass('Folder', Folder);
|
||||||
BaseItem.loadClass('Resource', Resource);
|
BaseItem.loadClass('Resource', Resource);
|
||||||
BaseItem.loadClass('Tag', Tag);
|
BaseItem.loadClass('Tag', Tag);
|
||||||
BaseItem.loadClass('NoteTag', NoteTag);
|
BaseItem.loadClass('NoteTag', NoteTag);
|
||||||
|
BaseItem.loadClass('MasterKey', MasterKey);
|
||||||
|
|
||||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||||
Setting.setConstant('appType', 'cli');
|
Setting.setConstant('appType', 'cli');
|
||||||
@ -79,10 +90,15 @@ async function switchClient(id) {
|
|||||||
BaseItem.db_ = databases_[id];
|
BaseItem.db_ = databases_[id];
|
||||||
Setting.db_ = databases_[id];
|
Setting.db_ = databases_[id];
|
||||||
|
|
||||||
|
BaseItem.encryptionService_ = encryptionServices_[id];
|
||||||
|
Resource.encryptionService_ = encryptionServices_[id];
|
||||||
|
|
||||||
|
Setting.setConstant('resourceDir', resourceDir(id));
|
||||||
|
|
||||||
return Setting.load();
|
return Setting.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDatabase(id = null) {
|
async function clearDatabase(id = null) {
|
||||||
if (id === null) id = currentClient_;
|
if (id === null) id = currentClient_;
|
||||||
|
|
||||||
let queries = [
|
let queries = [
|
||||||
@ -91,35 +107,65 @@ function clearDatabase(id = null) {
|
|||||||
'DELETE FROM resources',
|
'DELETE FROM resources',
|
||||||
'DELETE FROM tags',
|
'DELETE FROM tags',
|
||||||
'DELETE FROM note_tags',
|
'DELETE FROM note_tags',
|
||||||
|
'DELETE FROM master_keys',
|
||||||
|
'DELETE FROM settings',
|
||||||
|
|
||||||
'DELETE FROM deleted_items',
|
'DELETE FROM deleted_items',
|
||||||
'DELETE FROM sync_items',
|
'DELETE FROM sync_items',
|
||||||
];
|
];
|
||||||
|
|
||||||
return databases_[id].transactionExecBatch(queries);
|
await databases_[id].transactionExecBatch(queries);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupDatabase(id = null) {
|
async function setupDatabase(id = null) {
|
||||||
if (id === null) id = currentClient_;
|
if (id === null) id = currentClient_;
|
||||||
|
|
||||||
|
Setting.cancelScheduleSave();
|
||||||
|
Setting.cache_ = null;
|
||||||
|
|
||||||
if (databases_[id]) {
|
if (databases_[id]) {
|
||||||
return clearDatabase(id).then(() => {
|
await clearDatabase(id);
|
||||||
return Setting.load();
|
await Setting.load();
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = __dirname + '/data/test-' + id + '.sqlite';
|
const filePath = __dirname + '/data/test-' + id + '.sqlite';
|
||||||
return fs.unlink(filePath).catch(() => {
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(filePath);
|
||||||
|
} catch (error) {
|
||||||
// Don't care if the file doesn't exist
|
// Don't care if the file doesn't exist
|
||||||
}).then(() => {
|
};
|
||||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
|
||||||
// databases_[id].setLogger(logger);
|
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||||
// console.info(filePath);
|
await databases_[id].open({ name: filePath });
|
||||||
return databases_[id].open({ name: filePath }).then(() => {
|
|
||||||
BaseModel.db_ = databases_[id];
|
BaseModel.db_ = databases_[id];
|
||||||
return setupDatabase(id);
|
await Setting.load();
|
||||||
});
|
//return setupDatabase(id);
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
// return databases_[id].open({ name: filePath }).then(() => {
|
||||||
|
// BaseModel.db_ = databases_[id];
|
||||||
|
// return setupDatabase(id);
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
// return fs.unlink(filePath).catch(() => {
|
||||||
|
// // Don't care if the file doesn't exist
|
||||||
|
// }).then(() => {
|
||||||
|
// databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||||
|
// return databases_[id].open({ name: filePath }).then(() => {
|
||||||
|
// BaseModel.db_ = databases_[id];
|
||||||
|
// return setupDatabase(id);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceDir(id = null) {
|
||||||
|
if (id === null) id = currentClient_;
|
||||||
|
return __dirname + '/data/resources-' + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDatabaseAndSynchronizer(id = null) {
|
async function setupDatabaseAndSynchronizer(id = null) {
|
||||||
@ -127,14 +173,25 @@ async function setupDatabaseAndSynchronizer(id = null) {
|
|||||||
|
|
||||||
await setupDatabase(id);
|
await setupDatabase(id);
|
||||||
|
|
||||||
|
EncryptionService.instance_ = null;
|
||||||
|
DecryptionWorker.instance_ = null;
|
||||||
|
|
||||||
|
await fs.remove(resourceDir(id));
|
||||||
|
await fs.mkdirp(resourceDir(id), 0o755);
|
||||||
|
|
||||||
if (!synchronizers_[id]) {
|
if (!synchronizers_[id]) {
|
||||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
|
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
|
||||||
const syncTarget = new SyncTargetClass(db(id));
|
const syncTarget = new SyncTargetClass(db(id));
|
||||||
syncTarget.setFileApi(fileApi());
|
syncTarget.setFileApi(fileApi());
|
||||||
syncTarget.setLogger(logger);
|
syncTarget.setLogger(logger);
|
||||||
synchronizers_[id] = await syncTarget.synchronizer();
|
synchronizers_[id] = await syncTarget.synchronizer();
|
||||||
|
synchronizers_[id].autoStartDecryptionWorker_ = false; // For testing we disable this since it would make the tests non-deterministic
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encryptionServices_[id] = new EncryptionService();
|
||||||
|
decryptionWorkers_[id] = new DecryptionWorker();
|
||||||
|
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
|
||||||
|
|
||||||
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
|
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
|
||||||
fs.removeSync(syncDir)
|
fs.removeSync(syncDir)
|
||||||
fs.mkdirpSync(syncDir, 0o755);
|
fs.mkdirpSync(syncDir, 0o755);
|
||||||
@ -153,6 +210,35 @@ function synchronizer(id = null) {
|
|||||||
return synchronizers_[id];
|
return synchronizers_[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encryptionService(id = null) {
|
||||||
|
if (id === null) id = currentClient_;
|
||||||
|
return encryptionServices_[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptionWorker(id = null) {
|
||||||
|
if (id === null) id = currentClient_;
|
||||||
|
return decryptionWorkers_[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadEncryptionMasterKey(id = null, useExisting = false) {
|
||||||
|
const service = encryptionService(id);
|
||||||
|
|
||||||
|
let masterKey = null;
|
||||||
|
|
||||||
|
if (!useExisting) { // Create it
|
||||||
|
masterKey = await service.generateMasterKey('123456');
|
||||||
|
masterKey = await MasterKey.save(masterKey);
|
||||||
|
} else { // Use the one already available
|
||||||
|
materKey = await MasterKey.all();
|
||||||
|
if (!materKey.length) throw new Error('No mater key available');
|
||||||
|
masterKey = materKey[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.loadMasterKey(masterKey, '123456', true);
|
||||||
|
|
||||||
|
return masterKey;
|
||||||
|
}
|
||||||
|
|
||||||
function fileApi() {
|
function fileApi() {
|
||||||
if (fileApi_) return fileApi_;
|
if (fileApi_) return fileApi_;
|
||||||
|
|
||||||
@ -185,4 +271,43 @@ function fileApi() {
|
|||||||
return fileApi_;
|
return fileApi_;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId };
|
function objectsEqual(o1, o2) {
|
||||||
|
if (Object.getOwnPropertyNames(o1).length !== Object.getOwnPropertyNames(o2).length) return false;
|
||||||
|
for (let n in o1) {
|
||||||
|
if (!o1.hasOwnProperty(n)) continue;
|
||||||
|
if (o1[n] !== o2[n]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkThrowAsync(asyncFn) {
|
||||||
|
let hasThrown = false;
|
||||||
|
try {
|
||||||
|
await asyncFn();
|
||||||
|
} catch (error) {
|
||||||
|
hasThrown = true;
|
||||||
|
}
|
||||||
|
return hasThrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileContentEqual(path1, path2) {
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const content1 = fs.readFileSync(path1, 'base64');
|
||||||
|
const content2 = fs.readFileSync(path2, 'base64');
|
||||||
|
return content1 === content2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap an async test in a try/catch block so that done() is always called
|
||||||
|
// and display a proper error message instead of "unhandled promise error"
|
||||||
|
function asyncTest(callback) {
|
||||||
|
return async function(done) {
|
||||||
|
try {
|
||||||
|
await callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest };
|
@ -19,7 +19,8 @@
|
|||||||
"title": "Demo for Joplin CLI",
|
"title": "Demo for Joplin CLI",
|
||||||
"years": [
|
"years": [
|
||||||
2016,
|
2016,
|
||||||
2017
|
2017,
|
||||||
|
2018
|
||||||
],
|
],
|
||||||
"owner": "Laurent Cozic"
|
"owner": "Laurent Cozic"
|
||||||
},
|
},
|
||||||
|
@ -2,13 +2,14 @@ require('app-module-path').addPath(__dirname);
|
|||||||
|
|
||||||
const { BaseApplication } = require('lib/BaseApplication');
|
const { BaseApplication } = require('lib/BaseApplication');
|
||||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
const { _, setLocale } = require('lib/locale.js');
|
const { _, setLocale } = require('lib/locale.js');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||||
@ -18,6 +19,7 @@ const { defaultState } = require('lib/reducer.js');
|
|||||||
const packageInfo = require('./packageInfo.js');
|
const packageInfo = require('./packageInfo.js');
|
||||||
const AlarmService = require('lib/services/AlarmService.js');
|
const AlarmService = require('lib/services/AlarmService.js');
|
||||||
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
|
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
|
||||||
|
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||||
|
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
@ -255,7 +257,7 @@ class Application extends BaseApplication {
|
|||||||
name: 'search',
|
name: 'search',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}]
|
}],
|
||||||
}, {
|
}, {
|
||||||
label: _('Tools'),
|
label: _('Tools'),
|
||||||
submenu: [{
|
submenu: [{
|
||||||
@ -266,15 +268,26 @@ class Application extends BaseApplication {
|
|||||||
routeName: 'Status',
|
routeName: 'Status',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
type: 'separator',
|
||||||
|
screens: ['Main'],
|
||||||
},{
|
},{
|
||||||
label: _('Options'),
|
label: _('Encryption options'),
|
||||||
|
click: () => {
|
||||||
|
this.dispatch({
|
||||||
|
type: 'NAV_GO',
|
||||||
|
routeName: 'EncryptionConfig',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
label: _('General Options'),
|
||||||
click: () => {
|
click: () => {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'NAV_GO',
|
type: 'NAV_GO',
|
||||||
routeName: 'Config',
|
routeName: 'Config',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}]
|
}],
|
||||||
}, {
|
}, {
|
||||||
label: _('Help'),
|
label: _('Help'),
|
||||||
submenu: [{
|
submenu: [{
|
||||||
@ -288,7 +301,7 @@ class Application extends BaseApplication {
|
|||||||
let message = [
|
let message = [
|
||||||
p.description,
|
p.description,
|
||||||
'',
|
'',
|
||||||
'Copyright © 2016-2017 Laurent Cozic',
|
'Copyright © 2016-2018 Laurent Cozic',
|
||||||
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
|
_('%s %s (%s, %s)', p.name, p.version, Setting.value('env'), process.platform),
|
||||||
];
|
];
|
||||||
bridge().showMessageBox({
|
bridge().showMessageBox({
|
||||||
@ -354,7 +367,14 @@ class Application extends BaseApplication {
|
|||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'TAG_UPDATE_ALL',
|
type: 'TAG_UPDATE_ALL',
|
||||||
tags: tags,
|
items: tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
const masterKeys = await MasterKey.all();
|
||||||
|
|
||||||
|
this.dispatch({
|
||||||
|
type: 'MASTERKEY_UPDATE_ALL',
|
||||||
|
items: masterKeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.store().dispatch({
|
this.store().dispatch({
|
||||||
@ -387,6 +407,8 @@ class Application extends BaseApplication {
|
|||||||
// Wait for the first sync before updating the notifications, since synchronisation
|
// Wait for the first sync before updating the notifications, since synchronisation
|
||||||
// might change the notifications.
|
// might change the notifications.
|
||||||
AlarmService.updateAllNotifications();
|
AlarmService.updateAllNotifications();
|
||||||
|
|
||||||
|
DecryptionWorker.instance().scheduleStart();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const { Header } = require('./Header.min.js');
|
const { Header } = require('./Header.min.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
@ -22,6 +22,23 @@ class ConfigScreenComponent extends React.Component {
|
|||||||
this.setState({ settings: this.props.settings });
|
this.setState({ settings: this.props.settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keyValueToArray(kv) {
|
||||||
|
let output = [];
|
||||||
|
for (let k in kv) {
|
||||||
|
if (!kv.hasOwnProperty(k)) continue;
|
||||||
|
output.push({
|
||||||
|
key: k,
|
||||||
|
label: kv[k],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
output.sort((a, b) => {
|
||||||
|
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : +1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
settingToComponent(key, value) {
|
settingToComponent(key, value) {
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
@ -53,9 +70,10 @@ class ConfigScreenComponent extends React.Component {
|
|||||||
if (md.isEnum) {
|
if (md.isEnum) {
|
||||||
let items = [];
|
let items = [];
|
||||||
const settingOptions = md.options();
|
const settingOptions = md.options();
|
||||||
for (let k in settingOptions) {
|
let array = this.keyValueToArray(settingOptions);
|
||||||
if (!settingOptions.hasOwnProperty(k)) continue;
|
for (let i = 0; i < array.length; i++) {
|
||||||
items.push(<option value={k.toString()} key={k}>{settingOptions[k]}</option>);
|
const e = array[i];
|
||||||
|
items.push(<option value={e.key.toString()} key={e.key}>{settingOptions[e.key]}</option>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
191
ElectronClient/app/gui/EncryptionConfigScreen.jsx
Normal file
191
ElectronClient/app/gui/EncryptionConfigScreen.jsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const { connect } = require('react-redux');
|
||||||
|
const Setting = require('lib/models/Setting');
|
||||||
|
const BaseItem = require('lib/models/BaseItem');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
|
const { Header } = require('./Header.min.js');
|
||||||
|
const { themeStyle } = require('../theme.js');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const dialogs = require('./dialogs');
|
||||||
|
const shared = require('lib/components/shared/encryption-config-shared.js');
|
||||||
|
const pathUtils = require('lib/path-utils.js');
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
|
||||||
|
class EncryptionConfigScreenComponent extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
shared.constructor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.isMounted_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.isMounted_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
initState(props) {
|
||||||
|
return shared.initState(this, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshStats() {
|
||||||
|
return shared.refreshStats(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.initState(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this.initState(nextProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkPasswords() {
|
||||||
|
return shared.checkPasswords(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMasterKey(mk) {
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
|
const onSaveClick = () => {
|
||||||
|
return shared.onSavePasswordClick(this, mk);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPasswordChange = (event) => {
|
||||||
|
return shared.onPasswordChange(this, mk, event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||||
|
const active = this.props.activeMasterKeyId === mk.id ? '✔' : '';
|
||||||
|
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={mk.id}>
|
||||||
|
<td style={theme.textStyle}>{active}</td>
|
||||||
|
<td style={theme.textStyle}>{mk.id}</td>
|
||||||
|
<td style={theme.textStyle}>{mk.source_application}</td>
|
||||||
|
<td style={theme.textStyle}>{time.formatMsToLocal(mk.created_time)}</td>
|
||||||
|
<td style={theme.textStyle}>{time.formatMsToLocal(mk.updated_time)}</td>
|
||||||
|
<td style={theme.textStyle}><input type="password" value={password} onChange={(event) => onPasswordChange(event)}/> <button onClick={() => onSaveClick()}>{_('Save')}</button></td>
|
||||||
|
<td style={theme.textStyle}>{passwordOk}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const style = this.props.style;
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
const masterKeys = this.state.masterKeys;
|
||||||
|
const containerPadding = 10;
|
||||||
|
|
||||||
|
const headerStyle = {
|
||||||
|
width: style.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
padding: containerPadding,
|
||||||
|
overflow: 'auto',
|
||||||
|
height: style.height - theme.headerHeight - containerPadding * 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mkComps = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < masterKeys.length; i++) {
|
||||||
|
const mk = masterKeys[i];
|
||||||
|
mkComps.push(this.renderMasterKey(mk));
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggleButtonClick = async () => {
|
||||||
|
const isEnabled = Setting.value('encryption.enabled');
|
||||||
|
|
||||||
|
let answer = null;
|
||||||
|
if (isEnabled) {
|
||||||
|
answer = await dialogs.confirm(_('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||||
|
} else {
|
||||||
|
answer = await dialogs.prompt(_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.'), '', '', { type: 'password' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!answer) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEnabled) {
|
||||||
|
await EncryptionService.instance().disableEncryption();
|
||||||
|
} else {
|
||||||
|
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(answer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
await dialogs.alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedItemsInfo = this.props.encryptionEnabled ? <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p> : null;
|
||||||
|
const toggleButton = <button onClick={() => { onToggleButtonClick() }}>{this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')}</button>
|
||||||
|
|
||||||
|
let masterKeySection = null;
|
||||||
|
|
||||||
|
if (mkComps.length) {
|
||||||
|
masterKeySection = (
|
||||||
|
<div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Master Keys')}</h1>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th style={theme.textStyle}>{_('Active')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('ID')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Source')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Created')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Updated')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Password')}</th>
|
||||||
|
<th style={theme.textStyle}>{_('Password OK')}</th>
|
||||||
|
</tr>
|
||||||
|
{mkComps}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style={theme.textStyle}>{_('Note: Only one master key is going to be used for encryption (the one marked as "active"). Any of the keys might be used for decryption, depending on how the notes or notebooks were originally encrypted.')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Header style={headerStyle} />
|
||||||
|
<div style={containerStyle}>
|
||||||
|
<div style={{backgroundColor: theme.warningBackgroundColor, paddingLeft: 10, paddingRight: 10, paddingTop: 2, paddingBottom: 2 }}>
|
||||||
|
<p style={theme.textStyle}>
|
||||||
|
Important: This is a <b>beta</b> feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain.
|
||||||
|
</p>
|
||||||
|
<p style={theme.textStyle}>
|
||||||
|
If you wish to you use it, it is recommended that you keep a backup of your data. The simplest way is to regularly backup <b>{pathUtils.toSystemSlashes(Setting.value('profileDir'), process.platform)}</b>
|
||||||
|
</p>
|
||||||
|
<p style={theme.textStyle}>
|
||||||
|
For more information about End-To-End Encryption (E2EE) and how it is going to work, please check the documentation: <a onClick={() => {bridge().openExternal('http://joplin.cozic.net/help/e2ee.html')}} href="#">http://joplin.cozic.net/help/e2ee.html</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 style={theme.h1Style}>{_('Status')}</h1>
|
||||||
|
<p style={theme.textStyle}>{_('Encryption is:')} <strong>{this.props.encryptionEnabled ? _('Enabled') : _('Disabled')}</strong></p>
|
||||||
|
{decryptedItemsInfo}
|
||||||
|
{toggleButton}
|
||||||
|
{masterKeySection}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
return {
|
||||||
|
theme: state.settings.theme,
|
||||||
|
masterKeys: state.masterKeys,
|
||||||
|
passwords: state.settings['encryption.passwordCache'],
|
||||||
|
encryptionEnabled: state.settings['encryption.enabled'],
|
||||||
|
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
|
||||||
|
|
||||||
|
module.exports = { EncryptionConfigScreen };
|
@ -1,7 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const { Header } = require('./Header.min.js');
|
const { Header } = require('./Header.min.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
|
@ -5,12 +5,12 @@ const { SideBar } = require('./SideBar.min.js');
|
|||||||
const { NoteList } = require('./NoteList.min.js');
|
const { NoteList } = require('./NoteList.min.js');
|
||||||
const { NoteText } = require('./NoteText.min.js');
|
const { NoteText } = require('./NoteText.min.js');
|
||||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { uuid } = require('lib/uuid.js');
|
const { uuid } = require('lib/uuid.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const layoutUtils = require('lib/layout-utils.js');
|
const layoutUtils = require('lib/layout-utils.js');
|
||||||
@ -132,7 +132,7 @@ class MainScreenComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (command.name === 'renameNotebook') {
|
} else if (command.name === 'renameFolder') {
|
||||||
const folder = await Folder.load(command.id);
|
const folder = await Folder.load(command.id);
|
||||||
if (!folder) return;
|
if (!folder) return;
|
||||||
|
|
||||||
@ -143,7 +143,8 @@ class MainScreenComponent extends React.Component {
|
|||||||
onClose: async (answer) => {
|
onClose: async (answer) => {
|
||||||
if (answer !== null) {
|
if (answer !== null) {
|
||||||
try {
|
try {
|
||||||
await Folder.save({ id: folder.id, title: answer }, { userSideValidation: true });
|
folder.title = answer;
|
||||||
|
await Folder.save(folder, { fields: ['title'], userSideValidation: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
bridge().showErrorMessageBox(error.message);
|
bridge().showErrorMessageBox(error.message);
|
||||||
}
|
}
|
||||||
@ -288,8 +289,7 @@ class MainScreenComponent extends React.Component {
|
|||||||
const promptOptions = this.state.promptOptions;
|
const promptOptions = this.state.promptOptions;
|
||||||
const folders = this.props.folders;
|
const folders = this.props.folders;
|
||||||
const notes = this.props.notes;
|
const notes = this.props.notes;
|
||||||
const messageBoxVisible = this.props.hasDisabledSyncItems;
|
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
|
||||||
|
|
||||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
@ -343,13 +343,31 @@ class MainScreenComponent extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageComp = messageBoxVisible ? (
|
const onViewMasterKeysClick = () => {
|
||||||
<div style={styles.messageBox}>
|
this.props.dispatch({
|
||||||
<span style={theme.textStyle}>
|
type: 'NAV_GO',
|
||||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
routeName: 'EncryptionConfig',
|
||||||
</span>
|
});
|
||||||
</div>
|
}
|
||||||
) : null;
|
|
||||||
|
let messageComp = null;
|
||||||
|
|
||||||
|
if (messageBoxVisible) {
|
||||||
|
let msg = null;
|
||||||
|
if (this.props.hasDisabledSyncItems) {
|
||||||
|
msg = <span>{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a></span>
|
||||||
|
} else if (this.props.showMissingMasterKeyMessage) {
|
||||||
|
msg = <span>{_('Some items cannot be decrypted.')} <a href="#" onClick={() => { onViewMasterKeysClick() }}>{_('Set the password')}</a></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
messageComp = (
|
||||||
|
<div style={styles.messageBox}>
|
||||||
|
<span style={theme.textStyle}>
|
||||||
|
{msg}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
@ -383,6 +401,7 @@ const mapStateToProps = (state) => {
|
|||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
notes: state.notes,
|
notes: state.notes,
|
||||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||||
|
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ const React = require('react');
|
|||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
|
const BaseModel = require('lib/BaseModel');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
@ -56,23 +57,32 @@ class NoteListComponent extends React.Component {
|
|||||||
const noteIds = this.props.selectedNoteIds;
|
const noteIds = this.props.selectedNoteIds;
|
||||||
if (!noteIds.length) return;
|
if (!noteIds.length) return;
|
||||||
|
|
||||||
|
const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id));
|
||||||
|
|
||||||
|
let hasEncrypted = false;
|
||||||
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
if (!!notes[i].encryption_applied) hasEncrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
const menu = new Menu()
|
const menu = new Menu()
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
if (!hasEncrypted) {
|
||||||
this.props.dispatch({
|
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
||||||
type: 'WINDOW_COMMAND',
|
this.props.dispatch({
|
||||||
name: 'setTags',
|
type: 'WINDOW_COMMAND',
|
||||||
noteId: noteIds[0],
|
name: 'setTags',
|
||||||
});
|
noteId: noteIds[0],
|
||||||
}}));
|
});
|
||||||
|
}}));
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
||||||
for (let i = 0; i < noteIds.length; i++) {
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
const note = await Note.load(noteIds[i]);
|
const note = await Note.load(noteIds[i]);
|
||||||
await Note.save(Note.toggleIsTodo(note));
|
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
||||||
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
||||||
}
|
}
|
||||||
}}));
|
}}));
|
||||||
|
}
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
||||||
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
|
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
|
||||||
@ -120,7 +130,7 @@ class NoteListComponent extends React.Component {
|
|||||||
id: item.id,
|
id: item.id,
|
||||||
todo_completed: checked ? time.unixMs() : 0,
|
todo_completed: checked ? time.unixMs() : 0,
|
||||||
}
|
}
|
||||||
await Note.save(newNote);
|
await Note.save(newNote, { userSideValidation: true });
|
||||||
eventManager.emit('todoToggle', { noteId: item.id });
|
eventManager.emit('todoToggle', { noteId: item.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +164,7 @@ class NoteListComponent extends React.Component {
|
|||||||
onClick={(event) => { onTitleClick(event, item) }}
|
onClick={(event) => { onTitleClick(event, item) }}
|
||||||
onDragStart={(event) => onDragStart(event) }
|
onDragStart={(event) => onDragStart(event) }
|
||||||
>
|
>
|
||||||
{item.title}
|
{Note.displayTitle(item)}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { IconButton } = require('./IconButton.min.js');
|
const { IconButton } = require('./IconButton.min.js');
|
||||||
const Toolbar = require('./Toolbar.min.js');
|
const Toolbar = require('./Toolbar.min.js');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
@ -36,7 +36,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
webviewReady: false,
|
webviewReady: false,
|
||||||
scrollHeight: null,
|
scrollHeight: null,
|
||||||
editorScrollTop: 0,
|
editorScrollTop: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lastLoadedNoteId_ = null;
|
this.lastLoadedNoteId_ = null;
|
||||||
@ -167,6 +167,12 @@ class NoteTextComponent extends React.Component {
|
|||||||
async componentWillReceiveProps(nextProps) {
|
async componentWillReceiveProps(nextProps) {
|
||||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||||
await this.reloadNote(nextProps);
|
await this.reloadNote(nextProps);
|
||||||
|
if(this.editor_){
|
||||||
|
const session = this.editor_.editor.getSession();
|
||||||
|
const undoManager = session.getUndoManager();
|
||||||
|
undoManager.reset();
|
||||||
|
session.setUndoManager(undoManager);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
|
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
|
||||||
@ -334,6 +340,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
this.scheduleSave();
|
this.scheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async commandAttachFile() {
|
async commandAttachFile() {
|
||||||
const noteId = this.props.noteId;
|
const noteId = this.props.noteId;
|
||||||
if (!noteId) return;
|
if (!noteId) return;
|
||||||
@ -418,7 +425,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
|
|
||||||
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
||||||
|
|
||||||
if (!note) {
|
if (!note || !!note.encryption_applied) {
|
||||||
const emptyDivStyle = Object.assign({
|
const emptyDivStyle = Object.assign({
|
||||||
backgroundColor: 'black',
|
backgroundColor: 'black',
|
||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
@ -549,7 +556,6 @@ class NoteTextComponent extends React.Component {
|
|||||||
delete editorRootStyle.width;
|
delete editorRootStyle.width;
|
||||||
delete editorRootStyle.height;
|
delete editorRootStyle.height;
|
||||||
delete editorRootStyle.fontSize;
|
delete editorRootStyle.fontSize;
|
||||||
|
|
||||||
const editor = <AceEditor
|
const editor = <AceEditor
|
||||||
value={body}
|
value={body}
|
||||||
mode="markdown"
|
mode="markdown"
|
||||||
|
@ -4,13 +4,14 @@ const { createStore } = require('redux');
|
|||||||
const { connect, Provider } = require('react-redux');
|
const { connect, Provider } = require('react-redux');
|
||||||
|
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
|
||||||
const { MainScreen } = require('./MainScreen.min.js');
|
const { MainScreen } = require('./MainScreen.min.js');
|
||||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||||
|
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js');
|
||||||
const { Navigator } = require('./Navigator.min.js');
|
const { Navigator } = require('./Navigator.min.js');
|
||||||
|
|
||||||
const { app } = require('../app');
|
const { app } = require('../app');
|
||||||
@ -77,6 +78,7 @@ class RootComponent extends React.Component {
|
|||||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||||
|
EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') },
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -2,10 +2,10 @@ const React = require('react');
|
|||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||||
const { Synchronizer } = require('lib/synchronizer.js');
|
const { Synchronizer } = require('lib/synchronizer.js');
|
||||||
const { BaseModel } = require('lib/base-model.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
@ -107,6 +107,11 @@ class SideBarComponent extends React.Component {
|
|||||||
|
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
|
|
||||||
|
let item = null;
|
||||||
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||||
|
item = BaseModel.byId(this.props.folders, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
||||||
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@ -123,11 +128,11 @@ class SideBarComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
}}))
|
}}))
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||||
menu.append(new MenuItem({label: _('Rename'), click: async () => {
|
menu.append(new MenuItem({label: _('Rename'), click: async () => {
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'WINDOW_COMMAND',
|
type: 'WINDOW_COMMAND',
|
||||||
name: 'renameNotebook',
|
name: 'renameFolder',
|
||||||
id: itemId,
|
id: itemId,
|
||||||
});
|
});
|
||||||
}}))
|
}}))
|
||||||
@ -180,6 +185,8 @@ class SideBarComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemTitle = Folder.displayTitle(folder);
|
||||||
|
|
||||||
return <a
|
return <a
|
||||||
className="list-item"
|
className="list-item"
|
||||||
onDragOver={(event) => { onDragOver(event, folder) } }
|
onDragOver={(event) => { onDragOver(event, folder) } }
|
||||||
@ -189,14 +196,14 @@ class SideBarComponent extends React.Component {
|
|||||||
data-type={BaseModel.TYPE_FOLDER}
|
data-type={BaseModel.TYPE_FOLDER}
|
||||||
onContextMenu={(event) => this.itemContextMenu(event)}
|
onContextMenu={(event) => this.itemContextMenu(event)}
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}
|
style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
tagItem(tag, selected) {
|
tagItem(tag, selected) {
|
||||||
let style = Object.assign({}, this.style().listItem);
|
let style = Object.assign({}, this.style().listItem);
|
||||||
if (selected) style = Object.assign(style, this.style().listItemSelected);
|
if (selected) style = Object.assign(style, this.style().listItemSelected);
|
||||||
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{tag.title}</a>
|
return <a className="list-item" href="#" data-id={tag.id} data-type={BaseModel.TYPE_TAG} onContextMenu={(event) => this.itemContextMenu(event)} key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>{Tag.displayTitle(tag)}</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
searchItem(search, selected) {
|
searchItem(search, selected) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const { reg } = require('lib/registry.js');
|
const { reg } = require('lib/registry.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const { Header } = require('./Header.min.js');
|
const { Header } = require('./Header.min.js');
|
||||||
const { themeStyle } = require('../theme.js');
|
const { themeStyle } = require('../theme.js');
|
||||||
@ -70,7 +70,9 @@ class StatusScreenComponent extends React.Component {
|
|||||||
|
|
||||||
for (let n in section.body) {
|
for (let n in section.body) {
|
||||||
if (!section.body.hasOwnProperty(n)) continue;
|
if (!section.body.hasOwnProperty(n)) continue;
|
||||||
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
|
let text = section.body[n];
|
||||||
|
if (!text) text = '\xa0';
|
||||||
|
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{text}</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
33
ElectronClient/app/gui/dialogs.js
Normal file
33
ElectronClient/app/gui/dialogs.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const smalltalk = require('smalltalk');
|
||||||
|
|
||||||
|
class Dialogs {
|
||||||
|
|
||||||
|
async alert(message, title = '') {
|
||||||
|
await smalltalk.alert(title, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(message, title = '') {
|
||||||
|
try {
|
||||||
|
await smalltalk.confirm(title, message);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async prompt(message, title = '', defaultValue = '', options = null) {
|
||||||
|
if (options === null) options = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const answer = await smalltalk.prompt(title, message, defaultValue, options);
|
||||||
|
return answer;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogs = new Dialogs();
|
||||||
|
|
||||||
|
module.exports = dialogs;
|
@ -6,6 +6,18 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="css/font-awesome.min.css">
|
<link rel="stylesheet" href="css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||||
|
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||||
|
<style>
|
||||||
|
.smalltalk {
|
||||||
|
background-color: rgba(0,0,0,.5);
|
||||||
|
}
|
||||||
|
.smalltalk input {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.smalltalk .page {
|
||||||
|
max-width: 30em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="react-root"></div>
|
<div id="react-root"></div>
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/es_ES.json
Normal file
1
ElectronClient/app/locales/es_ES.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/hr_HR.json
Normal file
1
ElectronClient/app/locales/hr_HR.json
Normal file
File diff suppressed because one or more lines are too long
@ -2,7 +2,12 @@ var locales = {};
|
|||||||
locales['en_GB'] = require('./en_GB.json');
|
locales['en_GB'] = require('./en_GB.json');
|
||||||
locales['de_DE'] = require('./de_DE.json');
|
locales['de_DE'] = require('./de_DE.json');
|
||||||
locales['es_CR'] = require('./es_CR.json');
|
locales['es_CR'] = require('./es_CR.json');
|
||||||
|
locales['es_ES'] = require('./es_ES.json');
|
||||||
locales['fr_FR'] = require('./fr_FR.json');
|
locales['fr_FR'] = require('./fr_FR.json');
|
||||||
|
locales['hr_HR'] = require('./hr_HR.json');
|
||||||
locales['it_IT'] = require('./it_IT.json');
|
locales['it_IT'] = require('./it_IT.json');
|
||||||
|
locales['ja_JP'] = require('./ja_JP.json');
|
||||||
locales['pt_BR'] = require('./pt_BR.json');
|
locales['pt_BR'] = require('./pt_BR.json');
|
||||||
|
locales['ru_RU'] = require('./ru_RU.json');
|
||||||
|
locales['zh_CN'] = require('./zh_CN.json');
|
||||||
module.exports = { locales: locales };
|
module.exports = { locales: locales };
|
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/ja_JP.json
Normal file
1
ElectronClient/app/locales/ja_JP.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/ru_RU.json
Normal file
1
ElectronClient/app/locales/ru_RU.json
Normal file
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/zh_CN.json
Normal file
1
ElectronClient/app/locales/zh_CN.json
Normal file
File diff suppressed because one or more lines are too long
@ -4,21 +4,24 @@
|
|||||||
require('app-module-path').addPath(__dirname);
|
require('app-module-path').addPath(__dirname);
|
||||||
|
|
||||||
const { app } = require('./app.js');
|
const { app } = require('./app.js');
|
||||||
const { Folder } = require('lib/models/folder.js');
|
const Folder = require('lib/models/Folder.js');
|
||||||
const { Resource } = require('lib/models/resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const { BaseItem } = require('lib/models/base-item.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const { Note } = require('lib/models/note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const { Tag } = require('lib/models/tag.js');
|
const Tag = require('lib/models/Tag.js');
|
||||||
const { NoteTag } = require('lib/models/note-tag.js');
|
const NoteTag = require('lib/models/NoteTag.js');
|
||||||
const { Setting } = require('lib/models/setting.js');
|
const MasterKey = require('lib/models/MasterKey');
|
||||||
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { Logger } = require('lib/logger.js');
|
const { Logger } = require('lib/logger.js');
|
||||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||||
const { shimInit } = require('lib/shim-init-node.js');
|
const { shimInit } = require('lib/shim-init-node.js');
|
||||||
|
const EncryptionService = require('lib/services/EncryptionService');
|
||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
|
||||||
const fsDriver = new FsDriverNode();
|
const fsDriver = new FsDriverNode();
|
||||||
Logger.fsDriver_ = fsDriver;
|
Logger.fsDriver_ = fsDriver;
|
||||||
Resource.fsDriver_ = fsDriver;
|
Resource.fsDriver_ = fsDriver;
|
||||||
|
EncryptionService.fsDriver_ = fsDriver;
|
||||||
|
|
||||||
// That's not good, but it's to avoid circular dependency issues
|
// That's not good, but it's to avoid circular dependency issues
|
||||||
// in the BaseItem class.
|
// in the BaseItem class.
|
||||||
@ -27,10 +30,13 @@ BaseItem.loadClass('Folder', Folder);
|
|||||||
BaseItem.loadClass('Resource', Resource);
|
BaseItem.loadClass('Resource', Resource);
|
||||||
BaseItem.loadClass('Tag', Tag);
|
BaseItem.loadClass('Tag', Tag);
|
||||||
BaseItem.loadClass('NoteTag', NoteTag);
|
BaseItem.loadClass('NoteTag', NoteTag);
|
||||||
|
BaseItem.loadClass('MasterKey', MasterKey);
|
||||||
|
|
||||||
Setting.setConstant('appId', 'net.cozic.joplin-desktop');
|
Setting.setConstant('appId', 'net.cozic.joplin-desktop');
|
||||||
Setting.setConstant('appType', 'desktop');
|
Setting.setConstant('appType', 'desktop');
|
||||||
|
|
||||||
|
shimInit();
|
||||||
|
|
||||||
// Disable drag and drop of links inside application (which would
|
// Disable drag and drop of links inside application (which would
|
||||||
// open it as if the whole app was a browser)
|
// open it as if the whole app was a browser)
|
||||||
document.addEventListener('dragover', event => event.preventDefault());
|
document.addEventListener('dragover', event => event.preventDefault());
|
||||||
@ -44,8 +50,6 @@ document.addEventListener('auxclick', event => event.preventDefault());
|
|||||||
// which would open a new browser window.
|
// which would open a new browser window.
|
||||||
document.addEventListener('click', (event) => event.preventDefault());
|
document.addEventListener('click', (event) => event.preventDefault());
|
||||||
|
|
||||||
shimInit();
|
|
||||||
|
|
||||||
app().start(bridge().processArgv()).then(() => {
|
app().start(bridge().processArgv()).then(() => {
|
||||||
require('./gui/Root.min.js');
|
require('./gui/Root.min.js');
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
982
ElectronClient/app/package-lock.json
generated
982
ElectronClient/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Joplin",
|
"name": "Joplin",
|
||||||
"version": "0.10.39",
|
"version": "0.10.41",
|
||||||
"description": "Joplin for Desktop",
|
"description": "Joplin for Desktop",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -57,7 +57,7 @@
|
|||||||
"electron-window-state": "^4.1.1",
|
"electron-window-state": "^4.1.1",
|
||||||
"follow-redirects": "^1.2.5",
|
"follow-redirects": "^1.2.5",
|
||||||
"form-data": "^2.3.1",
|
"form-data": "^2.3.1",
|
||||||
"fs-extra": "^4.0.2",
|
"fs-extra": "^5.0.0",
|
||||||
"highlight.js": "^9.12.0",
|
"highlight.js": "^9.12.0",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.2.1",
|
||||||
"jssha": "^2.3.1",
|
"jssha": "^2.3.1",
|
||||||
@ -78,6 +78,7 @@
|
|||||||
"react-redux": "^5.0.6",
|
"react-redux": "^5.0.6",
|
||||||
"redux": "^3.7.2",
|
"redux": "^3.7.2",
|
||||||
"sharp": "^0.18.4",
|
"sharp": "^0.18.4",
|
||||||
|
"smalltalk": "^2.5.1",
|
||||||
"sprintf-js": "^1.1.1",
|
"sprintf-js": "^1.1.1",
|
||||||
"sqlite3": "^3.1.13",
|
"sqlite3": "^3.1.13",
|
||||||
"string-padding": "^1.0.2",
|
"string-padding": "^1.0.2",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { Setting } = require('lib/models/setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
|
||||||
const globalStyle = {
|
const globalStyle = {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -65,12 +65,16 @@ globalStyle.textStyle = {
|
|||||||
color: globalStyle.color,
|
color: globalStyle.color,
|
||||||
fontFamily: globalStyle.fontFamily,
|
fontFamily: globalStyle.fontFamily,
|
||||||
fontSize: globalStyle.fontSize,
|
fontSize: globalStyle.fontSize,
|
||||||
|
lineHeight: '1.6em',
|
||||||
};
|
};
|
||||||
|
|
||||||
globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
||||||
color: globalStyle.color2,
|
color: globalStyle.color2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
globalStyle.h1Style = Object.assign({}, globalStyle.textStyle);
|
||||||
|
globalStyle.h1Style.fontSize *= 1.5;
|
||||||
|
|
||||||
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
||||||
globalStyle.h2Style.fontSize *= 1.3;
|
globalStyle.h2Style.fontSize *= 1.3;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
BUILD_DIR="$ROOT_DIR/app"
|
BUILD_DIR="$ROOT_DIR/app"
|
||||||
|
|
||||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||||
|
|
||||||
cd "$BUILD_DIR"
|
cd "$BUILD_DIR"
|
||||||
npm run compile
|
npm run compile
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user