You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
349 Commits
cli-v0.10.
...
android-v1
Author | SHA1 | Date | |
---|---|---|---|
|
29a13a9943 | ||
|
3691ae4d13 | ||
|
4dda397c29 | ||
|
b4b058998d | ||
|
10919e415e | ||
|
4966d74864 | ||
|
c70ecb30a5 | ||
|
acc0d17e0f | ||
|
b509b878bf | ||
|
322ec2efa1 | ||
|
1232661b1e | ||
|
c46d123503 | ||
|
8f4060999f | ||
|
0addd86069 | ||
|
760086307b | ||
|
fc6558a64c | ||
|
eca500880d | ||
|
90bcd7c977 | ||
|
cca0c6eaf3 | ||
|
b0736002be | ||
|
51fc2d8e51 | ||
|
d87c192ff1 | ||
|
52ccf398a6 | ||
|
344d0e2687 | ||
|
1bc4d6b423 | ||
|
baa9ca7ea3 | ||
|
e4d477fb4c | ||
|
71319eee28 | ||
|
68b31526f8 | ||
|
0b2b7324d9 | ||
|
43512cf27b | ||
|
4218b65969 | ||
|
7244e12b78 | ||
|
a796ef5c66 | ||
|
9474a05aaa | ||
|
41df355a7e | ||
|
4f3ab87914 | ||
|
5d1a08707c | ||
|
4f822df80e | ||
|
951be5cbf6 | ||
|
b6c2341542 | ||
|
a6e6b49a9d | ||
|
3a4bbd571e | ||
|
feccc6150e | ||
|
a37b599a6b | ||
|
9347683fe3 | ||
|
3551c26e28 | ||
|
cfca0107eb | ||
|
81bc975193 | ||
|
7908fda451 | ||
|
cdbb7c4b0d | ||
|
414e57ec55 | ||
|
1871123066 | ||
|
87bc08bef5 | ||
|
214a39c3d3 | ||
|
ef0cc5e33e | ||
|
3a1fa583ab | ||
|
c1161ae017 | ||
|
1023ec6206 | ||
|
7841421c0d | ||
|
995d8c35dd | ||
|
b179471eff | ||
|
19a126ebfe | ||
|
7e56e5b587 | ||
|
acf0c79341 | ||
|
9fe7e23ffe | ||
|
c94cc93971 | ||
|
b26094eba8 | ||
|
89a5ccdf93 | ||
|
ce2da0e6dc | ||
|
f49d644b6a | ||
|
02ac0b8593 | ||
|
78e5eaf1e2 | ||
|
fc0d227396 | ||
|
f91c52cdf7 | ||
|
3f14878d0f | ||
|
69fd32e7c6 | ||
|
80801cedf0 | ||
|
480e4fa94b | ||
|
717c789836 | ||
|
f099376446 | ||
|
41fa9d093e | ||
|
e2f3f81eb6 | ||
|
5cab7aeb55 | ||
|
fa5f418c22 | ||
|
a25fcacace | ||
|
727ba7300e | ||
|
d25d9b3f44 | ||
|
9d762a4319 | ||
|
18d94c7585 | ||
|
af82345eb8 | ||
|
1e94a22986 | ||
|
e19a8a99ff | ||
|
f975009e24 | ||
|
90640fafc7 | ||
|
42e0e1e5a5 | ||
|
61f64fa933 | ||
|
0d0ffd6d27 | ||
|
023ccffd2e | ||
|
bc26098c7d | ||
|
7257a71a18 | ||
|
8ad8b73585 | ||
|
9a06815db9 | ||
|
66947d4954 | ||
|
3ec22185d5 | ||
|
0f05c23e26 | ||
|
74493fece0 | ||
|
557a96e814 | ||
|
4b23b419a4 | ||
|
8b7f5b1151 | ||
|
29e9ccf216 | ||
|
2c04f5c8bc | ||
|
5430a747e9 | ||
|
13bc185829 | ||
|
ed87581a8a | ||
|
2645ec96a8 | ||
|
d278d830f0 | ||
|
b4dce0ed46 | ||
|
e8416042d4 | ||
|
70adbe5e76 | ||
|
f66be08d1d | ||
|
fad96f5266 | ||
|
c33a7f5f47 | ||
|
28afbcde02 | ||
|
691292d2b3 | ||
|
30ff81064f | ||
|
f9f398ad98 | ||
|
537884bdcd | ||
|
d54400a7cb | ||
|
42c78264fb | ||
|
c52da82447 | ||
|
cca43624e4 | ||
|
dac1cd7668 | ||
|
b4c00db0e3 | ||
|
3ce393a8b2 | ||
|
2b627fe4ab | ||
|
fcf8a1649d | ||
|
8d3b050831 | ||
|
43297ef0a3 | ||
|
551fabdfc9 | ||
|
d6de56b2db | ||
|
9e979804f3 | ||
|
b8e0f182cc | ||
|
9a41b9e192 | ||
|
9b8f520b9f | ||
|
5b6019805c | ||
|
a4106436c4 | ||
|
f6b4eb511e | ||
|
eb67ac17a0 | ||
|
7b760d03ef | ||
|
2805ae2acf | ||
|
5cb5ccc781 | ||
|
0dba2821b6 | ||
|
1db7825b22 | ||
|
8a92d6ad70 | ||
|
138ad9fcad | ||
|
08cb518c25 | ||
|
6d04eab200 | ||
|
8a8cb51e1b | ||
|
5c66042a2d | ||
|
ae75181b02 | ||
|
9dc3238182 | ||
|
0a68749373 | ||
|
1519116291 | ||
|
d023d841e2 | ||
|
d7a1465d8e | ||
|
15848fc696 | ||
|
837ae2c9f2 | ||
|
6789b98ead | ||
|
29f6e74ee3 | ||
|
2780c38c45 | ||
|
4531838217 | ||
|
7bccf7f65d | ||
|
c62a24a9cb | ||
|
c6830499f7 | ||
|
d9f00a2539 | ||
|
def83c9119 | ||
|
b6cb0056c7 | ||
|
1669b5258a | ||
|
5a9e0bfc26 | ||
|
8f3fdb3afe | ||
|
7ab135c099 | ||
|
1cc27f2509 | ||
|
ef700b421c | ||
|
b9af5ac052 | ||
|
173f2d421d | ||
|
9f82c069c9 | ||
|
6ade09c228 | ||
|
5393a1399c | ||
|
fd29f20b2e | ||
|
c011b53d1f | ||
|
26e3a7b68c | ||
|
e70a291698 | ||
|
511bd57726 | ||
|
c6de8598dc | ||
|
7bee25599d | ||
|
773a1ad829 | ||
|
1a1e264fa4 | ||
|
5b99ecefca | ||
|
1bfeed377a | ||
|
86eee376bb | ||
|
6a7d368184 | ||
|
1da19ae98d | ||
|
f52c117b09 | ||
|
2551f96149 | ||
|
c984c19fee | ||
|
ac8e91e82e | ||
|
af50d80541 | ||
|
e355f4e49b | ||
|
738ef2b0fa | ||
|
9746a3964b | ||
|
9efbf74b6f | ||
|
c16ea6b237 | ||
|
b06a3b588f | ||
|
6ff67e0995 | ||
|
1a5c8d126d | ||
|
f632580eed | ||
|
1d73f0cdee | ||
|
99c7111f8c | ||
|
ae9806561a | ||
|
fffdf5b5b7 | ||
|
3de19c3db7 | ||
|
56e074b4ef | ||
|
1a79253780 | ||
|
b67908df11 | ||
|
6a5089f71d | ||
|
f710463b67 | ||
|
6ae0c3aba0 | ||
|
07c6347014 | ||
|
b10999e83e | ||
|
961b5bfd25 | ||
|
d1f1d1068a | ||
|
faade0afe2 | ||
|
a442a49e2f | ||
|
7d3fbbcaba | ||
|
d9bb7c3271 | ||
|
4d1dd17fa2 | ||
|
c5c6c777be | ||
|
1fd1a73fda | ||
|
feeb498a79 | ||
|
1d7f30d441 | ||
|
53da63e371 | ||
|
424443a2d8 | ||
|
08b58f0e4c | ||
|
c2a0d8600f | ||
|
ede3c2ce2f | ||
|
0b93515711 | ||
|
2f13e689b9 | ||
|
ea135a0d28 | ||
|
f67e4a03e4 | ||
|
e9268edeff | ||
|
a8576a55d6 | ||
|
eb500cdf9e | ||
|
7b9dc66121 | ||
|
bba2c68c6f | ||
|
c70d8bea78 | ||
|
176bda66ad | ||
|
78ce10ddf0 | ||
|
29f14681a8 | ||
|
aaf617e41c | ||
|
b99146ed7f | ||
|
d136161650 | ||
|
39051a27a1 | ||
|
30bc9dd551 | ||
|
cc2f665313 | ||
|
37c989ed28 | ||
|
adc5885980 | ||
|
8de5b4219d | ||
|
e096ddebd4 | ||
|
83398dd0bc | ||
|
ddc78ebb41 | ||
|
70b69eb31e | ||
|
3fa891e136 | ||
|
6f7a9f3295 | ||
|
8e8793943b | ||
|
83d9faf2fe | ||
|
f45a4fff8b | ||
|
1e02aa3120 | ||
|
77fec75f23 | ||
|
2c9feae20b | ||
|
a81788b3fa | ||
|
88cfba53a3 | ||
|
d659d975cd | ||
|
277ad90f72 | ||
|
f2e3bedde6 | ||
|
98c0f2315a | ||
|
0115e74163 | ||
|
685f541bb4 | ||
|
f9634ea283 | ||
|
6252a4d8c8 | ||
|
d13d7bec45 | ||
|
74e284d795 | ||
|
7b82dacbf2 | ||
|
f82162f94a | ||
|
e2d5128624 | ||
|
9074412472 | ||
|
0db9c66317 | ||
|
b22211fada | ||
|
7efeaa3a22 | ||
|
79efac2f71 | ||
|
9d7b7092f5 | ||
|
71e877d369 | ||
|
7e086a7730 | ||
|
3f810c71b0 | ||
|
500fbc5294 | ||
|
6d0f60d9a1 | ||
|
e19c26fdd1 | ||
|
86b86513d9 | ||
|
6ff19063ef | ||
|
6a75451539 | ||
|
d180e7b5e1 | ||
|
9e869a5b1f | ||
|
08d2655f13 | ||
|
bef7c38724 | ||
|
d1abf4971d | ||
|
d13c2cf8d7 | ||
|
1891eb4998 | ||
|
70b03971f6 | ||
|
38c050b47e | ||
|
0bf5c9ebdd | ||
|
6683da804b | ||
|
7750b954fc | ||
|
edbff5a26a | ||
|
18846c11ed | ||
|
cc02c1d585 | ||
|
26bf7c4d46 | ||
|
2959fa1080 | ||
|
3f4f154949 | ||
|
4c0b472f67 | ||
|
f5d26e0d81 | ||
|
e9bb5bee9d | ||
|
2c608bca3c | ||
|
d9c1e30e9b | ||
|
5bc72e2b44 | ||
|
df05d04dad | ||
|
888ac8f4c2 | ||
|
55266e5694 | ||
|
1008b1835b | ||
|
2ffa5419e2 | ||
|
bd20ecff78 | ||
|
a073514c46 | ||
|
c6ff14226f | ||
|
ee02f8255e | ||
|
5951ed3f55 | ||
|
f6fbf3ba0f | ||
|
9bce52a92a | ||
|
e44975622a | ||
|
92b857d83b | ||
|
8a282fd2e1 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -35,4 +35,9 @@ _vieux/
|
||||
_mydocs
|
||||
.DS_Store
|
||||
Assets/DownloadBadges*.psd
|
||||
node_modules
|
||||
node_modules
|
||||
Tools/github_oauth_token.txt
|
||||
_releases
|
||||
ReactNativeClient/lib/csstojs/
|
||||
ElectronClient/app/gui/note-viewer/fonts/
|
||||
Tools/commit_hook.txt
|
10
.travis.yml
10
.travis.yml
@@ -1,3 +1,6 @@
|
||||
# Only build tags (Doesn't work - doesn't build anything)
|
||||
if: tag IS present
|
||||
|
||||
rvm: 2.3.3
|
||||
|
||||
matrix:
|
||||
@@ -43,7 +46,8 @@ before_install:
|
||||
|
||||
script:
|
||||
- |
|
||||
cd ElectronClient/app
|
||||
rsync -aP ../../ReactNativeClient/lib/ lib/
|
||||
cd Tools
|
||||
npm install
|
||||
yarn dist
|
||||
cd ../ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && yarn dist
|
||||
|
15
BUILD.md
15
BUILD.md
@@ -17,17 +17,28 @@ If you get a node-gyp related error you might need to manually install it: `npm
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
|
||||
Before building any of the applications, you need to build the tools:
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
```
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
cd ElectronClient/app
|
||||
rsync -a ../../ReactNativeClient/lib/ lib/
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
|
||||
If there's an error `while loading shared libraries: libgconf-2.so.4: cannot open shared object file: No such file or directory`, run `sudo apt-get install libgconf-2-4`
|
||||
|
||||
For node-gyp to work, you might need to install the `windows-build-tools` using `npm install --global windows-build-tools`.
|
||||
|
||||
That will create the executable file in the `dist` directory.
|
||||
|
||||
From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
@@ -44,7 +55,7 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync -aP ../ReactNativeClient/locales/ build/locales/
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
20
CONTRIBUTING.md
Normal file
20
CONTRIBUTING.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Reporting a bug
|
||||
|
||||
Please check first that it [has not already been reported](https://github.com/laurent22/joplin/issues?utf8=%E2%9C%93&q=is%3Aissue). Also consider [enabling debug mode](https://github.com/laurent22/joplin/blob/master/README_debugging.md) before reporting the issue so that you can provide as much details as possible to help fix it.
|
||||
|
||||
If possible, **please provide a screenshot**. A screenshot showing the problem is often more useful than a paragraph describing it as it can make it immediately clear what the issue is.
|
||||
|
||||
# Feature requests
|
||||
|
||||
Again, please check that it has not already been requested. If it has, simply **up-vote the issue** - the ones with the most up-votes are likely to be implemented. Adding a "+1" comment does nothing.
|
||||
|
||||
# Adding new features
|
||||
|
||||
If you want to add a new feature, consider asking about it before implementing it to make sure it is within the scope of the project. Of course you are free to create the pull request directly but it is not guaranteed it is going to be accepted.
|
||||
|
||||
Building the apps is relatively easy - please [see the build instructions](https://github.com/laurent22/joplin/blob/master/BUILD.md) for more details.
|
||||
|
||||
# Coding style
|
||||
|
||||
- Only use tabs for indentation, not spaces.
|
||||
- Do not remove or add optional characters from other lines (such as colons or new line characters) as it can make the commit needlessly big, and create conflicts with other changes.
|
@@ -1,6 +1,6 @@
|
||||
const { _ } = require('lib/locale.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 http = require("http");
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const { reducer, defaultState } = require('lib/reducer.js');
|
||||
const { splitCommandString } = require('lib/string-utils.js');
|
||||
@@ -14,6 +14,7 @@ const chalk = require('chalk');
|
||||
const tk = require('terminal-kit');
|
||||
const TermWrapper = require('tkwidgets/framework/TermWrapper.js');
|
||||
const Renderer = require('tkwidgets/framework/Renderer.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
|
||||
const BaseWidget = require('tkwidgets/BaseWidget.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
@@ -34,37 +35,55 @@ const ConsoleWidget = require('./gui/ConsoleWidget.js');
|
||||
|
||||
class AppGui {
|
||||
|
||||
constructor(app, store) {
|
||||
this.app_ = app;
|
||||
this.store_ = store;
|
||||
constructor(app, store, keymap) {
|
||||
try {
|
||||
this.app_ = app;
|
||||
this.store_ = store;
|
||||
|
||||
BaseWidget.setLogger(app.logger());
|
||||
BaseWidget.setLogger(app.logger());
|
||||
|
||||
this.term_ = new TermWrapper(tk.terminal);
|
||||
this.term_ = new TermWrapper(tk.terminal);
|
||||
|
||||
this.renderer_ = null;
|
||||
this.logger_ = new Logger();
|
||||
this.buildUi();
|
||||
// Some keys are directly handled by the tkwidget framework
|
||||
// so they need to be remapped in a different way.
|
||||
this.tkWidgetKeys_ = {
|
||||
'focus_next': 'TAB',
|
||||
'focus_previous': 'SHIFT_TAB',
|
||||
'move_up': 'UP',
|
||||
'move_down': 'DOWN',
|
||||
'page_down': 'PAGE_DOWN',
|
||||
'page_up': 'PAGE_UP',
|
||||
};
|
||||
|
||||
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
|
||||
this.renderer_ = null;
|
||||
this.logger_ = new Logger();
|
||||
this.buildUi();
|
||||
|
||||
this.app_.on('modelAction', async (event) => {
|
||||
await this.handleModelAction(event.action);
|
||||
});
|
||||
this.renderer_ = new Renderer(this.term(), this.rootWidget_);
|
||||
|
||||
this.shortcuts_ = this.setupShortcuts();
|
||||
this.app_.on('modelAction', async (event) => {
|
||||
await this.handleModelAction(event.action);
|
||||
});
|
||||
|
||||
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
|
||||
this.keymap_ = this.setupKeymap(keymap);
|
||||
|
||||
this.commandCancelCalled_ = false;
|
||||
this.inputMode_ = AppGui.INPUT_MODE_NORMAL;
|
||||
|
||||
this.currentShortcutKeys_ = [];
|
||||
this.lastShortcutKeyTime_ = 0;
|
||||
this.commandCancelCalled_ = false;
|
||||
|
||||
// Recurrent sync is setup only when the GUI is started. In
|
||||
// a regular command it's not necessary since the process
|
||||
// exits right away.
|
||||
reg.setupRecurrentSync();
|
||||
this.currentShortcutKeys_ = [];
|
||||
this.lastShortcutKeyTime_ = 0;
|
||||
|
||||
// Recurrent sync is setup only when the GUI is started. In
|
||||
// a regular command it's not necessary since the process
|
||||
// exits right away.
|
||||
reg.setupRecurrentSync();
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
} catch (error) {
|
||||
this.fullScreen(false);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
store() {
|
||||
@@ -88,8 +107,8 @@ class AppGui {
|
||||
return this.term().restoreState(state);
|
||||
}
|
||||
|
||||
prompt(initialText = '', promptString = ':') {
|
||||
return this.widget('statusBar').prompt(initialText, promptString);
|
||||
prompt(initialText = '', promptString = ':', options = null) {
|
||||
return this.widget('statusBar').prompt(initialText, promptString, options);
|
||||
}
|
||||
|
||||
stdoutMaxWidth() {
|
||||
@@ -103,6 +122,7 @@ class AppGui {
|
||||
buildUi() {
|
||||
this.rootWidget_ = new ReduxRootWidget(this.store_);
|
||||
this.rootWidget_.name = 'root';
|
||||
this.rootWidget_.autoShortcutsEnabled = false;
|
||||
|
||||
const folderList = new FolderListWidget();
|
||||
folderList.style = {
|
||||
@@ -270,152 +290,26 @@ class AppGui {
|
||||
this.stdout(chalk.cyan.bold('> ' + cmd));
|
||||
}
|
||||
|
||||
setupShortcuts() {
|
||||
const shortcuts = {};
|
||||
setupKeymap(keymap) {
|
||||
const output = [];
|
||||
|
||||
shortcuts['TAB'] = {
|
||||
friendlyName: 'Tab',
|
||||
description: () => _('Give focus to next pane'),
|
||||
isDocOnly: true,
|
||||
}
|
||||
for (let i = 0; i < keymap.length; i++) {
|
||||
const item = Object.assign({}, keymap[i]);
|
||||
|
||||
shortcuts['SHIFT_TAB'] = {
|
||||
friendlyName: 'Shift+Tab',
|
||||
description: () => _('Give focus to previous pane'),
|
||||
isDocOnly: true,
|
||||
}
|
||||
if (!item.command) throw new Error('Missing command for keymap item: ' + JSON.stringify(item));
|
||||
|
||||
shortcuts[':'] = {
|
||||
description: () => _('Enter command line mode'),
|
||||
action: async () => {
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
this.addCommandToConsole(cmd);
|
||||
await this.processCommand(cmd);
|
||||
},
|
||||
};
|
||||
if (!('type' in item)) item.type = 'exec';
|
||||
|
||||
shortcuts['ESC'] = { // Built into terminal-kit inputField
|
||||
description: () => _('Exit command line mode'),
|
||||
isDocOnly: true,
|
||||
};
|
||||
|
||||
shortcuts['ENTER'] = {
|
||||
description: () => _('Edit the selected note'),
|
||||
action: () => {
|
||||
const w = this.widget('mainWindow').focusedWidget;
|
||||
if (w.name === 'folderList') {
|
||||
this.widget('noteList').focus();
|
||||
} else if (w.name === 'noteList' || w.name === 'noteText') {
|
||||
this.processCommand('edit $n');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
shortcuts['CTRL_C'] = {
|
||||
description: () => _('Cancel the current command.'),
|
||||
friendlyName: 'Ctrl+C',
|
||||
isDocOnly: true,
|
||||
}
|
||||
|
||||
shortcuts['CTRL_D'] = {
|
||||
description: () => _('Exit the application.'),
|
||||
friendlyName: 'Ctrl+D',
|
||||
isDocOnly: true,
|
||||
}
|
||||
|
||||
shortcuts['DELETE'] = {
|
||||
description: () => _('Delete the currently selected note or notebook.'),
|
||||
action: async () => {
|
||||
if (this.widget('folderList').hasFocus) {
|
||||
const item = this.widget('folderList').selectedJoplinItem;
|
||||
|
||||
if (!item) return;
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_FOLDER) {
|
||||
await this.processCommand('rmbook ' + item.id);
|
||||
} else if (item.type_ === BaseModel.TYPE_TAG) {
|
||||
this.stdout(_('To delete a tag, untag the associated notes.'));
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
this.store().dispatch({
|
||||
type: 'SEARCH_DELETE',
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
} else if (this.widget('noteList').hasFocus) {
|
||||
await this.processCommand('rmnote $n');
|
||||
} else {
|
||||
this.stdout(_('Please select the note or notebook to be deleted first.'));
|
||||
}
|
||||
if (item.command in this.tkWidgetKeys_) {
|
||||
item.type = 'tkwidgets';
|
||||
}
|
||||
};
|
||||
|
||||
shortcuts['BACKSPACE'] = {
|
||||
alias: 'DELETE',
|
||||
};
|
||||
item.canRunAlongOtherCommands = item.type === 'function' && ['toggle_metadata', 'toggle_console'].indexOf(item.command) >= 0;
|
||||
|
||||
shortcuts[' '] = {
|
||||
friendlyName: 'SPACE',
|
||||
description: () => _('Set a to-do as completed / not completed'),
|
||||
action: 'todo toggle $n',
|
||||
output.push(item);
|
||||
}
|
||||
|
||||
shortcuts['tc'] = {
|
||||
description: () => _('[t]oggle [c]onsole between maximized/minimized/hidden/visible.'),
|
||||
action: () => {
|
||||
if (!this.consoleIsShown()) {
|
||||
this.showConsole();
|
||||
this.minimizeConsole();
|
||||
} else {
|
||||
if (this.consoleIsMaximized()) {
|
||||
this.hideConsole();
|
||||
} else {
|
||||
this.maximizeConsole();
|
||||
}
|
||||
}
|
||||
},
|
||||
canRunAlongOtherCommands: true,
|
||||
}
|
||||
|
||||
shortcuts['/'] = {
|
||||
description: () => _('Search'),
|
||||
action: { type: 'prompt', initialText: 'search ""', cursorPosition: -2 },
|
||||
};
|
||||
|
||||
shortcuts['tm'] = {
|
||||
description: () => _('[t]oggle note [m]etadata.'),
|
||||
action: () => {
|
||||
this.toggleNoteMetadata();
|
||||
},
|
||||
canRunAlongOtherCommands: true,
|
||||
}
|
||||
|
||||
shortcuts['mn'] = {
|
||||
description: () => _('[M]ake a new [n]ote'),
|
||||
action: { type: 'prompt', initialText: 'mknote ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['mt'] = {
|
||||
description: () => _('[M]ake a new [t]odo'),
|
||||
action: { type: 'prompt', initialText: 'mktodo ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['mb'] = {
|
||||
description: () => _('[M]ake a new note[b]ook'),
|
||||
action: { type: 'prompt', initialText: 'mkbook ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['yn'] = {
|
||||
description: () => _('Copy ([Y]ank) the [n]ote to a notebook.'),
|
||||
action: { type: 'prompt', initialText: 'cp $n ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
shortcuts['dn'] = {
|
||||
description: () => _('Move the note to a notebook.'),
|
||||
action: { type: 'prompt', initialText: 'mv $n ""', cursorPosition: -2 },
|
||||
}
|
||||
|
||||
return shortcuts;
|
||||
return output;
|
||||
}
|
||||
|
||||
toggleConsole() {
|
||||
@@ -490,8 +384,16 @@ class AppGui {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
shortcuts() {
|
||||
return this.shortcuts_;
|
||||
keymap() {
|
||||
return this.keymap_;
|
||||
}
|
||||
|
||||
keymapItemByKey(key) {
|
||||
for (let i = 0; i < this.keymap_.length; i++) {
|
||||
const item = this.keymap_[i];
|
||||
if (item.keys.indexOf(key) >= 0) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
term() {
|
||||
@@ -522,18 +424,78 @@ class AppGui {
|
||||
}
|
||||
}
|
||||
|
||||
async processCommand(cmd) {
|
||||
async processFunctionCommand(cmd) {
|
||||
|
||||
if (cmd === 'activate') {
|
||||
|
||||
const w = this.widget('mainWindow').focusedWidget;
|
||||
if (w.name === 'folderList') {
|
||||
this.widget('noteList').focus();
|
||||
} else if (w.name === 'noteList' || w.name === 'noteText') {
|
||||
this.processPromptCommand('edit $n');
|
||||
}
|
||||
|
||||
} else if (cmd === 'delete') {
|
||||
|
||||
if (this.widget('folderList').hasFocus) {
|
||||
const item = this.widget('folderList').selectedJoplinItem;
|
||||
|
||||
if (!item) return;
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_FOLDER) {
|
||||
await this.processPromptCommand('rmbook ' + item.id);
|
||||
} else if (item.type_ === BaseModel.TYPE_TAG) {
|
||||
this.stdout(_('To delete a tag, untag the associated notes.'));
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
this.store().dispatch({
|
||||
type: 'SEARCH_DELETE',
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
} else if (this.widget('noteList').hasFocus) {
|
||||
await this.processPromptCommand('rmnote $n');
|
||||
} else {
|
||||
this.stdout(_('Please select the note or notebook to be deleted first.'));
|
||||
}
|
||||
|
||||
} else if (cmd === 'toggle_console') {
|
||||
|
||||
if (!this.consoleIsShown()) {
|
||||
this.showConsole();
|
||||
this.minimizeConsole();
|
||||
} else {
|
||||
if (this.consoleIsMaximized()) {
|
||||
this.hideConsole();
|
||||
} else {
|
||||
this.maximizeConsole();
|
||||
}
|
||||
}
|
||||
|
||||
} else if (cmd === 'toggle_metadata') {
|
||||
|
||||
this.toggleNoteMetadata();
|
||||
|
||||
} else if (cmd === 'enter_command_line_mode') {
|
||||
|
||||
const cmd = await this.widget('statusBar').prompt();
|
||||
if (!cmd) return;
|
||||
this.addCommandToConsole(cmd);
|
||||
await this.processPromptCommand(cmd);
|
||||
|
||||
} else {
|
||||
|
||||
throw new Error('Unknown command: ' + cmd);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async processPromptCommand(cmd) {
|
||||
if (!cmd) return;
|
||||
cmd = cmd.trim();
|
||||
if (!cmd.length) return;
|
||||
|
||||
this.logger().info('Got command: ' + cmd);
|
||||
|
||||
if (cmd === 'q' || cmd === 'wq' || cmd === 'qa') { // Vim bonus
|
||||
await this.app().exit();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let note = this.widget('noteList').currentItem;
|
||||
let folder = this.widget('folderList').currentItem;
|
||||
@@ -784,35 +746,34 @@ class AppGui {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const shortcutKey = this.currentShortcutKeys_.join('');
|
||||
let cmd = shortcutKey in this.shortcuts_ ? this.shortcuts_[shortcutKey] : null;
|
||||
let keymapItem = this.keymapItemByKey(shortcutKey);
|
||||
|
||||
// If this command is an alias to another command, resolve to the actual command
|
||||
if (cmd && cmd.alias) cmd = this.shortcuts_[cmd.alias];
|
||||
|
||||
let processShortcutKeys = !this.app().currentCommand() && cmd;
|
||||
if (cmd && cmd.canRunAlongOtherCommands) processShortcutKeys = true;
|
||||
let processShortcutKeys = !this.app().currentCommand() && keymapItem;
|
||||
if (keymapItem && keymapItem.canRunAlongOtherCommands) processShortcutKeys = true;
|
||||
if (statusBar.promptActive) processShortcutKeys = false;
|
||||
if (cmd && cmd.isDocOnly) processShortcutKeys = false;
|
||||
|
||||
if (processShortcutKeys) {
|
||||
this.logger().info('Shortcut:', shortcutKey, cmd.description());
|
||||
this.logger().info('Shortcut:', shortcutKey, keymapItem);
|
||||
|
||||
this.currentShortcutKeys_ = [];
|
||||
if (typeof cmd.action === 'function') {
|
||||
await cmd.action();
|
||||
} else if (typeof cmd.action === 'object') {
|
||||
if (cmd.action.type === 'prompt') {
|
||||
let promptOptions = {};
|
||||
if ('cursorPosition' in cmd.action) promptOptions.cursorPosition = cmd.action.cursorPosition;
|
||||
const commandString = await statusBar.prompt(cmd.action.initialText ? cmd.action.initialText : '', null, promptOptions);
|
||||
this.addCommandToConsole(commandString);
|
||||
await this.processCommand(commandString);
|
||||
} else {
|
||||
throw new Error('Unknown command: ' + JSON.stringify(cmd.action));
|
||||
}
|
||||
} else { // String
|
||||
this.stdout(cmd.action);
|
||||
await this.processCommand(cmd.action);
|
||||
|
||||
if (keymapItem.type === 'function') {
|
||||
this.processFunctionCommand(keymapItem.command);
|
||||
} else if (keymapItem.type === 'prompt') {
|
||||
let promptOptions = {};
|
||||
if ('cursorPosition' in keymapItem) promptOptions.cursorPosition = keymapItem.cursorPosition;
|
||||
const commandString = await statusBar.prompt(keymapItem.command ? keymapItem.command : '', null, promptOptions);
|
||||
this.addCommandToConsole(commandString);
|
||||
await this.processPromptCommand(commandString);
|
||||
} else if (keymapItem.type === 'exec') {
|
||||
this.stdout(keymapItem.command);
|
||||
await this.processPromptCommand(keymapItem.command);
|
||||
} else if (keymapItem.type === 'tkwidgets') {
|
||||
this.widget('root').handleKey(this.tkWidgetKeys_[keymapItem.command]);
|
||||
} else {
|
||||
throw new Error('Unknown command type: ' + JSON.stringify(keymapItem));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,12 +5,12 @@ const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { Database } = require('lib/database.js');
|
||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
@@ -21,6 +21,7 @@ const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
const EventEmitter = require('events');
|
||||
const Cache = require('lib/Cache');
|
||||
|
||||
class Application extends BaseApplication {
|
||||
|
||||
@@ -34,6 +35,7 @@ class Application extends BaseApplication {
|
||||
this.allCommandsLoaded_ = false;
|
||||
this.showStackTraces_ = false;
|
||||
this.gui_ = null;
|
||||
this.cache_ = new Cache();
|
||||
}
|
||||
|
||||
gui() {
|
||||
@@ -144,13 +146,15 @@ class Application extends BaseApplication {
|
||||
message += ' (' + options.answers.join('/') + ')';
|
||||
}
|
||||
|
||||
let answer = await this.gui().prompt('', message + ' ');
|
||||
let answer = await this.gui().prompt('', message + ' ', options);
|
||||
|
||||
if (options.type === 'boolean') {
|
||||
if (answer === null) return false; // Pressed ESCAPE
|
||||
if (!answer) answer = options.answers[0];
|
||||
let positiveIndex = options.booleanAnswerDefault == 'y' ? 0 : 1;
|
||||
return answer.toLowerCase() === options.answers[positiveIndex].toLowerCase();
|
||||
} else {
|
||||
return answer;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -221,12 +225,8 @@ class Application extends BaseApplication {
|
||||
async commandMetadata() {
|
||||
if (this.commandMetadata_) return this.commandMetadata_;
|
||||
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
const storage = require('node-persist');
|
||||
await storage.init({ dir: osTmpdir() + '/commandMetadata', ttl: 1000 * 60 * 60 * 24 });
|
||||
|
||||
let output = await storage.getItem('metadata');
|
||||
if (Setting.value('env') != 'dev' && output) {
|
||||
let output = await this.cache_.getItem('metadata');
|
||||
if (output) {
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
}
|
||||
@@ -240,7 +240,7 @@ class Application extends BaseApplication {
|
||||
output[n] = cmd.metadata();
|
||||
}
|
||||
|
||||
await storage.setItem('metadata', output);
|
||||
await this.cache_.setItem('metadata', output, 1000 * 60 * 60 * 24);
|
||||
|
||||
this.commandMetadata_ = output;
|
||||
return Object.assign({}, this.commandMetadata_);
|
||||
@@ -275,7 +275,7 @@ class Application extends BaseApplication {
|
||||
dummyGui() {
|
||||
return {
|
||||
isDummy: () => { return true; },
|
||||
prompt: (initialText = '', promptString = '') => { return cliUtils.prompt(initialText, promptString); },
|
||||
prompt: (initialText = '', promptString = '', options = null) => { return cliUtils.prompt(initialText, promptString, options); },
|
||||
showConsole: () => {},
|
||||
maximizeConsole: () => {},
|
||||
stdout: (text) => { console.info(text); },
|
||||
@@ -312,6 +312,63 @@ class Application extends BaseApplication {
|
||||
return this.activeCommand_;
|
||||
}
|
||||
|
||||
async loadKeymaps() {
|
||||
const defaultKeyMap = [
|
||||
{ "keys": [":"], "type": "function", "command": "enter_command_line_mode" },
|
||||
{ "keys": ["TAB"], "type": "function", "command": "focus_next" },
|
||||
{ "keys": ["SHIFT_TAB"], "type": "function", "command": "focus_previous" },
|
||||
{ "keys": ["UP"], "type": "function", "command": "move_up" },
|
||||
{ "keys": ["DOWN"], "type": "function", "command": "move_down" },
|
||||
{ "keys": ["PAGE_UP"], "type": "function", "command": "page_up" },
|
||||
{ "keys": ["PAGE_DOWN"], "type": "function", "command": "page_down" },
|
||||
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
|
||||
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
|
||||
{ "keys": [" "], "command": "todo toggle $n" },
|
||||
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
|
||||
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
|
||||
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mn"], "type": "prompt", "command": "mknote \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mt"], "type": "prompt", "command": "mktodo \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["mb"], "type": "prompt", "command": "mkbook \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["yn"], "type": "prompt", "command": "cp $n \"\"", "cursorPosition": -2 },
|
||||
{ "keys": ["dn"], "type": "prompt", "command": "mv $n \"\"", "cursorPosition": -2 }
|
||||
];
|
||||
|
||||
// Filter the keymap item by command so that items in keymap.json can override
|
||||
// the default ones.
|
||||
const itemsByCommand = {};
|
||||
|
||||
for (let i = 0; i < defaultKeyMap.length; i++) {
|
||||
itemsByCommand[defaultKeyMap[i].command] = defaultKeyMap[i]
|
||||
}
|
||||
|
||||
const filePath = Setting.value('profileDir') + '/keymap.json';
|
||||
if (await fs.pathExists(filePath)) {
|
||||
try {
|
||||
let configString = await fs.readFile(filePath, 'utf-8');
|
||||
configString = configString.replace(/^\s*\/\/.*/, ''); // Strip off comments
|
||||
const keymap = JSON.parse(configString);
|
||||
for (let keymapIndex = 0; keymapIndex < keymap.length; keymapIndex++) {
|
||||
const item = keymap[keymapIndex];
|
||||
itemsByCommand[item.command] = item;
|
||||
}
|
||||
} catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = 'Could not load keymap ' + filePath + '\n' + msg;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const output = [];
|
||||
for (let n in itemsByCommand) {
|
||||
if (!itemsByCommand.hasOwnProperty(n)) continue;
|
||||
output.push(itemsByCommand[n]);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async start(argv) {
|
||||
argv = await super.start(argv);
|
||||
|
||||
@@ -338,8 +395,10 @@ class Application extends BaseApplication {
|
||||
} else { // Otherwise open the GUI
|
||||
this.initRedux();
|
||||
|
||||
const keymap = await this.loadKeymaps();
|
||||
|
||||
const AppGui = require('./app-gui.js');
|
||||
this.gui_ = new AppGui(this, this.store());
|
||||
this.gui_ = new AppGui(this, this.store(), keymap);
|
||||
this.gui_.setLogger(this.logger_);
|
||||
await this.gui_.start();
|
||||
|
||||
@@ -354,7 +413,7 @@ class Application extends BaseApplication {
|
||||
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
tags: tags,
|
||||
items: tags,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
|
@@ -1,9 +1,10 @@
|
||||
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 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');
|
||||
var fs = require('fs-extra');
|
||||
|
||||
async function handleAutocompletionPromise(line) {
|
||||
// Auto-complete the command name
|
||||
@@ -48,7 +49,7 @@ async function handleAutocompletionPromise(line) {
|
||||
if (options.length > 1 && options[1].indexOf(next) === 0) {
|
||||
l.push(options[1]);
|
||||
} else if (options[0].indexOf(next) === 0) {
|
||||
l.push(options[2]);
|
||||
l.push(options[0]);
|
||||
}
|
||||
}
|
||||
if (l.length === 0) {
|
||||
@@ -71,8 +72,10 @@ async function handleAutocompletionPromise(line) {
|
||||
let argName = cmdUsage[positionalArgs - 1];
|
||||
argName = cliUtils.parseCommandArg(argName).name;
|
||||
|
||||
if (argName == 'note' || argName == 'note-pattern' && app().currentFolder()) {
|
||||
const notes = await Note.previews(app().currentFolder().id, { titlePattern: next + '*' });
|
||||
const currentFolder = app().currentFolder();
|
||||
|
||||
if (argName == 'note' || argName == 'note-pattern') {
|
||||
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
|
||||
l.push(...notes.map((n) => n.title));
|
||||
}
|
||||
|
||||
@@ -81,11 +84,22 @@ async function handleAutocompletionPromise(line) {
|
||||
l.push(...folders.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'item') {
|
||||
const notes = currentFolder ? await Note.previews(currentFolder.id, { titlePattern: next + '*' }) : [];
|
||||
const folders = await Folder.search({ titlePattern: next + '*' });
|
||||
l.push(...notes.map((n) => n.title), folders.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'tag') {
|
||||
let tags = await Tag.search({ titlePattern: next + '*' });
|
||||
l.push(...tags.map((n) => n.title));
|
||||
}
|
||||
|
||||
if (argName == 'file') {
|
||||
let files = await fs.readdir('.');
|
||||
l.push(...files);
|
||||
}
|
||||
|
||||
if (argName == 'tag-command') {
|
||||
let c = filterList(['add', 'remove', 'list'], next);
|
||||
l.push(...c);
|
||||
|
@@ -12,6 +12,10 @@ class BaseCommand {
|
||||
throw new Error('Usage not defined');
|
||||
}
|
||||
|
||||
encryptionCheck(item) {
|
||||
if (item && item.encryption_applied) throw new Error(_('Cannot change encrypted item'));
|
||||
}
|
||||
|
||||
description() {
|
||||
throw new Error('Description not defined');
|
||||
}
|
||||
@@ -28,10 +32,6 @@ class BaseCommand {
|
||||
return this.compatibleUis().indexOf(ui) >= 0;
|
||||
}
|
||||
|
||||
aliases() {
|
||||
return [];
|
||||
}
|
||||
|
||||
options() {
|
||||
return [];
|
||||
}
|
||||
|
@@ -5,10 +5,10 @@ const { Logger } = require('lib/logger.js');
|
||||
const { dirname } = require('lib/path-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
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
|
||||
// it can be a drop-in replacement, however initialText is not used (and cannot be
|
||||
// with readline.question?).
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':') {
|
||||
cliUtils.prompt = function(initialText = '', promptString = ':', options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
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({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
output: mutableStdout,
|
||||
terminal: true,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mutableStdout.muted = false;
|
||||
|
||||
rl.question(promptString, (answer) => {
|
||||
rl.close();
|
||||
if (!!options.secure) this.stdout_('');
|
||||
resolve(answer);
|
||||
});
|
||||
|
||||
mutableStdout.muted = !!options.secure;
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
@@ -19,6 +19,7 @@ class Command extends BaseCommand {
|
||||
let title = args['note'];
|
||||
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title, { parent: app().currentFolder() });
|
||||
this.encryptionCheck(note);
|
||||
if (!note) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
const localFilePath = args['file'];
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -21,10 +21,6 @@ class Command extends BaseCommand {
|
||||
];
|
||||
}
|
||||
|
||||
enabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
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);
|
||||
this.stdout(content);
|
||||
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const { app } = require('./app.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -23,7 +23,11 @@ class Command extends BaseCommand {
|
||||
const verbose = args.options.verbose;
|
||||
|
||||
const renderKeyValue = (name) => {
|
||||
const value = Setting.value(name);
|
||||
const md = Setting.settingMetadata(name);
|
||||
let value = Setting.value(name);
|
||||
if (typeof value === 'object' || Array.isArray(value)) value = JSON.stringify(value);
|
||||
if (md.secure) value = '********';
|
||||
|
||||
if (Setting.isEnum(name)) {
|
||||
return _('%s = %s (%s)', name, value, Setting.enumOptionsDoc(name));
|
||||
} else {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -16,8 +16,9 @@ class Command extends BaseCommand {
|
||||
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);
|
||||
commandInstance.encryptionCheck(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));
|
||||
|
||||
@@ -32,7 +33,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
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 { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
184
CliClient/app/command-e2ee.js
Normal file
184
CliClient/app/command-e2ee.js
Normal file
@@ -0,0 +1,184 @@
|
||||
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 {
|
||||
const content = await fs.readFile(fullPath, 'utf8');
|
||||
const item = await BaseItem.unserialize(content);
|
||||
const ItemClass = BaseItem.itemClass(item);
|
||||
|
||||
if (!ItemClass.encryptionSupported()) {
|
||||
otherItemCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
itemCount++;
|
||||
|
||||
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 { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-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.'));
|
||||
let note = await app().loadItem(BaseModel.TYPE_NOTE, title);
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
if (!note) {
|
||||
const ok = await this.prompt(_('Note does not exist: "%s". Create it?', title));
|
||||
if (!ok) return;
|
||||
@@ -79,7 +81,9 @@ class Command extends BaseCommand {
|
||||
const termState = app().gui().termSaveState();
|
||||
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
const result = spawnSync(editorPath, editorArgs, { stdio: 'inherit' });
|
||||
|
||||
if (result.error) this.stdout(_('Error opening note in editor: %s', result.error.message));
|
||||
|
||||
app().gui().termRestoreState(termState);
|
||||
app().gui().hideModalOverlay();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.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 { ReportService } = require('lib/services/report.js');
|
||||
const fs = require('fs-extra');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Exporter } = require('lib/services/exporter.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { renderCommandHelp } = require('./help-utils.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 { _ } = require('lib/locale.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
@@ -36,21 +36,22 @@ class Command extends BaseCommand {
|
||||
async action(args) {
|
||||
const stdoutWidth = app().commandStdoutMaxWidth();
|
||||
|
||||
if (args.command === 'shortcuts') {
|
||||
if (args.command === 'shortcuts' || args.command === 'keymap') {
|
||||
this.stdout(_('For information on how to customise the shortcuts please visit %s', 'http://joplin.cozic.net/terminal/#shortcuts'));
|
||||
this.stdout('');
|
||||
|
||||
if (app().gui().isDummy()) {
|
||||
throw new Error(_('Shortcuts are not available in CLI mode.'));
|
||||
}
|
||||
|
||||
const shortcuts = app().gui().shortcuts();
|
||||
const keymap = app().gui().keymap();
|
||||
|
||||
let rows = [];
|
||||
|
||||
for (let n in shortcuts) {
|
||||
if (!shortcuts.hasOwnProperty(n)) continue;
|
||||
const shortcut = shortcuts[n];
|
||||
if (!shortcut.description) continue;
|
||||
n = shortcut.friendlyName ? shortcut.friendlyName : n;
|
||||
rows.push([n, shortcut.description()]);
|
||||
for (let i = 0; i < keymap.length; i++) {
|
||||
const item = keymap[i];
|
||||
const keys = item.keys.map((k) => k === ' ' ? '(SPACE)' : k);
|
||||
rows.push([keys.join(', '), item.command]);
|
||||
}
|
||||
|
||||
cliUtils.printArray(this.stdout.bind(this), rows);
|
||||
@@ -78,7 +79,7 @@ class Command extends BaseCommand {
|
||||
this.stdout(_('To maximise/minimise the console, press "TC".'));
|
||||
this.stdout(_('To enter command line mode, press ":"'));
|
||||
this.stdout(_('To exit command line mode, press ESCAPE'));
|
||||
this.stdout(_('For the complete list of available keyboard shortcuts, type `help shortcuts`'));
|
||||
this.stdout(_('For the list of keyboard shortcuts and config options, type `help keymap`'));
|
||||
}
|
||||
|
||||
app().gui().showConsole();
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.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 { filename, basename } = require('lib/path-utils.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.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');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -14,10 +14,6 @@ class Command extends BaseCommand {
|
||||
return _('Creates a new notebook.');
|
||||
}
|
||||
|
||||
aliases() {
|
||||
return ['mkdir'];
|
||||
}
|
||||
|
||||
async action(args) {
|
||||
let folder = await Folder.save({ title: args['new-notebook'] }, { userSideValidation: true });
|
||||
app().switchCurrentFolder(folder);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -20,6 +20,7 @@ class Command extends BaseCommand {
|
||||
const name = args['name'];
|
||||
|
||||
const item = await app().loadItem('folderOrNote', pattern);
|
||||
this.encryptionCheck(item);
|
||||
if (!item) throw new Error(_('Cannot find "%s".', pattern));
|
||||
|
||||
const newItem = {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
|
@@ -1,11 +1,11 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.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 { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -35,6 +35,8 @@ class Command extends BaseCommand {
|
||||
if (!notes.length) throw new Error(_('Cannot find "%s".', title));
|
||||
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
this.encryptionCheck(notes[i]);
|
||||
|
||||
let newNote = {
|
||||
id: notes[i].id,
|
||||
type_: notes[i].type_,
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { Database } = require('lib/database.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 { ReportService } = require('lib/services/report.js');
|
||||
|
||||
|
@@ -2,8 +2,8 @@ const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { cliUtils } = require('./cli-utils.js');
|
||||
@@ -11,6 +11,7 @@ const md5 = require('md5');
|
||||
const locker = require('proper-lockfile');
|
||||
const fs = require('fs-extra');
|
||||
const osTmpdir = require('os-tmpdir');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
@@ -61,14 +62,28 @@ class Command extends BaseCommand {
|
||||
});
|
||||
}
|
||||
|
||||
async doAuth(syncTargetId) {
|
||||
async doAuth() {
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId_);
|
||||
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await this.oneDriveApiUtils_.oauthDance({
|
||||
log: (...s) => { return this.stdout(...s); }
|
||||
});
|
||||
this.oneDriveApiUtils_ = null;
|
||||
return auth;
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);
|
||||
|
||||
if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
|
||||
this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
|
||||
const auth = await this.oneDriveApiUtils_.oauthDance({
|
||||
log: (...s) => { return this.stdout(...s); }
|
||||
});
|
||||
this.oneDriveApiUtils_ = null;
|
||||
|
||||
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
|
||||
if (!auth) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
|
||||
return false;
|
||||
}
|
||||
|
||||
cancelAuth() {
|
||||
@@ -86,7 +101,7 @@ class Command extends BaseCommand {
|
||||
this.releaseLockFn_ = null;
|
||||
|
||||
// Lock is unique per profile/database
|
||||
const lockFilePath = osTmpdir() + '/synclock_' + md5(Setting.value('profileDir'));
|
||||
const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
|
||||
if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');
|
||||
|
||||
try {
|
||||
@@ -120,12 +135,8 @@ class Command extends BaseCommand {
|
||||
app().gui().showConsole();
|
||||
app().gui().maximizeConsole();
|
||||
|
||||
const auth = await this.doAuth(this.syncTargetId_);
|
||||
Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
|
||||
if (!auth) {
|
||||
this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
|
||||
return cleanUp();
|
||||
}
|
||||
const authDone = await this.doAuth();
|
||||
if (!authDone) return cleanUp();
|
||||
}
|
||||
|
||||
const sync = await syncTarget.synchronizer();
|
||||
|
@@ -1,8 +1,8 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
@@ -25,6 +25,8 @@ class Command extends BaseCommand {
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
|
||||
this.encryptionCheck(note);
|
||||
|
||||
let toSave = {
|
||||
id: note.id,
|
||||
};
|
||||
|
@@ -1,9 +1,9 @@
|
||||
const { BaseCommand } = require('./base-command.js');
|
||||
const { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
|
||||
const CommandDone = require('./command-done.js');
|
||||
@@ -19,7 +19,7 @@ class Command extends BaseCommand {
|
||||
}
|
||||
|
||||
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 { app } = require('./app.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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');
|
||||
|
||||
class Command extends BaseCommand {
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
const { time } = require('lib/time-utils.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 { FsDriverNode } = require('./fs-driver-node.js');
|
||||
const lodash = require('lodash');
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const Folder = require('lib/models/folder.js').Folder;
|
||||
const Tag = require('lib/models/tag.js').Tag;
|
||||
const BaseModel = require('lib/base-model.js').BaseModel;
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const ListWidget = require('tkwidgets/ListWidget.js');
|
||||
const _ = require('lib/locale.js')._;
|
||||
|
||||
@@ -24,9 +24,9 @@ class FolderListWidget extends ListWidget {
|
||||
if (item === '-') {
|
||||
output.push('-'.repeat(this.innerWidth));
|
||||
} else if (item.type_ === Folder.modelType()) {
|
||||
output.push(item.title);
|
||||
output.push(Folder.displayTitle(item));
|
||||
} else if (item.type_ === Tag.modelType()) {
|
||||
output.push('[' + item.title + ']');
|
||||
output.push('[' + Folder.displayTitle(item) + ']');
|
||||
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
||||
output.push(_('Search:'));
|
||||
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');
|
||||
|
||||
class NoteListWidget extends ListWidget {
|
||||
@@ -10,7 +10,7 @@ class NoteListWidget extends ListWidget {
|
||||
this.updateIndexFromSelectedNoteId_ = false;
|
||||
|
||||
this.itemRenderer = (note) => {
|
||||
let label = note.title; // + ' ' + note.id;
|
||||
let label = Note.displayTitle(note); // + ' ' + note.id;
|
||||
if (note.is_todo) {
|
||||
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');
|
||||
|
||||
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 { _ } = require('lib/locale.js');
|
||||
|
||||
@@ -44,7 +44,13 @@ class NoteWidget extends TextWidget {
|
||||
} else if (this.noteId_) {
|
||||
this.doAsync('loadNote', async () => {
|
||||
this.note_ = await Note.load(this.noteId_);
|
||||
this.text = this.note_ ? this.note_.title + "\n\n" + this.note_.body : '';
|
||||
|
||||
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;
|
||||
this.lastLoadedNoteId_ = this.noteId_;
|
||||
});
|
||||
|
@@ -42,6 +42,7 @@ class StatusBarWidget extends BaseWidget {
|
||||
};
|
||||
|
||||
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_.resolve = resolve;
|
||||
@@ -105,6 +106,8 @@ class StatusBarWidget extends BaseWidget {
|
||||
|
||||
this.term.showCursor(true);
|
||||
|
||||
const isSecurePrompt = !!this.promptState_.secure;
|
||||
|
||||
let options = {
|
||||
cancelable: true,
|
||||
history: this.history,
|
||||
@@ -115,6 +118,7 @@ class StatusBarWidget extends BaseWidget {
|
||||
};
|
||||
|
||||
if ('cursorPosition' in this.promptState_) options.cursorPosition = this.promptState_.cursorPosition;
|
||||
if (isSecurePrompt) options.echoChar = true;
|
||||
|
||||
this.inputEventEmitter_ = this.term.inputField(options, (error, input) => {
|
||||
let resolveResult = null;
|
||||
@@ -129,7 +133,7 @@ class StatusBarWidget extends BaseWidget {
|
||||
resolveResult = input ? input.trim() : input;
|
||||
// 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.
|
||||
if (input && input.length > 1) this.history_.push(input);
|
||||
if (!isSecurePrompt && input && input.length > 1) this.history_.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs-extra');
|
||||
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 { _, setLocale, languageCode } = require('lib/locale.js');
|
||||
|
||||
|
@@ -1,26 +1,36 @@
|
||||
#!/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);
|
||||
|
||||
const compareVersion = require('compare-version');
|
||||
const nodeVersion = process && process.versions && process.versions.node ? process.versions.node : '0.0.0';
|
||||
if (compareVersion(nodeVersion, '8.0.0') < 0) {
|
||||
console.error('Joplin requires Node 8+. Detected version ' + nodeVersion);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { app } = require('./app.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Resource } = require('lib/models/resource.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { NoteTag } = require('lib/models/note-tag.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const NoteTag = require('lib/models/NoteTag.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { shimInit } = require('lib/shim-init-node.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
|
||||
const fsDriver = new FsDriverNode();
|
||||
Logger.fsDriver_ = fsDriver;
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
// That's not good, but it's to avoid circular dependency issues
|
||||
// in the BaseItem class.
|
||||
@@ -29,6 +39,7 @@ BaseItem.loadClass('Folder', Folder);
|
||||
BaseItem.loadClass('Resource', Resource);
|
||||
BaseItem.loadClass('Tag', Tag);
|
||||
BaseItem.loadClass('NoteTag', NoteTag);
|
||||
BaseItem.loadClass('MasterKey', MasterKey);
|
||||
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
@@ -55,6 +66,53 @@ process.stdout.on('error', function( err ) {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// async function main() {
|
||||
// const WebDavApi = require('lib/WebDavApi');
|
||||
// const api = new WebDavApi('http://nextcloud.local/remote.php/dav/files/admin/Joplin', { username: 'admin', password: '1234567' });
|
||||
// const { FileApiDriverWebDav } = new require('lib/file-api-driver-webdav');
|
||||
// const driver = new FileApiDriverWebDav(api);
|
||||
|
||||
// const stat = await driver.stat('');
|
||||
// console.info(stat);
|
||||
|
||||
// // const stat = await driver.stat('testing.txt');
|
||||
// // console.info(stat);
|
||||
|
||||
|
||||
// // const content = await driver.get('testing.txta');
|
||||
// // console.info(content);
|
||||
|
||||
// // const content = await driver.get('testing.txta', { target: 'file', path: '/var/www/joplin/CliClient/testing-file.txt' });
|
||||
// // console.info(content);
|
||||
|
||||
// // const content = await driver.mkdir('newdir5');
|
||||
// // console.info(content);
|
||||
|
||||
// //await driver.put('myfile4.md', 'this is my content');
|
||||
|
||||
// // await driver.put('testimg.jpg', null, { source: 'file', path: '/mnt/d/test.jpg' });
|
||||
|
||||
// // await driver.delete('myfile4.md');
|
||||
|
||||
// // const deltaResult = await driver.delta('', {
|
||||
// // allItemIdsHandler: () => { return []; }
|
||||
// // });
|
||||
// // console.info(deltaResult);
|
||||
// }
|
||||
|
||||
// main().catch((error) => { console.error(error); });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
application.start(process.argv).catch((error) => {
|
||||
console.error(_('Fatal error:'));
|
||||
console.error(error);
|
||||
|
@@ -4,6 +4,6 @@ ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BUILD_DIR="$ROOT_DIR/build"
|
||||
|
||||
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"
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
File diff suppressed because it is too large
Load Diff
@@ -15,63 +15,12 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr ""
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr ""
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr ""
|
||||
|
||||
@@ -108,6 +57,9 @@ msgstr ""
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr ""
|
||||
@@ -165,6 +117,35 @@ msgstr ""
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr ""
|
||||
|
||||
@@ -182,6 +163,10 @@ msgstr ""
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr ""
|
||||
|
||||
@@ -205,6 +190,10 @@ msgstr ""
|
||||
msgid "Displays usage information."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr ""
|
||||
|
||||
@@ -240,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr ""
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
@@ -381,6 +370,14 @@ msgstr ""
|
||||
msgid "Sync to provided target (defaults to sync.target config value)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr ""
|
||||
|
||||
@@ -391,10 +388,6 @@ msgid ""
|
||||
"operation."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr ""
|
||||
@@ -488,6 +481,13 @@ msgid ""
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
@@ -506,6 +506,10 @@ msgstr ""
|
||||
msgid "Evernote Export Files"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
@@ -524,13 +528,22 @@ msgstr ""
|
||||
msgid "Search in all the notes"
|
||||
msgstr ""
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
msgid "General Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Help"
|
||||
@@ -539,6 +552,9 @@ msgstr ""
|
||||
msgid "Website and documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr ""
|
||||
|
||||
@@ -546,12 +562,41 @@ msgstr ""
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
@@ -559,48 +604,26 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
"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 "Please create a notebook first."
|
||||
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 "Note title:"
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr ""
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
@@ -630,6 +653,68 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr ""
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -649,6 +734,12 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -656,9 +747,22 @@ msgstr ""
|
||||
msgid "Attach file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr ""
|
||||
|
||||
msgid "to-do"
|
||||
msgstr ""
|
||||
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -671,9 +775,15 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -689,9 +799,6 @@ msgstr ""
|
||||
msgid "Notebooks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
@@ -709,12 +816,18 @@ msgstr ""
|
||||
msgid "File system"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr ""
|
||||
@@ -765,6 +878,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
@@ -780,6 +897,12 @@ msgstr ""
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr ""
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr ""
|
||||
|
||||
@@ -831,16 +954,34 @@ msgstr ""
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgid "Focus title"
|
||||
msgstr ""
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
|
||||
msgid "When creating a new note:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
@@ -855,9 +996,6 @@ msgstr ""
|
||||
msgid "%d hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr ""
|
||||
|
||||
@@ -865,8 +1003,8 @@ msgid "Synchronisation target"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
@@ -877,6 +1015,24 @@ msgid ""
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
@@ -885,7 +1041,13 @@ msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgid "%s (%s): %s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
@@ -930,10 +1092,10 @@ msgstr ""
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgid "Export Debug Report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
@@ -946,6 +1108,9 @@ msgstr ""
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr ""
|
||||
|
||||
@@ -955,6 +1120,23 @@ msgstr ""
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr ""
|
||||
@@ -962,6 +1144,12 @@ msgstr ""
|
||||
msgid "Edit notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr ""
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1372
CliClient/locales/eu.po
Normal file
1372
CliClient/locales/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Last-Translator: Laurent Cozic\n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr_FR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -15,63 +15,12 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr "Activer le volet suivant"
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr "Activer le volet précédent"
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr "Démarrer le mode de ligne de commande"
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr "Sortir du mode de ligne de commande"
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr "Éditer la note sélectionnée"
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr "Annuler la commande en cours."
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr "Quitter le logiciel."
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr "Supprimer la note ou carnet sélectionné."
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Pour supprimer une vignette, enlever là des notes associées."
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "Veuillez d'abord sélectionner un carnet."
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr "Marquer une tâches comme complétée / non-complétée"
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Chercher"
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr "Afficher/Cacher les métadonnées des notes."
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr "Créer une nouvelle note"
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr "Créer une nouvelle tâche"
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr "Créer un nouveau carnet"
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr "Copier la note dans un autre carnet."
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr "Déplacer la note vers un carnet."
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "Appuyez sur Ctrl+D ou tapez \"exit\" pour sortir du logiciel"
|
||||
|
||||
@@ -100,15 +49,18 @@ msgstr "o"
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Annulation de la synchronisation... Veuillez patienter."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Commande invalide : \"%s\""
|
||||
msgstr "Commande invalide : %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
"La commande \"%s\" est disponible uniquement en mode d'interface graphique"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr "Un objet crypté ne peut pas être modifié"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Paramètre requis manquant : %s"
|
||||
@@ -171,6 +123,39 @@ msgstr "Marquer la tâche comme complétée."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "La note n'est pas une tâche : \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
"Gérer la configuration E2EE (Cryptage de bout à bout). Les commandes sont "
|
||||
"`enable`, `disable`, `decrypt` et `status` et `target-status`."
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr "Entrer le mot de passe maître :"
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr "Opération annulée"
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
"Démarrage du décryptage... Veuillez patienter car cela pourrait prendre "
|
||||
"plusieurs minutes selon le nombre d'objets à décrypter."
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr "Décryptage complété."
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Activé"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr "Le cryptage est : %s"
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Éditer la note."
|
||||
|
||||
@@ -192,6 +177,10 @@ msgstr ""
|
||||
"Édition de la note en cours. Fermez l'éditeur de texte pour retourner à "
|
||||
"l'invite de commande."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr "Erreur lors de l'ouverture de la note dans l'éditeur de texte : %s"
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "La note a été enregistrée."
|
||||
|
||||
@@ -218,14 +207,20 @@ msgstr "Afficher l'URL de l'emplacement de la note."
|
||||
msgid "Displays usage information."
|
||||
msgstr "Affiche les informations d'utilisation."
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
"Pour personnaliser les raccourcis veuillez consulter la documentation à %s"
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Les raccourcis ne sont pas disponible en mode de ligne de commande."
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Type `help [command]` for more information about a command; or type `help "
|
||||
"all` for the complete usage information."
|
||||
msgstr "Tapez `help [command]` pour plus d'information sur une commande."
|
||||
msgstr ""
|
||||
"Tapez `help [command]` pour plus d'information sur une commande ; ou tapez "
|
||||
"`help all` pour l'aide complète."
|
||||
|
||||
msgid "The possible commands are:"
|
||||
msgstr "Les commandes possibles sont :"
|
||||
@@ -260,9 +255,8 @@ msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr "Pour sortir du mode ligne de commande, pressez ECHAP"
|
||||
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
msgstr ""
|
||||
"Pour la liste complète des raccourcis disponibles, tapez `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr "Pour la liste complète des raccourcis disponibles, tapez `help keymap`"
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
msgstr "Importer un carnet Evernote (fichier .enex)."
|
||||
@@ -379,6 +373,8 @@ msgstr "Supprimer le carnet sans demander la confirmation."
|
||||
|
||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||
msgstr ""
|
||||
"Effacer le carnet ? Toutes les notes dans ce carnet seront également "
|
||||
"effacées."
|
||||
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
msgstr "Supprimer les notes correspondants à <note-pattern>."
|
||||
@@ -396,13 +392,17 @@ msgstr "Supprimer la note ?"
|
||||
msgid "Searches for the given <pattern> in all the notes."
|
||||
msgstr "Chercher le motif <pattern> dans toutes les notes."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Sets the property <name> of the given <note> to the given [value]. Possible "
|
||||
"properties are:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Assigner la valeur [value] à la propriété <name> de la <note> donnée."
|
||||
msgstr ""
|
||||
"Assigner la valeur [value] à la propriété <name> de la <note> donnée. Les "
|
||||
"valeurs possibles sont :\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
msgid "Displays summary about the notes and notebooks."
|
||||
msgstr "Afficher un résumé des notes et carnets."
|
||||
@@ -415,6 +415,16 @@ msgstr ""
|
||||
"Synchroniser avec la cible donnée (par défaut, la valeur de configuration "
|
||||
"`sync.target`)."
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
"Non-connecté à %s. Veuillez fournir les identifiants et mots de passe "
|
||||
"manquants."
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "La synchronisation est déjà en cours."
|
||||
|
||||
@@ -428,10 +438,6 @@ msgstr ""
|
||||
"correctement. Si vous savez qu'aucune autre synchronisation est en cours, "
|
||||
"vous pouvez supprimer le fichier \"%s\" pour reprendre l'opération."
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr "Impossible d'autoriser le logiciel (jeton d'identification non-reçu)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Cible de la synchronisation : %s (%s)"
|
||||
@@ -538,6 +544,24 @@ msgid ""
|
||||
"\n"
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
"Bienvenue dans Joplin!\n"
|
||||
"\n"
|
||||
"Tapez `:help shortcuts` pour la liste des raccourcis claviers, ou simplement "
|
||||
"`:help` pour une vue d'ensemble.\n"
|
||||
"\n"
|
||||
"Par exemple, pour créer un carnet, pressez `mb` ; pour créer une note "
|
||||
"pressed `mn`."
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Au moins un objet est actuellement crypté et il se peut que vous deviez "
|
||||
"fournir votre mot de passe maître. Pour se faire, veuillez taper `e2ee "
|
||||
"decrypt`. Si vous avez déjà fourni ce mot de passe, les objets cryptés vont "
|
||||
"être décrypté en tâche de fond et seront disponible prochainement."
|
||||
|
||||
msgid "File"
|
||||
msgstr "Fichier"
|
||||
@@ -557,6 +581,10 @@ msgstr "Importer notes d'Evernote"
|
||||
msgid "Evernote Export Files"
|
||||
msgstr "Fichiers d'export Evernote"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr "Cacher %s"
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Quitter"
|
||||
|
||||
@@ -575,15 +603,23 @@ msgstr "Coller"
|
||||
msgid "Search in all the notes"
|
||||
msgstr "Chercher dans toutes les notes"
|
||||
|
||||
msgid "View"
|
||||
msgstr "Affichage"
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr "Basculer l'agencement de l'éditeur"
|
||||
|
||||
msgid "Tools"
|
||||
msgstr "Outils"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
msgstr "État de la synchronisation"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr "Options de cryptage"
|
||||
|
||||
msgid "General Options"
|
||||
msgstr "Options générales"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Aide"
|
||||
@@ -591,6 +627,9 @@ msgstr "Aide"
|
||||
msgid "Website and documentation"
|
||||
msgstr "Documentation en ligne"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr "Vérifier les mises à jour..."
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "A propos de Joplin"
|
||||
|
||||
@@ -598,18 +637,130 @@ msgstr "A propos de Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Ouvrir %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr "Quitter"
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Annulation"
|
||||
msgstr "Annuler"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
"Notes de version :\n"
|
||||
"\n"
|
||||
"%s"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "Oui"
|
||||
|
||||
msgid "No"
|
||||
msgstr "Non"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr "La version actuelle est à jour."
|
||||
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Vérifier config synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
msgstr "Les notes et paramètres se trouve dans : %s"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"continue?"
|
||||
msgstr ""
|
||||
"Désactiver le cryptage signifie que *toutes* les notes et fichiers vont être "
|
||||
"re-synchronisés et envoyés décryptés sur la cible de la synchronisation. "
|
||||
"Souhaitez vous continuer ?"
|
||||
|
||||
msgid ""
|
||||
"Enabling encryption means *all* your notes and attachments are going to be "
|
||||
"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 ""
|
||||
"Activer le cryptage signifie que *toutes* les notes et fichiers vont être re-"
|
||||
"synchronisés et envoyés cryptés vers la cible de la synchronisation. Ne "
|
||||
"perdez pas votre mot de passe car, pour des raisons de sécurité, ce sera la "
|
||||
"*seule* façon de décrypter les données ! Pour activer le cryptage, veuillez "
|
||||
"entrer votre mot de passe ci-dessous."
|
||||
|
||||
msgid "Disable encryption"
|
||||
msgstr "Désactiver le cryptage"
|
||||
|
||||
msgid "Enable encryption"
|
||||
msgstr "Activer le cryptage"
|
||||
|
||||
msgid "Master Keys"
|
||||
msgstr "Clefs maître"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "Source"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "Créé"
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "Mis à jour"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
msgid "Password OK"
|
||||
msgstr "Mot de passe 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 ""
|
||||
"Note : seule une clef maître va être utilisée pour le cryptage (celle "
|
||||
"marquée comme \"actif\" ci-dessus). N'importe quel clef peut-être utilisée "
|
||||
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
|
||||
"à l'origine."
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Clefs maître manquantes"
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
"Les clefs maître avec ces identifiants sont utilisées pour crypter certains "
|
||||
"de vos objets, cependant le logiciel n'y a pour l'instant pas accès. Il est "
|
||||
"probable qu'elle vont être prochainement disponible via la synchronisation."
|
||||
|
||||
msgid "Status"
|
||||
msgstr "État"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr "Le cryptage est :"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Retour"
|
||||
@@ -624,15 +775,9 @@ msgstr ""
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Veuillez d'abord sélectionner un carnet."
|
||||
|
||||
msgid "Note title:"
|
||||
msgstr "Titre de la note :"
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Veuillez d'abord créer un carnet d'abord"
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Titre de la tâche :"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Titre du carnet :"
|
||||
|
||||
@@ -645,48 +790,26 @@ msgstr "Séparez chaque étiquette par une virgule."
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Renommer le carnet :"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Set alarm:"
|
||||
msgstr "Définir ou modifier alarme"
|
||||
msgstr "Régler alarme :"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Chercher"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Disposition"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Impossible d'initialiser la synchronisation."
|
||||
msgstr "Certains objets ne peuvent être synchronisés."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
msgstr "Les voir maintenant"
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Certains objets ne peuvent être décryptés."
|
||||
|
||||
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 "Set the password"
|
||||
msgstr "Définir le mot de passe"
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Gérer les étiquettes"
|
||||
@@ -704,12 +827,17 @@ msgid "No notes in here. Create one by clicking on \"New note\"."
|
||||
msgstr ""
|
||||
"Pas de notes ici. Créez-en une en pressant le bouton \"Nouvelle note\"."
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur le bouton "
|
||||
"(+)"
|
||||
"Il n'y a pour l'instant aucun carnet. Créez-en un en cliquant sur \"Nouveau "
|
||||
"carnet\"."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr "Ouvrir..."
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr "Enregistrer sous..."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
@@ -718,9 +846,21 @@ msgstr "Lien ou message non géré : %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Attacher un fichier"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Définir ou modifier alarme"
|
||||
msgstr "Régler alarme"
|
||||
|
||||
msgid "to-do"
|
||||
msgstr "tâche"
|
||||
|
||||
msgid "note"
|
||||
msgstr "note"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Création de %s..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Rafraîchir"
|
||||
@@ -734,9 +874,14 @@ msgstr "Connexion OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importer"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
msgstr "État de la synchronisation"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr "Options de cryptage"
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Enlever cette étiquette de toutes les notes ?"
|
||||
@@ -753,15 +898,12 @@ msgstr "Synchroniser"
|
||||
msgid "Notebooks"
|
||||
msgstr "Carnets"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Étiquettes"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Recherches"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Please select where the sync status should be exported to"
|
||||
msgstr "Veuillez d'abord sélectionner un carnet."
|
||||
msgstr ""
|
||||
"Veuillez sélectionner un répertoire ou exporter l'état de la synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Usage: %s"
|
||||
@@ -774,12 +916,18 @@ msgstr "Paramètre inconnu : %s"
|
||||
msgid "File system"
|
||||
msgstr "Système de fichier"
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr "Nextcloud"
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dév (Pour tester uniquement)"
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr "WebDAV"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Paramètre inconnu : %s"
|
||||
@@ -803,6 +951,12 @@ msgid ""
|
||||
"\n"
|
||||
"Please consider using a regular OneDrive account."
|
||||
msgstr ""
|
||||
"Impossible de synchroniser avec OneDrive.\n"
|
||||
"\n"
|
||||
"Cette erreur se produit lors de l'utilisation de OneDrive for Business, qui "
|
||||
"malheureusement n'est pas compatible.\n"
|
||||
"\n"
|
||||
"Veuillez utiliser à la place un compte OneDrive normal."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Cannot access %s"
|
||||
@@ -832,6 +986,10 @@ msgstr "Objets supprimés localement : %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Objets distants supprimés : %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Téléchargés : %d/%d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "État : \"%s\"."
|
||||
@@ -847,6 +1005,12 @@ msgstr "Terminé : %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "La synchronisation est déjà en cours. État : %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr "Crypté"
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Les objets cryptés ne peuvent être modifiés"
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Conflits"
|
||||
|
||||
@@ -900,18 +1064,36 @@ msgstr "Clair"
|
||||
msgid "Dark"
|
||||
msgstr "Sombre"
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr "Tâches non-terminées en haut des listes"
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Enregistrer l'emplacement avec les notes"
|
||||
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Lors de la création d'une tâche :"
|
||||
|
||||
msgid "Focus title"
|
||||
msgstr "Curseur sur le titre"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr "Curseur sur corps du message"
|
||||
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Lors de la création d'une note :"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr "Afficher icône dans la zone de notifications"
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr "Niveau de zoom"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Mettre à jour le logiciel automatiquement"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Intervalle de synchronisation"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Désactivé"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d minutes"
|
||||
@@ -924,9 +1106,6 @@ msgstr "%d heure"
|
||||
msgid "%d hours"
|
||||
msgstr "%d heures"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Mettre à jour le logiciel automatiquement"
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr "Montrer les options avancées"
|
||||
|
||||
@@ -934,14 +1113,15 @@ msgid "Synchronisation target"
|
||||
msgstr "Cible de la synchronisation"
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
|
||||
"fichier, veuillez spécifier le répertoire avec `sync.2.path`."
|
||||
"La cible avec laquelle synchroniser. Chaque cible de synchronisation peut "
|
||||
"avoir des paramètres supplémentaires sous le nom `sync.NUM.NOM` (documentés "
|
||||
"ci-dessous)."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr ""
|
||||
msgstr "Répertoire avec lequel synchroniser (chemin absolu)"
|
||||
|
||||
msgid ""
|
||||
"The path to synchronise with when file system synchronisation is enabled. "
|
||||
@@ -950,16 +1130,43 @@ msgstr ""
|
||||
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
|
||||
"par système de fichier est activée. Voir `sync.target`."
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr "Nextcloud : URL WebDAV"
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr "Nextcloud : Nom utilisateur"
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr "Nextcloud : Mot de passe"
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr "WebDAV : URL"
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr "WebDAV : Nom utilisateur"
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr "WebDAV : Mot de passe"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
msgstr "Objets qui ne peuvent pas être synchronisés"
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s (%s) : %s"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
"Ces objets resteront sur l'appareil mais ne seront pas envoyé sur la cible "
|
||||
"de la synchronisation. Pour trouver ces objets, faite une recherche sur le "
|
||||
"titre ou l'identifiant de l'objet (affiché ci-dessus entre parenthèses)."
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Status de la synchronisation (objets synchro. / total)"
|
||||
@@ -1005,22 +1212,25 @@ msgstr "Supprimer ces notes ?"
|
||||
msgid "Log"
|
||||
msgstr "Journal"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "État"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Exporter rapport de débogage"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr "Config cryptage"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Configuration"
|
||||
|
||||
msgid "Move to notebook..."
|
||||
msgstr "Déplacer la note vers carnet..."
|
||||
msgstr "Déplacer vers..."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Déplacer %d notes vers carnet \"%s\" ?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr "Définir mot de passe de synchronisation."
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Sélectionner date"
|
||||
|
||||
@@ -1030,6 +1240,23 @@ msgstr "Confirmer"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Annuler synchronisation"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr "Clef maître %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Créé : %s"
|
||||
|
||||
msgid "Password:"
|
||||
msgstr "Mot de passe :"
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr "Mot de passe ne peut être vide"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "Activer"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
|
||||
@@ -1037,6 +1264,12 @@ msgstr "Ce carnet n'a pas pu être sauvegardé : %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Éditer le carnet"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr "Afficher tous"
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr "Erreurs seulement"
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Cette note a été modifiée :"
|
||||
|
||||
@@ -1066,10 +1299,10 @@ msgid "Hide metadata"
|
||||
msgstr "Cacher les métadonnées"
|
||||
|
||||
msgid "Show metadata"
|
||||
msgstr "Afficher les métadonnées"
|
||||
msgstr "Voir métadonnées"
|
||||
|
||||
msgid "View on map"
|
||||
msgstr "Voir emplacement sur carte"
|
||||
msgstr "Voir sur carte"
|
||||
|
||||
msgid "Delete notebook"
|
||||
msgstr "Supprimer le carnet"
|
||||
@@ -1092,9 +1325,80 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Bienvenue"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Some items cannot be decrypted."
|
||||
#~ msgstr "Impossible d'initialiser la synchronisation."
|
||||
#~ msgid "Give focus to next pane"
|
||||
#~ msgstr "Activer le volet suivant"
|
||||
|
||||
#~ msgid "Give focus to previous pane"
|
||||
#~ msgstr "Activer le volet précédent"
|
||||
|
||||
#~ msgid "Enter command line mode"
|
||||
#~ msgstr "Démarrer le mode de ligne de commande"
|
||||
|
||||
#~ msgid "Exit command line mode"
|
||||
#~ msgstr "Sortir du mode de ligne de commande"
|
||||
|
||||
#~ msgid "Edit the selected note"
|
||||
#~ msgstr "Éditer la note sélectionnée"
|
||||
|
||||
#~ msgid "Cancel the current command."
|
||||
#~ msgstr "Annuler la commande en cours."
|
||||
|
||||
#~ msgid "Exit the application."
|
||||
#~ msgstr "Quitter le logiciel."
|
||||
|
||||
#~ msgid "Delete the currently selected note or notebook."
|
||||
#~ msgstr "Supprimer la note ou carnet sélectionné."
|
||||
|
||||
#~ msgid "Set a to-do as completed / not completed"
|
||||
#~ msgstr "Marquer une tâches comme complétée / non-complétée"
|
||||
|
||||
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
#~ msgstr "Maximiser, minimiser, cacher ou rendre visible la console."
|
||||
|
||||
#~ msgid "[t]oggle note [m]etadata."
|
||||
#~ msgstr "Afficher/Cacher les métadonnées des notes."
|
||||
|
||||
#~ msgid "[M]ake a new [n]ote"
|
||||
#~ msgstr "Créer une nouvelle note"
|
||||
|
||||
#~ msgid "[M]ake a new [t]odo"
|
||||
#~ msgstr "Créer une nouvelle tâche"
|
||||
|
||||
#~ msgid "[M]ake a new note[b]ook"
|
||||
#~ msgstr "Créer un nouveau carnet"
|
||||
|
||||
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
#~ msgstr "Copier la note dans un autre carnet."
|
||||
|
||||
#~ msgid "Move the note to a notebook."
|
||||
#~ msgstr "Déplacer la note vers un carnet."
|
||||
|
||||
#~ msgid "Error"
|
||||
#~ msgstr "Erreur"
|
||||
|
||||
#~ msgid "WebDAV (Beta)"
|
||||
#~ msgstr "WebDAV (Bêta)"
|
||||
|
||||
#~ msgid "Could not download the update: %s"
|
||||
#~ msgstr "Impossible de télécharger la mise à jour : %s"
|
||||
|
||||
#~ msgid "New version downloaded - application will quit now and update..."
|
||||
#~ msgstr ""
|
||||
#~ "La nouvelle version a été téléchargée - le programme va se fermer et se "
|
||||
#~ "mettre à jour..."
|
||||
|
||||
#~ msgid "Could not install the update: %s"
|
||||
#~ msgstr "Impossible d'installer la mise à jour : %s"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
#~ msgstr ""
|
||||
#~ "La cible avec laquelle synchroniser. Pour synchroniser avec le système de "
|
||||
#~ "fichier, veuillez spécifier le répertoire avec `sync.2.path`."
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Titre de la tâche :"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Supprimer le carnet ?"
|
||||
@@ -1157,9 +1461,6 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Todo filter"
|
||||
#~ msgstr "Filtre des tâches"
|
||||
|
||||
#~ msgid "Show all"
|
||||
#~ msgstr "Afficher tous"
|
||||
|
||||
#~ msgid "Non-completed and recently completed ones"
|
||||
#~ msgstr "Tâches non-complétées et récentes"
|
||||
|
||||
@@ -1170,9 +1471,6 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Delete a note"
|
||||
#~ msgstr "Supprimer la note"
|
||||
|
||||
#~ msgid "%s (%s)"
|
||||
#~ msgstr "%s (%s)"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Show/Hide the console"
|
||||
#~ msgstr "Quitter le logiciel."
|
||||
|
@@ -17,69 +17,12 @@ msgstr ""
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr "Fokusiraj sljedeće okno"
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr "Fokusiraj prethodno okno"
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr "Otvori naredbeni redak"
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr "Napusti naredbeni redak"
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr "Uredi odabranu bilješku"
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr "Prekini trenutnu naredbu."
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr "Izađi iz aplikacije."
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr "Obriši odabranu bilješku ili bilježnicu."
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Da bi mogao obrisati oznaku, skini oznaku s povezanih bilješki."
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "Odaberi bilješku ili bilježnicu za brisanje."
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr "Postavi zadatak kao završen/nezavršen"
|
||||
|
||||
#, fuzzy
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Traži"
|
||||
|
||||
#, fuzzy
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr "[t]oggle note [m]etadata."
|
||||
|
||||
#, fuzzy
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr "[M]ake a new [n]ote"
|
||||
|
||||
#, fuzzy
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr "[M]ake a new [t]odo"
|
||||
|
||||
#, fuzzy
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr "[M]ake a new note[b]ook"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr "Premjesti bilješku u bilježnicu."
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "Pritisni Ctrl+D ili napiši \"exit\" za izlazak iz aplikacije"
|
||||
|
||||
@@ -116,6 +59,9 @@ msgstr "Ne postoji naredba: %s"
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "Naredba \"%s\" postoji samo u inačici s grafičkim sučeljem"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Nedostaje obavezni argument: %s"
|
||||
@@ -180,6 +126,36 @@ msgstr "Označava zadatak završenim."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "Bilješka nije zadatak: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enabled"
|
||||
msgstr "Onemogućeno"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Onemogućeno"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Uredi bilješku."
|
||||
|
||||
@@ -202,6 +178,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr ""
|
||||
"Počinjem uređivati bilješku. Za povratak u naredbeni redak, zatvori uređivač."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "Bilješka je spremljena."
|
||||
|
||||
@@ -227,6 +207,10 @@ msgstr "Prikazuje geolokacijski URL bilješke."
|
||||
msgid "Displays usage information."
|
||||
msgstr "Prikazuje informacije o korištenju."
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Prečaci nisu podržani u naredbenom retku."
|
||||
|
||||
@@ -270,8 +254,9 @@ msgstr "Za ulaz u naredbeni redak, pritisni \":\""
|
||||
msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr "Za izlaz iz naredbenog retka, pritisni Esc"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr "Za potpunu listu mogućih prečaca, upiši `help shortcuts`"
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
@@ -433,6 +418,16 @@ msgstr "Sinkronizira sa udaljenom pohranom podataka."
|
||||
msgid "Sync to provided target (defaults to sync.target config value)"
|
||||
msgstr "Sinkroniziraj sa metom (default je polje sync.target u konfiguraciji)"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "Sinkronizacija je već u toku."
|
||||
|
||||
@@ -444,12 +439,6 @@ msgid ""
|
||||
msgstr ""
|
||||
"Ako sinkronizacija nije u toku, obriši lock datoteku u \"%s\" i nastavi..."
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"Ovjera nije dovršena (nije dobivena potvrda ovjere - authentication token)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Meta sinkronizacije: %s (%s)"
|
||||
@@ -566,6 +555,13 @@ msgstr ""
|
||||
"\n"
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr "Datoteka"
|
||||
|
||||
@@ -584,6 +580,10 @@ msgstr "Uvezi Evernote bilješke"
|
||||
msgid "Evernote Export Files"
|
||||
msgstr "Evernote izvozne datoteke"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Izađi"
|
||||
|
||||
@@ -602,13 +602,23 @@ msgstr "Zalijepi"
|
||||
msgid "Search in all the notes"
|
||||
msgstr "Pretraži u svim bilješkama"
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr "Alati"
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Status sinkronizacije"
|
||||
|
||||
msgid "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "General Options"
|
||||
msgstr "Opcije"
|
||||
|
||||
msgid "Help"
|
||||
@@ -617,6 +627,9 @@ msgstr "Pomoć"
|
||||
msgid "Website and documentation"
|
||||
msgstr "Website i dokumentacija"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "O Joplinu"
|
||||
|
||||
@@ -624,12 +637,43 @@ msgstr "O Joplinu"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "On %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "U redu"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Odustani"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Obriši bilješke?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Prekini sinkronizaciju"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr "Bilješke i postavke su pohranjene u: %s"
|
||||
@@ -637,51 +681,27 @@ msgstr "Bilješke i postavke su pohranjene u: %s"
|
||||
msgid "Save"
|
||||
msgstr "Spremi"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Natrag"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
"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 ""
|
||||
"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u "
|
||||
"nju"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Prvo stvori bilježnicu."
|
||||
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 "Note title:"
|
||||
msgstr "Naslov bilješke:"
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Prvo stvori bilježnicu"
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Naslov zadatka:"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Naslov bilježnice:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Dodaj ili makni oznake:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Odvoji oznake zarezom."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Preimenuj bilježnicu:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Postavi upozorenje:"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Izgled"
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Neke stavke se ne mogu sinkronizirati."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr "Pogledaj ih sada"
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
@@ -710,6 +730,71 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Natrag"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr ""
|
||||
"Nova bilježnica \"%s\" će biti stvorena i datoteka \"%s\" će biti uvezena u "
|
||||
"nju"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Prvo stvori bilježnicu."
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Prvo stvori bilježnicu"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Naslov bilježnice:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Dodaj ili makni oznake:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Odvoji oznake zarezom."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Preimenuj bilježnicu:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Postavi upozorenje:"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Traži"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Izgled"
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Neke stavke se ne mogu sinkronizirati."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr "Pogledaj ih sada"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Neke stavke se ne mogu sinkronizirati."
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Dodaj ili makni oznake"
|
||||
|
||||
@@ -729,6 +814,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Ovdje nema bilježnica. Stvori novu pritiskom na \"Nova bilježnica\"."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Spremi promjene"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Nepodržana poveznica ili poruka: %s"
|
||||
@@ -736,9 +828,24 @@ msgstr "Nepodržana poveznica ili poruka: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Priloži datoteku"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Oznake"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Postavi upozorenje"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Novi zadatak"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nova bilješka"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Uvozim bilješke..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Osvježi"
|
||||
|
||||
@@ -751,9 +858,15 @@ msgstr "OneDrive Login"
|
||||
msgid "Import"
|
||||
msgstr "Uvoz"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Opcije"
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Status Sinkronizacije"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Makni ovu oznaku iz svih bilješki?"
|
||||
|
||||
@@ -769,9 +882,6 @@ msgstr "Sinkroniziraj"
|
||||
msgid "Notebooks"
|
||||
msgstr "Bilježnice"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Oznake"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Pretraživanja"
|
||||
|
||||
@@ -789,12 +899,18 @@ msgstr "Nepoznata zastavica: %s"
|
||||
msgid "File system"
|
||||
msgstr "Datotečni sustav"
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (Samo za testiranje)"
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nepoznata razina logiranja: %s"
|
||||
@@ -851,6 +967,10 @@ msgstr "Obrisane lokalne stavke: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Obrisane udaljene stavke: %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Stvorene lokalne stavke: %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "Stanje: \"%s\"."
|
||||
@@ -866,6 +986,13 @@ msgstr "Dovršeno: %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "Sinkronizacija je već u toku. Stanje: %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Neke stavke se ne mogu sinkronizirati."
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Sukobi"
|
||||
|
||||
@@ -919,18 +1046,40 @@ msgstr "Svijetla"
|
||||
msgid "Dark"
|
||||
msgstr "Tamna"
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
#, fuzzy
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr "Prikaži nezavršene zadatke na vrhu liste"
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Spremi geolokacijske podatke sa bilješkama"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Stvara novi zadatak."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Focus title"
|
||||
msgstr "Naslov bilješke:"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Stvara novu bilješku."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Automatsko instaliranje nove verzije"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Interval sinkronizacije"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Onemogućeno"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d minuta"
|
||||
@@ -943,9 +1092,6 @@ msgstr "%d sat"
|
||||
msgid "%d hours"
|
||||
msgstr "%d sati"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Automatsko instaliranje nove verzije"
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr "Prikaži napredne opcije"
|
||||
|
||||
@@ -953,11 +1099,9 @@ msgid "Synchronisation target"
|
||||
msgstr "Sinkroniziraj sa"
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
|
||||
"sustavom, postavi `sync.2.path` na ciljani direktorij."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr "Direktorij za sinkroniziranje (apsolutna putanja)"
|
||||
@@ -969,6 +1113,24 @@ msgstr ""
|
||||
"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa "
|
||||
"datotečnim sustavom omogućena. Vidi `sync.target`."
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
|
||||
@@ -976,9 +1138,15 @@ msgstr "Nevažeća vrijednost: \"%s\". Moguće vrijednosti su: %s."
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr "Stavke koje se ne mogu sinkronizirati"
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr "\"%s\": \"%s\""
|
||||
#, fuzzy, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s %s (%s)"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Status (sinkronizirane stavke / ukupni broj stavki)"
|
||||
@@ -1022,12 +1190,12 @@ msgstr "Obriši ove bilješke?"
|
||||
msgid "Log"
|
||||
msgstr "Log"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Izvezi Debug izvještaj"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Konfiguracija"
|
||||
|
||||
@@ -1038,6 +1206,9 @@ msgstr "Premjesti u bilježnicu..."
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Premjesti %d bilješke u bilježnicu \"%s\"?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Odaberi datum"
|
||||
|
||||
@@ -1047,6 +1218,24 @@ msgstr "Potvrdi"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Prekini sinkronizaciju"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Stvoreno: %d."
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enable"
|
||||
msgstr "Onemogućeno"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "Bilježnicu nije moguće snimiti: %s"
|
||||
@@ -1054,6 +1243,12 @@ msgstr "Bilježnicu nije moguće snimiti: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Uredi bilježnicu"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Bilješka je promijenjena:"
|
||||
|
||||
@@ -1107,6 +1302,69 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
|
||||
msgid "Welcome"
|
||||
msgstr "Dobro došli"
|
||||
|
||||
#~ msgid "Give focus to next pane"
|
||||
#~ msgstr "Fokusiraj sljedeće okno"
|
||||
|
||||
#~ msgid "Give focus to previous pane"
|
||||
#~ msgstr "Fokusiraj prethodno okno"
|
||||
|
||||
#~ msgid "Enter command line mode"
|
||||
#~ msgstr "Otvori naredbeni redak"
|
||||
|
||||
#~ msgid "Exit command line mode"
|
||||
#~ msgstr "Napusti naredbeni redak"
|
||||
|
||||
#~ msgid "Edit the selected note"
|
||||
#~ msgstr "Uredi odabranu bilješku"
|
||||
|
||||
#~ msgid "Cancel the current command."
|
||||
#~ msgstr "Prekini trenutnu naredbu."
|
||||
|
||||
#~ msgid "Exit the application."
|
||||
#~ msgstr "Izađi iz aplikacije."
|
||||
|
||||
#~ msgid "Delete the currently selected note or notebook."
|
||||
#~ msgstr "Obriši odabranu bilješku ili bilježnicu."
|
||||
|
||||
#~ msgid "Set a to-do as completed / not completed"
|
||||
#~ msgstr "Postavi zadatak kao završen/nezavršen"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Some items cannot be decrypted."
|
||||
#~ msgstr "Neke stavke se ne mogu sinkronizirati."
|
||||
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
#~ msgstr "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "[t]oggle note [m]etadata."
|
||||
#~ msgstr "[t]oggle note [m]etadata."
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "[M]ake a new [n]ote"
|
||||
#~ msgstr "[M]ake a new [n]ote"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "[M]ake a new [t]odo"
|
||||
#~ msgstr "[M]ake a new [t]odo"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "[M]ake a new note[b]ook"
|
||||
#~ msgstr "[M]ake a new note[b]ook"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
#~ msgstr "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
|
||||
#~ msgid "Move the note to a notebook."
|
||||
#~ msgstr "Premjesti bilješku u bilježnicu."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
#~ msgstr ""
|
||||
#~ "Meta sinkronizacije. U slučaju sinkroniziranja s vlastitim datotečnim "
|
||||
#~ "sustavom, postavi `sync.2.path` na ciljani direktorij."
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Naslov zadatka:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
@@ -16,65 +16,12 @@ msgstr ""
|
||||
"X-Generator: Poedit 2.0.3\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr "Pannello successivo"
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr "Pannello precedente"
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr "Accedi alla modalità linea di comando"
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr "Esci dalla modalità linea di comando"
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr "Modifica la nota selezionata"
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr "Cancella il comando corrente."
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr "Esci dall'applicazione."
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr "Elimina la nota o il blocco note selezionato."
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Elimina un'etichetta, togli l'etichetta associata alle note."
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "Per favore seleziona la nota o il blocco note da eliminare."
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr "Imposta un'attività come completata / non completata"
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr ""
|
||||
"Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/"
|
||||
"visibile."
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr "mos[t]ra/nascondi i [m]etadata nelle note."
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr "Crea ([M]ake) una nuova [n]ota"
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr "Crea ([M]ake) una nuova at[t]ività"
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr "Crea ([M]ake) un nuovo [b]locco note"
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr "Copia ([Y]) la [n]ota in un blocco note."
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr "Sposta la nota in un blocco note."
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "Premi Ctrl+D o digita \"exit\" per uscire dall'applicazione"
|
||||
|
||||
@@ -112,6 +59,9 @@ msgstr "Nessun comando: %s"
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "Il comando \"%s\" è disponibile solo nella modalità grafica"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Argomento richiesto mancante: %s"
|
||||
@@ -174,6 +124,36 @@ msgstr "Segna un'attività come completata."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "La nota non è un'attività: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enabled"
|
||||
msgstr "Disabilitato"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Disabilitato"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Modifica nota."
|
||||
|
||||
@@ -193,6 +173,10 @@ msgstr "Non esiste la nota: \"%s\". Desideri crearla?"
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr "Comincia a modificare la nota. Chiudi l'editor per tornare al prompt."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "La nota è stata salvata."
|
||||
|
||||
@@ -219,6 +203,10 @@ msgstr "Mostra l'URL di geolocalizzazione per la nota."
|
||||
msgid "Displays usage information."
|
||||
msgstr "Mostra le informazioni di utilizzo."
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Le scorciatoie non sono disponibili nella modalità CLI."
|
||||
|
||||
@@ -261,8 +249,9 @@ msgstr "Per entrare nella modalità command line, premi \":\""
|
||||
msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr "Per uscire dalla modalità command line, premi ESC"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr ""
|
||||
"Per la lista completa delle scorciatoie disponibili, digita `help shortcuts`"
|
||||
|
||||
@@ -416,6 +405,16 @@ msgstr ""
|
||||
"Sincronizza con l'obiettivo fornito (come predefinito il valore di "
|
||||
"configurazione sync.target)"
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"Autenticazione non completata (non è stato ricevuto alcun token di "
|
||||
"autenticazione)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "La sincronizzazione è in corso."
|
||||
|
||||
@@ -429,12 +428,6 @@ msgstr ""
|
||||
"sincronizzazione, è possibile eliminare il file di blocco in \"% s\" e "
|
||||
"riprendere l'operazione."
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"Autenticazione non completata (non è stato ricevuto alcun token di "
|
||||
"autenticazione)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Posizione di sincronizzazione: %s (%s)"
|
||||
@@ -544,6 +537,13 @@ msgid ""
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
@@ -562,6 +562,10 @@ msgstr "Importa le note da Evernote"
|
||||
msgid "Evernote Export Files"
|
||||
msgstr "Esposta i files di Evernote"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Esci"
|
||||
|
||||
@@ -580,13 +584,23 @@ msgstr "Incolla"
|
||||
msgid "Search in all the notes"
|
||||
msgstr "Cerca in tutte le note"
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr "Strumenti"
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Stato di sincronizzazione"
|
||||
|
||||
msgid "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "General Options"
|
||||
msgstr "Opzioni"
|
||||
|
||||
msgid "Help"
|
||||
@@ -595,6 +609,9 @@ msgstr "Aiuto"
|
||||
msgid "Website and documentation"
|
||||
msgstr "Sito web e documentazione"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "Informazione si Joplin"
|
||||
|
||||
@@ -602,12 +619,43 @@ msgstr "Informazione si Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Su %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Cancella"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Eliminare le note?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Cancella la sincronizzazione"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
@@ -615,49 +663,27 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Indietro"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr "Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato"
|
||||
"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 "Please create a notebook first."
|
||||
msgstr "Per favore prima crea un blocco note."
|
||||
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 "Note title:"
|
||||
msgstr "Titolo della Nota:"
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Per favore prima crea un blocco note"
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Titolo dell'attività:"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Titolo del blocco note:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Aggiungi or rimuovi etichetta:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Separa ogni etichetta da una virgola."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Rinomina il blocco note:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Imposta allarme:"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Disposizione"
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr "Mostrali ora"
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
@@ -688,6 +714,69 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Stato"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Indietro"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr "Il nuovo blocco note \"%s\" verrà creato e \"%s\" vi verrà importato"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Per favore prima crea un blocco note."
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Per favore prima crea un blocco note"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Titolo del blocco note:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Aggiungi or rimuovi etichetta:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Separa ogni etichetta da una virgola."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Rinomina il blocco note:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Imposta allarme:"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Cerca"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Disposizione"
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr "Mostrali ora"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Aggiungi o rimuovi etichetta"
|
||||
|
||||
@@ -708,6 +797,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Al momento non ci sono note. Creane una cliccando sul bottone (+)."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Salva i cambiamenti"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Collegamento o messaggio non supportato: %s"
|
||||
@@ -715,9 +811,24 @@ msgstr "Collegamento o messaggio non supportato: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Allega file"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etichette"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Imposta allarme"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nuova attività"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nuova nota"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importazione delle note..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Aggiorna"
|
||||
|
||||
@@ -730,9 +841,15 @@ msgstr "Login OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importa"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Opzioni"
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Stato della Sincronizzazione"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Rimuovere questa etichetta da tutte le note?"
|
||||
|
||||
@@ -748,9 +865,6 @@ msgstr "Sincronizza"
|
||||
msgid "Notebooks"
|
||||
msgstr "Blocchi note"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etichette"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Ricerche"
|
||||
|
||||
@@ -769,12 +883,18 @@ msgstr "Etichetta sconosciuta: %s"
|
||||
msgid "File system"
|
||||
msgstr "File system"
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (solo per test)"
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Livello di log sconosciuto: %s"
|
||||
@@ -833,6 +953,10 @@ msgstr "Elementi locali eliminati: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Elementi remoti eliminati: %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Elementi locali creati: %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "Stato: \"%s\"."
|
||||
@@ -848,6 +972,13 @@ msgstr "Completata: %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "La sincronizzazione è già in corso. Stato: %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Conflitti"
|
||||
|
||||
@@ -901,18 +1032,40 @@ msgstr "Chiaro"
|
||||
msgid "Dark"
|
||||
msgstr "Scuro"
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
#, fuzzy
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr "Mostra todo inclompleti in cima alla lista"
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Salva geo-localizzazione con le note"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Crea una nuova attività."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Focus title"
|
||||
msgstr "Titolo della Nota:"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Crea una nuova nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Aggiorna automaticamente l'applicazione"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Intervallo di sincronizzazione"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Disabilitato"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d minuti"
|
||||
@@ -925,9 +1078,6 @@ msgstr "%d ora"
|
||||
msgid "%d hours"
|
||||
msgstr "%d ore"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Aggiorna automaticamente l'applicazione"
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr "Mostra opzioni avanzate"
|
||||
|
||||
@@ -935,12 +1085,9 @@ msgid "Synchronisation target"
|
||||
msgstr "Destinazione di sincronizzazione"
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"La destinazione della sincronizzazione. Se si sincronizza con il file "
|
||||
"system, impostare ' Sync. 2. Path ' per specificare la directory di "
|
||||
"destinazione."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr ""
|
||||
@@ -952,6 +1099,24 @@ msgstr ""
|
||||
"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi "
|
||||
"`sync.target`."
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
|
||||
@@ -959,9 +1124,15 @@ msgstr "Oprione non valida: \"%s\". I valori possibili sono: %s."
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr "Elementi che non possono essere sincronizzati"
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr "\"%s\": \"%s\""
|
||||
#, fuzzy, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s %s (%s)"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Stato di sincronizzazione (Elementi sincronizzati / Elementi totali)"
|
||||
@@ -1005,12 +1176,12 @@ msgstr "Cancellare queste note?"
|
||||
msgid "Log"
|
||||
msgstr "Log"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Stato"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Esporta il Report di Debug"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Configurazione"
|
||||
|
||||
@@ -1021,6 +1192,9 @@ msgstr "Sposta sul blocco note..."
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Spostare le note %d sul blocco note \"%s\"?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Seleziona la data"
|
||||
|
||||
@@ -1030,6 +1204,24 @@ msgstr "Conferma"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Cancella la sincronizzazione"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Creato: %d."
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enable"
|
||||
msgstr "Disabilitato"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "Il blocco note non può essere salvato: %s"
|
||||
@@ -1037,6 +1229,12 @@ msgstr "Il blocco note non può essere salvato: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Modifica blocco note"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Questa note è stata modificata:"
|
||||
|
||||
@@ -1092,9 +1290,69 @@ msgstr ""
|
||||
msgid "Welcome"
|
||||
msgstr "Benvenuto"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Some items cannot be decrypted."
|
||||
#~ msgstr "Alcuni elementi non possono essere sincronizzati."
|
||||
#~ msgid "Give focus to next pane"
|
||||
#~ msgstr "Pannello successivo"
|
||||
|
||||
#~ msgid "Give focus to previous pane"
|
||||
#~ msgstr "Pannello precedente"
|
||||
|
||||
#~ msgid "Enter command line mode"
|
||||
#~ msgstr "Accedi alla modalità linea di comando"
|
||||
|
||||
#~ msgid "Exit command line mode"
|
||||
#~ msgstr "Esci dalla modalità linea di comando"
|
||||
|
||||
#~ msgid "Edit the selected note"
|
||||
#~ msgstr "Modifica la nota selezionata"
|
||||
|
||||
#~ msgid "Cancel the current command."
|
||||
#~ msgstr "Cancella il comando corrente."
|
||||
|
||||
#~ msgid "Exit the application."
|
||||
#~ msgstr "Esci dall'applicazione."
|
||||
|
||||
#~ msgid "Delete the currently selected note or notebook."
|
||||
#~ msgstr "Elimina la nota o il blocco note selezionato."
|
||||
|
||||
#~ msgid "Set a to-do as completed / not completed"
|
||||
#~ msgstr "Imposta un'attività come completata / non completata"
|
||||
|
||||
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
#~ msgstr ""
|
||||
#~ "Scegli lo s[t]ato della [c]onsole: massimizzato/minimizzato/nascosto/"
|
||||
#~ "visibile."
|
||||
|
||||
#~ msgid "[t]oggle note [m]etadata."
|
||||
#~ msgstr "mos[t]ra/nascondi i [m]etadata nelle note."
|
||||
|
||||
#~ msgid "[M]ake a new [n]ote"
|
||||
#~ msgstr "Crea ([M]ake) una nuova [n]ota"
|
||||
|
||||
#~ msgid "[M]ake a new [t]odo"
|
||||
#~ msgstr "Crea ([M]ake) una nuova at[t]ività"
|
||||
|
||||
#~ msgid "[M]ake a new note[b]ook"
|
||||
#~ msgstr "Crea ([M]ake) un nuovo [b]locco note"
|
||||
|
||||
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
#~ msgstr "Copia ([Y]) la [n]ota in un blocco note."
|
||||
|
||||
#~ msgid "Move the note to a notebook."
|
||||
#~ msgstr "Sposta la nota in un blocco note."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
#~ msgstr ""
|
||||
#~ "La destinazione della sincronizzazione. Se si sincronizza con il file "
|
||||
#~ "system, impostare ' Sync. 2. Path ' per specificare la directory di "
|
||||
#~ "destinazione."
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Titolo dell'attività:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "\"%s\": \"%s\""
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Eliminare il blocco note?"
|
||||
|
1353
CliClient/locales/ja_JP.po
Normal file
1353
CliClient/locales/ja_JP.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,63 +15,12 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr ""
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr ""
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr ""
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr ""
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr ""
|
||||
|
||||
@@ -108,6 +57,9 @@ msgstr ""
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr ""
|
||||
@@ -165,6 +117,35 @@ msgstr ""
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr ""
|
||||
|
||||
@@ -182,6 +163,10 @@ msgstr ""
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr ""
|
||||
|
||||
@@ -205,6 +190,10 @@ msgstr ""
|
||||
msgid "Displays usage information."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr ""
|
||||
|
||||
@@ -240,7 +229,7 @@ msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr ""
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
@@ -381,6 +370,14 @@ msgstr ""
|
||||
msgid "Sync to provided target (defaults to sync.target config value)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr ""
|
||||
|
||||
@@ -391,10 +388,6 @@ msgid ""
|
||||
"operation."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr ""
|
||||
@@ -488,6 +481,13 @@ msgid ""
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
@@ -506,6 +506,10 @@ msgstr ""
|
||||
msgid "Evernote Export Files"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr ""
|
||||
|
||||
@@ -524,13 +528,22 @@ msgstr ""
|
||||
msgid "Search in all the notes"
|
||||
msgstr ""
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
msgid "General Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Help"
|
||||
@@ -539,6 +552,9 @@ msgstr ""
|
||||
msgid "Website and documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr ""
|
||||
|
||||
@@ -546,12 +562,41 @@ msgstr ""
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr ""
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
msgid "No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
@@ -559,48 +604,26 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
"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 "Please create a notebook first."
|
||||
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 "Note title:"
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr ""
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
@@ -630,6 +653,68 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr ""
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -649,6 +734,12 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr ""
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Save as..."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr ""
|
||||
@@ -656,9 +747,22 @@ msgstr ""
|
||||
msgid "Attach file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr ""
|
||||
|
||||
msgid "to-do"
|
||||
msgstr ""
|
||||
|
||||
msgid "note"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr ""
|
||||
|
||||
@@ -671,9 +775,15 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr ""
|
||||
|
||||
@@ -689,9 +799,6 @@ msgstr ""
|
||||
msgid "Notebooks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "Searches"
|
||||
msgstr ""
|
||||
|
||||
@@ -709,12 +816,18 @@ msgstr ""
|
||||
msgid "File system"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr ""
|
||||
@@ -765,6 +878,10 @@ msgstr ""
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr ""
|
||||
@@ -780,6 +897,12 @@ msgstr ""
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr ""
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr ""
|
||||
|
||||
@@ -831,16 +954,34 @@ msgstr ""
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disabled"
|
||||
msgid "Focus title"
|
||||
msgstr ""
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
|
||||
msgid "When creating a new note:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
@@ -855,9 +996,6 @@ msgstr ""
|
||||
msgid "%d hours"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr ""
|
||||
|
||||
@@ -865,8 +1003,8 @@ msgid "Synchronisation target"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
@@ -877,6 +1015,24 @@ msgid ""
|
||||
"See `sync.target`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
@@ -885,7 +1041,13 @@ msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgid "%s (%s): %s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
@@ -930,10 +1092,10 @@ msgstr ""
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgid "Export Debug Report"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
@@ -946,6 +1108,9 @@ msgstr ""
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr ""
|
||||
|
||||
@@ -955,6 +1120,23 @@ msgstr ""
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr ""
|
||||
@@ -962,6 +1144,12 @@ msgstr ""
|
||||
msgid "Edit notebook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr ""
|
||||
|
||||
|
1385
CliClient/locales/nl_BE.po
Normal file
1385
CliClient/locales/nl_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,63 +16,12 @@ msgstr ""
|
||||
"X-Generator: Poedit 2.0.5\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr "Dar o foco para o próximo painel"
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr "Dar o foco para o painel anterior"
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr "Entrar no modo de linha de comando"
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr "Sair do modo de linha de comando"
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr "Editar nota selecionada"
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr "Cancelar comando atual."
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr "Sair da aplicação."
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr "Excluir nota selecionada ou notebook."
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Para eliminar uma tag, remova a tag das notas associadas a ela."
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "Por favor, primeiro, selecione a nota ou caderno a excluir."
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr "Marcar uma tarefa como completada / não completada"
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr "al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível."
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Procurar"
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr "al[t]ernar [m]etadados de notas."
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr "Criar ([M]ake) uma nova [n]ota"
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr "Criar ([M]ake) nova [t]arefa"
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr "Criar ([M]ake) novo caderno (note[b]ook)"
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr "Copiar ([Y]ank) a [n]ota a um caderno."
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr "Mover nota para um caderno."
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "Digite Ctrl+D ou \"exit\" para sair da aplicação"
|
||||
|
||||
@@ -109,6 +58,9 @@ msgstr "Comando inválido: \"%s\""
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "O comando \"%s\" está disponível somente em modo gráfico"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Argumento requerido faltando: %s"
|
||||
@@ -171,6 +123,36 @@ msgstr "Marca uma tarefa como feita."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "Nota não é uma tarefa: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enabled"
|
||||
msgstr "Desabilitado"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Desabilitado"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Editar nota."
|
||||
|
||||
@@ -190,6 +172,10 @@ msgstr "A nota não existe: \"%s\". Criar?"
|
||||
msgid "Starting to edit note. Close the editor to get back to the prompt."
|
||||
msgstr "Começando a editar a nota. Feche o editor para voltar ao prompt."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "Nota gravada."
|
||||
|
||||
@@ -216,6 +202,10 @@ msgstr "Exibe uma URL de geolocalização para a nota."
|
||||
msgid "Displays usage information."
|
||||
msgstr "Exibe informações de uso."
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Os atalhos não estão disponíveis no modo CLI."
|
||||
|
||||
@@ -257,8 +247,9 @@ msgstr "Para entrar no modo de linha de comando, pressione \":\""
|
||||
msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr "Para sair do modo de linha de comando, pressione o ESC"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr ""
|
||||
"Para a lista completa de atalhos de teclado disponíveis, digite `help "
|
||||
"shortcuts`"
|
||||
@@ -412,6 +403,15 @@ msgstr ""
|
||||
"Sincronizar para destino fornecido (p padrão é o valor de configuração sync."
|
||||
"target)"
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"A autenticação não foi concluída (não recebeu um token de autenticação)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "A sincronização já está em andamento."
|
||||
|
||||
@@ -425,11 +425,6 @@ msgstr ""
|
||||
"está ocorrendo, você pode excluir o arquivo de bloqueio em \"%s\" e retomar "
|
||||
"a operação."
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr ""
|
||||
"A autenticação não foi concluída (não recebeu um token de autenticação)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Alvo de sincronização: %s (%s)"
|
||||
@@ -538,6 +533,13 @@ msgid ""
|
||||
"For example, to create a notebook press `mb`; to create a note press `mn`."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
|
||||
msgid "File"
|
||||
msgstr "Arquivo"
|
||||
|
||||
@@ -556,6 +558,10 @@ msgstr "Importar notas do Evernote"
|
||||
msgid "Evernote Export Files"
|
||||
msgstr "Arquivos de Exportação do Evernote"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Sair"
|
||||
|
||||
@@ -574,6 +580,12 @@ msgstr "Colar"
|
||||
msgid "Search in all the notes"
|
||||
msgstr "Pesquisar em todas as notas"
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr "Ferramentas"
|
||||
|
||||
@@ -581,7 +593,11 @@ msgstr "Ferramentas"
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Alvo de sincronização"
|
||||
|
||||
msgid "Options"
|
||||
msgid "Encryption options"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "General Options"
|
||||
msgstr "Opções"
|
||||
|
||||
msgid "Help"
|
||||
@@ -590,6 +606,9 @@ msgstr "Ajuda"
|
||||
msgid "Website and documentation"
|
||||
msgstr "Website e documentação"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr ""
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "Sobre o Joplin"
|
||||
|
||||
@@ -597,12 +616,43 @@ msgstr "Sobre o Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "Em %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Cancelar"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Excluir notas?"
|
||||
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Cancelar sincronização"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr ""
|
||||
@@ -610,50 +660,26 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Voltar"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
"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 ""
|
||||
"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Primeiro, crie um caderno."
|
||||
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 "Note title:"
|
||||
msgstr "Título da nota:"
|
||||
msgid "Disable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Primeiro, crie um caderno"
|
||||
msgid "Enable encryption"
|
||||
msgstr ""
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Título da tarefa:"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Título do caderno:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Adicionar ou remover tags:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Separe cada tag por vírgula."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Renomear caderno:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Definir alarme:"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Layout"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Não é possível inicializar o sincronizador."
|
||||
|
||||
msgid "View them now"
|
||||
msgid "Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid "Active"
|
||||
@@ -685,6 +711,71 @@ msgid ""
|
||||
"how the notes or notebooks were originally encrypted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Missing Master Keys"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Voltar"
|
||||
|
||||
#, javascript-format
|
||||
msgid ""
|
||||
"New notebook \"%s\" will be created and file \"%s\" will be imported into it"
|
||||
msgstr ""
|
||||
"O novo caderno \"%s\" será criado e o arquivo \"%s\" será importado para ele"
|
||||
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Primeiro, crie um caderno."
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Primeiro, crie um caderno"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Título do caderno:"
|
||||
|
||||
msgid "Add or remove tags:"
|
||||
msgstr "Adicionar ou remover tags:"
|
||||
|
||||
msgid "Separate each tag by a comma."
|
||||
msgstr "Separe cada tag por vírgula."
|
||||
|
||||
msgid "Rename notebook:"
|
||||
msgstr "Renomear caderno:"
|
||||
|
||||
msgid "Set alarm:"
|
||||
msgstr "Definir alarme:"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Procurar"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Layout"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Não é possível inicializar o sincronizador."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Não é possível inicializar o sincronizador."
|
||||
|
||||
msgid "Set the password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Adicionar ou remover tags"
|
||||
|
||||
@@ -705,6 +796,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Atualmente, não há notas. Crie uma, clicando no botão (+)."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Gravar alterações"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Link ou mensagem não suportada: %s"
|
||||
@@ -712,9 +810,24 @@ msgstr "Link ou mensagem não suportada: %s"
|
||||
msgid "Attach file"
|
||||
msgstr "Anexar arquivo"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Definir alarme"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Nova tarefa"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Nova nota"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Importando notas ..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Atualizar"
|
||||
|
||||
@@ -727,10 +840,16 @@ msgstr "Login no OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importar"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Opções"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Alvo de sincronização"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Remover esta tag de todas as notas?"
|
||||
|
||||
@@ -746,9 +865,6 @@ msgstr "Sincronizar"
|
||||
msgid "Notebooks"
|
||||
msgstr "Cadernos"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Tags"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Pesquisas"
|
||||
|
||||
@@ -767,12 +883,18 @@ msgstr "Flag desconhecido: %s"
|
||||
msgid "File system"
|
||||
msgstr "Sistema de arquivos"
|
||||
|
||||
msgid "Nextcloud"
|
||||
msgstr ""
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (apenas para testes)"
|
||||
|
||||
msgid "WebDAV"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Nível de log desconhecido: %s"
|
||||
@@ -831,6 +953,10 @@ msgstr "Itens locais excluídos: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Itens remotos excluídos: %d."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Itens locais criados: %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "Estado: \"%s\"."
|
||||
@@ -846,6 +972,13 @@ msgstr "Completado: %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "Sincronização já em andamento. Estado: %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Não é possível inicializar o sincronizador."
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Conflitos"
|
||||
|
||||
@@ -900,18 +1033,40 @@ msgstr "Light"
|
||||
msgid "Dark"
|
||||
msgstr "Dark"
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
#, fuzzy
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr "Mostrar tarefas incompletas no topo das listas"
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Salvar geolocalização com notas"
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "Cria uma nova tarefa."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Focus title"
|
||||
msgstr "Título da nota:"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "When creating a new note:"
|
||||
msgstr "Cria uma nova nota."
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Atualizar automaticamente o aplicativo"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Intervalo de sincronização"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Desabilitado"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d minutos"
|
||||
@@ -924,9 +1079,6 @@ msgstr "%d hora"
|
||||
msgid "%d hours"
|
||||
msgstr "%d horas"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Atualizar automaticamente o aplicativo"
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr "Mostrar opções avançadas"
|
||||
|
||||
@@ -934,11 +1086,9 @@ msgid "Synchronisation target"
|
||||
msgstr "Alvo de sincronização"
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"O alvo para sincronizar. Se estiver sincronizando com o sistema de arquivos, "
|
||||
"configure `sync.2.path` para especificar o diretório de destino."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr ""
|
||||
@@ -950,6 +1100,24 @@ msgstr ""
|
||||
"O caminho para sincronizar, quando a sincronização do sistema de arquivos "
|
||||
"está habilitada. Veja `sync.target`."
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV username"
|
||||
msgstr ""
|
||||
|
||||
msgid "WebDAV password"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
|
||||
@@ -957,8 +1125,14 @@ msgstr "Valor da opção inválida: \"%s\". Os valores possíveis são: %s."
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
#, fuzzy, javascript-format
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s %s (%s)"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
@@ -1003,12 +1177,12 @@ msgstr "Excluir estas notas?"
|
||||
msgid "Log"
|
||||
msgstr "Log"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Exportar Relatório de Debug"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr ""
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Configuração"
|
||||
|
||||
@@ -1019,6 +1193,9 @@ msgstr "Mover para o caderno..."
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Mover %d notas para o caderno \"%s\"?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr ""
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Selecionar data"
|
||||
|
||||
@@ -1028,6 +1205,24 @@ msgstr "Confirmar"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Cancelar sincronização"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Criado: %d."
|
||||
|
||||
msgid "Password:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Enable"
|
||||
msgstr "Desabilitado"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "O caderno não pôde ser salvo: %s"
|
||||
@@ -1035,6 +1230,12 @@ msgstr "O caderno não pôde ser salvo: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Editar caderno"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
msgid "Errors only"
|
||||
msgstr ""
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Esta nota foi modificada:"
|
||||
|
||||
@@ -1088,9 +1289,64 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
|
||||
msgid "Welcome"
|
||||
msgstr "Bem-vindo"
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Some items cannot be decrypted."
|
||||
#~ msgstr "Não é possível inicializar o sincronizador."
|
||||
#~ msgid "Give focus to next pane"
|
||||
#~ msgstr "Dar o foco para o próximo painel"
|
||||
|
||||
#~ msgid "Give focus to previous pane"
|
||||
#~ msgstr "Dar o foco para o painel anterior"
|
||||
|
||||
#~ msgid "Enter command line mode"
|
||||
#~ msgstr "Entrar no modo de linha de comando"
|
||||
|
||||
#~ msgid "Exit command line mode"
|
||||
#~ msgstr "Sair do modo de linha de comando"
|
||||
|
||||
#~ msgid "Edit the selected note"
|
||||
#~ msgstr "Editar nota selecionada"
|
||||
|
||||
#~ msgid "Cancel the current command."
|
||||
#~ msgstr "Cancelar comando atual."
|
||||
|
||||
#~ msgid "Exit the application."
|
||||
#~ msgstr "Sair da aplicação."
|
||||
|
||||
#~ msgid "Delete the currently selected note or notebook."
|
||||
#~ msgstr "Excluir nota selecionada ou notebook."
|
||||
|
||||
#~ msgid "Set a to-do as completed / not completed"
|
||||
#~ msgstr "Marcar uma tarefa como completada / não completada"
|
||||
|
||||
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
#~ msgstr ""
|
||||
#~ "al[t]ernar [c]onsole entre maximizado / minimizado / oculto / visível."
|
||||
|
||||
#~ msgid "[t]oggle note [m]etadata."
|
||||
#~ msgstr "al[t]ernar [m]etadados de notas."
|
||||
|
||||
#~ msgid "[M]ake a new [n]ote"
|
||||
#~ msgstr "Criar ([M]ake) uma nova [n]ota"
|
||||
|
||||
#~ msgid "[M]ake a new [t]odo"
|
||||
#~ msgstr "Criar ([M]ake) nova [t]arefa"
|
||||
|
||||
#~ msgid "[M]ake a new note[b]ook"
|
||||
#~ msgstr "Criar ([M]ake) novo caderno (note[b]ook)"
|
||||
|
||||
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
#~ msgstr "Copiar ([Y]ank) a [n]ota a um caderno."
|
||||
|
||||
#~ msgid "Move the note to a notebook."
|
||||
#~ msgstr "Mover nota para um caderno."
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
#~ msgstr ""
|
||||
#~ "O alvo para sincronizar. Se estiver sincronizando com o sistema de "
|
||||
#~ "arquivos, configure `sync.2.path` para especificar o diretório de destino."
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Título da tarefa:"
|
||||
|
||||
#~ msgid "Delete notebook?"
|
||||
#~ msgstr "Excluir caderno?"
|
||||
|
@@ -7,73 +7,22 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Joplin-CLI 1.0.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: rtmkrlv <artyom.karlov@gmail.com>\n"
|
||||
"Last-Translator: Artyom Karlov <artyom.karlov@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: ru_RU\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.4\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
msgid "Give focus to next pane"
|
||||
msgstr "Переключиться на следующую панель"
|
||||
|
||||
msgid "Give focus to previous pane"
|
||||
msgstr "Переключиться на предыдущую панель"
|
||||
|
||||
msgid "Enter command line mode"
|
||||
msgstr "Войти в режим командной строки"
|
||||
|
||||
msgid "Exit command line mode"
|
||||
msgstr "Выйти из режима командной строки"
|
||||
|
||||
msgid "Edit the selected note"
|
||||
msgstr "Редактировать выбранную заметку"
|
||||
|
||||
msgid "Cancel the current command."
|
||||
msgstr "Отменить текущую команду."
|
||||
|
||||
msgid "Exit the application."
|
||||
msgstr "Выйти из приложения."
|
||||
|
||||
msgid "Delete the currently selected note or notebook."
|
||||
msgstr "Удалить текущую выбранную заметку или блокнот."
|
||||
|
||||
msgid "To delete a tag, untag the associated notes."
|
||||
msgstr "Чтобы удалить тег, уберите его с ассоциированных с ним заметок."
|
||||
|
||||
msgid "Please select the note or notebook to be deleted first."
|
||||
msgstr "Сначала выберите заметку или блокнот, которые должны быть удалены."
|
||||
|
||||
msgid "Set a to-do as completed / not completed"
|
||||
msgstr "Отметить задачу как завершённую/незавершённую"
|
||||
|
||||
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
msgstr "[tc] переключить консоль между развёрнутой/свёрнутой/скрытой/видимой."
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
msgid "[t]oggle note [m]etadata."
|
||||
msgstr "[tm] переключить отображение метаданных заметки."
|
||||
|
||||
msgid "[M]ake a new [n]ote"
|
||||
msgstr "[mn] создать новую заметку"
|
||||
|
||||
msgid "[M]ake a new [t]odo"
|
||||
msgstr "[mt] создать новую задачу"
|
||||
|
||||
msgid "[M]ake a new note[b]ook"
|
||||
msgstr "[mb] создать новый блокнот"
|
||||
|
||||
msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
msgstr "[yn] копировать заметку в блокнот."
|
||||
|
||||
msgid "Move the note to a notebook."
|
||||
msgstr "Переместить заметку в блокнот."
|
||||
|
||||
msgid "Press Ctrl+D or type \"exit\" to exit the application"
|
||||
msgstr "Для выхода из приложения нажмите Ctrl+D или введите «exit»"
|
||||
|
||||
@@ -111,6 +60,9 @@ msgstr "Нет такой команды: %s"
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "Команда «%s» доступна только в режиме GUI"
|
||||
|
||||
msgid "Cannot change encrypted item"
|
||||
msgstr "Не удалось изменить зашифрованный элемент"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Missing required argument: %s"
|
||||
msgstr "Отсутствует требуемый аргумент: %s"
|
||||
@@ -120,7 +72,7 @@ msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
msgid "Your choice: "
|
||||
msgstr "Ваш выбор:"
|
||||
msgstr "Ваш выбор: "
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid answer: %s"
|
||||
@@ -173,6 +125,39 @@ msgstr "Отмечает задачу как завершённую."
|
||||
msgid "Note is not a to-do: \"%s\""
|
||||
msgstr "Заметка не является задачей: «%s»"
|
||||
|
||||
msgid ""
|
||||
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
|
||||
"`status` and `target-status`."
|
||||
msgstr ""
|
||||
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
|
||||
"`status` и `target-status`."
|
||||
|
||||
msgid "Enter master password:"
|
||||
msgstr "Введите мастер-пароль:"
|
||||
|
||||
msgid "Operation cancelled"
|
||||
msgstr "Операция отменена"
|
||||
|
||||
msgid ""
|
||||
"Starting decryption... Please wait as it may take several minutes depending "
|
||||
"on how much there is to decrypt."
|
||||
msgstr ""
|
||||
"Запуск расшифровки... Пожалуйста, ожидайте. Время расшифровки зависит от "
|
||||
"объёма расшифровываемых данных."
|
||||
|
||||
msgid "Completed decryption."
|
||||
msgstr "Расшифровка завершена."
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "Включено"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключено"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Encryption is: %s"
|
||||
msgstr "Шифрование: %s"
|
||||
|
||||
msgid "Edit note."
|
||||
msgstr "Редактировать заметку."
|
||||
|
||||
@@ -194,6 +179,10 @@ msgstr ""
|
||||
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
|
||||
"командной строке."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Error opening note in editor: %s"
|
||||
msgstr "Ошибка при открытии заметки в редакторе: %s"
|
||||
|
||||
msgid "Note has been saved."
|
||||
msgstr "Заметка сохранена."
|
||||
|
||||
@@ -219,6 +208,10 @@ msgstr "Выводит URL геолокации для заметки."
|
||||
msgid "Displays usage information."
|
||||
msgstr "Выводит информацию об использовании."
|
||||
|
||||
#, javascript-format
|
||||
msgid "For information on how to customise the shortcuts please visit %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Shortcuts are not available in CLI mode."
|
||||
msgstr "Ярлыки недоступны в режиме командной строки."
|
||||
|
||||
@@ -243,7 +236,7 @@ msgstr ""
|
||||
"элемент."
|
||||
|
||||
msgid "To move from one pane to another, press Tab or Shift+Tab."
|
||||
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab"
|
||||
msgstr "Чтобы переключаться между панелями, нажимайте Tab или Shift+Tab."
|
||||
|
||||
msgid ""
|
||||
"Use the arrows and page up/down to scroll the lists and text areas "
|
||||
@@ -261,11 +254,12 @@ msgstr "Чтобы войти в режим командной строки, н
|
||||
msgid "To exit command line mode, press ESCAPE"
|
||||
msgstr "Чтобы выйти из режима командной строки, нажмите ESCAPE"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
|
||||
"For the list of keyboard shortcuts and config options, type `help keymap`"
|
||||
msgstr ""
|
||||
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
|
||||
"shortcuts`."
|
||||
"shortcuts`"
|
||||
|
||||
msgid "Imports an Evernote notebook file (.enex file)."
|
||||
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
|
||||
@@ -421,6 +415,15 @@ msgstr ""
|
||||
"Синхронизация с заданной целью (по умолчанию — значение конфигурации sync."
|
||||
"target)"
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr "Аутентификация не была завершена (не получен токен аутентификации)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Not authentified with %s. Please provide any missing credentials."
|
||||
msgstr ""
|
||||
"Не аутентифицировано с %s. Пожалуйста, предоставьте все недостающие данные."
|
||||
|
||||
msgid "Synchronisation is already in progress."
|
||||
msgstr "Синхронизация уже выполняется."
|
||||
|
||||
@@ -434,10 +437,6 @@ msgstr ""
|
||||
"производится, вы можете удалить файл блокировки в «%s» и возобновить "
|
||||
"операцию."
|
||||
|
||||
msgid ""
|
||||
"Authentication was not completed (did not receive an authentication token)."
|
||||
msgstr "Аутентификация не была завершена (не получен токен аутентификации)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Synchronisation target: %s (%s)"
|
||||
msgstr "Цель синхронизации: %s (%s)"
|
||||
@@ -553,6 +552,17 @@ msgstr ""
|
||||
"Например, для создания блокнота нужно ввести `mb`, для создания заметки — "
|
||||
"`mn`."
|
||||
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
|
||||
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
|
||||
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
|
||||
"расшифруются в фоновом режиме и вскоре станут доступны."
|
||||
|
||||
msgid "File"
|
||||
msgstr "Файл"
|
||||
|
||||
@@ -571,11 +581,15 @@ msgstr "Импортировать заметки из Evernote"
|
||||
msgid "Evernote Export Files"
|
||||
msgstr "Файлы экспорта Evernote"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Hide %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Quit"
|
||||
msgstr "Выход"
|
||||
|
||||
msgid "Edit"
|
||||
msgstr "Редактировать"
|
||||
msgstr "Правка"
|
||||
|
||||
msgid "Copy"
|
||||
msgstr "Копировать"
|
||||
@@ -589,14 +603,23 @@ msgstr "Вставить"
|
||||
msgid "Search in all the notes"
|
||||
msgstr "Поиск во всех заметках"
|
||||
|
||||
msgid "View"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle editor layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tools"
|
||||
msgstr "Инструменты"
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Статус синхронизации"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Настройки"
|
||||
msgid "Encryption options"
|
||||
msgstr "Настройки шифрования"
|
||||
|
||||
msgid "General Options"
|
||||
msgstr "Основные настройки"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Помощь"
|
||||
@@ -604,6 +627,9 @@ msgstr "Помощь"
|
||||
msgid "Website and documentation"
|
||||
msgstr "Сайт и документация"
|
||||
|
||||
msgid "Check for updates..."
|
||||
msgstr "Проверить обновления..."
|
||||
|
||||
msgid "About Joplin"
|
||||
msgstr "О Joplin"
|
||||
|
||||
@@ -611,12 +637,44 @@ msgstr "О Joplin"
|
||||
msgid "%s %s (%s, %s)"
|
||||
msgstr "%s %s (%s, %s)"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Open %s"
|
||||
msgstr "В %s: %s"
|
||||
|
||||
msgid "Exit"
|
||||
msgstr ""
|
||||
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid ""
|
||||
"Release notes:\n"
|
||||
"\n"
|
||||
"%s"
|
||||
msgstr "Удалить заметки?"
|
||||
|
||||
#, fuzzy
|
||||
msgid "An update is available, do you want to download it now?"
|
||||
msgstr "Доступно обновление. Обновить сейчас?"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "No"
|
||||
msgstr "N"
|
||||
|
||||
msgid "Current version is up-to-date."
|
||||
msgstr "Вы используете самую свежую версию."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Check synchronisation configuration"
|
||||
msgstr "Отменить синхронизацию"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Notes and settings are stored in: %s"
|
||||
msgstr "Заметки и настройки сохранены в: %s"
|
||||
@@ -624,6 +682,83 @@ msgstr "Заметки и настройки сохранены в: %s"
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
msgid ""
|
||||
"Disabling encryption means *all* your notes and attachments are going to be "
|
||||
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
|
||||
"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 "ID"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "Источник"
|
||||
|
||||
msgid "Created"
|
||||
msgstr "Создан"
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "Обновлён"
|
||||
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
msgid "Password OK"
|
||||
msgstr "Пароль 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 ""
|
||||
"Внимание: Для шифрования может быть использован только один мастер-ключ "
|
||||
"(отмеченный как «активный»). Для расшифровки может использоваться любой из "
|
||||
"ключей, в зависимости от того, как изначально были зашифрованы заметки или "
|
||||
"блокноты."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Missing Master Keys"
|
||||
msgstr "Мастер-ключи"
|
||||
|
||||
msgid ""
|
||||
"The master keys with these IDs are used to encrypt some of your items, "
|
||||
"however the application does not currently have access to them. It is likely "
|
||||
"they will eventually be downloaded via synchronisation."
|
||||
msgstr ""
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Encryption is:"
|
||||
msgstr "Шифрование:"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Назад"
|
||||
|
||||
@@ -635,15 +770,9 @@ msgstr "Будет создан новый блокнот «%s» и в него
|
||||
msgid "Please create a notebook first."
|
||||
msgstr "Сначала создайте блокнот."
|
||||
|
||||
msgid "Note title:"
|
||||
msgstr "Название заметки:"
|
||||
|
||||
msgid "Please create a notebook first"
|
||||
msgstr "Сначала создайте блокнот"
|
||||
|
||||
msgid "To-do title:"
|
||||
msgstr "Название задачи:"
|
||||
|
||||
msgid "Notebook title:"
|
||||
msgstr "Название блокнота:"
|
||||
|
||||
@@ -659,6 +788,9 @@ msgstr "Переименовать блокнот:"
|
||||
msgid "Set alarm:"
|
||||
msgstr "Установить напоминание:"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
msgid "Layout"
|
||||
msgstr "Вид"
|
||||
|
||||
@@ -668,32 +800,11 @@ msgstr "Некоторые элементы не могут быть синхр
|
||||
msgid "View them now"
|
||||
msgstr "Просмотреть их сейчас"
|
||||
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgid "Some items cannot be decrypted."
|
||||
msgstr "Некоторые элементы не могут быть расшифрованы."
|
||||
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
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 "Set the password"
|
||||
msgstr "Установить пароль"
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Добавить или удалить теги"
|
||||
@@ -714,6 +825,13 @@ msgid ""
|
||||
"There is currently no notebook. Create one by clicking on \"New notebook\"."
|
||||
msgstr "Сейчас здесь нет блокнотов. Создайте новый нажав «Новый блокнот»."
|
||||
|
||||
msgid "Open..."
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Save as..."
|
||||
msgstr "Сохранить изменения"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported link or message: %s"
|
||||
msgstr "Неподдерживаемая ссыка или сообщение: %s"
|
||||
@@ -721,9 +839,24 @@ msgstr "Неподдерживаемая ссыка или сообщение: %
|
||||
msgid "Attach file"
|
||||
msgstr "Прикрепить файл"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Теги"
|
||||
|
||||
msgid "Set alarm"
|
||||
msgstr "Установить напоминание"
|
||||
|
||||
#, fuzzy
|
||||
msgid "to-do"
|
||||
msgstr "Новая задача"
|
||||
|
||||
#, fuzzy
|
||||
msgid "note"
|
||||
msgstr "Новая заметка"
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "Creating new %s..."
|
||||
msgstr "Импорт заметок..."
|
||||
|
||||
msgid "Refresh"
|
||||
msgstr "Обновить"
|
||||
|
||||
@@ -736,9 +869,15 @@ msgstr "Вход в OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Импорт"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Статус синхронизации"
|
||||
|
||||
msgid "Encryption Options"
|
||||
msgstr "Настройки шифрования"
|
||||
|
||||
msgid "Remove this tag from all the notes?"
|
||||
msgstr "Убрать этот тег со всех заметок?"
|
||||
|
||||
@@ -754,9 +893,6 @@ msgstr "Синхронизировать"
|
||||
msgid "Notebooks"
|
||||
msgstr "Блокноты"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Теги"
|
||||
|
||||
msgid "Searches"
|
||||
msgstr "Запросы"
|
||||
|
||||
@@ -774,12 +910,20 @@ msgstr "Неизвестный флаг: %s"
|
||||
msgid "File system"
|
||||
msgstr "Файловая система"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Nextcloud"
|
||||
msgstr "Nextcloud (Beta)"
|
||||
|
||||
msgid "OneDrive"
|
||||
msgstr "OneDrive"
|
||||
|
||||
msgid "OneDrive Dev (For testing only)"
|
||||
msgstr "OneDrive Dev (только для тестирования)"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV"
|
||||
msgstr "Nextcloud WebDAV URL"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unknown log level: %s"
|
||||
msgstr "Неизвестный уровень лога: %s"
|
||||
@@ -838,6 +982,10 @@ msgstr "Удалено локальных элементов: %d."
|
||||
msgid "Deleted remote items: %d."
|
||||
msgstr "Удалено удалённых элементов: %d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Fetched items: %d/%d."
|
||||
msgstr "Получено элементов: %d/%d."
|
||||
|
||||
#, javascript-format
|
||||
msgid "State: \"%s\"."
|
||||
msgstr "Статус: «%s»."
|
||||
@@ -853,6 +1001,12 @@ msgstr "Завершено: %s"
|
||||
msgid "Synchronisation is already in progress. State: %s"
|
||||
msgstr "Синхронизация уже выполняется. Статус: %s"
|
||||
|
||||
msgid "Encrypted"
|
||||
msgstr "Зашифровано"
|
||||
|
||||
msgid "Encrypted items cannot be modified"
|
||||
msgstr "Зашифрованные элементы не могут быть изменены"
|
||||
|
||||
msgid "Conflicts"
|
||||
msgstr "Конфликты"
|
||||
|
||||
@@ -906,18 +1060,37 @@ msgstr "Светлая"
|
||||
msgid "Dark"
|
||||
msgstr "Тёмная"
|
||||
|
||||
msgid "Show uncompleted todos on top of the lists"
|
||||
#, fuzzy
|
||||
msgid "Show uncompleted to-dos on top of the lists"
|
||||
msgstr "Показывать незавершённые задачи вверху списков"
|
||||
|
||||
msgid "Save geo-location with notes"
|
||||
msgstr "Сохранять информацию о геолокации в заметках"
|
||||
|
||||
msgid "When creating a new to-do:"
|
||||
msgstr "При создании новой задачи:"
|
||||
|
||||
msgid "Focus title"
|
||||
msgstr "Фокус на названии"
|
||||
|
||||
msgid "Focus body"
|
||||
msgstr "Фокус на содержимом"
|
||||
|
||||
msgid "When creating a new note:"
|
||||
msgstr "При создании новой заметки:"
|
||||
|
||||
msgid "Show tray icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Set application zoom percentage"
|
||||
msgstr "Масштаб приложения в процентах"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Автоматически обновлять приложение"
|
||||
|
||||
msgid "Synchronisation interval"
|
||||
msgstr "Интервал синхронизации"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "Отключена"
|
||||
|
||||
#, javascript-format
|
||||
msgid "%d minutes"
|
||||
msgstr "%d минут"
|
||||
@@ -930,9 +1103,6 @@ msgstr "%d час"
|
||||
msgid "%d hours"
|
||||
msgstr "%d часов"
|
||||
|
||||
msgid "Automatically update the application"
|
||||
msgstr "Автоматически обновлять приложение"
|
||||
|
||||
msgid "Show advanced options"
|
||||
msgstr "Показывать расширенные настройки"
|
||||
|
||||
@@ -940,11 +1110,11 @@ msgid "Synchronisation target"
|
||||
msgstr "Цель синхронизации"
|
||||
|
||||
msgid ""
|
||||
"The target to synchonise to. If synchronising with the file system, set "
|
||||
"`sync.2.path` to specify the target directory."
|
||||
"The target to synchonise to. Each sync target may have additional parameters "
|
||||
"which are named as `sync.NUM.NAME` (all documented below)."
|
||||
msgstr ""
|
||||
"То, с чем будет осуществляться синхронизация. При синхронизации с файловой "
|
||||
"системой в `sync.2.path` указывается целевой каталог."
|
||||
"Цель синхронизации. Каждая цель синхронизации может иметь дополнительные "
|
||||
"параметры, именованные как «sync.NUM.NAME» (все описаны ниже)."
|
||||
|
||||
msgid "Directory to synchronise with (absolute path)"
|
||||
msgstr "Каталог синхронизации (абсолютный путь)"
|
||||
@@ -956,6 +1126,27 @@ msgstr ""
|
||||
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
|
||||
"`sync.target`."
|
||||
|
||||
msgid "Nextcloud WebDAV URL"
|
||||
msgstr "Nextcloud WebDAV URL"
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr "Имя пользователя Nextcloud"
|
||||
|
||||
msgid "Nextcloud password"
|
||||
msgstr "Пароль Nextcloud"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV URL"
|
||||
msgstr "Nextcloud WebDAV URL"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV username"
|
||||
msgstr "Имя пользователя Nextcloud"
|
||||
|
||||
#, fuzzy
|
||||
msgid "WebDAV password"
|
||||
msgstr "Установить пароль"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
|
||||
@@ -964,8 +1155,17 @@ msgid "Items that cannot be synchronised"
|
||||
msgstr "Элементы, которые не могут быть синхронизированы"
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr "«%s»: «%s»"
|
||||
msgid "%s (%s): %s"
|
||||
msgstr "%s (%s): %s"
|
||||
|
||||
msgid ""
|
||||
"These items will remain on the device but will not be uploaded to the sync "
|
||||
"target. In order to find these items, either search for the title or the ID "
|
||||
"(which is displayed in brackets above)."
|
||||
msgstr ""
|
||||
"Эти элементы будут оставаться на устройстве, но не будут загружены в целевой "
|
||||
"объект синхронизации. Чтобы найти эти элементы, воспользуйтесь поиском по "
|
||||
"названию или ID (который указывается в скобках выше)."
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Статус синхронизации (элементов синхронизировано/всего)"
|
||||
@@ -1009,12 +1209,12 @@ msgstr "Удалить эти заметки?"
|
||||
msgid "Log"
|
||||
msgstr "Лог"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Статус"
|
||||
|
||||
msgid "Export Debug Report"
|
||||
msgstr "Экспортировать отладочный отчёт"
|
||||
|
||||
msgid "Encryption Config"
|
||||
msgstr "Конфигурация шифрования"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "Конфигурация"
|
||||
|
||||
@@ -1025,6 +1225,9 @@ msgstr "Переместить в блокнот..."
|
||||
msgid "Move %d notes to notebook \"%s\"?"
|
||||
msgstr "Переместить %d заметок в блокнот «%s»?"
|
||||
|
||||
msgid "Press to set the decryption password."
|
||||
msgstr "Нажмите, чтобы установить пароль для расшифровки."
|
||||
|
||||
msgid "Select date"
|
||||
msgstr "Выбрать дату"
|
||||
|
||||
@@ -1034,6 +1237,23 @@ msgstr "Подтвердить"
|
||||
msgid "Cancel synchronisation"
|
||||
msgstr "Отменить синхронизацию"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Master Key %s"
|
||||
msgstr "Мастер-ключ %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Created: %s"
|
||||
msgstr "Создано: %s"
|
||||
|
||||
msgid "Password:"
|
||||
msgstr "Пароль:"
|
||||
|
||||
msgid "Password cannot be empty"
|
||||
msgstr "Пароль не может быть пустым"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "Включено"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The notebook could not be saved: %s"
|
||||
msgstr "Не удалось сохранить блокнот: %s"
|
||||
@@ -1041,6 +1261,13 @@ msgstr "Не удалось сохранить блокнот: %s"
|
||||
msgid "Edit notebook"
|
||||
msgstr "Редактировать блокнот"
|
||||
|
||||
msgid "Show all"
|
||||
msgstr ""
|
||||
|
||||
#, fuzzy
|
||||
msgid "Errors only"
|
||||
msgstr "Ошибка"
|
||||
|
||||
msgid "This note has been modified:"
|
||||
msgstr "Эта заметка была изменена:"
|
||||
|
||||
@@ -1093,3 +1320,78 @@ msgstr "У вас сейчас нет блокнота. Создайте его
|
||||
|
||||
msgid "Welcome"
|
||||
msgstr "Добро пожаловать"
|
||||
|
||||
#~ msgid "Give focus to next pane"
|
||||
#~ msgstr "Переключиться на следующую панель"
|
||||
|
||||
#~ msgid "Give focus to previous pane"
|
||||
#~ msgstr "Переключиться на предыдущую панель"
|
||||
|
||||
#~ msgid "Enter command line mode"
|
||||
#~ msgstr "Войти в режим командной строки"
|
||||
|
||||
#~ msgid "Exit command line mode"
|
||||
#~ msgstr "Выйти из режима командной строки"
|
||||
|
||||
#~ msgid "Edit the selected note"
|
||||
#~ msgstr "Редактировать выбранную заметку"
|
||||
|
||||
#~ msgid "Cancel the current command."
|
||||
#~ msgstr "Отменить текущую команду."
|
||||
|
||||
#~ msgid "Exit the application."
|
||||
#~ msgstr "Выйти из приложения."
|
||||
|
||||
#~ msgid "Delete the currently selected note or notebook."
|
||||
#~ msgstr "Удалить текущую выбранную заметку или блокнот."
|
||||
|
||||
#~ msgid "Set a to-do as completed / not completed"
|
||||
#~ msgstr "Отметить задачу как завершённую/незавершённую"
|
||||
|
||||
#~ msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
|
||||
#~ msgstr ""
|
||||
#~ "[tc] переключить консоль между развёрнутой/свёрнутой/скрытой/видимой."
|
||||
|
||||
#~ msgid "[t]oggle note [m]etadata."
|
||||
#~ msgstr "[tm] переключить отображение метаданных заметки."
|
||||
|
||||
#~ msgid "[M]ake a new [n]ote"
|
||||
#~ msgstr "[mn] создать новую заметку"
|
||||
|
||||
#~ msgid "[M]ake a new [t]odo"
|
||||
#~ msgstr "[mt] создать новую задачу"
|
||||
|
||||
#~ msgid "[M]ake a new note[b]ook"
|
||||
#~ msgstr "[mb] создать новый блокнот"
|
||||
|
||||
#~ msgid "Copy ([Y]ank) the [n]ote to a notebook."
|
||||
#~ msgstr "[yn] копировать заметку в блокнот."
|
||||
|
||||
#~ msgid "Move the note to a notebook."
|
||||
#~ msgstr "Переместить заметку в блокнот."
|
||||
|
||||
#~ msgid "Error"
|
||||
#~ msgstr "Ошибка"
|
||||
|
||||
#~ msgid "Could not download the update: %s"
|
||||
#~ msgstr "Не удалось загрузить обновление: %s"
|
||||
|
||||
#~ msgid "New version downloaded - application will quit now and update..."
|
||||
#~ msgstr ""
|
||||
#~ "Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
|
||||
|
||||
#~ msgid "Could not install the update: %s"
|
||||
#~ msgstr "Не удалось установить обновление: %s"
|
||||
|
||||
#~ msgid ""
|
||||
#~ "The target to synchonise to. If synchronising with the file system, set "
|
||||
#~ "`sync.2.path` to specify the target directory."
|
||||
#~ msgstr ""
|
||||
#~ "То, с чем будет осуществляться синхронизация. При синхронизации с "
|
||||
#~ "файловой системой в `sync.2.path` указывается целевой каталог."
|
||||
|
||||
#~ msgid "To-do title:"
|
||||
#~ msgstr "Название задачи:"
|
||||
|
||||
#~ msgid "\"%s\": \"%s\""
|
||||
#~ msgstr "«%s»: «%s»"
|
||||
|
1316
CliClient/locales/zh_CN.po
Normal file
1316
CliClient/locales/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
102
CliClient/package-lock.json
generated
102
CliClient/package-lock.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.84",
|
||||
"version": "1.0.98",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz",
|
||||
"integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=",
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
|
||||
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
|
||||
"requires": {
|
||||
"co": "4.6.0",
|
||||
"fast-deep-equal": "1.0.0",
|
||||
@@ -64,6 +64,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.1.3.tgz",
|
||||
"integrity": "sha1-Cq0hEjaXlas/F+M3RFVtLs9UdWY="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -85,6 +90,11 @@
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"base-64": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
|
||||
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
|
||||
@@ -197,6 +207,11 @@
|
||||
"delayed-stream": "1.0.0"
|
||||
}
|
||||
},
|
||||
"compare-version": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz",
|
||||
"integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -411,12 +426,12 @@
|
||||
}
|
||||
},
|
||||
"fs-extra": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
|
||||
"integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz",
|
||||
"integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11",
|
||||
"jsonfile": "3.0.1",
|
||||
"jsonfile": "4.0.0",
|
||||
"universalify": "0.1.1"
|
||||
}
|
||||
},
|
||||
@@ -437,7 +452,7 @@
|
||||
"ndarray": "1.0.18",
|
||||
"ndarray-pack": "1.2.1",
|
||||
"node-bitmap": "0.0.1",
|
||||
"omggif": "1.0.8",
|
||||
"omggif": "1.0.9",
|
||||
"parse-data-uri": "0.2.0",
|
||||
"pngjs": "2.3.1",
|
||||
"request": "2.83.0",
|
||||
@@ -489,7 +504,7 @@
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
|
||||
"integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
|
||||
"requires": {
|
||||
"ajv": "5.3.0",
|
||||
"ajv": "5.5.2",
|
||||
"har-schema": "2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -728,9 +743,9 @@
|
||||
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
|
||||
"integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
|
||||
"requires": {
|
||||
"graceful-fs": "4.1.11"
|
||||
}
|
||||
@@ -948,9 +963,9 @@
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"omggif": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.8.tgz",
|
||||
"integrity": "sha1-F483sqsLPXtG7ToORr0HkLWNNTA="
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.9.tgz",
|
||||
"integrity": "sha1-3LcCTazVDFK00wPwSALJHAV8dl8="
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
@@ -1045,6 +1060,11 @@
|
||||
"strict-uri-encode": "1.1.0"
|
||||
}
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz",
|
||||
"integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
@@ -1099,6 +1119,11 @@
|
||||
"uuid": "3.1.0"
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz",
|
||||
@@ -1915,9 +1940,9 @@
|
||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
|
||||
},
|
||||
"string-kit": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.3.tgz",
|
||||
"integrity": "sha512-G2T92klsuE+S9mqdKQyWurFweNQV5X+FRzSKTqYHRdaVUN/4dL6urbYJJ+xb9ep/4XWm+4RNT8j3acncNhFRBg==",
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.4.tgz",
|
||||
"integrity": "sha512-imrOojdsXlL6xzfERCxvc/iA9Zwpzbfs+qeP6VB0s0rQVnMc3Nwkyhge0e8Uoayph7PVAwPNmLpohox27G3fgA==",
|
||||
"requires": {
|
||||
"xregexp": "3.2.0"
|
||||
}
|
||||
@@ -2014,15 +2039,15 @@
|
||||
}
|
||||
},
|
||||
"terminal-kit": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.0.tgz",
|
||||
"integrity": "sha512-ir0I2QtcBDSg2w0UvohlqdDpGlS3S2UYBG4NnYKnK/4VywgnbfxgdpXN3el0uCH3OeH6fG38luW7RmDM96FqUw==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.3.tgz",
|
||||
"integrity": "sha512-ZHtuElnBhK0IXOYNvQ7eYgaArwEoOv7saQc4Q0Z9p02JeC7iajC20/odV77BKB3jw/Qthvf9mpASf8gNDYv7xQ==",
|
||||
"requires": {
|
||||
"async-kit": "2.2.3",
|
||||
"get-pixels": "3.3.0",
|
||||
"ndarray": "1.0.18",
|
||||
"nextgen-events": "0.10.2",
|
||||
"string-kit": "0.6.3",
|
||||
"string-kit": "0.6.4",
|
||||
"tree-kit": "0.5.26"
|
||||
}
|
||||
},
|
||||
@@ -2032,16 +2057,16 @@
|
||||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"tkwidgets": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.20.tgz",
|
||||
"integrity": "sha512-9wGsMrrFJvE/6TKUc0dEFFhwxvZLeNsYOxnpy1JCwyk/hYCEF70nuvk7VvJeG4TPaQBaGKPj6c7pCgdREvz4Jw==",
|
||||
"version": "0.5.25",
|
||||
"resolved": "https://registry.npmjs.org/tkwidgets/-/tkwidgets-0.5.25.tgz",
|
||||
"integrity": "sha512-f+12QbxNCLg9Jou5JoPJxATGLmzpDAQeM7QRTXvuqdEB/QvPD9+UlPUL7eYJP1QJv2zzT6EIWWbdpDkXPEtzCQ==",
|
||||
"requires": {
|
||||
"chalk": "2.3.0",
|
||||
"emphasize": "1.5.0",
|
||||
"node-emoji": "git+https://github.com/laurent22/node-emoji.git#9fa01eac463e94dde1316ef8c53089eeef4973b5",
|
||||
"slice-ansi": "1.0.0",
|
||||
"string-width": "2.1.1",
|
||||
"terminal-kit": "1.14.0",
|
||||
"terminal-kit": "1.14.3",
|
||||
"wrap-ansi": "3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -2095,6 +2120,15 @@
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
|
||||
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.2.0.tgz",
|
||||
"integrity": "sha512-DT1XbYAfmQP65M/mE6OALxmXzZ/z1+e5zk2TcSKe/KiYbNGZxgtttzC0mR/sjopbpOXcbniq7eIKmocJnUWlEw==",
|
||||
"requires": {
|
||||
"querystringify": "1.0.0",
|
||||
"requires-port": "1.0.0"
|
||||
}
|
||||
},
|
||||
"url-to-options": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz",
|
||||
@@ -2139,6 +2173,20 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
|
||||
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
|
||||
"requires": {
|
||||
"sax": "1.2.4",
|
||||
"xmlbuilder": "9.0.4"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
|
||||
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
|
||||
},
|
||||
"xregexp": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",
|
||||
|
@@ -14,11 +14,12 @@
|
||||
"title": "Joplin CLI",
|
||||
"years": [
|
||||
2016,
|
||||
2017
|
||||
2017,
|
||||
2018
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.84",
|
||||
"version": "1.0.98",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -27,9 +28,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^2.2.0",
|
||||
"async-mutex": "^0.1.3",
|
||||
"base-64": "^0.1.0",
|
||||
"compare-version": "^0.1.2",
|
||||
"follow-redirects": "^1.2.4",
|
||||
"form-data": "^2.1.4",
|
||||
"fs-extra": "^3.0.1",
|
||||
"fs-extra": "^5.0.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
@@ -54,9 +58,11 @@
|
||||
"string-to-stream": "^1.1.0",
|
||||
"strip-ansi": "^4.0.0",
|
||||
"tcp-port-used": "^0.1.2",
|
||||
"tkwidgets": "^0.5.20",
|
||||
"tkwidgets": "^0.5.25",
|
||||
"url-parse": "^1.2.0",
|
||||
"uuid": "^3.0.1",
|
||||
"word-wrap": "^1.2.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"yargs-parser": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -9,4 +9,10 @@ bash $SCRIPT_DIR/build.sh
|
||||
cp "$SCRIPT_DIR/package.json" build/
|
||||
cp "$SCRIPT_DIR/../README.md" build/
|
||||
cd "$SCRIPT_DIR/build"
|
||||
npm publish
|
||||
npm publish
|
||||
|
||||
NEW_VERSION=$(cat package.json | jq -r .version)
|
||||
git add -A
|
||||
git commit -m "CLI v$NEW_VERSION"
|
||||
git tag "cli-v$NEW_VERSION"
|
||||
git push && git push --tags
|
@@ -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_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,15 @@
|
||||
#!/bin/bash
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BUILD_DIR="$ROOT_DIR/tests-build"
|
||||
TEST_FILE="$1"
|
||||
|
||||
rsync -a --exclude "node_modules/" "$ROOT_DIR/tests/" "$BUILD_DIR/"
|
||||
rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||
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 tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js)
|
||||
else
|
||||
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
|
||||
fi
|
47
CliClient/tests/ArrayUtils.js
Normal file
47
CliClient/tests/ArrayUtils.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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('ArrayUtils', 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();
|
||||
});
|
||||
|
||||
it('should find items using binary search', async (done) => {
|
||||
let items = ['aaa', 'ccc', 'bbb'];
|
||||
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(-1); // Array not sorted!
|
||||
items.sort();
|
||||
expect(ArrayUtils.binarySearch(items, 'bbb')).toBe(1);
|
||||
expect(ArrayUtils.binarySearch(items, 'ccc')).toBe(2);
|
||||
expect(ArrayUtils.binarySearch(items, 'oops')).toBe(-1);
|
||||
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(0);
|
||||
|
||||
items = [];
|
||||
expect(ArrayUtils.binarySearch(items, 'aaa')).toBe(-1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
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();
|
||||
});
|
||||
|
||||
});
|
32
CliClient/tests/models_Setting.js
Normal file
32
CliClient/tests/models_Setting.js
Normal file
@@ -0,0 +1,32 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
describe('models_Setting', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
done();
|
||||
});
|
||||
|
||||
it('should return only sub-values', asyncTest(async () => {
|
||||
const settings = {
|
||||
'sync.5.path': 'http://example.com',
|
||||
'sync.5.username': 'testing',
|
||||
}
|
||||
|
||||
let output = Setting.subValues('sync.5', settings);
|
||||
expect(output['path']).toBe('http://example.com');
|
||||
expect(output['username']).toBe('testing');
|
||||
|
||||
output = Setting.subValues('sync.4', settings);
|
||||
expect('path' in output).toBe(false);
|
||||
expect('username' in output).toBe(false);
|
||||
}));
|
||||
|
||||
});
|
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,21 +1,25 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId } = 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 { setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, checkThrowAsync, asyncTest } = require('test-utils.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const fs = require('fs-extra');
|
||||
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 { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.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
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000 + 30000; // The first test is slow because the database needs to be built
|
||||
|
||||
async function allItems() {
|
||||
let folders = await Folder.all();
|
||||
@@ -23,7 +27,40 @@ async function allItems() {
|
||||
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) {
|
||||
let error = null;
|
||||
try {
|
||||
let files = await fileApi().list();
|
||||
files = files.items;
|
||||
@@ -38,31 +75,40 @@ async function localItemsSameAsRemote(locals, expect) {
|
||||
expect(!!remote).toBe(true);
|
||||
if (!remote) continue;
|
||||
|
||||
if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
|
||||
expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
|
||||
} else {
|
||||
expect(remote.updated_time).toBe(dbItem.updated_time);
|
||||
}
|
||||
// if (syncTargetId() == SyncTargetRegistry.nameToId('filesystem')) {
|
||||
// expect(remote.updated_time).toBe(Math.floor(dbItem.updated_time / 1000) * 1000);
|
||||
// } else {
|
||||
// expect(remote.updated_time).toBe(dbItem.updated_time);
|
||||
// }
|
||||
|
||||
let remoteContent = await fileApi().get(path);
|
||||
|
||||
remoteContent = dbItem.type_ == BaseModel.TYPE_NOTE ? await Note.unserialize(remoteContent) : await Folder.unserialize(remoteContent);
|
||||
expect(remoteContent.title).toBe(dbItem.title);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBe(null);
|
||||
}
|
||||
|
||||
let insideBeforeEach = false;
|
||||
|
||||
describe('Synchronizer', function() {
|
||||
|
||||
beforeEach( async (done) => {
|
||||
beforeEach(async (done) => {
|
||||
insideBeforeEach = true;
|
||||
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await setupDatabaseAndSynchronizer(2);
|
||||
await switchClient(1);
|
||||
done();
|
||||
|
||||
insideBeforeEach = false;
|
||||
});
|
||||
|
||||
it('should create remote items', async (done) => {
|
||||
it('should create remote items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
|
||||
@@ -71,11 +117,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update remote item', async (done) => {
|
||||
it('should update remote item', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
let note = await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@@ -86,11 +130,9 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create local items', async (done) => {
|
||||
it('should create local items', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "folder1" });
|
||||
await Note.save({ title: "un", parent_id: folder.id });
|
||||
await synchronizer().start();
|
||||
@@ -102,11 +144,9 @@ describe('Synchronizer', function() {
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should update local items', async (done) => {
|
||||
it('should update local items', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -131,11 +171,9 @@ describe('Synchronizer', function() {
|
||||
let all = await allItems();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve note conflicts', async (done) => {
|
||||
it('should resolve note conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -174,11 +212,9 @@ describe('Synchronizer', function() {
|
||||
if (!noteUpdatedFromRemote.hasOwnProperty(n)) continue;
|
||||
expect(noteUpdatedFromRemote[n]).toBe(note2[n], 'Property: ' + n);
|
||||
}
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should resolve folders conflicts', async (done) => {
|
||||
it('should resolve folders conflicts', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -209,11 +245,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let folder1_final = await Folder.load(folder1.id);
|
||||
expect(folder1_final.title).toBe(folder1_modRemote.title);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete remote notes', async (done) => {
|
||||
it('should delete remote notes', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -236,11 +270,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should delete local notes', async (done) => {
|
||||
it('should not created deleted_items entries for items deleted via sync', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -248,21 +280,44 @@ describe('Synchronizer', function() {
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
await Note.delete(note1.id);
|
||||
await Folder.delete(folder1.id);
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
await synchronizer().start();
|
||||
let items = await allItems();
|
||||
expect(items.length).toBe(1);
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should delete remote folder', async (done) => {
|
||||
it('should delete local notes', asyncTest(async () => {
|
||||
// For these tests we pass the context around for each user. This is to make sure that the "deletedItemsProcessed"
|
||||
// property of the basicDelta() function is cleared properly at the end of a sync operation. If it is not cleared
|
||||
// it means items will no longer be deleted locally via sync.
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
let note2 = await Note.save({ title: "deux", parent_id: folder1.id });
|
||||
let context1 = await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
let context2 = await synchronizer().start();
|
||||
await Note.delete(note1.id);
|
||||
context2 = await synchronizer().start({ context: context2 });
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
context1 = await synchronizer().start({ context: context1 });
|
||||
let items = await allItems();
|
||||
expect(items.length).toBe(2);
|
||||
let deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
expect(deletedItems.length).toBe(0);
|
||||
await Note.delete(note2.id);
|
||||
context1 = await synchronizer().start({ context: context1 });
|
||||
}));
|
||||
|
||||
it('should delete remote folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@@ -278,12 +333,10 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let all = await allItems();
|
||||
localItemsSameAsRemote(all, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
it('should delete local folder', async (done) => {
|
||||
it('should delete local folder', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let folder2 = await Folder.save({ title: "folder2" });
|
||||
await synchronizer().start();
|
||||
@@ -303,12 +356,10 @@ describe('Synchronizer', function() {
|
||||
await synchronizer().start();
|
||||
|
||||
let items = await allItems();
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
await localItemsSameAsRemote(items, expect);
|
||||
}));
|
||||
|
||||
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" });
|
||||
await synchronizer().start();
|
||||
|
||||
@@ -326,11 +377,9 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].title).toBe('note1');
|
||||
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 note = await Note.save({ title: "note", parent_id: folder.title });
|
||||
await synchronizer().start();
|
||||
@@ -350,12 +399,10 @@ describe('Synchronizer', function() {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].title).toBe('folder');
|
||||
|
||||
localItemsSameAsRemote(items, expect);
|
||||
|
||||
done();
|
||||
});
|
||||
await localItemsSameAsRemote(items, expect);
|
||||
}));
|
||||
|
||||
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
|
||||
// 2 deletes item 2, they should both end up with no items after sync.
|
||||
|
||||
@@ -391,11 +438,9 @@ describe('Synchronizer', function() {
|
||||
|
||||
expect(items1.length).toBe(0);
|
||||
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 note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
@@ -425,11 +470,9 @@ describe('Synchronizer', function() {
|
||||
let unconflictedNotes = await Note.unconflictedNotes();
|
||||
|
||||
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 folder2 = await Folder.save({ title: "folder2" });
|
||||
let note1 = await Note.save({ title: "un", parent_id: folder1.id });
|
||||
@@ -457,11 +500,9 @@ describe('Synchronizer', function() {
|
||||
let items = await allItems();
|
||||
|
||||
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" });
|
||||
|
||||
await switchClient(2);
|
||||
@@ -493,11 +534,15 @@ describe('Synchronizer', function() {
|
||||
remoteF2 = await Folder.load(remoteF2.id);
|
||||
|
||||
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 n1 = await Note.save({ title: "mynote" });
|
||||
let n2 = await Note.save({ title: "mynote2" });
|
||||
@@ -507,6 +552,12 @@ describe('Synchronizer', function() {
|
||||
await switchClient(2);
|
||||
|
||||
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);
|
||||
expect(!!remoteTag).toBe(true);
|
||||
expect(remoteTag.id).toBe(tag.id);
|
||||
@@ -532,11 +583,17 @@ describe('Synchronizer', function() {
|
||||
noteIds = await Tag.noteIds(tag.id);
|
||||
expect(noteIds.length).toBe(1);
|
||||
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 n1 = await Note.save({ title: "mynote", parent_id: f1.id, is_conflict: 1 });
|
||||
await synchronizer().start();
|
||||
@@ -548,11 +605,9 @@ describe('Synchronizer', function() {
|
||||
let folders = await Folder.all()
|
||||
expect(notes.length).toBe(0);
|
||||
expect(folders.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', async (done) => {
|
||||
it('should not try to delete on remote conflicted notes that have been deleted', asyncTest(async () => {
|
||||
let f1 = await Folder.save({ title: "folder" });
|
||||
let n1 = await Note.save({ title: "mynote", parent_id: f1.id });
|
||||
await synchronizer().start();
|
||||
@@ -565,17 +620,13 @@ describe('Synchronizer', function() {
|
||||
const deletedItems = await BaseItem.deletedItems(syncTargetId());
|
||||
|
||||
expect(deletedItems.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not consider it is a conflict if neither the title nor body of the note have changed', async (done) => {
|
||||
// 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.
|
||||
}));
|
||||
|
||||
async function ignorableNoteConflictTest(withEncryption) {
|
||||
if (withEncryption) {
|
||||
Setting.setValue('encryption.enabled', true);
|
||||
await loadEncryptionMasterKey();
|
||||
}
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
@@ -584,6 +635,10 @@ describe('Synchronizer', function() {
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
if (withEncryption) {
|
||||
await loadEncryptionMasterKey(null, true);
|
||||
await decryptionWorker().start();
|
||||
}
|
||||
let note2 = await Note.load(note1.id);
|
||||
note2.todo_completed = time.unixMs()-1;
|
||||
await Note.save(note2);
|
||||
@@ -598,48 +653,71 @@ describe('Synchronizer', function() {
|
||||
note2conf = await Note.load(note1.id);
|
||||
await synchronizer().start();
|
||||
|
||||
let conflictedNotes = await Note.conflictedNotes();
|
||||
expect(conflictedNotes.length).toBe(0);
|
||||
if (!withEncryption) {
|
||||
// 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();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].id).toBe(note1.id);
|
||||
expect(notes[0].todo_completed).toBe(note2.todo_completed);
|
||||
let conflictedNotes = await Note.conflictedNotes();
|
||||
expect(conflictedNotes.length).toBe(0);
|
||||
|
||||
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 note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
await synchronizer().start();
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
synchronizer().debugFlags_ = ['cancelDeltaLoop2'];
|
||||
synchronizer().testingHooks_ = ['cancelDeltaLoop2'];
|
||||
let context = await synchronizer().start();
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(0);
|
||||
|
||||
synchronizer().debugFlags_ = [];
|
||||
synchronizer().testingHooks_ = [];
|
||||
await synchronizer().start({ context: context });
|
||||
notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
}));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('items should skip items that cannot be synced', async (done) => {
|
||||
it('should skip items that cannot be synced', asyncTest(async () => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
const noteId = note1.id;
|
||||
await synchronizer().start();
|
||||
let disabledItems = await BaseItem.syncDisabledItems();
|
||||
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
expect(disabledItems.length).toBe(0);
|
||||
await Note.save({ id: noteId, title: "un mod", });
|
||||
synchronizer().debugFlags_ = ['cannotSync'];
|
||||
synchronizer().testingHooks_ = ['rejectedByTarget'];
|
||||
await synchronizer().start();
|
||||
synchronizer().debugFlags_ = [];
|
||||
synchronizer().testingHooks_ = [];
|
||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||
|
||||
await switchClient(2);
|
||||
@@ -651,10 +729,281 @@ describe('Synchronizer', function() {
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
disabledItems = await BaseItem.syncDisabledItems();
|
||||
disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
|
||||
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 () => {
|
||||
while (insideBeforeEach) await time.msleep(500);
|
||||
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
|
||||
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);
|
||||
}));
|
||||
|
||||
it('should create remote items with UTF-8 content', asyncTest(async () => {
|
||||
let folder = await Folder.save({ title: "Fahrräder" });
|
||||
await Note.save({ title: "Fahrräder", body: "Fahrräder", parent_id: folder.id });
|
||||
let all = await allItems();
|
||||
|
||||
await synchronizer().start();
|
||||
|
||||
await localItemsSameAsRemote(all, expect);
|
||||
}));
|
||||
|
||||
});
|
@@ -1,34 +1,47 @@
|
||||
const fs = require('fs-extra');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const { BaseModel } = require('lib/base-model.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 { NoteTag } = require('lib/models/note-tag.js');
|
||||
const BaseModel = require('lib/BaseModel.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 NoteTag = require('lib/models/NoteTag.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseItem } = require('lib/models/base-item.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const MasterKey = require('lib/models/MasterKey');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { shimInit } = require('lib/shim-init-node.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const SyncTargetMemory = require('lib/SyncTargetMemory.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||
const WebDavApi = require('lib/WebDavApi');
|
||||
|
||||
let databases_ = [];
|
||||
let synchronizers_ = [];
|
||||
let encryptionServices_ = [];
|
||||
let decryptionWorkers_ = [];
|
||||
let fileApi_ = null;
|
||||
let currentClient_ = 1;
|
||||
|
||||
shimInit();
|
||||
|
||||
const fsDriver = new FsDriverNode();
|
||||
Logger.fsDriver_ = fsDriver;
|
||||
Resource.fsDriver_ = fsDriver;
|
||||
EncryptionService.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
const logDir = __dirname + '/../tests/logs';
|
||||
fs.mkdirpSync(logDir, 0o755);
|
||||
@@ -36,26 +49,34 @@ fs.mkdirpSync(logDir, 0o755);
|
||||
SyncTargetRegistry.addClass(SyncTargetMemory);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
|
||||
const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
||||
const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
|
||||
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
|
||||
//const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
|
||||
const syncDir = __dirname + '/../tests/sync';
|
||||
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 100;//400;
|
||||
|
||||
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
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('Folder', Folder);
|
||||
BaseItem.loadClass('Resource', Resource);
|
||||
BaseItem.loadClass('Tag', Tag);
|
||||
BaseItem.loadClass('NoteTag', NoteTag);
|
||||
BaseItem.loadClass('MasterKey', MasterKey);
|
||||
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
|
||||
Setting.autoSaveEnabled = false;
|
||||
|
||||
function syncTargetId() {
|
||||
return syncTargetId_;
|
||||
}
|
||||
@@ -79,10 +100,15 @@ async function switchClient(id) {
|
||||
BaseItem.db_ = databases_[id];
|
||||
Setting.db_ = databases_[id];
|
||||
|
||||
BaseItem.encryptionService_ = encryptionServices_[id];
|
||||
Resource.encryptionService_ = encryptionServices_[id];
|
||||
|
||||
Setting.setConstant('resourceDir', resourceDir(id));
|
||||
|
||||
return Setting.load();
|
||||
}
|
||||
|
||||
function clearDatabase(id = null) {
|
||||
async function clearDatabase(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
|
||||
let queries = [
|
||||
@@ -91,35 +117,46 @@ function clearDatabase(id = null) {
|
||||
'DELETE FROM resources',
|
||||
'DELETE FROM tags',
|
||||
'DELETE FROM note_tags',
|
||||
'DELETE FROM master_keys',
|
||||
'DELETE FROM settings',
|
||||
|
||||
'DELETE FROM deleted_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_;
|
||||
|
||||
Setting.cancelScheduleSave();
|
||||
Setting.cache_ = null;
|
||||
|
||||
if (databases_[id]) {
|
||||
return clearDatabase(id).then(() => {
|
||||
return Setting.load();
|
||||
});
|
||||
await clearDatabase(id);
|
||||
await Setting.load();
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
}).then(() => {
|
||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
// databases_[id].setLogger(logger);
|
||||
// console.info(filePath);
|
||||
return databases_[id].open({ name: filePath }).then(() => {
|
||||
BaseModel.db_ = databases_[id];
|
||||
return setupDatabase(id);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
databases_[id] = new JoplinDatabase(new DatabaseDriverNode());
|
||||
await databases_[id].open({ name: filePath });
|
||||
|
||||
BaseModel.db_ = databases_[id];
|
||||
await Setting.load();
|
||||
}
|
||||
|
||||
function resourceDir(id = null) {
|
||||
if (id === null) id = currentClient_;
|
||||
return __dirname + '/data/resources-' + id;
|
||||
}
|
||||
|
||||
async function setupDatabaseAndSynchronizer(id = null) {
|
||||
@@ -127,20 +164,26 @@ async function setupDatabaseAndSynchronizer(id = null) {
|
||||
|
||||
await setupDatabase(id);
|
||||
|
||||
EncryptionService.instance_ = null;
|
||||
DecryptionWorker.instance_ = null;
|
||||
|
||||
await fs.remove(resourceDir(id));
|
||||
await fs.mkdirp(resourceDir(id), 0o755);
|
||||
|
||||
if (!synchronizers_[id]) {
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId_);
|
||||
const syncTarget = new SyncTargetClass(db(id));
|
||||
syncTarget.setFileApi(fileApi());
|
||||
syncTarget.setLogger(logger);
|
||||
synchronizers_[id] = await syncTarget.synchronizer();
|
||||
synchronizers_[id].autoStartDecryptionWorker_ = false; // For testing we disable this since it would make the tests non-deterministic
|
||||
}
|
||||
|
||||
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
|
||||
fs.removeSync(syncDir)
|
||||
fs.mkdirpSync(syncDir, 0o755);
|
||||
} else {
|
||||
await fileApi().format();
|
||||
}
|
||||
encryptionServices_[id] = new EncryptionService();
|
||||
decryptionWorkers_[id] = new DecryptionWorker();
|
||||
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
|
||||
|
||||
await fileApi().clearRoot();
|
||||
}
|
||||
|
||||
function db(id = null) {
|
||||
@@ -153,6 +196,35 @@ function synchronizer(id = null) {
|
||||
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() {
|
||||
if (fileApi_) return fileApi_;
|
||||
|
||||
@@ -162,7 +234,17 @@ function fileApi() {
|
||||
fileApi_ = new FileApi(syncDir, new FileApiDriverLocal());
|
||||
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('memory')) {
|
||||
fileApi_ = new FileApi('/root', new FileApiDriverMemory());
|
||||
} else if (syncTargetId_ == SyncTargetRegistry.nameToId('nextcloud')) {
|
||||
const options = {
|
||||
baseUrl: () => 'http://nextcloud.local/remote.php/dav/files/admin/JoplinTest',
|
||||
username: () => 'admin',
|
||||
password: () => '123456',
|
||||
};
|
||||
|
||||
const api = new WebDavApi(options);
|
||||
fileApi_ = new FileApi('', new FileApiDriverWebDav(api));
|
||||
}
|
||||
|
||||
// } else if (syncTargetId == Setting.SYNC_TARGET_ONEDRIVE) {
|
||||
// let auth = require('./onedrive-auth.json');
|
||||
// if (!auth) {
|
||||
@@ -182,7 +264,47 @@ function fileApi() {
|
||||
|
||||
fileApi_.setLogger(logger);
|
||||
fileApi_.setSyncTargetId(syncTargetId_);
|
||||
fileApi_.requestRepeatCount_ = 0;
|
||||
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",
|
||||
"years": [
|
||||
2016,
|
||||
2017
|
||||
2017,
|
||||
2018
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
|
@@ -1,9 +1,11 @@
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BrowserWindow } = require('electron');
|
||||
const { BrowserWindow, Menu, Tray } = require('electron');
|
||||
const { shim } = require('lib/shim');
|
||||
const url = require('url')
|
||||
const path = require('path')
|
||||
const urlUtils = require('lib/urlUtils.js');
|
||||
const { dirname, basename } = require('lib/path-utils');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
class ElectronAppWrapper {
|
||||
|
||||
@@ -12,6 +14,8 @@ class ElectronAppWrapper {
|
||||
this.env_ = env;
|
||||
this.win_ = null;
|
||||
this.willQuitApp_ = false;
|
||||
this.tray_ = null;
|
||||
this.buildDir_ = null;
|
||||
}
|
||||
|
||||
electronApp() {
|
||||
@@ -59,14 +63,30 @@ class ElectronAppWrapper {
|
||||
}))
|
||||
|
||||
// Uncomment this to view errors if the application does not start
|
||||
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
|
||||
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
|
||||
|
||||
this.win_.on('close', (event) => {
|
||||
if (this.willQuitApp_ || process.platform !== 'darwin') {
|
||||
this.win_ = null;
|
||||
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
|
||||
// otherwise the window is simply hidden, and will be re-open once the app is "activated" (which happens when the
|
||||
// user clicks on the icon in the task bar).
|
||||
|
||||
// On Windows and Linux, the app is closed when the window is closed *except* if the tray icon is used. In which
|
||||
// case the app must be explicitely closed with Ctrl+Q or by right-clicking on the tray icon and selecting "Exit".
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
if (this.willQuitApp_) {
|
||||
this.win_ = null;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
}
|
||||
} else {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
if (this.trayShown() && !this.willQuitApp_) {
|
||||
event.preventDefault();
|
||||
this.win_.hide();
|
||||
} else {
|
||||
this.win_ = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,6 +113,44 @@ class ElectronAppWrapper {
|
||||
this.electronApp_.quit();
|
||||
}
|
||||
|
||||
trayShown() {
|
||||
return !!this.tray_;
|
||||
}
|
||||
|
||||
buildDir() {
|
||||
if (this.buildDir_) return this.buildDir_;
|
||||
let dir = __dirname + '/build';
|
||||
if (!fs.pathExistsSync(dir)) {
|
||||
dir = dirname(__dirname) + '/build';
|
||||
if (!fs.pathExistsSync(dir)) throw new Error('Cannot find build dir');
|
||||
}
|
||||
|
||||
this.buildDir_ = dir;
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Note: this must be called only after the "ready" event of the app has been dispatched
|
||||
createTray(contextMenu) {
|
||||
try {
|
||||
const iconFilename = this.env_ === 'dev' ? '16x16-dev.png' : '16x16.png';
|
||||
this.tray_ = new Tray(this.buildDir() + '/icons/' + iconFilename)
|
||||
this.tray_.setToolTip(this.electronApp_.getName())
|
||||
this.tray_.setContextMenu(contextMenu)
|
||||
|
||||
this.tray_.on('click', () => {
|
||||
this.window().show();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Cannot create tray", error);
|
||||
}
|
||||
}
|
||||
|
||||
destroyTray() {
|
||||
if (!this.tray_) return;
|
||||
this.tray_.destroy();
|
||||
this.tray_ = null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
// Since we are doing other async things before creating the window, we might miss
|
||||
// the "ready" event. So we use the function below to make sure that the app is ready.
|
||||
|
@@ -2,13 +2,14 @@ require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { BaseApplication } = require('lib/BaseApplication');
|
||||
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 { 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 os = require('os');
|
||||
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 { sprintf } = require('sprintf-js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
@@ -18,6 +19,7 @@ const { defaultState } = require('lib/reducer.js');
|
||||
const packageInfo = require('./packageInfo.js');
|
||||
const AlarmService = require('lib/services/AlarmService.js');
|
||||
const AlarmServiceDriverNode = require('lib/services/AlarmServiceDriverNode');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -47,6 +49,10 @@ class Application extends BaseApplication {
|
||||
return true;
|
||||
}
|
||||
|
||||
checkForUpdateLoggerPath() {
|
||||
return Setting.value('profileDir') + '/log-autoupdater.txt';
|
||||
}
|
||||
|
||||
reducer(state = appDefaultState, action) {
|
||||
let newState = state;
|
||||
|
||||
@@ -134,6 +140,10 @@ class Application extends BaseApplication {
|
||||
this.refreshMenu();
|
||||
}
|
||||
|
||||
if (action.type == 'SETTING_UPDATE_ONE' && action.key == 'showTrayIcon' || action.type == 'SETTING_UPDATE_ALL') {
|
||||
this.updateTray();
|
||||
}
|
||||
|
||||
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync();
|
||||
}
|
||||
@@ -190,6 +200,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
}, {
|
||||
label: _('New notebook'),
|
||||
accelerator: 'CommandOrControl+B',
|
||||
screens: ['Main'],
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
@@ -218,6 +229,14 @@ class Application extends BaseApplication {
|
||||
},
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
platforms: ['darwin'],
|
||||
}, {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: 'CommandOrControl+H',
|
||||
click: () => { bridge().window().hide() }
|
||||
}, {
|
||||
type: 'separator',
|
||||
}, {
|
||||
@@ -229,17 +248,17 @@ class Application extends BaseApplication {
|
||||
label: _('Edit'),
|
||||
submenu: [{
|
||||
label: _('Copy'),
|
||||
screens: ['Main', 'OneDriveLogin'],
|
||||
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
|
||||
role: 'copy',
|
||||
accelerator: 'CommandOrControl+C',
|
||||
}, {
|
||||
label: _('Cut'),
|
||||
screens: ['Main', 'OneDriveLogin'],
|
||||
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
|
||||
role: 'cut',
|
||||
accelerator: 'CommandOrControl+X',
|
||||
}, {
|
||||
label: _('Paste'),
|
||||
screens: ['Main', 'OneDriveLogin'],
|
||||
screens: ['Main', 'OneDriveLogin', 'Config', 'EncryptionConfig'],
|
||||
role: 'paste',
|
||||
accelerator: 'CommandOrControl+V',
|
||||
}, {
|
||||
@@ -255,7 +274,20 @@ class Application extends BaseApplication {
|
||||
name: 'search',
|
||||
});
|
||||
},
|
||||
}]
|
||||
}],
|
||||
}, {
|
||||
label: _('View'),
|
||||
submenu: [{
|
||||
label: _('Toggle editor layout'),
|
||||
screens: ['Main'],
|
||||
accelerator: 'CommandOrControl+L',
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'toggleVisiblePanes',
|
||||
});
|
||||
}
|
||||
}],
|
||||
}, {
|
||||
label: _('Tools'),
|
||||
submenu: [{
|
||||
@@ -266,21 +298,38 @@ class Application extends BaseApplication {
|
||||
routeName: 'Status',
|
||||
});
|
||||
}
|
||||
}, {
|
||||
type: 'separator',
|
||||
screens: ['Main'],
|
||||
},{
|
||||
label: _('Options'),
|
||||
label: _('Encryption options'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'EncryptionConfig',
|
||||
});
|
||||
}
|
||||
},{
|
||||
label: _('General Options'),
|
||||
accelerator: 'CommandOrControl+,',
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Config',
|
||||
});
|
||||
}
|
||||
}]
|
||||
}],
|
||||
}, {
|
||||
label: _('Help'),
|
||||
submenu: [{
|
||||
label: _('Website and documentation'),
|
||||
accelerator: 'F1',
|
||||
click () { bridge().openExternal('http://joplin.cozic.net') }
|
||||
}, {
|
||||
label: _('Check for updates...'),
|
||||
click: () => {
|
||||
bridge().checkForUpdates(false, bridge().window(), this.checkForUpdateLoggerPath());
|
||||
}
|
||||
}, {
|
||||
label: _('About Joplin'),
|
||||
click: () => {
|
||||
@@ -288,7 +337,7 @@ class Application extends BaseApplication {
|
||||
let message = [
|
||||
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),
|
||||
];
|
||||
bridge().showMessageBox({
|
||||
@@ -308,10 +357,13 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
function removeUnwantedItems(template, screen) {
|
||||
const platform = shim.platformName();
|
||||
|
||||
let output = [];
|
||||
for (let i = 0; i < template.length; i++) {
|
||||
const t = Object.assign({}, template[i]);
|
||||
if (t.screens && t.screens.indexOf(screen) < 0) continue;
|
||||
if (t.platforms && t.platforms.indexOf(platform) < 0) continue;
|
||||
if (t.submenu) t.submenu = removeUnwantedItems(t.submenu, screen);
|
||||
if (('submenu' in t) && isEmptyMenu(t.submenu)) continue;
|
||||
output.push(t);
|
||||
@@ -327,6 +379,28 @@ class Application extends BaseApplication {
|
||||
this.lastMenuScreen_ = screen;
|
||||
}
|
||||
|
||||
updateTray() {
|
||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||
// Might be fixed in Electron 18.x but no non-beta release yet.
|
||||
if (!shim.isWindows() && !shim.isMac()) return;
|
||||
|
||||
const app = bridge().electronApp();
|
||||
|
||||
if (app.trayShown() === Setting.value('showTrayIcon')) return;
|
||||
|
||||
if (!Setting.value('showTrayIcon')) {
|
||||
app.destroyTray();
|
||||
} else {
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: _('Open %s', app.electronApp().getName()), click: () => { app.window().show(); } },
|
||||
{ type: 'separator' },
|
||||
{ label: _('Exit'), click: () => { app.exit() } },
|
||||
])
|
||||
app.createTray(contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
async start(argv) {
|
||||
argv = await super.start(argv);
|
||||
|
||||
@@ -354,7 +428,14 @@ class Application extends BaseApplication {
|
||||
|
||||
this.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
tags: tags,
|
||||
items: tags,
|
||||
});
|
||||
|
||||
const masterKeys = await MasterKey.all();
|
||||
|
||||
this.dispatch({
|
||||
type: 'MASTERKEY_UPDATE_ALL',
|
||||
items: masterKeys,
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
@@ -365,17 +446,20 @@ class Application extends BaseApplication {
|
||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||
// but then doesn't install it on exit.
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
const runAutoUpdateCheck = function() {
|
||||
const runAutoUpdateCheck = () => {
|
||||
if (Setting.value('autoUpdateEnabled')) {
|
||||
bridge().checkForUpdatesAndNotify(Setting.value('profileDir') + '/log-autoupdater.txt');
|
||||
bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath());
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check on startup
|
||||
setTimeout(() => { runAutoUpdateCheck() }, 5000);
|
||||
// For those who leave the app always open
|
||||
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
|
||||
// Then every x hours
|
||||
setInterval(() => { runAutoUpdateCheck() }, 12 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
this.updateTray();
|
||||
|
||||
setTimeout(() => {
|
||||
AlarmService.garbageCollect();
|
||||
}, 1000 * 60 * 60);
|
||||
@@ -387,6 +471,8 @@ class Application extends BaseApplication {
|
||||
// Wait for the first sync before updating the notifications, since synchronisation
|
||||
// might change the notifications.
|
||||
AlarmService.updateAllNotifications();
|
||||
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -43,7 +43,7 @@ class Bridge {
|
||||
const {dialog} = require('electron');
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
const filePath = dialog.showSaveDialog(options);
|
||||
const filePath = dialog.showSaveDialog(this.window(), options);
|
||||
if (filePath) {
|
||||
this.lastSelectedPath_ = filePath;
|
||||
}
|
||||
@@ -55,27 +55,27 @@ class Bridge {
|
||||
if (!options) options = {};
|
||||
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
|
||||
if (!('createDirectory' in options)) options.createDirectory = true;
|
||||
const filePaths = dialog.showOpenDialog(options);
|
||||
const filePaths = dialog.showOpenDialog(this.window(), options);
|
||||
if (filePaths && filePaths.length) {
|
||||
this.lastSelectedPath_ = dirname(filePaths[0]);
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
showMessageBox(options) {
|
||||
showMessageBox(window, options) {
|
||||
const {dialog} = require('electron');
|
||||
return dialog.showMessageBox(options);
|
||||
return dialog.showMessageBox(window, options);
|
||||
}
|
||||
|
||||
showErrorMessageBox(message) {
|
||||
return this.showMessageBox({
|
||||
return this.showMessageBox(this.window(), {
|
||||
type: 'error',
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
showConfirmMessageBox(message) {
|
||||
const result = this.showMessageBox({
|
||||
const result = this.showMessageBox(this.window(), {
|
||||
type: 'question',
|
||||
message: message,
|
||||
buttons: [_('OK'), _('Cancel')],
|
||||
@@ -84,7 +84,7 @@ class Bridge {
|
||||
}
|
||||
|
||||
showInfoMessageBox(message) {
|
||||
const result = this.showMessageBox({
|
||||
const result = this.showMessageBox(this.window(), {
|
||||
type: 'info',
|
||||
message: message,
|
||||
buttons: [_('OK')],
|
||||
@@ -108,21 +108,26 @@ class Bridge {
|
||||
return require('electron').shell.openItem(fullPath)
|
||||
}
|
||||
|
||||
async checkForUpdatesAndNotify(logFilePath) {
|
||||
if (!this.autoUpdater_) {
|
||||
this.autoUpdateLogger_ = new Logger();
|
||||
this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
|
||||
this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
|
||||
this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
|
||||
this.autoUpdater_ = require("electron-updater").autoUpdater;
|
||||
this.autoUpdater_.logger = this.autoUpdateLogger_;
|
||||
}
|
||||
// async checkForUpdatesAndNotify(logFilePath) {
|
||||
// if (!this.autoUpdater_) {
|
||||
// this.autoUpdateLogger_ = new Logger();
|
||||
// this.autoUpdateLogger_.addTarget('file', { path: logFilePath });
|
||||
// this.autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
|
||||
// this.autoUpdateLogger_.info('checkForUpdatesAndNotify: Initializing...');
|
||||
// this.autoUpdater_ = require("electron-updater").autoUpdater;
|
||||
// this.autoUpdater_.logger = this.autoUpdateLogger_;
|
||||
// }
|
||||
|
||||
try {
|
||||
await this.autoUpdater_.checkForUpdatesAndNotify();
|
||||
} catch (error) {
|
||||
this.autoUpdateLogger_.error(error);
|
||||
}
|
||||
// try {
|
||||
// await this.autoUpdater_.checkForUpdatesAndNotify();
|
||||
// } catch (error) {
|
||||
// this.autoUpdateLogger_.error(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
checkForUpdates(inBackground, window, logFilePath) {
|
||||
const { checkForUpdates } = require('./checkForUpdates.js');
|
||||
checkForUpdates(inBackground, window, logFilePath);
|
||||
}
|
||||
|
||||
}
|
||||
|
BIN
ElectronClient/app/build/icons/16x16-dev.png
Normal file
BIN
ElectronClient/app/build/icons/16x16-dev.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 611 B |
144
ElectronClient/app/checkForUpdates.js
Normal file
144
ElectronClient/app/checkForUpdates.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const { dialog } = require('electron')
|
||||
const { autoUpdater } = require('electron-updater')
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
let autoUpdateLogger_ = new Logger();
|
||||
let checkInBackground_ = false;
|
||||
let isCheckingForUpdate_ = false;
|
||||
let parentWindow_ = null;
|
||||
|
||||
// Note: Electron Builder's autoUpdater is incredibly buggy so currently it's only used
|
||||
// to detect if a new version is present. If it is, the download link is simply opened
|
||||
// in a new browser window.
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
function htmlToText_(html) {
|
||||
let output = html.replace(/\n/g, '');
|
||||
output = output.replace(/<li>/g, '- ');
|
||||
output = output.replace(/<p>/g, '');
|
||||
output = output.replace(/<\/p>/g, '\n');
|
||||
output = output.replace(/<\/li>/g, '\n');
|
||||
output = output.replace(/<ul>/g, '');
|
||||
output = output.replace(/<\/ul>/g, '');
|
||||
output = output.replace(/<.*?>/g, '');
|
||||
output = output.replace(/<\/.*?>/g, '');
|
||||
return output;
|
||||
}
|
||||
|
||||
function showErrorMessageBox(message) {
|
||||
return dialog.showMessageBox(parentWindow_, {
|
||||
type: 'error',
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
function onCheckStarted() {
|
||||
autoUpdateLogger_.info('checkForUpdates: Starting...');
|
||||
isCheckingForUpdate_ = true;
|
||||
}
|
||||
|
||||
function onCheckEnded() {
|
||||
autoUpdateLogger_.info('checkForUpdates: Done.');
|
||||
isCheckingForUpdate_ = false;
|
||||
}
|
||||
|
||||
autoUpdater.on('error', (error) => {
|
||||
autoUpdateLogger_.error(error);
|
||||
if (checkInBackground_) return onCheckEnded();
|
||||
showErrorMessageBox(error == null ? "unknown" : (error.stack || error).toString())
|
||||
onCheckEnded();
|
||||
})
|
||||
|
||||
function findDownloadFilename_(info) {
|
||||
// { version: '1.0.64',
|
||||
// files:
|
||||
// [ { url: 'Joplin-1.0.64-mac.zip',
|
||||
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==' },
|
||||
// { url: 'Joplin-1.0.64.dmg',
|
||||
// sha512: 'jAewQQoJ3nCaOj8hWDgf0sc3LBbAWQtiKqfTflK8Hc3Dh7fAy9jRHfFAZKFUZ9ll95Bun0DVsLq8wLSUrdsMXw==',
|
||||
// size: 77104485 } ],
|
||||
// path: 'Joplin-1.0.64-mac.zip',
|
||||
// sha512: 'OlemXqhq/fSifx7EutvMzfoCI/1kGNl10i8nkvACEDfVXwP8hankDBXEC0+GxSArsZuxOh3U1+C+4j72SfIUew==',
|
||||
// releaseDate: '2018-02-16T00:13:01.634Z',
|
||||
// releaseName: 'v1.0.64',
|
||||
// releaseNotes: '<p>Still more fixes and im...' }
|
||||
|
||||
if (!info) return null;
|
||||
|
||||
if (!info.files) {
|
||||
// info.path seems to contain a default, though not a good one,
|
||||
// so the loop below if preferable to find the right file.
|
||||
return info.path;
|
||||
}
|
||||
|
||||
for (let i = 0; i < info.files.length; i++) {
|
||||
const f = info.files[i].url; // Annoyingly this is called "url" but it's obviously not a url, so hopefully it won't change later on and become one.
|
||||
if (f.indexOf('.exe') >= 0) return f;
|
||||
if (f.indexOf('.dmg') >= 0) return f;
|
||||
}
|
||||
|
||||
return info.path;
|
||||
}
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
const filename = findDownloadFilename_(info);
|
||||
|
||||
if (!info.version || !filename) {
|
||||
if (checkInBackground_) return onCheckEnded();
|
||||
showErrorMessageBox(('Could not get version info: ' + JSON.stringify(info)));
|
||||
return onCheckEnded();
|
||||
}
|
||||
|
||||
const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/v' + info.version + '/' + filename;
|
||||
|
||||
let releaseNotes = info.releaseNotes + '';
|
||||
if (releaseNotes) releaseNotes = '\n\n' + _('Release notes:\n\n%s', htmlToText_(releaseNotes));
|
||||
|
||||
const buttonIndex = dialog.showMessageBox(parentWindow_, {
|
||||
type: 'info',
|
||||
message: _('An update is available, do you want to download it now?' + releaseNotes),
|
||||
buttons: [_('Yes'), _('No')]
|
||||
});
|
||||
|
||||
onCheckEnded();
|
||||
|
||||
if (buttonIndex === 0) require('electron').shell.openExternal(downloadUrl);
|
||||
})
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
if (checkInBackground_) return onCheckEnded();
|
||||
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
|
||||
onCheckEnded();
|
||||
})
|
||||
|
||||
function checkForUpdates(inBackground, window, logFilePath) {
|
||||
if (isCheckingForUpdate_) {
|
||||
autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
parentWindow_ = window;
|
||||
|
||||
onCheckStarted();
|
||||
|
||||
if (logFilePath && !autoUpdateLogger_.targets().length) {
|
||||
autoUpdateLogger_ = new Logger();
|
||||
autoUpdateLogger_.addTarget('file', { path: logFilePath });
|
||||
autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG);
|
||||
autoUpdateLogger_.info('checkForUpdates: Initializing...');
|
||||
autoUpdater.logger = autoUpdateLogger_;
|
||||
}
|
||||
|
||||
checkInBackground_ = inBackground;
|
||||
|
||||
try {
|
||||
autoUpdater.checkForUpdates()
|
||||
} catch (error) {
|
||||
autoUpdateLogger_.error(error);
|
||||
if (!checkInBackground_) showErrorMessageBox(error.message);
|
||||
onCheckEnded();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.checkForUpdates = checkForUpdates
|
3
ElectronClient/app/dev-app-update.yml-FORTESTING
Normal file
3
ElectronClient/app/dev-app-update.yml-FORTESTING
Normal file
@@ -0,0 +1,3 @@
|
||||
owner: laurent22
|
||||
repo: joplin
|
||||
provider: github
|
@@ -1,20 +1,28 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
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 { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const pathUtils = require('lib/path-utils.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
settings: {},
|
||||
shared.init(this);
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
}
|
||||
|
||||
this.rowStyle_ = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +52,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
let output = null;
|
||||
|
||||
const rowStyle = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
const rowStyle = this.rowStyle_;
|
||||
|
||||
const labelStyle = Object.assign({}, theme.textStyle, {
|
||||
display: 'inline-block',
|
||||
@@ -58,9 +64,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = value;
|
||||
this.setState({ settings: settings });
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
}
|
||||
|
||||
// Component key needs to be key+value otherwise it doesn't update when the settings change.
|
||||
@@ -89,24 +93,39 @@ class ConfigScreenComponent extends React.Component {
|
||||
updateSettingValue(key, !value)
|
||||
}
|
||||
|
||||
// Hack: The {key+value.toString()} is needed as otherwise the checkbox doesn't update when the state changes.
|
||||
// There's probably a better way to do this but can't figure it out.
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div key={key+value.toString()} style={rowStyle}>
|
||||
<div style={controlStyle}>
|
||||
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
|
||||
<input id={'setting_checkbox_' + key} type="checkbox" checked={!!value} onChange={(event) => { onCheckboxClick(event) }}/><label onClick={(event) => { onCheckboxClick(event) }} style={labelStyle} htmlFor={'setting_checkbox_' + key}>{md.label()}</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_STRING) {
|
||||
const onTextChange = (event) => {
|
||||
const settings = Object.assign({}, this.state.settings);
|
||||
settings[key] = event.target.value;
|
||||
this.setState({ settings: settings });
|
||||
updateSettingValue(key, event.target.value);
|
||||
}
|
||||
|
||||
const inputStyle = Object.assign({}, controlStyle, { width: '50%', minWidth: '20em' });
|
||||
const inputType = md.secure === true ? 'password' : 'text';
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<input type="text" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
<input type={inputType} style={inputStyle} value={this.state.settings[key]} onChange={(event) => {onTextChange(event)}} />
|
||||
</div>
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_INT) {
|
||||
const onNumChange = (event) => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={key} style={rowStyle}>
|
||||
<div style={labelStyle}><label>{md.label()}</label></div>
|
||||
<input type="number" style={controlStyle} value={this.state.settings[key]} onChange={(event) => {onNumChange(event)}} min={md.minimum} max={md.maximum} step={md.step}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@@ -117,10 +136,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
}
|
||||
|
||||
onSaveClick() {
|
||||
for (let n in this.state.settings) {
|
||||
if (!this.state.settings.hasOwnProperty(n)) continue;
|
||||
Setting.setValue(n, this.state.settings[n]);
|
||||
}
|
||||
shared.saveSettings(this);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
@@ -130,7 +146,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const style = Object.assign({}, this.props.style, { overflow: 'auto' });
|
||||
const settings = this.state.settings;
|
||||
|
||||
const headerStyle = {
|
||||
@@ -142,23 +158,28 @@ class ConfigScreenComponent extends React.Component {
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
display: this.state.settings === this.props.settings ? 'none' : 'inline-block',
|
||||
display: this.state.changedSettingKeys.length ? 'inline-block' : 'none',
|
||||
marginRight: 10,
|
||||
}
|
||||
|
||||
let settingComps = [];
|
||||
let keys = Setting.keys(true, 'desktop');
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!(key in settings)) {
|
||||
console.warn('Missing setting: ' + key);
|
||||
continue;
|
||||
}
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
const comp = this.settingToComponent(key, settings[key]);
|
||||
if (!comp) continue;
|
||||
settingComps.push(comp);
|
||||
const settingComps = shared.settingsToComponents(this, 'desktop', settings);
|
||||
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
{messages[0]}
|
||||
{messages.length >= 1 ? (<p>{messages[1]}</p>) : null}
|
||||
</div>);
|
||||
|
||||
settingComps.push(
|
||||
<div key="check_sync_config_button" style={this.rowStyle_}>
|
||||
<button disabled={this.state.checkSyncConfigResult === 'checking'} onClick={this.checkSyncConfig_}>{_('Check synchronisation configuration')}</button>
|
||||
{ statusComp }
|
||||
</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
|
222
ElectronClient/app/gui/EncryptionConfigScreen.jsx
Normal file
222
ElectronClient/app/gui/EncryptionConfigScreen.jsx
Normal file
@@ -0,0 +1,222 @@
|
||||
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 = [];
|
||||
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
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 = <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p>;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(<tr key={id}><td style={theme.textStyle}>{id}</td></tr>);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<div>
|
||||
<h1 style={theme.h1Style}>{_('Missing Master Keys')}</h1>
|
||||
<p style={theme.textStyle}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th style={theme.textStyle}>{_('ID')}</th>
|
||||
</tr>
|
||||
{ rows }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header style={headerStyle} />
|
||||
<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}
|
||||
{nonExistingMasterKeySection}
|
||||
</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'],
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
};
|
||||
};
|
||||
|
||||
const EncryptionConfigScreen = connect(mapStateToProps)(EncryptionConfigScreenComponent);
|
||||
|
||||
module.exports = { EncryptionConfigScreen };
|
@@ -1,7 +1,7 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
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 { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -25,9 +25,7 @@ class ImportScreenComponent extends React.Component {
|
||||
doImport: true,
|
||||
filePath: newProps.filePath,
|
||||
messages: [],
|
||||
});
|
||||
|
||||
this.doImport();
|
||||
}, () => { this.doImport() });
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -5,12 +5,12 @@ const { SideBar } = require('./SideBar.min.js');
|
||||
const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Note = require('lib/models/Note.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 { _ } = require('lib/locale.js');
|
||||
const layoutUtils = require('lib/layout-utils.js');
|
||||
@@ -44,16 +44,14 @@ class MainScreenComponent extends React.Component {
|
||||
const folderId = Setting.value('activeFolderId');
|
||||
if (!folderId) return;
|
||||
|
||||
const note = await Note.save({
|
||||
title: title,
|
||||
const newNote = {
|
||||
parent_id: folderId,
|
||||
is_todo: isTodo ? 1 : 0,
|
||||
});
|
||||
Note.updateGeolocation(note.id);
|
||||
};
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: note.id,
|
||||
type: 'NOTE_SET_NEW_ONE',
|
||||
item: newNote,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,30 +63,14 @@ class MainScreenComponent extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('Note title:'),
|
||||
onClose: async (answer) => {
|
||||
if (answer) await createNewNote(answer, false);
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
},
|
||||
});
|
||||
await createNewNote(null, false);
|
||||
} else if (command.name === 'newTodo') {
|
||||
if (!this.props.folders.length) {
|
||||
bridge().showErrorMessageBox(_('Please create a notebook first'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('To-do title:'),
|
||||
onClose: async (answer) => {
|
||||
if (answer) await createNewNote(answer, true);
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
},
|
||||
});
|
||||
await createNewNote(null, true);
|
||||
} else if (command.name === 'newNotebook') {
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
@@ -132,7 +114,7 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'renameNotebook') {
|
||||
} else if (command.name === 'renameFolder') {
|
||||
const folder = await Folder.load(command.id);
|
||||
if (!folder) return;
|
||||
|
||||
@@ -143,7 +125,8 @@ class MainScreenComponent extends React.Component {
|
||||
onClose: async (answer) => {
|
||||
if (answer !== null) {
|
||||
try {
|
||||
await Folder.save({ id: folder.id, title: answer }, { userSideValidation: true });
|
||||
folder.title = answer;
|
||||
await Folder.save(folder, { fields: ['title'], userSideValidation: true });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
@@ -180,6 +163,8 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'toggleVisiblePanes') {
|
||||
this.toggleVisiblePanes();
|
||||
} else if (command.name === 'editAlarm') {
|
||||
const note = await Note.load(command.noteId);
|
||||
|
||||
@@ -288,24 +273,25 @@ class MainScreenComponent extends React.Component {
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const folders = this.props.folders;
|
||||
const notes = this.props.notes;
|
||||
const messageBoxVisible = this.props.hasDisabledSyncItems;
|
||||
|
||||
const messageBoxVisible = this.props.hasDisabledSyncItems || this.props.showMissingMasterKeyMessage;
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, messageBoxVisible);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const selectedFolderId = this.props.selectedFolderId;
|
||||
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
|
||||
|
||||
const headerButtons = [];
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New note'),
|
||||
iconName: 'fa-file-o',
|
||||
enabled: !!folders.length,
|
||||
enabled: !!folders.length && !onConflictFolder,
|
||||
onClick: () => { this.doCommand({ name: 'newNote' }) },
|
||||
});
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New to-do'),
|
||||
iconName: 'fa-check-square-o',
|
||||
enabled: !!folders.length,
|
||||
enabled: !!folders.length && !onConflictFolder,
|
||||
onClick: () => { this.doCommand({ name: 'newTodo' }) },
|
||||
});
|
||||
|
||||
@@ -325,9 +311,7 @@ class MainScreenComponent extends React.Component {
|
||||
title: _('Layout'),
|
||||
iconName: 'fa-columns',
|
||||
enabled: !!notes.length,
|
||||
onClick: () => {
|
||||
this.toggleVisiblePanes();
|
||||
},
|
||||
onClick: () => { this.doCommand({ name: 'toggleVisiblePanes' }) },
|
||||
});
|
||||
|
||||
if (!this.promptOnClose_) {
|
||||
@@ -343,13 +327,31 @@ class MainScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
const messageComp = messageBoxVisible ? (
|
||||
<div style={styles.messageBox}>
|
||||
<span style={theme.textStyle}>
|
||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
const onViewMasterKeysClick = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'EncryptionConfig',
|
||||
});
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={style}>
|
||||
@@ -383,6 +385,8 @@ const mapStateToProps = (state) => {
|
||||
folders: state.folders,
|
||||
notes: state.notes,
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -3,6 +3,7 @@ const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Menu = bridge().Menu;
|
||||
@@ -53,26 +54,44 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
const currentItemId = event.currentTarget.getAttribute('data-id');
|
||||
if (!currentItemId) return;
|
||||
|
||||
let noteIds = [];
|
||||
if (this.props.selectedNoteIds.indexOf(currentItemId) < 0) {
|
||||
noteIds = [currentItemId];
|
||||
} else {
|
||||
noteIds = this.props.selectedNoteIds;
|
||||
}
|
||||
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id));
|
||||
|
||||
let hasEncrypted = false;
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
if (!!notes[i].encryption_applied) hasEncrypted = true;
|
||||
}
|
||||
|
||||
const menu = new Menu()
|
||||
|
||||
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: noteIds[0],
|
||||
});
|
||||
}}));
|
||||
if (!hasEncrypted) {
|
||||
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: noteIds[0],
|
||||
});
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const note = await Note.load(noteIds[i]);
|
||||
await Note.save(Note.toggleIsTodo(note));
|
||||
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
||||
}
|
||||
}}));
|
||||
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const note = await Note.load(noteIds[i]);
|
||||
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
||||
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
||||
}
|
||||
}}));
|
||||
}
|
||||
|
||||
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
||||
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
|
||||
@@ -120,14 +139,17 @@ class NoteListComponent extends React.Component {
|
||||
id: item.id,
|
||||
todo_completed: checked ? time.unixMs() : 0,
|
||||
}
|
||||
await Note.save(newNote);
|
||||
await Note.save(newNote, { userSideValidation: true });
|
||||
eventManager.emit('todoToggle', { noteId: item.id });
|
||||
}
|
||||
|
||||
const hPadding = 10;
|
||||
|
||||
let style = Object.assign({ width: width }, this.style().listItem);
|
||||
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) style = Object.assign(style, this.style().listItemSelected);
|
||||
|
||||
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) {
|
||||
style = Object.assign(style, this.style().listItemSelected);
|
||||
}
|
||||
|
||||
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
|
||||
// but don't know how it will look in other OSes.
|
||||
@@ -153,8 +175,9 @@ class NoteListComponent extends React.Component {
|
||||
style={listItemTitleStyle}
|
||||
onClick={(event) => { onTitleClick(event, item) }}
|
||||
onDragStart={(event) => onDragStart(event) }
|
||||
data-id={item.id}
|
||||
>
|
||||
{item.title}
|
||||
{Note.displayTitle(item)}
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
@@ -162,8 +185,9 @@ class NoteListComponent extends React.Component {
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
let notes = this.props.notes.slice();
|
||||
|
||||
if (!this.props.notes.length) {
|
||||
if (!notes.length) {
|
||||
const padding = 10;
|
||||
const emptyDivStyle = Object.assign({
|
||||
padding: padding + 'px',
|
||||
@@ -182,7 +206,7 @@ class NoteListComponent extends React.Component {
|
||||
itemHeight={this.style().listItem.height}
|
||||
style={style}
|
||||
className={"note-list"}
|
||||
items={this.props.notes}
|
||||
items={notes}
|
||||
itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } }
|
||||
></ItemList>
|
||||
);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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 { Setting } = require('lib/models/setting.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { IconButton } = require('./IconButton.min.js');
|
||||
const Toolbar = require('./Toolbar.min.js');
|
||||
const { connect } = require('react-redux');
|
||||
@@ -16,6 +16,7 @@ const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const { shim } = require('lib/shim.js');
|
||||
const eventManager = require('../eventManager');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
@@ -37,6 +38,12 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: false,
|
||||
scrollHeight: null,
|
||||
editorScrollTop: 0,
|
||||
newNote: null,
|
||||
|
||||
// If the current note was just created, and the title has never been
|
||||
// changed by the user, this variable contains that note ID. Used
|
||||
// to automatically set the title.
|
||||
newAndNoTitleChangeNoteId: null,
|
||||
};
|
||||
|
||||
this.lastLoadedNoteId_ = null;
|
||||
@@ -75,7 +82,10 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
async componentWillMount() {
|
||||
let note = null;
|
||||
if (this.props.noteId) {
|
||||
|
||||
if (this.props.newNote) {
|
||||
note = Object.assign({}, this.props.newNote);
|
||||
} else if (this.props.noteId) {
|
||||
note = await Note.load(this.props.noteId);
|
||||
}
|
||||
|
||||
@@ -106,15 +116,26 @@ class NoteTextComponent extends React.Component {
|
||||
eventManager.removeListener('todoToggle', this.onTodoToggle_);
|
||||
}
|
||||
|
||||
async saveIfNeeded() {
|
||||
async saveIfNeeded(saveIfNewNote = false) {
|
||||
const forceSave = saveIfNewNote && (this.state.note && !this.state.note.id);
|
||||
|
||||
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
if (!shared.isModified(this)) return;
|
||||
if (!forceSave) {
|
||||
if (!shared.isModified(this)) return;
|
||||
}
|
||||
await shared.saveNoteButton_press(this);
|
||||
}
|
||||
|
||||
async saveOneProperty(name, value) {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
if (this.state.note && !this.state.note.id) {
|
||||
const note = Object.assign({}, this.state.note);
|
||||
note[name] = value;
|
||||
this.setState({ note: note });
|
||||
this.scheduleSave();
|
||||
} else {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
scheduleSave() {
|
||||
@@ -128,17 +149,32 @@ class NoteTextComponent extends React.Component {
|
||||
if (!options) options = {};
|
||||
if (!('noReloadIfLocalChanges' in options)) options.noReloadIfLocalChanges = false;
|
||||
|
||||
const noteId = props.noteId;
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
const note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
|
||||
if (!options.noReloadIfLocalChanges && this.isModified()) return;
|
||||
await this.saveIfNeeded();
|
||||
|
||||
// If the note hasn't been changed, exit now
|
||||
if (this.state.note && note) {
|
||||
let diff = Note.diffObjects(this.state.note, note);
|
||||
delete diff.type_;
|
||||
if (!Object.getOwnPropertyNames(diff).length) return;
|
||||
const previousNote = this.state.note ? Object.assign({}, this.state.note) : null;
|
||||
|
||||
const stateNoteId = this.state.note ? this.state.note.id : null;
|
||||
let noteId = null;
|
||||
let note = null;
|
||||
let loadingNewNote = true;
|
||||
|
||||
if (props.newNote) {
|
||||
note = Object.assign({}, props.newNote);
|
||||
this.lastLoadedNoteId_ = null;
|
||||
} else {
|
||||
noteId = props.noteId;
|
||||
loadingNewNote = stateNoteId !== noteId;
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
|
||||
if (options.noReloadIfLocalChanges && this.isModified()) return;
|
||||
|
||||
// If the note hasn't been changed, exit now
|
||||
if (this.state.note && note) {
|
||||
let diff = Note.diffObjects(this.state.note, note);
|
||||
delete diff.type_;
|
||||
if (!Object.getOwnPropertyNames(diff).length) return;
|
||||
}
|
||||
}
|
||||
|
||||
this.mdToHtml_ = null;
|
||||
@@ -146,26 +182,58 @@ class NoteTextComponent extends React.Component {
|
||||
// If we are loading nothing (noteId == null), make sure to
|
||||
// set webviewReady to false too because the webview component
|
||||
// is going to be removed in render().
|
||||
const webviewReady = this.webview_ && this.state.webviewReady && noteId;
|
||||
const webviewReady = this.webview_ && this.state.webviewReady && (noteId || props.newNote);
|
||||
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
// Scroll back to top when loading new note
|
||||
if (loadingNewNote) {
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
|
||||
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// and then (in the renderer callback) to the value we actually need. The first
|
||||
// operation helps clear the scroll position cache. See:
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
this.editorSetScrollTop(1);
|
||||
this.restoreScrollTop_ = 0;
|
||||
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// and then (in the renderer callback) to the value we actually need. The first
|
||||
// operation helps clear the scroll position cache. See:
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
this.editorSetScrollTop(1);
|
||||
this.restoreScrollTop_ = 0;
|
||||
|
||||
this.setState({
|
||||
if (note) {
|
||||
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
|
||||
|
||||
if (Setting.value(focusSettingName) === 'title') {
|
||||
if (this.titleField_) this.titleField_.focus();
|
||||
} else {
|
||||
if (this.editor_) this.editor_.editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.editor_) {
|
||||
const session = this.editor_.editor.getSession();
|
||||
const undoManager = session.getUndoManager();
|
||||
undoManager.reset();
|
||||
session.setUndoManager(undoManager);
|
||||
this.editor_.editor.clearSelection();
|
||||
this.editor_.editor.moveCursorTo(0,0);
|
||||
}
|
||||
}
|
||||
|
||||
let newState = {
|
||||
note: note,
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
webviewReady: webviewReady,
|
||||
});
|
||||
};
|
||||
|
||||
if (!note) {
|
||||
newState.newAndNoTitleChangeNoteId = null;
|
||||
} else if (note.id !== this.state.newAndNoTitleChangeNoteId) {
|
||||
newState.newAndNoTitleChangeNoteId = null;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
if (nextProps.newNote) {
|
||||
await this.reloadNote(nextProps);
|
||||
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
await this.reloadNote(nextProps);
|
||||
}
|
||||
|
||||
@@ -184,6 +252,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
title_changeText(event) {
|
||||
shared.noteComponent_change(this, 'title', event.target.value);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
@@ -196,7 +265,7 @@ class NoteTextComponent extends React.Component {
|
||||
shared.showMetadata_onPress(this);
|
||||
}
|
||||
|
||||
webview_ipcMessage(event) {
|
||||
async webview_ipcMessage(event) {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
@@ -218,6 +287,32 @@ class NoteTextComponent extends React.Component {
|
||||
} else if (msg === 'percentScroll') {
|
||||
this.ignoreNextEditorScroll_ = true;
|
||||
this.setEditorPercentScroll(arg0);
|
||||
} else if (msg === 'contextMenu') {
|
||||
const itemType = arg0 && arg0.type;
|
||||
|
||||
const menu = new Menu()
|
||||
|
||||
if (itemType === 'image') {
|
||||
const resource = await Resource.load(arg0.resourceId);
|
||||
const resourcePath = Resource.fullPath(resource);
|
||||
|
||||
menu.append(new MenuItem({label: _('Open...'), click: async () => {
|
||||
bridge().openExternal(resourcePath);
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Save as...'), click: async () => {
|
||||
const filePath = bridge().showSaveDialog({
|
||||
defaultPath: resource.filename ? resource.filename : resource.title,
|
||||
});
|
||||
if (!filePath) return;
|
||||
await fs.copy(resourcePath, filePath);
|
||||
}}));
|
||||
} else {
|
||||
reg.logger().error('Unhandled item type: ' + itemType);
|
||||
return;
|
||||
}
|
||||
|
||||
menu.popup(bridge().window());
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceId = msg.substr('joplin://'.length);
|
||||
Resource.load(resourceId).then((resource) => {
|
||||
@@ -334,17 +429,15 @@ class NoteTextComponent extends React.Component {
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
async commandAttachFile() {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
|
||||
async commandAttachFile() {
|
||||
const filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||
});
|
||||
if (!filePaths || !filePaths.length) return;
|
||||
|
||||
await this.saveIfNeeded();
|
||||
let note = await Note.load(noteId);
|
||||
await this.saveIfNeeded(true);
|
||||
let note = await Note.load(this.state.note.id);
|
||||
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
@@ -362,20 +455,29 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
commandSetAlarm() {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
async commandSetAlarm() {
|
||||
await this.saveIfNeeded(true);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'editAlarm',
|
||||
noteId: noteId,
|
||||
noteId: this.state.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
async commandSetTags() {
|
||||
await this.saveIfNeeded(true);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: this.state.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
const note = this.state.note;
|
||||
if (!note) return;
|
||||
|
||||
const menu = new Menu()
|
||||
|
||||
@@ -383,29 +485,26 @@ class NoteTextComponent extends React.Component {
|
||||
return this.commandAttachFile();
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
|
||||
return this.commandSetAlarm();
|
||||
menu.append(new MenuItem({label: _('Tags'), click: async () => {
|
||||
return this.commandSetTags();
|
||||
}}));
|
||||
|
||||
if (!!note.is_todo) {
|
||||
menu.append(new MenuItem({label: _('Set alarm'), click: async () => {
|
||||
return this.commandSetAlarm();
|
||||
}}));
|
||||
}
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
// shouldComponentUpdate(nextProps, nextState) {
|
||||
// //console.info('NEXT PROPS', JSON.stringify(nextProps));
|
||||
// console.info('NEXT STATE ====================');
|
||||
// for (var n in nextProps) {
|
||||
// if (!nextProps.hasOwnProperty(n)) continue;
|
||||
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const note = this.state.note;
|
||||
const body = note ? note.body : '';
|
||||
const body = note && note.body ? note.body : '';
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
|
||||
const isTodo = note && !!note.is_todo;
|
||||
|
||||
const borderWidth = 1;
|
||||
|
||||
@@ -418,7 +517,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
||||
|
||||
if (!note) {
|
||||
if (!note || !!note.encryption_applied) {
|
||||
const emptyDivStyle = Object.assign({
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.1,
|
||||
@@ -501,7 +600,14 @@ class NoteTextComponent extends React.Component {
|
||||
},
|
||||
postMessageSyntax: 'ipcRenderer.sendToHost',
|
||||
};
|
||||
const html = this.mdToHtml().render(body, theme, mdOptions);
|
||||
|
||||
let bodyToRender = body;
|
||||
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = '*' + _('This not has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
|
||||
}
|
||||
|
||||
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
|
||||
this.webview_.send('setHtml', html);
|
||||
}
|
||||
|
||||
@@ -513,6 +619,12 @@ class NoteTextComponent extends React.Component {
|
||||
onClick: () => { return this.commandAttachFile(); },
|
||||
});
|
||||
|
||||
toolbarItems.push({
|
||||
title: _('Tags'),
|
||||
iconName: 'fa-tags',
|
||||
onClick: () => { return this.commandSetTags(); },
|
||||
});
|
||||
|
||||
if (note.is_todo) {
|
||||
toolbarItems.push({
|
||||
title: Note.needAlarm(note) ? time.formatMsToLocal(note.todo_due) : _('Set alarm'),
|
||||
@@ -529,9 +641,11 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
const titleEditor = <input
|
||||
type="text"
|
||||
ref={(elem) => { this.titleField_ = elem; } }
|
||||
style={titleEditorStyle}
|
||||
value={note ? note.title : ''}
|
||||
value={note && note.title ? note.title : ''}
|
||||
onChange={(event) => { this.title_changeText(event); }}
|
||||
placeholder={ this.props.newNote ? _('Creating new %s...', isTodo ? _('to-do') : _('note')) : '' }
|
||||
/>
|
||||
|
||||
const titleBarMenuButton = <IconButton style={{
|
||||
@@ -549,7 +663,6 @@ class NoteTextComponent extends React.Component {
|
||||
delete editorRootStyle.width;
|
||||
delete editorRootStyle.height;
|
||||
delete editorRootStyle.fontSize;
|
||||
|
||||
const editor = <AceEditor
|
||||
value={body}
|
||||
mode="markdown"
|
||||
@@ -599,6 +712,7 @@ const mapStateToProps = (state) => {
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
syncStarted: state.syncStarted,
|
||||
newNote: state.newNote,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -42,17 +42,20 @@ class PromptDialog extends React.Component {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const paddingTop = 20;
|
||||
|
||||
this.styles_.modalLayer = {
|
||||
zIndex: 9999,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
height: height - paddingTop,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
display: visible ? 'flex' : 'none',
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
paddingTop: paddingTop + 'px',
|
||||
};
|
||||
|
||||
this.styles_.promptDialog = {
|
||||
@@ -88,24 +91,6 @@ class PromptDialog extends React.Component {
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
// shouldComponentUpdate(nextProps, nextState) {
|
||||
// console.info(JSON.stringify(nextProps)+JSON.stringify(nextState));
|
||||
|
||||
// console.info('NEXT PROPS ====================');
|
||||
// for (var n in nextProps) {
|
||||
// if (!nextProps.hasOwnProperty(n)) continue;
|
||||
// console.info(n + ' = ' + (nextProps[n] === this.props[n]));
|
||||
// }
|
||||
|
||||
// console.info('NEXT STATE ====================');
|
||||
// for (var n in nextState) {
|
||||
// if (!nextState.hasOwnProperty(n)) continue;
|
||||
// console.info(n + ' = ' + (nextState[n] === this.state[n]));
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
@@ -4,13 +4,14 @@ const { createStore } = require('redux');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
|
||||
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 { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
|
||||
const { app } = require('../app');
|
||||
@@ -77,6 +78,7 @@ class RootComponent extends React.Component {
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||
EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') },
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -2,10 +2,10 @@ const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
@@ -35,6 +35,7 @@ class SideBarComponent extends React.Component {
|
||||
alignItems: 'center',
|
||||
cursor: 'default',
|
||||
opacity: 0.8,
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
listItemSelected: {
|
||||
backgroundColor: theme.selectedColor2,
|
||||
@@ -107,6 +108,11 @@ class SideBarComponent extends React.Component {
|
||||
|
||||
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 () => {
|
||||
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
||||
if (!ok) return;
|
||||
@@ -123,11 +129,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 () => {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'renameNotebook',
|
||||
name: 'renameFolder',
|
||||
id: itemId,
|
||||
});
|
||||
}}))
|
||||
@@ -180,6 +186,8 @@ class SideBarComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const itemTitle = Folder.displayTitle(folder);
|
||||
|
||||
return <a
|
||||
className="list-item"
|
||||
onDragOver={(event) => { onDragOver(event, folder) } }
|
||||
@@ -189,14 +197,14 @@ class SideBarComponent extends React.Component {
|
||||
data-type={BaseModel.TYPE_FOLDER}
|
||||
onContextMenu={(event) => this.itemContextMenu(event)}
|
||||
key={folder.id}
|
||||
style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}
|
||||
style={style} onClick={() => {this.folderItem_click(folder)}}>{itemTitle}
|
||||
</a>
|
||||
}
|
||||
|
||||
tagItem(tag, selected) {
|
||||
let style = Object.assign({}, this.style().listItem);
|
||||
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) {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
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 { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
@@ -70,7 +70,9 @@ class StatusScreenComponent extends React.Component {
|
||||
|
||||
for (let n in section.body) {
|
||||
if (!section.body.hasOwnProperty(n)) continue;
|
||||
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
|
||||
let text = section.body[n];
|
||||
if (!text) text = '\xa0';
|
||||
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{text}</div>);
|
||||
}
|
||||
|
||||
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;
|
@@ -1,179 +1,215 @@
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="hlScriptContainer"></div>
|
||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const contentElement = document.getElementById('content');
|
||||
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
let hljsScriptAdded = false;
|
||||
let hljsLoaded = false;
|
||||
|
||||
function loadHljs(callback) {
|
||||
hljsScriptAdded = true;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.onload = function () {
|
||||
hljsLoaded = true;
|
||||
applyHljs();
|
||||
};
|
||||
script.src = 'highlight/highlight.pack.js';
|
||||
document.getElementById('hlScriptContainer').appendChild(script);
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
link.href = 'highlight/styles/atom-one-light.css';
|
||||
document.getElementById('hlScriptContainer').appendChild(link);
|
||||
}
|
||||
|
||||
function loadAndApplyHljs() {
|
||||
var codeElements = document.getElementsByClassName('code');
|
||||
if (!codeElements.length) return;
|
||||
|
||||
if (!hljsScriptAdded) {
|
||||
this.loadHljs();
|
||||
return;
|
||||
}
|
||||
|
||||
// If HLJS is not loaded yet, no need to do anything. When it loads
|
||||
// it will automatically apply the style to all the code elements.
|
||||
if (hljsLoaded) applyHljs(codeElements);
|
||||
}
|
||||
|
||||
function applyHljs(codeElements) {
|
||||
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
|
||||
|
||||
for (var i = 0; i < codeElements.length; i++) {
|
||||
hljs.highlightBlock(codeElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// / Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
|
||||
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
|
||||
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
|
||||
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
|
||||
// it at any time knowing that it's not going to be changed because the content height has changed.
|
||||
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
|
||||
// one second after the content has been updated.
|
||||
//
|
||||
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
|
||||
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
|
||||
|
||||
let percentScroll_ = 0;
|
||||
let checkScrollIID_ = null;
|
||||
|
||||
function setPercentScroll(percent) {
|
||||
percentScroll_ = percent;
|
||||
contentElement.scrollTop = percentScroll_ * maxScrollTop();
|
||||
}
|
||||
|
||||
function percentScroll() {
|
||||
return percentScroll_;
|
||||
}
|
||||
|
||||
function restorePercentScroll() {
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
contentElement.innerHTML = html;
|
||||
|
||||
loadAndApplyHljs();
|
||||
|
||||
// Remove the bullet from "ul" for checkbox lists and extra padding
|
||||
const checkboxes = document.getElementsByClassName('checkbox');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const cb = checkboxes[i];
|
||||
const ul = cb.parentElement.parentElement;
|
||||
if (!ul) {
|
||||
console.warn('Unexpected layout for checkbox');
|
||||
continue;
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.paddingLeft = 0;
|
||||
|
||||
#content {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
<div id="hlScriptContainer"></div>
|
||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const contentElement = document.getElementById('content');
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
let hljsScriptAdded = false;
|
||||
let hljsLoaded = false;
|
||||
|
||||
function loadHljs(callback) {
|
||||
hljsScriptAdded = true;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.onload = function () {
|
||||
hljsLoaded = true;
|
||||
applyHljs();
|
||||
};
|
||||
script.src = 'highlight/highlight.pack.js';
|
||||
document.getElementById('hlScriptContainer').appendChild(script);
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
link.href = 'highlight/styles/atom-one-light.css';
|
||||
document.getElementById('hlScriptContainer').appendChild(link);
|
||||
}
|
||||
|
||||
let previousContentHeight = contentElement.scrollHeight;
|
||||
let startTime = Date.now();
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
function loadAndApplyHljs() {
|
||||
var codeElements = document.getElementsByClassName('code');
|
||||
if (!codeElements.length) return;
|
||||
|
||||
if (!checkScrollIID_) {
|
||||
checkScrollIID_ = setInterval(() => {
|
||||
const h = contentElement.scrollHeight;
|
||||
if (h !== previousContentHeight) {
|
||||
previousContentHeight = h;
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
if (!hljsScriptAdded) {
|
||||
this.loadHljs();
|
||||
return;
|
||||
}
|
||||
|
||||
// If HLJS is not loaded yet, no need to do anything. When it loads
|
||||
// it will automatically apply the style to all the code elements.
|
||||
if (hljsLoaded) applyHljs(codeElements);
|
||||
}
|
||||
|
||||
function applyHljs(codeElements) {
|
||||
if (typeof codeElements === 'undefined') codeElements = document.getElementsByClassName('code');
|
||||
|
||||
for (var i = 0; i < codeElements.length; i++) {
|
||||
try {
|
||||
hljs.highlightBlock(codeElements[i]);
|
||||
} catch (error) {
|
||||
console.warn('Cannot highlight code', error);
|
||||
}
|
||||
if (Date.now() - startTime >= 1000) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// / Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// Note: the scroll position source of truth is "percentScroll_". This is easier to manage than scrollTop because
|
||||
// the scrollTop value depends on the images being loaded or not. For example, if the scrollTop is saved while
|
||||
// images are being displayed then restored while images are being reloaded, the new scrollTop might be changed
|
||||
// so that it is not greater than contentHeight. On the other hand, with percentScroll it is possible to restore
|
||||
// it at any time knowing that it's not going to be changed because the content height has changed.
|
||||
// To restore percentScroll the "checkScrollIID" interval is used. It constantly resets the scroll position during
|
||||
// one second after the content has been updated.
|
||||
//
|
||||
// ignoreNextScroll is used to differentiate between scroll event from the users and those that are the result
|
||||
// of programmatically changing scrollTop. We only want to respond to events initiated by the user.
|
||||
|
||||
let percentScroll_ = 0;
|
||||
let checkScrollIID_ = null;
|
||||
|
||||
function setPercentScroll(percent) {
|
||||
percentScroll_ = percent;
|
||||
contentElement.scrollTop = percentScroll_ * maxScrollTop();
|
||||
}
|
||||
|
||||
function percentScroll() {
|
||||
return percentScroll_;
|
||||
}
|
||||
|
||||
function restorePercentScroll() {
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
updateBodyHeight();
|
||||
|
||||
contentElement.innerHTML = html;
|
||||
|
||||
loadAndApplyHljs();
|
||||
|
||||
// Remove the bullet from "ul" for checkbox lists and extra padding
|
||||
const checkboxes = document.getElementsByClassName('checkbox');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const cb = checkboxes[i];
|
||||
const ul = cb.parentElement.parentElement;
|
||||
if (!ul) {
|
||||
console.warn('Unexpected layout for checkbox');
|
||||
continue;
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.paddingLeft = 0;
|
||||
}
|
||||
|
||||
let ignoreNextScrollEvent = false;
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
if (checkScrollIID_) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
let previousContentHeight = contentElement.scrollHeight;
|
||||
let startTime = Date.now();
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
|
||||
if (!checkScrollIID_) {
|
||||
checkScrollIID_ = setInterval(() => {
|
||||
const h = contentElement.scrollHeight;
|
||||
if (h !== previousContentHeight) {
|
||||
previousContentHeight = h;
|
||||
ignoreNextScrollEvent = true;
|
||||
restorePercentScroll();
|
||||
}
|
||||
if (Date.now() - startTime >= 1000) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
|
||||
let ignoreNextScrollEvent = false;
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
if (checkScrollIID_) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
}
|
||||
|
||||
ignoreNextScrollEvent = true;
|
||||
setPercentScroll(percent);
|
||||
});
|
||||
|
||||
function maxScrollTop() {
|
||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
}
|
||||
|
||||
ignoreNextScrollEvent = true;
|
||||
setPercentScroll(percent);
|
||||
});
|
||||
|
||||
function maxScrollTop() {
|
||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
}
|
||||
|
||||
contentElement.addEventListener('scroll', function(e) {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
return;
|
||||
// The body element needs to have a fixed height for the content to be scrollable
|
||||
function updateBodyHeight() {
|
||||
document.getElementById('body').style.height = window.innerHeight + 'px';
|
||||
}
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
setPercentScroll(percent);
|
||||
ipcRenderer.sendToHost('percentScroll', percent);
|
||||
});
|
||||
|
||||
// Disable drag and drop otherwise it's possible to drop a URL
|
||||
// on it and it will open in the view as a website.
|
||||
document.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
contentElement.addEventListener('scroll', function(e) {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
return;
|
||||
}
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
setPercentScroll(percent);
|
||||
ipcRenderer.sendToHost('percentScroll', percent);
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', function(event) {
|
||||
const element = event.target;
|
||||
if (element && element.getAttribute('data-resource-id')) {
|
||||
ipcRenderer.sendToHost('contextMenu', {
|
||||
type: element.getAttribute('src') ? 'image' : 'link',
|
||||
resourceId: element.getAttribute('data-resource-id'),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Disable drag and drop otherwise it's possible to drop a URL
|
||||
// on it and it will open in the view as a website.
|
||||
document.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
document.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
updateBodyHeight();
|
||||
});
|
||||
|
||||
updateBodyHeight();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@@ -6,6 +6,23 @@
|
||||
<link rel="stylesheet" href="style.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/smalltalk/css/smalltalk.css">
|
||||
<style>
|
||||
.smalltalk {
|
||||
background-color: rgba(0,0,0,.5);
|
||||
}
|
||||
.smalltalk input {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.smalltalk .page {
|
||||
max-width: 30em;
|
||||
}
|
||||
.ace_editor * {
|
||||
/* Necessary to make sure Russian text is displayed properly */
|
||||
/* https://github.com/laurent22/joplin/issues/155 */
|
||||
font-family: monospace !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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
File diff suppressed because one or more lines are too long
1
ElectronClient/app/locales/eu.json
Normal file
1
ElectronClient/app/locales/eu.json
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user