1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-27 20:29:45 +02:00

Compare commits

...

108 Commits

Author SHA1 Message Date
Laurent Cozic
9a06815db9 Electron release v0.10.58 2018-02-05 17:34:39 +00:00
Laurent Cozic
66947d4954 Fixing appveyor script 2018-02-05 17:34:31 +00:00
Laurent Cozic
3ec22185d5 Electron release v0.10.57 2018-02-05 17:32:54 +00:00
Laurent Cozic
0f05c23e26 Fixing deployment script 2018-02-05 17:32:48 +00:00
Laurent Cozic
74493fece0 Android release v0.10.83 2018-02-05 17:32:00 +00:00
Laurent Cozic
557a96e814 Electron release v0.10.56 2018-02-05 17:27:38 +00:00
Laurent Cozic
4b23b419a4 Electron release v0.10.55 2018-02-05 17:27:09 +00:00
Laurent Cozic
8b7f5b1151 Fix scrolling issue in Electron app 2018-02-04 18:45:52 +00:00
Laurent Cozic
29e9ccf216 Electron: Reverted to older Sharp version to fix Ubuntu issue 2018-02-04 18:31:13 +00:00
Laurent Cozic
2c04f5c8bc Improved Android and Electron release process 2018-02-04 18:05:07 +00:00
Laurent Cozic
5430a747e9 Improved Android and Electron release process 2018-02-04 17:55:22 +00:00
Laurent Cozic
13bc185829 Improved Android and Electron release process 2018-02-04 17:51:42 +00:00
Laurent Cozic
ed87581a8a Improved Android and Electron release process 2018-02-04 17:48:29 +00:00
Laurent Cozic
2645ec96a8 Fixed tool utils 2018-02-04 17:44:32 +00:00
Laurent Cozic
d278d830f0 Improved Android and Electron release process 2018-02-04 17:42:33 +00:00
Laurent Cozic
b4dce0ed46 All: Added Katex support 2018-02-04 17:12:24 +00:00
Laurent Cozic
e8416042d4 Merge branch 'master' into math-support 2018-02-02 20:35:32 +00:00
Laurent Cozic
70adbe5e76 Added flags 2018-02-01 20:21:54 +00:00
Laurent Cozic
f66be08d1d Added list of translated languages to README file 2018-02-01 20:15:31 +00:00
Laurent Cozic
fad96f5266 All: Added section to list missing master keys 2018-02-01 19:01:20 +00:00
Laurent Cozic
c33a7f5f47 Updated French translation 2018-02-02 00:08:37 +00:00
Laurent Cozic
28afbcde02 Updated translations 2018-02-02 00:02:47 +00:00
Laurent Cozic
691292d2b3 Merge branch 'master' of github.com:laurent22/joplin 2018-02-01 23:40:15 +00:00
Laurent Cozic
30ff81064f All: Made WebDAV driver more generic to support services other than Nextcloud and added WebDAV sync target 2018-02-01 23:40:05 +00:00
Laurent Cozic
f9f398ad98 Merge pull request #194 from rtmkrlv/russian-locale
Update Russian translation
2018-02-01 15:26:15 +01:00
rtmkrlv
537884bdcd Merge branch 'master' into russian-locale 2018-02-01 15:50:39 +02:00
rtmkrlv
d54400a7cb Updated Russian translation 2018-02-01 15:43:26 +02:00
Laurent Cozic
42c78264fb Update website 2018-01-31 20:21:38 +00:00
Laurent Cozic
c52da82447 Electron: Fix: Don't allow adding notes and to-do to conflict notebook 2018-01-31 20:19:11 +00:00
Laurent Cozic
cca43624e4 Electron: Added tray icon support 2018-01-31 20:10:32 +00:00
Laurent Cozic
dac1cd7668 Mobile: Allow filtering log by warning/error 2018-01-31 19:51:29 +00:00
Laurent Cozic
b4c00db0e3 Electron release v0.10.54 2018-01-31 19:40:40 +00:00
Laurent Cozic
3ce393a8b2 Electron release v0.10.53 2018-01-31 19:34:47 +00:00
Laurent Cozic
2b627fe4ab Fixed auto-update check 2018-01-31 19:34:38 +00:00
Laurent Cozic
fcf8a1649d CLI v0.10.92 2018-01-31 19:28:31 +00:00
Laurent Cozic
8d3b050831 Update website 2018-01-31 19:26:21 +00:00
Laurent Cozic
43297ef0a3 Updated translations 2018-01-31 19:14:32 +00:00
Laurent Cozic
551fabdfc9 Tweak and error handling on auto-update 2018-01-31 19:10:45 +00:00
Laurent Cozic
d6de56b2db All: Fixed crash when having invalid UTF-8 string in text editor 2018-01-31 19:01:11 +00:00
Laurent Cozic
9e979804f3 Electron release v0.10.52 2018-01-31 17:53:18 +00:00
Laurent Cozic
b8e0f182cc Android release v0.10.81 2018-01-31 17:51:22 +00:00
Laurent Cozic
9a41b9e192 Electron: Improved auto-update process to avoid random crashes 2018-01-30 22:35:50 +00:00
Laurent Cozic
9b8f520b9f Electron: Allow focusing either title or body when creating a new note or to-odo 2018-01-30 21:49:22 +00:00
Laurent Cozic
5b6019805c Electron: Fixed auto-title when title is manually entered first 2018-01-30 21:36:54 +00:00
Laurent Cozic
a4106436c4 Fixed delta function when processing many items 2018-01-30 21:10:54 +00:00
Laurent Cozic
f6b4eb511e Add Content-Size header for WebDAV, which is required by some services 2018-01-30 20:24:09 +00:00
Laurent Cozic
eb67ac17a0 Allow decryption to continue even if an item cannot be decrypted 2018-01-30 20:15:05 +00:00
Laurent Cozic
7b760d03ef All: Handle case where file is left half-uploaded on Nextcloud instance (possibly an ocloud.de issue only) 2018-01-30 20:10:36 +00:00
Laurent Cozic
2805ae2acf Fixed crash when calling fetch() with invalid URL in RN app 2018-01-30 19:01:07 +00:00
Laurent Cozic
5cb5ccc781 All: Optimised Nextcloud sync delta functionality 2018-01-29 20:51:14 +00:00
Laurent Cozic
0dba2821b6 Merge pull request #189 from strobeltobias/patch-2
Update German translation
2018-01-29 18:59:20 +00:00
Tobias Strobel
1db7825b22 Update German translation
Grammar and wording fixes, translating new strings.
2018-01-29 14:24:50 +01:00
Laurent Cozic
8a92d6ad70 Update website 2018-01-28 18:47:22 +00:00
Laurent Cozic
138ad9fcad Mobile: Fixes #114: Update geolocation in metadata after it has been set 2018-01-28 18:42:43 +00:00
Laurent Cozic
08cb518c25 Check if current folder exists 2018-01-28 18:19:56 +00:00
Laurent Cozic
6d04eab200 Merge pull request #185 from pf-siedler/autocomp_file_and_item
Autocompletion for files and items
2018-01-28 18:17:01 +00:00
Laurent Cozic
8a8cb51e1b Electron release v0.10.51 2018-01-28 18:07:08 +00:00
Laurent Cozic
5c66042a2d CLI v0.10.91 2018-01-28 17:59:34 +00:00
Laurent Cozic
ae75181b02 Electron release v0.10.50 2018-01-28 17:52:15 +00:00
Laurent Cozic
9dc3238182 Added beta notice 2018-01-28 17:51:38 +00:00
Laurent Cozic
0a68749373 Android release v0.10.79 2018-01-28 17:45:54 +00:00
Laurent Cozic
1519116291 Updated French translation 2018-01-28 17:43:21 +00:00
Laurent Cozic
d023d841e2 Update translations 2018-01-28 17:38:30 +00:00
Laurent Cozic
d7a1465d8e Skip sync report events in log 2018-01-28 17:38:17 +00:00
Laurent Cozic
15848fc696 Closing a resource is async 2018-01-28 17:37:51 +00:00
Laurent Cozic
837ae2c9f2 Send only one NOT_LOADED event per master key 2018-01-28 17:37:29 +00:00
Laurent Cozic
6789b98ead Return fileNotFound error when file cannot be opened so that it is skipped by synchroniser 2018-01-28 17:37:03 +00:00
Laurent Cozic
29f6e74ee3 Convert fs errors to normal errors 2018-01-28 17:36:36 +00:00
Laurent Cozic
2780c38c45 Fixed WebDAV error handling 2018-01-28 17:36:11 +00:00
Laurent Cozic
4531838217 Display stars for secure config value 2018-01-28 17:35:20 +00:00
pf-siedler
7bccf7f65d Implement auto completaion for <item> usage 2018-01-28 23:12:54 +09:00
pf-siedler
c62a24a9cb Implement auto completion for <file> usage
Suggest file names from current directory.
2018-01-28 22:59:58 +09:00
Laurent Cozic
c6830499f7 Fixed Travis 2018-01-28 11:48:29 +00:00
Laurent Cozic
d9f00a2539 Electron release v0.10.49 2018-01-26 17:21:34 +00:00
Laurent Cozic
def83c9119 Merge branch 'master' of github.com:laurent22/joplin 2018-01-26 17:19:58 +00:00
Laurent Cozic
b6cb0056c7 CLI v0.10.90 2018-01-26 17:19:42 +00:00
Laurent Cozic
1669b5258a Fixed detection of encrypted item 2018-01-25 23:01:18 +00:00
Laurent Cozic
5a9e0bfc26 Handle password text input in mobile and desktop 2018-01-25 22:44:09 +00:00
Laurent Cozic
8f3fdb3afe Tweaks to make sure Nextcloud driver passes all test units 2018-01-25 21:15:58 +00:00
Laurent Cozic
7ab135c099 Various tweaks to get Nextcloud working in mobile 2018-01-25 20:48:01 +00:00
Laurent Cozic
1cc27f2509 Got Nextcloud sync to work in Electron 2018-01-25 19:01:14 +00:00
Laurent Cozic
ef700b421c Update CONTRIBUTING.md 2018-01-25 13:35:04 +00:00
Laurent Cozic
b9af5ac052 Update issue_template.md 2018-01-25 13:34:07 +00:00
Laurent Cozic
173f2d421d Update PULL_REQUEST_TEMPLATE 2018-01-25 13:32:19 +00:00
Laurent Cozic
9f82c069c9 Update CONTRIBUTING.md 2018-01-25 13:31:17 +00:00
Laurent Cozic
6ade09c228 Merge branch 'master' into webdav 2018-01-24 17:25:34 +00:00
Laurent Cozic
5393a1399c Finished WebDAV driver 2018-01-23 20:10:20 +00:00
Laurent Cozic
fd29f20b2e Electron: Fix checkbox issue in config screen 2018-01-23 18:31:49 +00:00
Laurent Cozic
c011b53d1f Electron: Upgraded Electron to 1.7.11 to fix security vulnerability 2018-01-23 18:10:30 +00:00
Laurent Cozic
26e3a7b68c Merge branch 'master' of github.com:laurent22/joplin 2018-01-23 17:53:19 +00:00
Laurent Cozic
e70a291698 Merge pull request #174 from gabcoh/fix171
Fix #171
2018-01-23 11:26:11 +00:00
Laurent Cozic
511bd57726 Merge pull request #175 from alexdevero/add-font-size-settings
Add font size settings
2018-01-23 11:25:47 +00:00
Laurent Cozic
c6de8598dc Electron release v0.10.48 2018-01-22 19:10:29 +00:00
Laurent Cozic
7bee25599d Removed uneeded code 2018-01-22 19:06:50 +00:00
Laurent Cozic
773a1ad829 Travis: only build tags 2018-01-21 20:03:40 +00:00
Laurent Cozic
1a1e264fa4 All: Refactored so that memory and file sync target use same delta logic 2018-01-21 19:45:32 +00:00
Laurent Cozic
5b99ecefca Merge branch 'master' into webdav 2018-01-21 19:10:39 +00:00
Laurent Cozic
1bfeed377a All: Optimised file sync logic so that it doesn't fetch the content of
all the items on each sync. Also limit the number of items in a batch
to 1000
2018-01-21 18:54:47 +00:00
Laurent Cozic
86eee376bb All: Handle case where resource blob is missing during sync 2018-01-21 17:48:50 +00:00
Laurent Cozic
6a7d368184 All: Started Nextcloud support 2018-01-21 17:01:37 +00:00
Alex Devero
1da19ae98d Fix indentation 2018-01-19 14:11:40 +01:00
Alex Devero
f52c117b09 Add font size settings 2018-01-19 13:27:44 +01:00
Laurent Cozic
2551f96149 Fixed Readme 2018-01-18 22:43:37 +00:00
Laurent Cozic
c984c19fee Android release v0.10.78 2018-01-18 22:35:05 +00:00
Laurent Cozic
ac8e91e82e Started FAQ 2018-01-18 22:34:27 +00:00
Gabe Cohen
af50d80541 Fix #171 2018-01-18 14:29:13 -06:00
Laurent Cozic
e355f4e49b Fixed license 2018-01-18 20:14:05 +00:00
Laurent Cozic
53da63e371 Trying to add math support 2018-01-11 19:51:01 +00:00
125 changed files with 4461 additions and 999 deletions

5
.gitignore vendored
View File

@@ -37,4 +37,7 @@ _mydocs
Assets/DownloadBadges*.psd
node_modules
Tools/github_oauth_token.txt
_releases
_releases
ReactNativeClient/lib/csstojs/
ElectronClient/app/gui/note-viewer/fonts/
Tools/commit_hook.txt

View File

@@ -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 --delete ../../ReactNativeClient/lib/ lib/
cd Tools
npm install
yarn dist
cd ../ElectronClient/app
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
npm install && yarn dist

View File

@@ -1,6 +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.
# Style
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.

View File

@@ -4,6 +4,7 @@ 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);

View File

@@ -23,8 +23,11 @@ class Command extends BaseCommand {
const verbose = args.options.verbose;
const renderKeyValue = (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 {

View File

@@ -131,7 +131,6 @@ class Command extends BaseCommand {
} else if (stat.isDirectory()) {
continue;
} else {
itemCount++;
const content = await fs.readFile(fullPath, 'utf8');
const item = await BaseItem.unserialize(content);
const ItemClass = BaseItem.itemClass(item);
@@ -141,6 +140,8 @@ class Command extends BaseCommand {
continue;
}
itemCount++;
const isEncrypted = await EncryptionService.instance().itemIsEncrypted(item);
if (isEncrypted) {

View File

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

View File

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

View File

@@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Samuel Blickle <blickle.samuel@gmail.com>\n"
"Last-Translator: Tobias Strobel <git@strobeltobias.de>\n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.5\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Give focus to next pane"
@@ -50,7 +50,7 @@ msgstr ""
"soll."
msgid "Set a to-do as completed / not completed"
msgstr "Ein To-Do as abgeschlossen / nicht abgeschlossen markieren"
msgstr "Ein To-Do als abgeschlossen / nicht abgeschlossen markieren"
#, fuzzy
msgid "[t]oggle [c]onsole between maximized/minimized/hidden/visible."
@@ -117,7 +117,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Der Befehl \"%s\" ist nur im GUI Modus verfügbar"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Kann verschlüsseltes Objekt nicht ändern"
#, javascript-format
msgid "Missing required argument: %s"
@@ -187,31 +187,34 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Verwaltet die E2EE-Konfiguration. Die Befehle sind `enable`, `disable`, "
"`decrypt`, `status` und `target-status`."
msgid "Enter master password:"
msgstr ""
msgstr "Master-Passwort eingeben:"
msgid "Operation cancelled"
msgstr ""
msgstr "Vorgang abgebrochen"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Entschlüsselung starten.... Warte bitte, da es einige Minuten dauern kann, "
"je nachdem, wie viel es zu entschlüsseln gibt."
msgid "Completed decryption."
msgstr ""
msgstr "Entschlüsselung abgeschlossen."
#, fuzzy
msgid "Enabled"
msgstr "Deaktiviert"
msgstr "Aktiviert"
msgid "Disabled"
msgstr "Deaktiviert"
#, javascript-format
msgid "Encryption is: %s"
msgstr ""
msgstr "Die Verschlüsselung ist: %s"
msgid "Edit note."
msgstr "Notiz bearbeiten."
@@ -234,6 +237,10 @@ msgstr ""
"Beginne die Notiz zu bearbeiten. Schließe das Textverarbeitungsprogramm, um "
"zurück zum Terminal zu gelangen."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Fehler beim Öffnen der Notiz im Editor: %s"
msgid "Note has been saved."
msgstr "Die Notiz wurde gespeichert."
@@ -263,13 +270,12 @@ msgstr "Zeigt die Nutzungsstatistik an."
msgid "Shortcuts are not available in CLI mode."
msgstr "Tastenkürzel sind im CLI Modus nicht verfügbar."
#, fuzzy
msgid ""
"Type `help [command]` for more information about a command; or type `help "
"all` for the complete usage information."
msgstr ""
"Tippe `help [Befehl]` ein, um mehr Informationen über einen Befehl zu "
"erhalten."
"Tippe `help [Befehl]` für weitere Informationen über einen Befehl; oder "
"tippe `help all` für die vollständigen Informationen zur Befehlsverwendung."
msgid "The possible commands are:"
msgstr "Mögliche Befehle sind:"
@@ -470,8 +476,19 @@ msgstr ""
"Mit dem angegebenen Ziel synchronisieren (voreingestellt auf den sync.target "
"Optionswert)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
"Keine Authentifizierung mit %s. Gib bitte alle fehlenden Zugangsdaten an."
msgid "Synchronisation is already in progress."
msgstr "Synchronisation ist bereits im Gange."
msgstr "Synchronisation wird bereits ausgeführt."
#, javascript-format
msgid ""
@@ -483,12 +500,6 @@ msgstr ""
"Synchronisation im Gange ist, kannst du die Sperrdatei \"%s\" löschen und "
"fortfahren."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
"Authentifizierung wurde nicht abgeschlossen (keinen Authentifizierung-Token "
"erhalten)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisationsziel: %s (%s)"
@@ -500,9 +511,8 @@ msgid "Starting synchronisation..."
msgstr "Starte Synchronisation..."
msgid "Cancelling... Please wait."
msgstr "Breche ab... Bitte warten."
msgstr "Abbrechen... Bitte warten."
#, fuzzy
msgid ""
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
"[tag] from [note], or to list the notes associated with [tag]. The command "
@@ -511,7 +521,7 @@ msgstr ""
"<tag-command> kann \"add\", \"remove\" or \"list\" sein, um eine "
"[Markierung] zu [Notiz] zuzuweisen oder zu entfernen, oder um mit "
"[Markierung] markierte Notizen anzuzeigen. Mit dem Befehl `tag list` können "
"alle Notizen angezeigt werden."
"alle Markierungen angezeigt werden."
#, javascript-format
msgid "Invalid command: \"%s\""
@@ -546,7 +556,7 @@ msgid "%s %s (%s)"
msgstr "%s %s (%s)"
msgid "Enum"
msgstr ""
msgstr "Aufzählung"
#, javascript-format
msgid "Type: %s."
@@ -612,6 +622,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Ein oder mehrere Objekte sind derzeit verschlüsselt und es kann erforderlich "
"sein, ein Master-Passwort zu hinterlegen. Gib dazu bitte `e2ee decrypt` ein. "
"Wenn du das Passwort bereits eingegeben hast, werden die verschlüsselten "
"Objekte im Hintergrund entschlüsselt und stehen in Kürze zur Verfügung."
msgid "File"
msgstr "Datei"
@@ -656,11 +670,10 @@ msgid "Synchronisation status"
msgstr "Status der Synchronisation"
msgid "Encryption options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
#, fuzzy
msgid "General Options"
msgstr "Optionen"
msgstr "Allgemeine Einstellungen"
msgid "Help"
msgstr "Hilfe"
@@ -668,6 +681,9 @@ msgstr "Hilfe"
msgid "Website and documentation"
msgstr "Webseite und Dokumentation"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Über Joplin"
@@ -675,12 +691,46 @@ msgstr "Über Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Auf %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Abbrechen"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notizen und Einstellungen gespeichert in: %s"
@@ -693,6 +743,9 @@ msgid ""
"re-synchronised and sent unencrypted to the sync target. Do you wish to "
"continue?"
msgstr ""
"Durch die Deaktivierung der Verschlüsselung werden *alle* Notizen und "
"Anhänge neu synchronisiert und unverschlüsselt an das Synchronisierungsziel "
"gesendet. Möchtest du fortfahren?"
msgid ""
"Enabling encryption means *all* your notes and attachments are going to be "
@@ -700,15 +753,20 @@ msgid ""
"password as, for security purposes, this will be the *only* way to decrypt "
"the data! To enable encryption, please enter your password below."
msgstr ""
"Durch das Aktivieren der Verschlüsselung werden alle Notizen und Anhänge neu "
"synchronisiert und verschlüsselt an das Synchronisationsziel gesendet. Achte "
"darauf, dass du das Passwort nicht verlierst, da dies aus Sicherheitsgründen "
"die einzige Möglichkeit ist, deine Daten zu entschlüsseln! Um die "
"Verschlüsselung zu aktivieren, gib bitte unten dein Passwort ein."
msgid "Disable encryption"
msgstr ""
msgstr "Verschlüsselung deaktivieren"
msgid "Enable encryption"
msgstr ""
msgstr "Verschlüsselung aktivieren"
msgid "Master Keys"
msgstr ""
msgstr "Hauptschlüssel"
msgid "Active"
msgstr "Aktiv"
@@ -741,11 +799,21 @@ msgstr ""
"verwendet werden, abhängig davon, wie die jeweiligen Notizen oder "
"Notizbücher ursprünglich verschlüsselt wurden."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hauptschlüssel"
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 ""
msgstr "Die Verschlüsselung ist:"
msgid "Back"
msgstr "Zurück"
@@ -787,12 +855,11 @@ msgstr "Manche Objekte können nicht synchronisiert werden."
msgid "View them now"
msgstr "Zeige sie jetzt an"
#, fuzzy
msgid "Some items cannot be decrypted."
msgstr "Kann Synchronisierer nicht initialisieren."
msgstr "Einige Objekte können nicht entschlüsselt werden."
msgid "Set the password"
msgstr ""
msgstr "Setze ein Passwort"
msgid "Add or remove tags"
msgstr "Markierungen hinzufügen oder entfernen"
@@ -831,7 +898,7 @@ msgid "Refresh"
msgstr "Aktualisieren"
msgid "Clear"
msgstr ""
msgstr "Leeren"
msgid "OneDrive Login"
msgstr "OneDrive Login"
@@ -846,7 +913,7 @@ msgid "Synchronisation Status"
msgstr "Synchronisations Status"
msgid "Encryption Options"
msgstr ""
msgstr "Verschlüsselungsoptionen"
msgid "Remove this tag from all the notes?"
msgstr "Diese Markierung von allen Notizen entfernen?"
@@ -884,12 +951,18 @@ msgstr "Unbekanntes Argument: %s"
msgid "File system"
msgstr "Dateisystem"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Nur für Tests)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Unbekanntes Log Level: %s"
@@ -948,16 +1021,16 @@ msgstr "Lokale Objekte gelöscht: %d."
msgid "Deleted remote items: %d."
msgstr "Remote Objekte gelöscht: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Lokale Objekte erstellt: %d."
msgstr "Geladene Objekte: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
msgstr "Status: \"%s\"."
msgid "Cancelling..."
msgstr "Breche ab..."
msgstr "Abbrechen..."
#, javascript-format
msgid "Completed: %s"
@@ -968,11 +1041,10 @@ msgid "Synchronisation is already in progress. State: %s"
msgstr "Synchronisation ist bereits im Gange. Status: %s"
msgid "Encrypted"
msgstr ""
msgstr "Verschlüsselt"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Manche Objekte können nicht synchronisiert werden."
msgstr "Verschlüsselte Objekte können nicht verändert werden."
msgid "Conflicts"
msgstr "Konflikte"
@@ -1035,6 +1107,30 @@ msgstr "Zeige unvollständige To-Dos oben in der Liste"
msgid "Save geo-location with notes"
msgstr "Momentanen Standort zusammen mit Notizen speichern"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Erstellt ein neues To-Do."
#, fuzzy
msgid "Focus title"
msgstr "Notiz Titel:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Erstellt eine neue Notiz."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr "Einstellen des Anwendungszooms"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Synchronisation interval"
msgstr "Synchronisationsinterval"
@@ -1050,9 +1146,6 @@ msgstr "%d Stunde"
msgid "%d hours"
msgstr "%d Stunden"
msgid "Automatically update the application"
msgstr "Die Applikation automatisch aktualisieren"
msgid "Show advanced options"
msgstr "Erweiterte Optionen anzeigen"
@@ -1060,12 +1153,12 @@ msgid "Synchronisation target"
msgstr "Synchronisationsziel"
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 ""
"Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit dem "
"Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2.path`, um "
"den Zielpfad zu spezifizieren."
"Das Ziel, mit dem synchronisiert werden soll. Jedes Synchronisationsziel "
"kann zusätzliche Parameter haben, die als `sync.NUM.NAME` (alle unten "
"dokumentiert) bezeichnet werden."
msgid "Directory to synchronise with (absolute path)"
msgstr "Verzeichnis zum synchronisieren (absoluter Pfad)"
@@ -1074,8 +1167,29 @@ msgid ""
"The path to synchronise with when file system synchronisation is enabled. "
"See `sync.target`."
msgstr ""
"Der Pfad, mit dem synchronisiert wird, wenn Dateisystem-Synchronisation "
"aktiviert ist. Siehe `sync.target`."
"Der Pfad, mit dem synchronisiert werden soll, wenn die Dateisystem-"
"Synchronisation aktiviert ist. Siehe `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Nexcloud Benutzername"
msgid "Nexcloud password"
msgstr "Nexcloud Passwort"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Nexcloud Benutzername"
#, fuzzy
msgid "WebDAV password"
msgstr "Setze ein Passwort"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
@@ -1084,15 +1198,18 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
msgid "Items that cannot be synchronised"
msgstr "Objekte können nicht synchronisiert werden"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%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 ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)"
msgstr "Synchronisationsstatus (synchronisierte Objekte / gesamte Objekte)"
@@ -1136,13 +1253,13 @@ msgid "Delete these notes?"
msgstr "Sollen diese Notizen gelöscht werden?"
msgid "Log"
msgstr "Log"
msgstr "Protokoll"
msgid "Export Debug Report"
msgstr "Fehlerbreicht exportieren"
msgstr "Fehlerbericht exportieren"
msgid "Encryption Config"
msgstr ""
msgstr "Verschlüsselungskonfiguration"
msgid "Configuration"
msgstr "Konfiguration"
@@ -1155,7 +1272,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "%d Notizen in das Notizbuch \"%s\" verschieben?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Tippe hier, um das Entschlüsselungspasswort festzulegen."
msgid "Select date"
msgstr "Datum auswählen"
@@ -1168,22 +1285,20 @@ msgstr "Synchronisation abbrechen"
#, javascript-format
msgid "Master Key %s"
msgstr ""
msgstr "Hauptschlüssel %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Erstellt: %d."
msgstr "Erstellt: %s"
#, fuzzy
msgid "Password:"
msgstr "Passwort"
msgstr "Passwort:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Passwort darf nicht leer sein"
#, fuzzy
msgid "Enable"
msgstr "Deaktiviert"
msgstr "Aktivieren"
#, javascript-format
msgid "The notebook could not be saved: %s"
@@ -1192,6 +1307,12 @@ msgstr "Dieses Notizbuch konnte nicht gespeichert werden: %s"
msgid "Edit notebook"
msgstr "Notizbuch bearbeiten"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Diese Notiz wurde verändert:"
@@ -1248,8 +1369,13 @@ msgstr ""
msgid "Welcome"
msgstr "Willkommen"
#~ msgid "Note title:"
#~ msgstr "Notizen Titel:"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Das Synchronisationsziel, mit dem synchronisiert werden soll. Wenn mit "
#~ "dem Dateisystem synchronisiert werden soll, setze den Wert zu `sync.2."
#~ "path`, um den Zielpfad zu spezifizieren."
#~ msgid "To-do title:"
#~ msgstr "To-Do Titel:"

View File

@@ -214,6 +214,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 ""
@@ -413,6 +417,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 ""
@@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -581,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -588,12 +599,45 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -650,6 +694,15 @@ 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 ""
@@ -785,12 +838,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -923,6 +982,27 @@ 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 ""
@@ -938,9 +1018,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +1025,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)"
@@ -960,6 +1037,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 ""
@@ -1071,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -224,6 +224,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -449,6 +453,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -462,10 +474,6 @@ msgstr ""
"curso, puedes eliminar el archivo de bloqueo en \"%s\" y reanudar la "
"operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -639,6 +647,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentacion"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -646,12 +657,46 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "Ok"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -710,6 +755,15 @@ 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 "Estatus"
@@ -854,6 +908,9 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
@@ -861,6 +918,9 @@ msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev(Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -1009,6 +1069,30 @@ msgstr "Mostrar lista de tareas incompletas al inio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar notas con geo-licalización"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -1024,22 +1108,16 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizacion automatica de la aplicación"
msgid "Show advanced options"
msgstr "Mostrar opciones "
msgid "Synchronisation target"
msgstr "Sincronización de objetivo"
#, fuzzy
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 ""
"El objetivo para sincronizarse a. Si sincronizando con el sistema de "
"archivos, establecer `sync.2.path` especifique el directorio destino."
msgid "Directory to synchronise with (absolute path)"
msgstr ""
@@ -1051,6 +1129,24 @@ msgstr ""
"La ubicacion para sincronizar cuando el sistema de archivo tenga habilitada "
"la sincronización. Ver `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 inválido de opción: \"%s\". Los válores inválidos son: %s."
@@ -1168,6 +1264,12 @@ msgstr "Esta libreta no pudo ser guardada: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1225,8 +1327,13 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid "Note title:"
#~ msgstr "Título de nota:"
#, fuzzy
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El objetivo para sincronizarse a. Si sincronizando con el sistema de "
#~ "archivos, establecer `sync.2.path` especifique el directorio destino."
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

View File

@@ -225,6 +225,10 @@ msgstr "La nota no existe: \"%s\". Crearla?"
msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr "Iniciando a editar una nota. Cierra el editor para regresar al prompt."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "La nota a sido guardada."
@@ -458,6 +462,14 @@ msgstr ""
"Sincronizar con objetivo proveído ( por defecto al valor de configuración "
"sync.target)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Sincronzación en progreso."
@@ -471,10 +483,6 @@ msgstr ""
"sincronización en curso puede eliminar el archivo de bloqueo «%s» y reanudar "
"la operación."
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Autenticación no completada (no se recibió token de autenticación)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Objetivo de sincronización: %s (%s)"
@@ -653,6 +661,9 @@ msgstr "Ayuda"
msgid "Website and documentation"
msgstr "Sitio web y documentación"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Acerca de Joplin"
@@ -660,12 +671,46 @@ msgstr "Acerca de Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "En %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Cancelar"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Las notas y los ajustes se guardan en: %s"
@@ -722,6 +767,15 @@ 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 "Estado"
@@ -858,12 +912,18 @@ msgstr "Etiqueta desconocida: %s"
msgid "File system"
msgstr "Sistema de archivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Solo para pruebas)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nivel de log desconocido: %s"
@@ -1008,6 +1068,30 @@ msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Save geo-location with notes"
msgstr "Guardar geolocalización en las notas"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Crea una nueva lista de tareas."
#, fuzzy
msgid "Focus title"
msgstr "Título de la nota:"
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Crea una nueva nota."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Synchronisation interval"
msgstr "Intervalo de sincronización"
@@ -1023,9 +1107,6 @@ msgstr "%d hora"
msgid "%d hours"
msgstr "%d horas"
msgid "Automatically update the application"
msgstr "Actualizar la aplicación automáticamente"
msgid "Show advanced options"
msgstr "Mostrar opciones avanzadas"
@@ -1033,11 +1114,9 @@ msgid "Synchronisation target"
msgstr "Destino de sincronización"
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 ""
"El destino de la sincronización. Si se sincroniza con el sistema de "
"archivos, indique el directorio destino en «sync.2.path»."
msgid "Directory to synchronise with (absolute path)"
msgstr "Directorio con el que sincronizarse (ruta completa)"
@@ -1049,6 +1128,24 @@ msgstr ""
"La ruta a la que sincronizar cuando se activa la sincronización con sistema "
"de archivos. Vea «sync.target»."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 "Opción inválida: «%s». Los valores posibles son: %s."
@@ -1161,6 +1258,12 @@ msgstr "No se ha podido guardar esta libreta: %s"
msgid "Edit notebook"
msgstr "Editar libreta"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Esta nota ha sido modificada:"
@@ -1215,8 +1318,12 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenido"
#~ msgid "Note title:"
#~ msgstr "Título de la nota:"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "El destino de la sincronización. Si se sincroniza con el sistema de "
#~ "archivos, indique el directorio destino en «sync.2.path»."
#~ msgid "To-do title:"
#~ msgstr "Títuto de lista de tareas:"

View File

@@ -7,13 +7,15 @@ 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"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.3\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
msgid "Give focus to next pane"
msgstr "Activer le volet suivant"
@@ -228,6 +230,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."
@@ -458,6 +464,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."
@@ -471,10 +487,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)"
@@ -654,6 +666,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"
@@ -661,11 +676,47 @@ 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"
msgid "Error"
msgstr "Erreur"
msgid "An update is available, do you want to update now?"
msgstr ""
"Une mise à jour est disponible, souhaitez vous la télécharger maintenant ?"
msgid "Yes"
msgstr "Oui"
msgid "No"
msgstr "Non"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Impossible de télécharger la mise à jour : %s"
msgid "Current version is up-to-date."
msgstr "La version actuelle est à jour."
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..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Impossible d'installer la mise à jour : %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
@@ -735,6 +786,18 @@ msgstr ""
"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"
@@ -876,12 +939,18 @@ msgstr "Paramètre inconnu : %s"
msgid "File system"
msgstr "Système de fichier"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dév (Pour tester uniquement)"
msgid "WebDAV (Beta)"
msgstr "WebDAV (Bêta)"
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Paramètre inconnu : %s"
@@ -1024,6 +1093,27 @@ 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"
@@ -1039,9 +1129,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"
@@ -1049,11 +1136,12 @@ 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 "Répertoire avec lequel synchroniser (chemin absolu)"
@@ -1065,6 +1153,24 @@ msgstr ""
"Le chemin du répertoire avec lequel synchroniser lorsque la synchronisation "
"par système de fichier est activée. Voir `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nextcloud : URL WebDAV"
msgid "Nexcloud username"
msgstr "Nextcloud : Nom utilisateur"
msgid "Nexcloud 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."
@@ -1139,7 +1245,7 @@ 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\"?"
@@ -1181,6 +1287,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 :"
@@ -1236,8 +1348,12 @@ msgstr ""
msgid "Welcome"
msgstr "Bienvenue"
#~ msgid "Note title:"
#~ msgstr "Titre de la note :"
#~ 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 :"
@@ -1303,9 +1419,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"

View File

@@ -235,6 +235,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."
@@ -466,6 +470,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."
@@ -477,12 +491,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)"
@@ -661,6 +669,9 @@ msgstr "Pomoć"
msgid "Website and documentation"
msgstr "Website i dokumentacija"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "O Joplinu"
@@ -668,12 +679,46 @@ 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"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Bilješke i postavke su pohranjene u: %s"
@@ -730,6 +775,15 @@ 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"
@@ -868,12 +922,18 @@ msgstr "Nepoznata zastavica: %s"
msgid "File system"
msgstr "Datotečni sustav"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Samo za testiranje)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nepoznata razina logiranja: %s"
@@ -1015,6 +1075,30 @@ 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"
@@ -1030,9 +1114,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"
@@ -1040,11 +1121,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)"
@@ -1056,6 +1135,24 @@ msgstr ""
"Putanja do direktorija za sinkronizaciju u slučaju kad je sinkronizacija sa "
"datotečnim sustavom omogućena. Vidi `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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."
@@ -1168,6 +1265,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:"
@@ -1221,8 +1324,12 @@ msgstr "Trenutno nemaš nijednu bilježnicu. Stvori novu klikom na (+) gumb."
msgid "Welcome"
msgstr "Dobro došli"
#~ msgid "Note title:"
#~ msgstr "Naslov bilješke:"
#~ msgid ""
#~ "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:"

View File

@@ -226,6 +226,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."
@@ -449,6 +453,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."
@@ -462,12 +476,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)"
@@ -639,6 +647,9 @@ msgstr "Aiuto"
msgid "Website and documentation"
msgstr "Sito web e documentazione"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Informazione si Joplin"
@@ -646,12 +657,46 @@ 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"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -710,6 +755,15 @@ 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"
@@ -848,12 +902,18 @@ msgstr "Etichetta sconosciuta: %s"
msgid "File system"
msgstr "File system"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (solo per test)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Livello di log sconosciuto: %s"
@@ -997,6 +1057,30 @@ 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"
@@ -1012,9 +1096,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"
@@ -1022,12 +1103,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 ""
@@ -1039,6 +1117,24 @@ msgstr ""
"Il percorso di sincronizzazione quando la sincronizzazione è abilitata. Vedi "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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."
@@ -1151,6 +1247,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:"
@@ -1206,8 +1308,13 @@ msgstr ""
msgid "Welcome"
msgstr "Benvenuto"
#~ msgid "Note title:"
#~ msgstr "Titolo della Nota:"
#~ 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à:"

View File

@@ -223,6 +223,10 @@ msgstr "\"%s\"というノートはありません。お作りいたしますか
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 "ノートは保存されました。"
@@ -446,6 +450,14 @@ msgstr "リモート保存領域と同期します。"
msgid "Sync to provided target (defaults to sync.target config value)"
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 ""
msgid "Synchronisation is already in progress."
msgstr "同期はすでに実行中です。"
@@ -458,10 +470,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)"
@@ -638,6 +646,9 @@ msgstr "ヘルプ"
msgid "Website and documentation"
msgstr "Webサイトとドキュメント"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Joplinについて"
@@ -645,12 +656,45 @@ msgstr "Joplinについて"
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr "キャンセル"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "ノートと設定は、%sに保存されます。"
@@ -711,6 +755,15 @@ msgstr ""
"注意:\"active\"に指定されたマスターキーのみが暗号化に使用されます。暗号化に"
"使用されたキーの応じて、すべてのキーが暗号解除のために使用されます。"
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 "状態"
@@ -849,12 +902,18 @@ msgstr "不明なフラグ: %s"
msgid "File system"
msgstr "ファイルシステム"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -1000,6 +1059,30 @@ msgstr "未完のToDoをリストの上部に表示"
msgid "Save geo-location with notes"
msgstr "ノートに位置情報を保存"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "新しいToDoを作成します。"
#, fuzzy
msgid "Focus title"
msgstr "ノートの題名:"
msgid "Focus body"
msgstr ""
#, fuzzy
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 "同期間隔"
@@ -1015,9 +1098,6 @@ msgstr "%d 時間"
msgid "%d hours"
msgstr "%d 時間"
msgid "Automatically update the application"
msgstr "アプリケーションの自動更新"
msgid "Show advanced options"
msgstr "詳細な設定の表示"
@@ -1025,11 +1105,9 @@ 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`を同期先"
"のディレクトリに設定してください。"
msgid "Directory to synchronise with (absolute path)"
msgstr "同期先のディレクトリ(絶対パス)"
@@ -1041,6 +1119,24 @@ msgstr ""
"ファイルシステム同期の有効時に同期を行うパスです。`sync.target`も参考にしてく"
"ださい。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 "無効な設定値: \"%s\"。有効な値は: %sです。"
@@ -1153,6 +1249,12 @@ msgstr "ノートブックは保存できませんでした:%s"
msgid "Edit notebook"
msgstr "ノートブックの編集"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "ノートは変更されています:"
@@ -1208,8 +1310,12 @@ msgstr ""
msgid "Welcome"
msgstr "ようこそ"
#~ msgid "Note title:"
#~ msgstr "ノートの題名:"
#~ 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 "ToDoの題名:"

View File

@@ -214,6 +214,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 ""
@@ -413,6 +417,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 ""
@@ -423,10 +435,6 @@ msgid ""
"operation."
msgstr ""
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr ""
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr ""
@@ -581,6 +589,9 @@ msgstr ""
msgid "Website and documentation"
msgstr ""
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr ""
@@ -588,12 +599,45 @@ msgstr ""
msgid "%s %s (%s, %s)"
msgstr ""
#, javascript-format
msgid "Open %s"
msgstr ""
msgid "Exit"
msgstr ""
msgid "OK"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
msgid "No"
msgstr ""
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -650,6 +694,15 @@ 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 ""
@@ -785,12 +838,18 @@ msgstr ""
msgid "File system"
msgstr ""
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr ""
msgid "OneDrive Dev (For testing only)"
msgstr ""
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr ""
@@ -923,6 +982,27 @@ 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 ""
@@ -938,9 +1018,6 @@ msgstr ""
msgid "%d hours"
msgstr ""
msgid "Automatically update the application"
msgstr ""
msgid "Show advanced options"
msgstr ""
@@ -948,8 +1025,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)"
@@ -960,6 +1037,24 @@ msgid ""
"See `sync.target`."
msgstr ""
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 ""
@@ -1071,6 +1166,12 @@ msgstr ""
msgid "Edit notebook"
msgstr ""
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr ""

View File

@@ -229,6 +229,10 @@ msgid "Starting to edit note. Close the editor to get back to the prompt."
msgstr ""
"Bewerken notitie gestart. Sluit de editor om terug naar de prompt te gaan."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr ""
msgid "Note has been saved."
msgstr "Notitie is opgeslaan."
@@ -459,6 +463,14 @@ msgid "Sync to provided target (defaults to sync.target config value)"
msgstr ""
"Synchroniseer naar opgegeven doel (standaard sync.target configuratie optie)"
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
msgstr ""
msgid "Synchronisation is already in progress."
msgstr "Synchronisatie reeds bezig."
@@ -472,10 +484,6 @@ msgstr ""
"is, kan de lock file verwijderd worden op \"%s\" en verder gegaan worden met "
"de synchronisatie. "
msgid ""
"Authentication was not completed (did not receive an authentication token)."
msgstr "Authenticatie was niet voltooid (geen authenticatietoken ontvangen)."
#, javascript-format
msgid "Synchronisation target: %s (%s)"
msgstr "Synchronisatiedoel: %s (%s)"
@@ -656,6 +664,9 @@ msgstr "Help"
msgid "Website and documentation"
msgstr "Website en documentatie"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Over Joplin"
@@ -663,12 +674,46 @@ msgstr "Over Joplin"
msgid "%s %s (%s, %s)"
msgstr "%s %s (%s, %s)"
#, fuzzy, javascript-format
msgid "Open %s"
msgstr "Op %s: %s"
msgid "Exit"
msgstr ""
msgid "OK"
msgstr "OK"
msgid "Cancel"
msgstr "Annuleer"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Notities en instellingen zijn opgeslaan in %s"
@@ -736,6 +781,16 @@ msgstr ""
"(aangeduid met \"active\"). Alle sleutels kunnen gebruikt worden voor "
"decodering, afhankelijk van hoe de notitieboeken initieel versleuteld zijn."
#, fuzzy
msgid "Missing Master Keys"
msgstr "Hoofdsleutels"
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"
@@ -875,12 +930,18 @@ msgstr "Onbekende optie: %s"
msgid "File system"
msgstr "Bestandssysteem"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (Alleen voor testen)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Onbekend log level: %s"
@@ -1026,6 +1087,29 @@ msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
msgid "Save geo-location with notes"
msgstr "Sla geo-locatie op bij notities"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "Maakt nieuwe to-do aan."
msgid "Focus title"
msgstr ""
msgid "Focus body"
msgstr ""
#, fuzzy
msgid "When creating a new note:"
msgstr "Maakt een nieuwe notitie aan."
msgid "Show tray icon"
msgstr ""
msgid "Set application zoom percentage"
msgstr ""
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Synchronisation interval"
msgstr "Synchronisatie interval"
@@ -1041,9 +1125,6 @@ msgstr "%d uur"
msgid "%d hours"
msgstr "%d uren"
msgid "Automatically update the application"
msgstr "Update de applicatie automatisch"
msgid "Show advanced options"
msgstr "Toon geavanceerde opties"
@@ -1051,11 +1132,9 @@ msgid "Synchronisation target"
msgstr "Synchronisatiedoel"
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 ""
"Het doel om mee te synchroniseren. Indien synchroniseren met het "
"bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."
msgid "Directory to synchronise with (absolute path)"
msgstr "Folder om mee te synchroniseren (absolute pad)"
@@ -1067,6 +1146,26 @@ msgstr ""
"Het pad om mee te synchroniseren als bestandssysteem synchronisatie is "
"ingeschakeld. Zie `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
#, fuzzy
msgid "Nexcloud password"
msgstr "Stel wachtwoord in"
msgid "WebDAV URL"
msgstr ""
msgid "WebDAV username"
msgstr ""
#, fuzzy
msgid "WebDAV password"
msgstr "Stel wachtwoord in"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Ongeldige optie: \"%s\". Geldige waarden zijn: %s."
@@ -1182,6 +1281,12 @@ msgstr "Het notitieboek kon niet opgeslaan worden: %s"
msgid "Edit notebook"
msgstr "Bewerk notitieboek"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "Deze notitie werd aangepast:"
@@ -1236,3 +1341,10 @@ msgstr ""
msgid "Welcome"
msgstr "Welkom"
#~ msgid ""
#~ "The target to synchonise to. If synchronising with the file system, set "
#~ "`sync.2.path` to specify the target directory."
#~ msgstr ""
#~ "Het doel om mee te synchroniseren. Indien synchroniseren met het "
#~ "bestandssysteem, zet `sync.2.path` om de doelfolder in te stellen."

View File

@@ -223,6 +223,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."
@@ -445,6 +449,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."
@@ -458,11 +471,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)"
@@ -634,6 +642,9 @@ msgstr "Ajuda"
msgid "Website and documentation"
msgstr "Website e documentação"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "Sobre o Joplin"
@@ -641,12 +652,46 @@ 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"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -705,6 +750,15 @@ 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"
@@ -846,12 +900,18 @@ msgstr "Flag desconhecido: %s"
msgid "File system"
msgstr "Sistema de arquivos"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (apenas para testes)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Nível de log desconhecido: %s"
@@ -996,6 +1056,30 @@ 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"
@@ -1011,9 +1095,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"
@@ -1021,11 +1102,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 ""
@@ -1037,6 +1116,24 @@ msgstr ""
"O caminho para sincronizar, quando a sincronização do sistema de arquivos "
"está habilitada. Veja `sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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."
@@ -1149,6 +1246,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:"
@@ -1202,8 +1305,12 @@ msgstr "Você não possui cadernos. Crie um clicando no botão (+)."
msgid "Welcome"
msgstr "Bem-vindo"
#~ msgid "Note title:"
#~ msgstr "Título da nota:"
#~ msgid ""
#~ "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:"

View File

@@ -7,13 +7,13 @@ 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.5\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"
@@ -112,7 +112,7 @@ msgid "The command \"%s\" is only available in GUI mode"
msgstr "Команда «%s» доступна только в режиме GUI"
msgid "Cannot change encrypted item"
msgstr ""
msgstr "Не удалось изменить зашифрованный элемент"
#, javascript-format
msgid "Missing required argument: %s"
@@ -123,7 +123,7 @@ msgid "%s: %s"
msgstr "%s: %s"
msgid "Your choice: "
msgstr "Ваш выбор:"
msgstr "Ваш выбор: "
#, javascript-format
msgid "Invalid answer: %s"
@@ -180,22 +180,24 @@ msgid ""
"Manages E2EE configuration. Commands are `enable`, `disable`, `decrypt`, "
"`status` and `target-status`."
msgstr ""
"Управляет конфигурацией E2EE. Команды: `enable`, `disable`, `decrypt`, "
"`status` и `target-status`."
#, fuzzy
msgid "Enter master password:"
msgstr "Установить пароль"
msgstr "Введите мастер-пароль:"
msgid "Operation cancelled"
msgstr ""
msgstr "Операция отменена"
msgid ""
"Starting decryption... Please wait as it may take several minutes depending "
"on how much there is to decrypt."
msgstr ""
"Запуск расшифровки... Пожалуйста, ожидайте. Время расшифровки зависит от "
"объёма расшифровываемых данных."
#, fuzzy
msgid "Completed decryption."
msgstr "Включить шифрование"
msgstr "Расшифровка завершена."
msgid "Enabled"
msgstr "Включено"
@@ -203,9 +205,9 @@ msgstr "Включено"
msgid "Disabled"
msgstr "Отключено"
#, fuzzy, javascript-format
#, javascript-format
msgid "Encryption is: %s"
msgstr "Шифрование:"
msgstr "Шифрование: %s"
msgid "Edit note."
msgstr "Редактировать заметку."
@@ -228,6 +230,10 @@ msgstr ""
"Запуск редактирования заметки. Закройте редактор, чтобы вернуться к "
"командной строке."
#, javascript-format
msgid "Error opening note in editor: %s"
msgstr "Ошибка при открытии заметки в редакторе: %s"
msgid "Note has been saved."
msgstr "Заметка сохранена."
@@ -277,7 +283,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 "
@@ -299,7 +305,7 @@ msgid ""
"For the complete list of available keyboard shortcuts, type `help shortcuts`"
msgstr ""
"Для просмотра списка доступных клавиатурных сочетаний введите `help "
"shortcuts`."
"shortcuts`"
msgid "Imports an Evernote notebook file (.enex file)."
msgstr "Импортирует файл блокнотов Evernote (.enex-файл)."
@@ -455,6 +461,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 "Синхронизация уже выполняется."
@@ -468,10 +483,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)"
@@ -593,6 +604,10 @@ msgid ""
"supplied the password, the encrypted items are being decrypted in the "
"background and will be available soon."
msgstr ""
"Один или несколько элементов сейчас зашифрованы и может потребоваться, чтобы "
"вы предоставили мастер-пароль. Для этого введите, пожалуйста, «e2ee "
"decrypt». Если пароль уже был вами предоставлен, зашифрованные элементы "
"расшифруются в фоновом режиме и вскоре станут доступны."
msgid "File"
msgstr "Файл"
@@ -636,13 +651,11 @@ msgstr "Инструменты"
msgid "Synchronisation status"
msgstr "Статус синхронизации"
#, fuzzy
msgid "Encryption options"
msgstr "Настройки шифрования"
#, fuzzy
msgid "General Options"
msgstr "Настройки"
msgstr "Основные настройки"
msgid "Help"
msgstr "Помощь"
@@ -650,6 +663,9 @@ msgstr "Помощь"
msgid "Website and documentation"
msgstr "Сайт и документация"
msgid "Check for updates..."
msgstr "Проверить обновления..."
msgid "About Joplin"
msgstr "О Joplin"
@@ -657,12 +673,47 @@ 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 "Отмена"
msgid "Error"
msgstr "Ошибка"
msgid "An update is available, do you want to update now?"
msgstr "Доступно обновление. Обновить сейчас?"
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "N"
#, javascript-format
msgid "Could not download the update: %s"
msgstr "Не удалось загрузить обновление: %s"
msgid "Current version is up-to-date."
msgstr "Вы используете самую свежую версию."
msgid "New version downloaded - application will quit now and update..."
msgstr ""
"Новая версия загружена — приложение сейчас будет закрыто и обновлено..."
#, javascript-format
msgid "Could not install the update: %s"
msgstr "Не удалось установить обновление: %s"
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr "Заметки и настройки сохранены в: %s"
@@ -731,6 +782,16 @@ 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 "Статус"
@@ -866,12 +927,18 @@ msgstr "Неизвестный флаг: %s"
msgid "File system"
msgstr "Файловая система"
msgid "Nextcloud (Beta)"
msgstr "Nextcloud (Beta)"
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive Dev (только для тестирования)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "Неизвестный уровень лога: %s"
@@ -930,9 +997,9 @@ msgstr "Удалено локальных элементов: %d."
msgid "Deleted remote items: %d."
msgstr "Удалено удалённых элементов: %d."
#, fuzzy, javascript-format
#, javascript-format
msgid "Fetched items: %d/%d."
msgstr "Создано локальных элементов: %d."
msgstr "Получено элементов: %d/%d."
#, javascript-format
msgid "State: \"%s\"."
@@ -949,13 +1016,11 @@ msgstr "Завершено: %s"
msgid "Synchronisation is already in progress. State: %s"
msgstr "Синхронизация уже выполняется. Статус: %s"
#, fuzzy
msgid "Encrypted"
msgstr "Шифрование:"
msgstr "Зашифровано"
#, fuzzy
msgid "Encrypted items cannot be modified"
msgstr "Некоторые элементы не могут быть синхронизированы."
msgstr "Зашифрованные элементы не могут быть изменены"
msgid "Conflicts"
msgstr "Конфликты"
@@ -1016,6 +1081,27 @@ 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 "Интервал синхронизации"
@@ -1031,9 +1117,6 @@ msgstr "%d час"
msgid "%d hours"
msgstr "%d часов"
msgid "Automatically update the application"
msgstr "Автоматически обновлять приложение"
msgid "Show advanced options"
msgstr "Показывать расширенные настройки"
@@ -1041,11 +1124,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 "Каталог синхронизации (абсолютный путь)"
@@ -1057,6 +1140,27 @@ msgstr ""
"Путь для синхронизации при включённой синхронизации с файловой системой. См. "
"`sync.target`."
msgid "Nexcloud WebDAV URL"
msgstr "Nexcloud WebDAV URL"
msgid "Nexcloud username"
msgstr "Имя пользователя Nexcloud"
msgid "Nexcloud password"
msgstr "Пароль Nexcloud"
#, fuzzy
msgid "WebDAV URL"
msgstr "Nexcloud WebDAV URL"
#, fuzzy
msgid "WebDAV username"
msgstr "Имя пользователя Nexcloud"
#, fuzzy
msgid "WebDAV password"
msgstr "Установить пароль"
#, javascript-format
msgid "Invalid option value: \"%s\". Possible values are: %s."
msgstr "Неверное значение параметра: «%s». Доступные значения: %s."
@@ -1064,15 +1168,18 @@ msgstr "Неверное значение параметра: «%s». Досту
msgid "Items that cannot be synchronised"
msgstr "Элементы, которые не могут быть синхронизированы"
#, fuzzy, javascript-format
#, javascript-format
msgid "%s (%s): %s"
msgstr "%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 "Статус синхронизации (элементов синхронизировано/всего)"
@@ -1119,9 +1226,8 @@ msgstr "Лог"
msgid "Export Debug Report"
msgstr "Экспортировать отладочный отчёт"
#, fuzzy
msgid "Encryption Config"
msgstr "Шифрование:"
msgstr "Конфигурация шифрования"
msgid "Configuration"
msgstr "Конфигурация"
@@ -1134,7 +1240,7 @@ msgid "Move %d notes to notebook \"%s\"?"
msgstr "Переместить %d заметок в блокнот «%s»?"
msgid "Press to set the decryption password."
msgstr ""
msgstr "Нажмите, чтобы установить пароль для расшифровки."
msgid "Select date"
msgstr "Выбрать дату"
@@ -1145,22 +1251,20 @@ msgstr "Подтвердить"
msgid "Cancel synchronisation"
msgstr "Отменить синхронизацию"
#, fuzzy, javascript-format
#, javascript-format
msgid "Master Key %s"
msgstr "Мастер-ключи"
msgstr "Мастер-ключ %s"
#, fuzzy, javascript-format
#, javascript-format
msgid "Created: %s"
msgstr "Создано: %d."
msgstr "Создано: %s"
#, fuzzy
msgid "Password:"
msgstr "Пароль"
msgstr "Пароль:"
msgid "Password cannot be empty"
msgstr ""
msgstr "Пароль не может быть пустым"
#, fuzzy
msgid "Enable"
msgstr "Включено"
@@ -1171,6 +1275,13 @@ msgstr "Не удалось сохранить блокнот: %s"
msgid "Edit notebook"
msgstr "Редактировать блокнот"
msgid "Show all"
msgstr ""
#, fuzzy
msgid "Errors only"
msgstr "Ошибка"
msgid "This note has been modified:"
msgstr "Эта заметка была изменена:"
@@ -1224,8 +1335,12 @@ msgstr "У вас сейчас нет блокнота. Создайте его
msgid "Welcome"
msgstr "Добро пожаловать"
#~ msgid "Note title:"
#~ msgstr "Название заметки:"
#~ 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 "Название задачи:"

View File

@@ -218,6 +218,10 @@ msgstr "此笔记不存在:\"%s\"。是否创建?"
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 "笔记已被保存。"
@@ -426,6 +430,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 "同步正在进行中。"
@@ -438,10 +450,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)"
@@ -605,6 +613,9 @@ msgstr "帮助"
msgid "Website and documentation"
msgstr "网站与文档"
msgid "Check for updates..."
msgstr ""
msgid "About Joplin"
msgstr "关于Joplin"
@@ -612,12 +623,46 @@ 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 "确认"
msgid "Cancel"
msgstr "取消"
msgid "Error"
msgstr ""
msgid "An update is available, do you want to update now?"
msgstr ""
msgid "Yes"
msgstr ""
#, fuzzy
msgid "No"
msgstr "否"
#, javascript-format
msgid "Could not download the update: %s"
msgstr ""
msgid "Current version is up-to-date."
msgstr ""
msgid "New version downloaded - application will quit now and update..."
msgstr ""
#, javascript-format
msgid "Could not install the update: %s"
msgstr ""
#, javascript-format
msgid "Notes and settings are stored in: %s"
msgstr ""
@@ -676,6 +721,15 @@ 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 "状态"
@@ -814,12 +868,18 @@ msgstr "未知标记:%s"
msgid "File system"
msgstr "文件系统"
msgid "Nextcloud (Beta)"
msgstr ""
msgid "OneDrive"
msgstr "OneDrive"
msgid "OneDrive Dev (For testing only)"
msgstr "OneDrive开发员(仅测试用)"
msgid "WebDAV (Beta)"
msgstr ""
#, javascript-format
msgid "Unknown log level: %s"
msgstr "未知日志level:%s"
@@ -958,6 +1018,30 @@ msgstr "在列表上方显示未完成的待办事项"
msgid "Save geo-location with notes"
msgstr "保存笔记时同时保存地理定位信息"
#, fuzzy
msgid "When creating a new to-do:"
msgstr "创建新待办事项。"
#, fuzzy
msgid "Focus title"
msgstr "笔记标题:"
msgid "Focus body"
msgstr ""
#, fuzzy
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 "同步间隔"
@@ -973,9 +1057,6 @@ msgstr "%d小时"
msgid "%d hours"
msgstr "%d小时"
msgid "Automatically update the application"
msgstr "自动更新此程序"
msgid "Show advanced options"
msgstr "显示高级选项"
@@ -983,9 +1064,9 @@ 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."
msgstr "同步的目标。若与文件系统同步,设置`sync.2.path`为指定目标目录。"
"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)"
msgstr ""
@@ -995,6 +1076,24 @@ msgid ""
"See `sync.target`."
msgstr "当文件系统同步开启时的同步路径。参考`sync.target`。"
msgid "Nexcloud WebDAV URL"
msgstr ""
msgid "Nexcloud username"
msgstr ""
msgid "Nexcloud 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 "无效的选项值:\"%s\"。可用值为:%s。"
@@ -1107,6 +1206,12 @@ msgstr "此笔记本无法保存:%s"
msgid "Edit notebook"
msgstr "编辑笔记本"
msgid "Show all"
msgstr ""
msgid "Errors only"
msgstr ""
msgid "This note has been modified:"
msgstr "此笔记已被修改:"
@@ -1158,8 +1263,10 @@ msgstr "您当前没有任何笔记本。点击(+)按钮创建新笔记本。"
msgid "Welcome"
msgstr "欢迎"
#~ msgid "Note title:"
#~ msgstr "笔记标题:"
#~ 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 "待办事项标题:"

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "0.10.88",
"version": "0.10.92",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -85,6 +85,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",
@@ -1050,6 +1055,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",
@@ -1104,6 +1114,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",
@@ -2100,6 +2115,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",
@@ -2144,6 +2168,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",

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.88",
"version": "0.10.92",
"bin": {
"joplin": "./main.js"
},
@@ -28,6 +28,7 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"base-64": "^0.1.0",
"compare-version": "^0.1.2",
"follow-redirects": "^1.2.4",
"form-data": "^2.1.4",
@@ -57,8 +58,10 @@
"strip-ansi": "^4.0.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.21",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"yargs-parser": "^7.0.0"
},
"devDependencies": {

View File

@@ -11,7 +11,7 @@ cp "$SCRIPT_DIR/../README.md" build/
cd "$SCRIPT_DIR/build"
npm publish
NEW_VERSION=$("cat package.json | jq -r .version")
NEW_VERSION=$(cat package.json | jq -r .version)
git add -A
git commit -m "CLI v$NEW_VERSION"
git tag "cli-v$NEW_VERSION"

View File

@@ -8,7 +8,7 @@ process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('Encryption', function() {
describe('ArrayUtils', function() {
beforeEach(async (done) => {
done();
@@ -29,4 +29,19 @@ describe('Encryption', function() {
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();
});
});

View File

@@ -19,7 +19,7 @@ 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 = 35000; // The first test is slow because the database needs to be built
async function allItems() {
let folders = await Folder.all();
@@ -74,11 +74,11 @@ 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);
@@ -304,7 +304,7 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
let deletedItems = await BaseItem.deletedItems(syncTargetId());
expect(deletedItems.length).toBe(0);
}));
}));
it('should delete remote folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -322,8 +322,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let all = await allItems();
localItemsSameAsRemote(all, expect);
}));
await localItemsSameAsRemote(all, expect);
}));
it('should delete local folder', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -345,8 +345,8 @@ describe('Synchronizer', function() {
await synchronizer().start();
let items = await allItems();
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
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" });
@@ -388,8 +388,8 @@ describe('Synchronizer', function() {
expect(items.length).toBe(1);
expect(items[0].title).toBe('folder');
localItemsSameAsRemote(items, expect);
}));
await localItemsSameAsRemote(items, expect);
}));
it('should cross delete all folders', asyncTest(async () => {
// If client1 and 2 have two folders, client 1 deletes item 1 and client
@@ -459,7 +459,7 @@ describe('Synchronizer', function() {
let unconflictedNotes = await Note.unconflictedNotes();
expect(unconflictedNotes.length).toBe(0);
}));
}));
it('should handle conflict when remote folder is deleted then local folder is renamed', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
@@ -489,7 +489,7 @@ describe('Synchronizer', function() {
let items = await allItems();
expect(items.length).toBe(1);
}));
}));
it('should allow duplicate folder titles', asyncTest(async () => {
let localF1 = await Folder.save({ title: "folder" });
@@ -575,10 +575,12 @@ describe('Synchronizer', function() {
}
it('should sync tags', asyncTest(async () => {
await shoudSyncTagTest(false); }));
await shoudSyncTagTest(false);
}));
it('should sync encrypted tags', asyncTest(async () => {
await shoudSyncTagTest(true); }));
await shoudSyncTagTest(true);
}));
it('should not sync notes with conflicts', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder" });
@@ -683,12 +685,12 @@ describe('Synchronizer', function() {
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);
@@ -702,9 +704,9 @@ describe('Synchronizer', function() {
let disabledItems = await BaseItem.syncDisabledItems(syncTargetId());
expect(disabledItems.length).toBe(0);
await Note.save({ id: noteId, title: "un mod", });
synchronizer().debugFlags_ = ['rejectedByTarget'];
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);
@@ -848,7 +850,7 @@ describe('Synchronizer', function() {
}));
it('should sync resources', asyncTest(async () => {
while (insideBeforeEach) await time.msleep(100);
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 });

View File

@@ -15,6 +15,7 @@ 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');
@@ -22,8 +23,10 @@ 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_ = [];
@@ -46,12 +49,16 @@ fs.mkdirpSync(logDir, 0o755);
SyncTargetRegistry.addClass(SyncTargetMemory);
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
const syncTargetId_ = SyncTargetRegistry.nameToId('nextcloud');
//const syncTargetId_ = SyncTargetRegistry.nameToId('memory');
const syncTargetId_ = SyncTargetRegistry.nameToId('filesystem');
//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 : 10;//400;
console.info('Testing with sync target: ' + SyncTargetRegistry.idToName(syncTargetId_));
const logger = new Logger();
logger.addTarget('console');
@@ -174,12 +181,7 @@ async function setupDatabaseAndSynchronizer(id = null) {
decryptionWorkers_[id] = new DecryptionWorker();
decryptionWorkers_[id].setEncryptionService(encryptionServices_[id]);
if (syncTargetId_ == SyncTargetRegistry.nameToId('filesystem')) {
fs.removeSync(syncDir)
fs.mkdirpSync(syncDir, 0o755);
} else {
await fileApi().format();
}
await fileApi().clearRoot();
}
function db(id = null) {
@@ -230,7 +232,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) {

View File

@@ -1,5 +1,5 @@
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')
@@ -12,6 +12,7 @@ class ElectronAppWrapper {
this.env_ = env;
this.win_ = null;
this.willQuitApp_ = false;
this.tray_ = null;
}
electronApp() {
@@ -62,11 +63,27 @@ class ElectronAppWrapper {
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 +110,27 @@ class ElectronAppWrapper {
this.electronApp_.quit();
}
trayShown() {
return !!this.tray_;
}
// Note: this must be called only after the "ready" event of the app has been dispatched
createTray(contextMenu) {
this.tray_ = new Tray(__dirname + '/build/icons/16x16.png')
this.tray_.setToolTip(this.electronApp_.getName())
this.tray_.setContextMenu(contextMenu)
this.tray_.on('click', () => {
this.window().show();
});
}
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.

View File

@@ -49,6 +49,10 @@ class Application extends BaseApplication {
return true;
}
checkForUpdateLoggerPath() {
return Setting.value('profileDir') + '/log-autoupdater.txt';
}
reducer(state = appDefaultState, action) {
let newState = state;
@@ -136,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();
}
@@ -294,6 +302,11 @@ class Application extends BaseApplication {
label: _('Website and documentation'),
accelerator: 'F1',
click () { bridge().openExternal('http://joplin.cozic.net') }
}, {
label: _('Check for updates...'),
click: () => {
bridge().checkForUpdates(false, this.checkForUpdateLoggerPath());
}
}, {
label: _('About Joplin'),
click: () => {
@@ -340,6 +353,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);
@@ -385,9 +420,9 @@ 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, this.checkForUpdateLoggerPath());
}
}
@@ -396,6 +431,8 @@ class Application extends BaseApplication {
setInterval(() => { runAutoUpdateCheck() }, 2 * 60 * 60 * 1000);
}
this.updateTray();
setTimeout(() => {
AlarmService.garbageCollect();
}, 1000 * 60 * 60);

View File

@@ -125,6 +125,11 @@ class Bridge {
}
}
checkForUpdates(inBackground, logFilePath) {
const { checkForUpdates } = require('./checkForUpdates.js');
checkForUpdates(inBackground, logFilePath);
}
}
let bridge_ = null;

View File

@@ -0,0 +1,72 @@
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;
autoUpdater.autoDownload = false;
autoUpdater.on('error', (error) => {
autoUpdateLogger_.error(error);
if (checkInBackground_) return;
dialog.showErrorBox(_('Error'), error == null ? "unknown" : (error.stack || error).toString())
})
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
message: _('An update is available, do you want to update now?'),
buttons: [_('Yes'), _('No')]
}, (buttonIndex) => {
if (buttonIndex === 0) {
try {
autoUpdater.downloadUpdate()
} catch (error) {
autoUpdateLogger_.error(error);
dialog.showErrorBox(_('Error'), _('Could not download the update: %s', error.message));
}
}
})
})
autoUpdater.on('update-not-available', () => {
if (checkInBackground_) return;
dialog.showMessageBox({ message: _('Current version is up-to-date.') })
})
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({ message: _('New version downloaded - application will quit now and update...') }, () => {
setTimeout(() => {
try {
autoUpdater.quitAndInstall();
} catch (error) {
autoUpdateLogger_.error(error);
dialog.showErrorBox(_('Error'), _('Could not install the update: %s', error.message));
}
}, 100);
})
})
function checkForUpdates(inBackground, logFilePath) {
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_) dialog.showErrorBox(_('Error'), error.message);
}
}
module.exports.checkForUpdates = checkForUpdates

View File

@@ -0,0 +1,3 @@
owner: laurent22
repo: joplin
provider: github

View File

@@ -89,24 +89,38 @@ 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 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={controlStyle} 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 {

View File

@@ -92,10 +92,14 @@ class EncryptionConfigScreenComponent extends React.Component {
};
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 () => {
@@ -121,7 +125,7 @@ class EncryptionConfigScreenComponent extends React.Component {
}
}
const decryptedItemsInfo = this.props.encryptionEnabled ? <p style={theme.textStyle}>{shared.decryptedStatText(this)}</p> : null;
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;
@@ -149,6 +153,31 @@ class EncryptionConfigScreenComponent extends React.Component {
);
}
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} />
@@ -169,6 +198,7 @@ class EncryptionConfigScreenComponent extends React.Component {
{decryptedItemsInfo}
{toggleButton}
{masterKeySection}
{nonExistingMasterKeySection}
</div>
</div>
);
@@ -183,6 +213,7 @@ const mapStateToProps = (state) => {
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
};

View File

@@ -274,20 +274,22 @@ class MainScreenComponent extends React.Component {
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' }) },
});
@@ -384,6 +386,7 @@ const mapStateToProps = (state) => {
notes: state.notes,
hasDisabledSyncItems: state.hasDisabledSyncItems,
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
selectedFolderId: state.selectedFolderId,
};
};

View File

@@ -190,13 +190,21 @@ class NoteTextComponent extends React.Component {
this.editorSetScrollTop(1);
this.restoreScrollTop_ = 0;
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.focus();
this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0);
}
@@ -576,6 +584,7 @@ class NoteTextComponent extends React.Component {
const titleEditor = <input
type="text"
ref={(elem) => { this.titleField_ = elem; } }
style={titleEditorStyle}
value={note && note.title ? note.title : ''}
onChange={(event) => { this.title_changeText(event); }}

View File

@@ -1,179 +1,205 @@
<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);
});
// 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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,15 @@
// Make it possible to require("/lib/...") without specifying full path
require('app-module-path').addPath(__dirname);
// Disable React message in console "Download the React DevTools for a better development experience"
// https://stackoverflow.com/questions/42196819/disable-hide-download-the-react-devtools#42196820
__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
supportsFiber: true,
inject: function() {},
onCommitFiberRoot: function() {},
onCommitFiberUnmount: function() {},
};
const { app } = require('./app.js');
const Folder = require('lib/models/Folder.js');
const Resource = require('lib/models/Resource.js');

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "0.10.58",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -29,9 +29,9 @@
"optional": true
},
"@types/node": {
"version": "7.0.46",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.46.tgz",
"integrity": "sha512-u+JAi1KtmaUoU/EHJkxoiuvzyo91FCE41Z9TZWWcOUU3P8oUdlDLdrGzCGWySPgbRMD17B0B+1aaJLYI9egQ6A==",
"version": "7.0.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.52.tgz",
"integrity": "sha512-jjpyQsKGsOF/wUElNjfPULk+d8PKvJOIXk3IUeBYYmNCy5dMWfrI+JiixYNw8ppKOlcRwWTXFl0B+i5oGrf95Q==",
"dev": true
},
"ajv": {
@@ -563,8 +563,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base-64": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"base64-js": {
"version": "1.2.0",
@@ -604,6 +608,14 @@
"readable-stream": "2.3.3"
}
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
"requires": {
"inherits": "2.0.3"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
@@ -721,7 +733,6 @@
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
@@ -866,8 +877,7 @@
"capture-stack-trace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz",
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=",
"dev": true
"integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0="
},
"caseless": {
"version": "0.12.0",
@@ -924,7 +934,8 @@
"chownr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE="
"integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
"dev": true
},
"chromium-pickle-js": {
"version": "0.2.0",
@@ -968,9 +979,9 @@
"dev": true
},
"color": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz",
"integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-1.0.3.tgz",
"integrity": "sha1-5I6DLYXxTvaU+0aIEcLVz+cptV0=",
"requires": {
"color-convert": "1.9.1",
"color-string": "1.5.2"
@@ -1021,8 +1032,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.0",
@@ -1084,7 +1094,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
"integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
"dev": true,
"requires": {
"capture-stack-trace": "1.0.0"
}
@@ -1183,14 +1192,6 @@
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
},
"decompress-response": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
"integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
"requires": {
"mimic-response": "1.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@@ -1227,11 +1228,6 @@
"repeating": "2.0.1"
}
},
"detect-libc": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
"integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U="
},
"dmg-builder": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-2.1.6.tgz",
@@ -1282,8 +1278,7 @@
"duplexer3": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=",
"dev": true
"integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
},
"ecc-jsbn": {
"version": "0.1.1",
@@ -1302,12 +1297,12 @@
"dev": true
},
"electron": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.9.tgz",
"integrity": "sha1-rdVOn4+D7QL2UZ7BATX2mLGTNs8=",
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/electron/-/electron-1.7.11.tgz",
"integrity": "sha1-mTtqp54OeafPzDafTIE/vZoLCNk=",
"dev": true,
"requires": {
"@types/node": "7.0.46",
"@types/node": "7.0.52",
"electron-download": "3.3.0",
"extract-zip": "1.6.6"
}
@@ -2011,8 +2006,18 @@
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
"integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
"requires": {
"graceful-fs": "4.1.11",
"inherits": "2.0.3",
"mkdirp": "0.5.1",
"rimraf": "2.6.2"
}
},
"fullstore": {
"version": "1.1.0",
@@ -2058,8 +2063,7 @@
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
},
"getpass": {
"version": "0.1.7",
@@ -2080,7 +2084,6 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
@@ -2129,7 +2132,6 @@
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
"dev": true,
"requires": {
"create-error-class": "3.0.2",
"duplexer3": "0.1.4",
@@ -2147,8 +2149,7 @@
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=",
"dev": true
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
}
}
},
@@ -2308,7 +2309,6 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
@@ -2507,14 +2507,12 @@
"is-redirect": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=",
"dev": true
"integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
},
"is-retry-allowed": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=",
"dev": true
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
},
"is-stream": {
"version": "1.1.0",
@@ -2667,6 +2665,14 @@
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
"integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po="
},
"katex": {
"version": "0.9.0-beta1",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.9.0-beta1.tgz",
"integrity": "sha512-M7c7Eihp665Bh9wDR0xg/PdE1OuCa15PsiDQSBYyr+xJR8WrFP8nxdNF1lNUCBPzEup4zECG2jFUIZnU66xBRQ==",
"requires": {
"match-at": "0.1.1"
}
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@@ -2799,8 +2805,7 @@
"lowercase-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz",
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=",
"dev": true
"integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY="
},
"lru-cache": {
"version": "4.1.1",
@@ -2847,6 +2852,11 @@
"uc.micro": "1.0.3"
}
},
"match-at": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/match-at/-/match-at-0.1.1.tgz",
"integrity": "sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q=="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
@@ -2950,16 +2960,10 @@
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=",
"dev": true
},
"mimic-response": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz",
"integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
}
@@ -2969,22 +2973,6 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.1.tgz",
"integrity": "sha512-u1aUllxPJUI07cOqzR7reGmQxmCqlH88uIIsf6XZFEWgw7gXKpJdR+5R9Y3KEDmWYkdIz9wXZs3C0jOPxejk/Q==",
"requires": {
"yallist": "3.0.2"
}
},
"minizlib": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.0.4.tgz",
"integrity": "sha512-sN4U9tIJtBRwKbwgFh9qJfrPIQ/GGTRr1MGqkgOeMTLy8/lM0FcWU//FqlnZ3Vb7gJ+Mxh3FOg1EklibdajbaQ==",
"requires": {
"minipass": "2.2.1"
}
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@@ -3287,8 +3275,7 @@
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
@@ -3414,8 +3401,7 @@
"prepend-http": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
},
"preserve": {
"version": "0.2.0",
@@ -3526,6 +3512,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="
},
"rabin-bindings": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/rabin-bindings/-/rabin-bindings-1.7.3.tgz",
@@ -3862,11 +3853,15 @@
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
"integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
"dev": true,
"requires": {
"glob": "7.1.2"
}
@@ -3923,17 +3918,16 @@
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"sharp": {
"version": "0.18.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.18.4.tgz",
"integrity": "sha1-/jKcDwaJbCiqJDdt8f/wKuV/LTQ=",
"version": "0.17.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.17.3.tgz",
"integrity": "sha1-SEzSpwyQA3CUjcxD4WX3gwa/9Io=",
"requires": {
"caw": "2.0.1",
"color": "2.0.1",
"detect-libc": "0.2.0",
"color": "1.0.3",
"got": "6.7.1",
"nan": "2.7.0",
"semver": "5.4.1",
"simple-get": "2.7.0",
"tar": "3.2.1"
"tar": "2.2.1"
}
},
"shebang-command": {
@@ -3962,21 +3956,6 @@
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"dev": true
},
"simple-concat": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
"integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
},
"simple-get": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.7.0.tgz",
"integrity": "sha512-RkE9rGPHcxYZ/baYmgJtOSM63vH0Vyq+ma5TijBcLla41SWlh8t6XYIGMR/oeZcmr+/G8k+zrClkkVrtnQ0esg==",
"requires": {
"decompress-response": "3.3.0",
"once": "1.4.0",
"simple-concat": "1.0.0"
}
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -4921,15 +4900,13 @@
"integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0="
},
"tar": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-3.2.1.tgz",
"integrity": "sha512-ZSzds1E0IqutvMU8HxjMaU8eB7urw2fGwTq88ukDOVuUIh0656l7/P7LiVPxhO5kS4flcRJQk8USG+cghQbTUQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
"integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
"requires": {
"chownr": "1.0.1",
"minipass": "2.2.1",
"minizlib": "1.0.4",
"mkdirp": "0.5.1",
"yallist": "3.0.2"
"block-stream": "0.0.9",
"fstream": "1.0.11",
"inherits": "2.0.3"
}
},
"tar-fs": {
@@ -5047,8 +5024,7 @@
"timed-out": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=",
"dev": true
"integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
},
"to-fast-properties": {
"version": "1.0.3",
@@ -5194,11 +5170,19 @@
}
}
},
"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-parse-lax": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
"integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
"dev": true,
"requires": {
"prepend-http": "1.0.4"
}
@@ -5334,6 +5318,22 @@
"integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=",
"dev": true
},
"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"
},
"dependencies": {
"xmlbuilder": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz",
"integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8="
}
}
},
"xmlbuilder": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
@@ -5361,11 +5361,6 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
},
"yargs": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "0.10.47",
"version": "0.10.58",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -8,8 +8,8 @@
"pack": "node_modules/.bin/electron-builder --dir",
"dist": "node_modules/.bin/electron-builder",
"publish": "build -p always",
"postinstall": "node compile-jsx.js && node compile-package-info.js",
"compile": "node compile-jsx.js && node compile-package-info.js"
"postinstall": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
"compile": "node compile-jsx.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
},
"repository": {
"type": "git",
@@ -34,14 +34,15 @@
"asar": false
},
"linux": {
"asar": false
"asar": false,
"category": "Office"
}
},
"homepage": "https://github.com/laurent22/joplin#readme",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1",
"electron": "^1.7.9",
"electron": "^1.7.11",
"electron-builder": "^19.45.4"
},
"optionalDependencies": {
@@ -51,6 +52,7 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"base-64": "^0.1.0",
"electron-context-menu": "^0.9.1",
"electron-log": "^2.2.11",
"electron-updater": "^2.16.1",
@@ -61,6 +63,7 @@
"highlight.js": "^9.12.0",
"html-entities": "^1.2.1",
"jssha": "^2.3.1",
"katex": "^0.9.0-beta1",
"levenshtein": "^1.0.5",
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
@@ -77,13 +80,15 @@
"react-dom": "^16.0.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"sharp": "^0.18.4",
"sharp": "^0.17.3",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",
"string-padding": "^1.0.2",
"string-to-stream": "^1.1.0",
"tcp-port-used": "^0.1.2",
"uuid": "^3.1.0"
"url-parse": "^1.2.0",
"uuid": "^3.1.0",
"xml2js": "^0.4.19"
}
}

View File

@@ -1,7 +1,7 @@
const Setting = require('lib/models/Setting.js');
const globalStyle = {
fontSize: 12,
fontSize: 12 * Setting.value('style.zoom')/100,
fontFamily: 'sans-serif',
margin: 15, // No text and no interactive component should be within this margin
itemMarginTop: 10,

20
LICENSE
View File

@@ -1,7 +1,21 @@
MIT License
Copyright (c) 2016-2018 Laurent Cozic
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,3 +1,5 @@
License MIT
Copyright (c) 2016-2018 Laurent Cozic
L'autorisation est accordée, gracieusement, à toute personne acquérant une copie de ce logiciel et des fichiers de documentation associés (le "Logiciel"), de commercialiser le Logiciel sans restriction, notamment les droits d'utiliser, de copier, de modifier, de fusionner, de publier, de distribuer, de sous-licencier et/ou de vendre des copies du Logiciel, ainsi que d'autoriser les personnes auxquelles le Logiciel est fourni à le faire, sous réserve des conditions suivantes :

View File

@@ -4,7 +4,7 @@ Joplin is a free, open source note taking and to-do application, which can handl
Notes exported from Evernote via .enex files [can be imported](#importing-notes-from-evernote) into Joplin, including the formatted content (which is converted to Markdown), resources (images, attachments, etc.) and complete metadata (geolocation, updated time, created time, etc.).
The notes can be [synchronised](#synchronisation) with various targets including the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
The notes can be [synchronised](#synchronisation) with various targets including [Nextcloud](https://nextcloud.com/), the file system (for example with a network directory) or with Microsoft OneDrive. When synchronising the notes, notebooks, tags and other metadata are saved to plain text files which can be easily inspected, backed up and moved around.
Joplin is still under development but is out of Beta and should be suitable for every day use. The UI of the terminal client is built on top of the great [terminal-kit](https://github.com/cronvel/terminal-kit) library, the desktop client using [Electron](https://electronjs.org/), and the Android client front end is done using [React Native](https://facebook.github.io/react-native/).
@@ -18,15 +18,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download
-----------------|--------
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-Setup-0.10.43.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.43/Joplin-0.10.43-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
Windows | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-Setup-0.10.54.exe'><img alt='Get it on Windows' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeWindows.png'/></a>
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-0.10.54.dmg'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeMacOS.png'/></a>
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v0.10.54/Joplin-0.10.54-x86_64.AppImage'><img alt='Get it on macOS' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeLinux.png'/></a>
## Mobile applications
Operating System | Download | Alt. Download
-----------------|---------------------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.75/joplin-v0.10.75.apk)
-----------------|----------|----------------
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin/releases/download/android-v0.10.83/joplin-v0.10.83.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://raw.githubusercontent.com/laurent22/joplin/master/docs/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -53,7 +53,7 @@ For usage information, please refer to the full [Joplin Terminal Application Doc
- Desktop, mobile and terminal applications.
- Support notes, to-dos, tags and notebooks.
- Offline first, so the entire data is always available on the device even without an internet connection.
- Ability to synchronise with multiple targets, including the file system and OneDrive (NextCloud and Dropbox are planned).
- Ability to synchronise with multiple targets, including NextCloud, the file system and OneDrive (Dropbox is planned).
- Synchronises to a plain text format, which can be easily manipulated, backed up, or exported to a different format.
- Markdown notes, which are rendered with images and formatting in the desktop and mobile applications.
- Tag support
@@ -85,13 +85,38 @@ In general the way to import notes from any application into Joplin is to conver
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A NextCloud driver, and a Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
On the **desktop application**, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
## Nextcloud synchronisation
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
**Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. 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 the profile directory of the desktop or terminal application.**
On the **desktop application** or **mobile application**, go to the config screen and select Nextcloud as the synchronisation target. Then input [the WebDAV URL](https://docs.nextcloud.com/server/9/user_manual/files/access_webdav.html), this is normally `https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin` (make sure to create the "Joplin" directory in Nextcloud and to replace USERNAME by your Nextcloud username), and set the username and password.
On the **terminal application**, you will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/Joplin
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
:config sync.target 5
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
## WebDAV synchronisation
**Important: This is a beta feature. It has been extensively tested and is already in use by some users, but it is possible that some bugs remain. 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 the profile directory of the desktop or terminal application.**
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
On the **desktop application** or **mobile application**, select "OneDrive" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the "Synchronise" button in the sidebar. You will be asked to login to OneDrive to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive).
On the **terminal application**, to initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync
@@ -121,23 +146,43 @@ If for any reason the notifications do not work, please [open an issue](https://
# Localisation
Joplin is currently available in English, French, Spanish, German, Portuguese, Chinese, Japanese, Russian, Croatian, Dutch and Italian. If you would like to contribute a translation, it is quite straightforward, please follow these steps:
Joplin is currently available in the languages below. If you would like to contribute a **new translation**, it is quite straightforward, please follow these steps:
- [Download Poedit](https://poedit.net/), the translation editor, and install it.
- [Download the file to be translated](https://raw.githubusercontent.com/laurent22/joplin/master/CliClient/locales/joplin.pot).
- In Poedit, open this .pot file, go into the Catalog menu and click Configuration. Change "Country" and "Language" to your own country and language.
- From then you can translate the file. Once it is done, please either [open a pull request](https://github.com/laurent22/joplin/pulls) or send the file to [this address](https://raw.githubusercontent.com/laurent22/joplin/master/Assets/Adresse.png).
To **update a translation**, follow the same steps as above but instead of getting the .pot file, get the .po file for your language from there: https://github.com/laurent22/joplin/tree/master/CliClient/locales
This translation will apply to the three applications - desktop, mobile and terminal.
Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Code | Last translator | Percent done
---|---|---|---|---
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/hr.png) | Croatian | hr_HR | Hrvoje Mandić <trbuhom@net.hr> | 72%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/de.png) | Deutsch | de_DE | Tobias Strobel <git@strobeltobias.de> | 92%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/gb.png) | English | en_GB | | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/es.png) | Español | es_ES | Lucas Vieites | 80%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cr.png) | Español (Costa Rica) | es_CR | | 68%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/fr.png) | Français | fr_FR | Laurent Cozic | 100%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/it.png) | Italiano | it_IT | | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/be.png) | Nederlands | nl_BE | | 90%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/br.png) | Português (Brasil) | pt_BR | | 74%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/ru.png) | Русский | ru_RU | Artyom Karlov <artyom.karlov@gmail.com> | 96%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/cn.png) | 中文 (简体) | zh_CN | RCJacH <RCJacH@outlook.com> | 76%
![](https://raw.githubusercontent.com/stevenrskelton/flag-icon/master/png/16/country-4x3/jp.png) | 日本語 | ja_JP | | 74%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Contributing
Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/master/CONTRIBUTING.md
# Coming features
- NextCloud support
- All: End to end encryption
- Mobile: manage tags
- Windows: Tray icon
- Desktop apps: Tag auto-complete
- Desktop apps: Dark theme

View File

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

View File

@@ -111,11 +111,30 @@ To import Evernote data, follow these steps:
# Synchronisation
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as OneDrive or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or OneDrive, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with OneDrive (by default) or the local filesystem. A Dropbox driver will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
Currently, synchronisation is possible with Nextcloud and OneDrive (by default) or the local filesystem. A Dropbox one will also be available once [this React Native bug](https://github.com/facebook/react-native/issues/14445) is fixed. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). After that, the application will synchronise in the background whenever it is running. It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
## Nextcloud synchronisation
You will need to set the `sync.target` config variable and all the `sync.5.path`, `sync.5.username` and `sync.5.password` config variables to, respectively the Nextcloud WebDAV URL, your username and your password. This can be done from the command line mode using:
:config sync.target 5
:config sync.5.path https://example.com/nextcloud/remote.php/dav/files/USERNAME/
:config sync.5.username YOUR_USERNAME
:config sync.5.password YOUR_PASSWORD
If synchronisation does not work, please consult the logs in the app profile directory - it is often due to a misconfigured URL or password. The log should indicate what the exact issue is.
## WebDAV synchronisation
Select the "WebDAV" synchronisation target and follow the same instructions as for Nextcloud above.
## OneDrive synchronisation
When syncing with OneDrive, Joplin creates a sub-directory in OneDrive, in /Apps/Joplin and read/write the notes and notebooks from it. The application does not have access to anything outside this directory.
To initiate the synchronisation process, type `:sync`. You will be asked to follow a link to authorise the application (simply input your Microsoft credentials - you do not need to register with OneDrive). It is possible to also synchronise outside of the user interface by typing `joplin sync` from the terminal. This can be used to setup a cron script to synchronise at regular interval. For example, this would do it every 30 minutes:
*/30 * * * * /path/to/joplin sync

View File

@@ -90,8 +90,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 90
versionName "0.10.75"
versionCode 2097261
versionName "0.10.83"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -13,4 +13,28 @@ ArrayUtils.removeElement = function(array, element) {
return array;
}
// https://stackoverflow.com/a/10264318/561309
ArrayUtils.binarySearch = function(items, value) {
var startIndex = 0,
stopIndex = items.length - 1,
middle = Math.floor((stopIndex + startIndex)/2);
while(items[middle] != value && startIndex < stopIndex){
//adjust search area
if (value < items[middle]){
stopIndex = middle - 1;
} else if (value > items[middle]){
startIndex = middle + 1;
}
//recalculate middle
middle = Math.floor((stopIndex + startIndex)/2);
}
//make sure it's the right value
return (items[middle] != value) ? -1 : middle;
}
module.exports = ArrayUtils;

View File

@@ -26,12 +26,16 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
SyncTargetRegistry.addClass(SyncTargetNextcloud);
SyncTargetRegistry.addClass(SyncTargetWebDAV);
class BaseApplication {

View File

@@ -30,6 +30,10 @@ class BaseSyncTarget {
return false;
}
authRouteName() {
return null;
}
static id() {
throw new Error('id() not implemented');
}

View File

@@ -2,11 +2,7 @@ class JoplinError extends Error {
constructor(message, code = null) {
super(message);
this.code_ = code;
}
get code() {
return this.code_;
this.code = code;
}
}

View File

@@ -5,6 +5,7 @@ const Resource = require('lib/models/Resource.js');
const ModelCache = require('lib/ModelCache');
const { shim } = require('lib/shim.js');
const md5 = require('md5');
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
class MdToHtml {
@@ -28,7 +29,7 @@ class MdToHtml {
const r = resources[n];
k.push(r.id);
}
k.push(md5(body));
k.push(md5(escape(body))); // https://github.com/pvorb/node-md5/issues/41
k.push(md5(JSON.stringify(style)));
k.push(md5(JSON.stringify(options)));
return k.join('_');
@@ -73,7 +74,7 @@ class MdToHtml {
renderImage_(attrs, options) {
const loadResource = async (id) => {
console.info('Loading resource: ' + id);
// console.info('Loading resource: ' + id);
// Initially set to to an empty object to make
// it clear that it is being loaded. Otherwise
@@ -156,27 +157,56 @@ class MdToHtml {
}
}
renderTokens_(tokens, options) {
customCodeHandler_(language) {
if (!language) return null;
const handlers = {};
handlers['katex'] = new MdToHtml_Katex();
return language in handlers ? handlers[language] : null;
}
parseInlineCodeLanguage_(content) {
const m = content.match(/^\{\.([a-zA-Z0-9]+)\}/);
if (m && m.length >= 2) {
const language = m[1];
return {
language: language,
newContent: content.substr(language.length + 3),
};
}
return null;
}
renderTokens_(markdownIt, tokens, options) {
let output = [];
let previousToken = null;
let anchorAttrs = [];
let extraCssBlocks = {};
for (let i = 0; i < tokens.length; i++) {
const t = tokens[i];
let t = tokens[i];
const nextToken = i < tokens.length ? tokens[i+1] : null;
let tag = t.tag;
let openTag = null;
let closeTag = null;
let attrs = t.attrs ? t.attrs : [];
let tokenContent = t.content ? t.content : null;
const isCodeBlock = tag === 'code' && t.block;
const isInlineCode = t.type === 'code_inline';
const codeBlockLanguage = t && t.info ? t.info : null;
let codeBlockHandler = null;
// if (t.map) attrs.push(['data-map', t.map.join(':')]);
if (isCodeBlock) codeBlockHandler = this.customCodeHandler_(codeBlockLanguage);
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
// Markdown-it render list items as <li><p>Text<p></li> which makes it
// complicated to style and layout the HTML, so we remove this extra
// <p> here and below in closeTag.
openTag = null;
} else if (isInlineCode) {
openTag = null;
} else if (tag && t.type.indexOf('_open') >= 0) {
openTag = tag;
} else if (tag && t.type.indexOf('_close') >= 0) {
@@ -186,7 +216,11 @@ class MdToHtml {
} else if (t.type === 'link_open') {
openTag = 'a';
} else if (isCodeBlock) {
openTag = 'pre';
if (codeBlockHandler) {
openTag = null;
} else {
openTag = 'pre';
}
}
if (openTag) {
@@ -201,12 +235,26 @@ class MdToHtml {
if (isCodeBlock) {
const codeAttrs = ['code'];
if (t.info) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
if (!codeBlockHandler) {
if (codeBlockLanguage) codeAttrs.push(t.info); // t.info contains the language when the token is a codeblock
output.push('<code class="' + codeAttrs.join(' ') + '">');
}
} else if (isInlineCode) {
const result = this.parseInlineCodeLanguage_(tokenContent);
if (result) {
codeBlockHandler = this.customCodeHandler_(result.language);
tokenContent = result.newContent;
}
if (!codeBlockHandler) {
output.push('<code>');
}
}
if (codeBlockHandler) codeBlockHandler.loadAssets();
if (t.type === 'image') {
if (t.content) attrs.push(['title', t.content]);
if (tokenContent) attrs.push(['title', tokenContent]);
output.push(this.renderImage_(attrs, options));
} else if (t.type === 'softbreak') {
output.push('<br/>');
@@ -214,11 +262,15 @@ class MdToHtml {
output.push('<hr/>');
} else {
if (t.children) {
const parsedChildren = this.renderTokens_(t.children, options);
const parsedChildren = this.renderTokens_(markdownIt, t.children, options);
output = output.concat(parsedChildren);
} else {
if (t.content) {
output.push(htmlentities(t.content));
if (tokenContent) {
if ((isCodeBlock || isInlineCode) && codeBlockHandler) {
output = codeBlockHandler.processContent(output, tokenContent);
} else {
output.push(htmlentities(tokenContent));
}
}
}
}
@@ -230,10 +282,18 @@ class MdToHtml {
} else if (tag && t.type.indexOf('inline') >= 0) {
closeTag = openTag;
} else if (isCodeBlock) {
closeTag = openTag;
if (!codeBlockHandler) closeTag = openTag;
}
if (isCodeBlock) output.push('</code>');
if (isCodeBlock) {
if (!codeBlockHandler) {
output.push('</code>');
}
} else if (isInlineCode) {
if (!codeBlockHandler) {
output.push('</code>');
}
}
if (closeTag) {
if (closeTag === 'a') {
@@ -243,8 +303,28 @@ class MdToHtml {
}
}
if (codeBlockHandler) {
const extraCss = codeBlockHandler.extraCss();
const name = codeBlockHandler.name();
if (extraCss && !(name in extraCssBlocks)) {
extraCssBlocks[name] = extraCss;
}
}
previousToken = t;
}
// Insert the extra CSS at the top of the HTML
const temp = ['<style>'];
for (let n in extraCssBlocks) {
if (!extraCssBlocks.hasOwnProperty(n)) continue;
temp.push(extraCssBlocks[n]);
}
temp.push('</style>');
output = temp.concat(output);
return output.join('');
}
@@ -260,7 +340,6 @@ class MdToHtml {
breaks: true,
linkify: true,
});
const env = {};
// Hack to make checkboxes clickable. Ideally, checkboxes should be parsed properly in
// renderTokens_(), but for now this hack works. Marking it with HORRIBLE_HACK so
@@ -278,12 +357,14 @@ class MdToHtml {
}
}
const env = {};
const tokens = md.parse(body, env);
let renderedBody = this.renderTokens_(md, tokens, options);
// console.info(body);
// console.info(tokens);
let renderedBody = this.renderTokens_(tokens, options);
// console.info(renderedBody);
if (HORRIBLE_HACK) {
let loopCount = 0;
@@ -374,9 +455,14 @@ class MdToHtml {
width: auto;
max-width: 100%;
}
.katex .mfrac .frac-line:before {
/* top: 50%; */
/* padding-bottom: .7em; */
}
`;
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>';
const styleHtml = '<style>' + normalizeCss + "\n" + css + '</style>'; //+ '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css">';
const output = styleHtml + renderedBody;

View File

@@ -0,0 +1,44 @@
const { shim } = require('lib/shim');
const katex = require('katex');
const katexCss = require('lib/csstojs/katex.css.js');
const Setting = require('lib/models/Setting');
class MdToHtml_Katex {
name() {
return 'katex';
}
processContent(renderedTokens, content) {
try {
const renderered = katex.renderToString(content);
renderedTokens.push(renderered);
} catch (error) {
renderedTokens.push('Cannot render Katex content: ' + error.message);
}
return renderedTokens;
}
extraCss() {
return katexCss;
}
async loadAssets() {
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
// them as an asset and copy them from there (or load them from there by modifying Katex CSS), but for now that will do.
if (shim.isReactNative()) {
// Fonts must go under the resourceDir directory because this is the baseUrl of NoteBodyViewer
const baseDir = Setting.value('resourceDir');
await shim.fsDriver().mkdir(baseDir + '/fonts');
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Main-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Main-Regular.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
}
}
}
module.exports = MdToHtml_Katex;

View File

@@ -0,0 +1,54 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetNextcloud extends BaseSyncTarget {
static id() {
return 5;
}
constructor(db, options = null) {
super(db, options);
// this.authenticated_ = false;
}
static targetName() {
return 'nextcloud';
}
static label() {
return _('Nextcloud (Beta)');
}
isAuthenticated() {
return true;
//return this.authenticated_;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.5.path'),
username: () => Setting.value('sync.5.username'),
password: () => Setting.value('sync.5.password'),
};
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetNextcloud.id());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetNextcloud;

View File

@@ -9,15 +9,15 @@ const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
class SyncTargetOneDrive extends BaseSyncTarget {
static id() {
return 3;
}
constructor(db, options = null) {
super(db, options);
this.api_ = null;
}
static id() {
return 3;
}
static targetName() {
return 'onedrive';
}
@@ -38,6 +38,10 @@ class SyncTargetOneDrive extends BaseSyncTarget {
return parameters().oneDrive;
}
authRouteName() {
return 'OneDriveLogin';
}
api() {
if (this.api_) return this.api_;

View File

@@ -23,6 +23,18 @@ class SyncTargetRegistry {
throw new Error('Name not found: ' + name);
}
static idToMetadata(id) {
for (let n in this.reg_) {
if (!this.reg_.hasOwnProperty(n)) continue;
if (this.reg_[n].id === id) return this.reg_[n];
}
throw new Error('ID not found: ' + id);
}
static idToName(id) {
return this.idToMetadata(id).name;
}
static idAndLabelPlainObject() {
let output = {};
for (let n in this.reg_) {

View File

@@ -0,0 +1,52 @@
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { _ } = require('lib/locale.js');
const Setting = require('lib/models/Setting.js');
const { FileApi } = require('lib/file-api.js');
const { Synchronizer } = require('lib/synchronizer.js');
const WebDavApi = require('lib/WebDavApi');
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
class SyncTargetWebDAV extends BaseSyncTarget {
static id() {
return 6;
}
constructor(db, options = null) {
super(db, options);
}
static targetName() {
return 'webdav';
}
static label() {
return _('WebDAV (Beta)');
}
isAuthenticated() {
return true;
}
async initFileApi() {
const options = {
baseUrl: () => Setting.value('sync.6.path'),
username: () => Setting.value('sync.6.username'),
password: () => Setting.value('sync.6.password'),
};
const api = new WebDavApi(options);
const driver = new FileApiDriverWebDav(api);
const fileApi = new FileApi('', driver);
fileApi.setSyncTargetId(SyncTargetWebDAV.id());
fileApi.setLogger(this.logger());
return fileApi;
}
async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}
module.exports = SyncTargetWebDAV;

View File

@@ -0,0 +1,245 @@
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim.js');
const parseXmlString = require('xml2js').parseString;
const JoplinError = require('lib/JoplinError');
const URL = require('url-parse');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const base64 = require('base-64');
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
// implementations use the same namespaces. If not, extra processing can be done in `nameProcessor`, for
// example to convert a custom namespace to "d:" so that it can be used by the rest of the code.
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
class WebDavApi {
constructor(options) {
this.logger_ = new Logger();
this.options_ = options;
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
authToken() {
if (!this.options_.username() || !this.options_.password()) return null;
return base64.encode(this.options_.username() + ':' + this.options_.password());
}
baseUrl() {
return this.options_.baseUrl();
}
relativeBaseUrl() {
const url = new URL(this.baseUrl());
return url.pathname + url.query;
}
async xmlToJson(xml) {
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
const nameProcessor = (name) => {
if (name.indexOf('xmlns:') !== 0) {
// Check if the current name is within the DAV namespace. If it is, normalise it
// by moving it to the "d:" namespace, which is what all the functions are using.
const p = name.split(':');
if (p.length == 2) {
const ns = p[0];
if (davNamespaces.indexOf(ns) >= 0) {
name = 'd:' + p[1];
}
}
}
return name.toLowerCase();
};
const attrValueProcessor = (value, name) => {
if (value.toLowerCase() === 'dav:') {
const p = name.split(':');
davNamespaces.push(p[p.length - 1]);
}
}
const options = {
tagNameProcessors: [nameProcessor],
attrNameProcessors: [nameProcessor],
attrValueProcessors: [attrValueProcessor]
}
return new Promise((resolve, reject) => {
parseXmlString(xml, options, (error, result) => {
if (error) {
resolve(null); // Error handled by caller which will display the XML text (or plain text) if null is returned from this function
return;
}
resolve(result);
});
});
}
valueFromJson(json, keys, type) {
let output = json;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// console.info(key, typeof key, typeof output, typeof output === 'object' && (key in output), Array.isArray(output));
if (typeof key === 'number' && !Array.isArray(output)) return null;
if (typeof key === 'string' && (typeof output !== 'object' || !(key in output))) return null;
output = output[key];
}
if (type === 'string') {
// If the XML has not attribute the value is directly a string
// If the XML node has attributes, the value is under "_".
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
if (typeof output === 'object' && '_' in output) output = output['_'];
if (typeof output !== 'string') return null;
return output;
}
if (type === 'object') {
if (!Array.isArray(output) && typeof output === 'object') return output;
return null;
}
if (type === 'array') {
return Array.isArray(output) ? output : null;
}
return null;
}
stringFromJson(json, keys) {
return this.valueFromJson(json, keys, 'string');
}
objectFromJson(json, keys) {
return this.valueFromJson(json, keys, 'object');
}
arrayFromJson(json, keys) {
return this.valueFromJson(json, keys, 'array');
}
async execPropFind(path, depth, fields = null, options = null) {
if (fields === null) fields = ['d:getlastmodified'];
let fieldsXml = '';
for (let i = 0; i < fields.length; i++) {
fieldsXml += '<' + fields[i] + '/>';
}
// To find all available properties:
//
// const body=`<?xml version="1.0" encoding="utf-8" ?>
// <propfind xmlns="DAV:">
// <propname/>
// </propfind>`;
const body = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:">
<d:prop xmlns:oc="http://owncloud.org/ns">
` + fieldsXml + `
</d:prop>
</d:propfind>`;
return this.exec('PROPFIND', path, body, { 'Depth': depth }, options);
}
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
// <d:propfind xmlns:d="DAV:">
// <d:prop xmlns:oc="http://owncloud.org/ns">
// <d:getlastmodified/>
// </d:prop>
// </d:propfind>'
async exec(method, path = '', body = null, headers = null, options = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = 'json';
if (!options.target) options.target = 'string';
const authToken = this.authToken();
if (authToken) headers['Authorization'] = 'Basic ' + authToken;
if (typeof body === 'string') headers['Content-Length'] = body.length;
const fetchOptions = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
const url = this.baseUrl() + '/' + path;
let response = null;
// console.info('WebDAV', method + ' ' + url, headers, options);
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
response = await shim.fetch(url, fetchOptions);
} else { // file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = () => {
return (responseText + '').substr(0, 1024);
}
let responseJson_ = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
responseJson_ = await this.xmlToJson(responseText);
if (!responseJson_) throw new JoplinError('Cannot parse JSON response: ' + shortResponseText(), response.status);
return responseJson_;
}
if (!response.ok) {
// When using fetchBlob we only get a string (not xml or json) back
if (options.target === 'file') throw new JoplinError(shortResponseText(), response.status);
const json = await loadResponseJson();
if (json && json['d:error']) {
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join("\n") : shortResponseText();
throw new JoplinError(method + ' ' + path + ': ' + message + ' (' + code + ')', response.status);
}
throw new JoplinError(method + ' ' + path + ': ' + shortResponseText(), response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
// null responses are possible, for example for DELETE calls
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw new Error('Not a valid JSON response: ' + shortResponseText());
return output;
}
}
module.exports = WebDavApi;

View File

@@ -13,7 +13,7 @@ const globalStyle = {
fontSizeSmaller: 14,
dividerColor: "#dddddd",
selectedColor: '#e5e5e5',
disabledOpacity: 0.3,
disabledOpacity: 0.2,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",

View File

@@ -51,7 +51,19 @@ class NoteBodyViewer extends Component {
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
};
const html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);
html = `
<!DOCTYPE html>
<html>
<head>
</head>
<body>
` + html + `
</body>
</html>
`;
let webViewStyle = {}
// On iOS, the onLoadEnd() event is never fired so always

View File

@@ -372,7 +372,7 @@ class ScreenHeaderComponent extends Component {
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
for (let i = 0; i < this.props.folders.length; i++) {
let f = this.props.folders[i];
output.push({ label: f.title, value: f.id });
output.push({ label: Folder.displayTitle(f), value: f.id });
}
output.sort((a, b) => {
if (a.value === null) return -1;

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView } = require('react-native');
const { TouchableOpacity, Linking, View, Switch, Slider, StyleSheet, Text, Button, ScrollView, TextInput } = require('react-native');
const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js');
const { _, setLocale } = require('lib/locale.js');
@@ -17,6 +17,23 @@ class ConfigScreenComponent extends BaseScreenComponent {
constructor() {
super();
this.styles_ = {};
this.state = {
settings: {},
settingsChanged: false,
};
this.saveButton_press = () => {
for (let n in this.state.settings) {
if (!this.state.settings.hasOwnProperty(n)) continue;
Setting.setValue(n, this.state.settings[n]);
}
this.setState({settingsChanged:false});
};
}
componentWillMount() {
this.setState({ settings: this.props.settings });
}
styles() {
@@ -83,7 +100,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
let output = null;
const updateSettingValue = (key, value) => {
Setting.setValue(key, value);
const settings = Object.assign({}, this.state.settings);
settings[key] = value;
this.setState({
settings: settings,
settingsChanged: true,
});
console.info(settings['sync.5.path']);
}
const md = Setting.settingMetadata(key);
@@ -135,23 +159,33 @@ class ConfigScreenComponent extends BaseScreenComponent {
<Slider key="control" style={this.styles().settingControl} value={value} onValueChange={(value) => updateSettingValue(key, value)} />
</View>
);
} else if (md.type == Setting.TYPE_STRING) {
return (
<View key={key} style={this.styles().settingContainer}>
<Text key="label" style={this.styles().settingText}>{md.label()}</Text>
<TextInput key="control" style={this.styles().settingControl} value={value} onChangeText={(value) => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
</View>
);
} else {
//throw new Error('Unsupported setting type: ' + setting.type);
//throw new Error('Unsupported setting type: ' + md.type);
}
return output;
}
render() {
const settings = this.props.settings;
const settings = this.state.settings;
const keys = Setting.keys(true, 'mobile');
let settingComps = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
//if (key == 'sync.target' && !settings.showAdvancedOptions) continue;
if (!Setting.isPublic(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);
@@ -173,11 +207,14 @@ class ConfigScreenComponent extends BaseScreenComponent {
</View>
);
//style={this.styles().body}
return (
<View style={this.rootStyle(this.props.theme).root}>
<ScreenHeader title={_('Configuration')}/>
<ScreenHeader
title={_('Configuration')}
showSaveButton={true}
saveButtonDisabled={!this.state.settingsChanged}
onSaveButtonPress={this.saveButton_press}
/>
<ScrollView >
{ settingComps }
</ScrollView>

View File

@@ -159,9 +159,15 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
const mkComps = [];
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
for (let i = 0; i < masterKeys.length; i++) {
const mk = masterKeys[i];
mkComps.push(this.renderMasterKey(i+1, mk));
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
}
const onToggleButtonClick = async () => {
@@ -183,6 +189,24 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
}
};
let nonExistingMasterKeySection = null;
if (nonExistingMasterKeyIds.length) {
const rows = [];
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
const id = nonExistingMasterKeyIds[i];
rows.push(<Text style={this.styles().normalText} key={id}>{id}</Text>);
}
nonExistingMasterKeySection = (
<View>
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
<Text style={this.styles().normalText}>{_('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.')}</Text>
<View style={{marginTop: 10}}>{rows}</View>
</View>
);
}
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
const toggleButton = !this.state.passwordPromptShow ? <View style={{marginTop: 10}}><Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button></View> : null;
@@ -204,6 +228,7 @@ class EncryptionConfigScreenComponent extends BaseScreenComponent {
{toggleButton}
{passwordPromptComp}
{mkComps}
{nonExistingMasterKeySection}
<View style={{flex:1, height: 20}}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
@@ -221,6 +246,7 @@ const EncryptionConfigScreen = connect(
passwords: state.settings['encryption.passwordCache'],
encryptionEnabled: state.settings['encryption.enabled'],
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
notLoadedMasterKeys: state.notLoadedMasterKeys,
};
}
)(EncryptionConfigScreenComponent)

View File

@@ -23,6 +23,7 @@ class LogScreenComponent extends BaseScreenComponent {
});
this.state = {
dataSource: ds,
showErrorsOnly: false,
};
this.styles_ = {};
}
@@ -62,13 +63,24 @@ class LogScreenComponent extends BaseScreenComponent {
this.resfreshLogEntries();
}
resfreshLogEntries() {
reg.logger().lastEntries(1000).then((entries) => {
resfreshLogEntries(showErrorsOnly = null) {
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR]
reg.logger().lastEntries(1000, { levels: levels }).then((entries) => {
const newDataSource = this.state.dataSource.cloneWithRows(entries);
this.setState({ dataSource: newDataSource });
});
}
toggleErrorsOnly() {
const showErrorsOnly = !this.state.showErrorsOnly;
this.setState({ showErrorsOnly: showErrorsOnly });
this.resfreshLogEntries(showErrorsOnly);
}
render() {
let renderRow = (item) => {
let textStyle = this.styles().rowText;
@@ -91,7 +103,14 @@ class LogScreenComponent extends BaseScreenComponent {
renderRow={renderRow}
enableEmptySections={true}
/>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
<View style={{flexDirection: 'row'}}>
<View style={{flex:1, marginRight: 5 }}>
<Button title={_("Refresh")} onPress={() => { this.resfreshLogEntries(); }}/>
</View>
<View style={{flex:1}}>
<Button title={this.state.showErrorsOnly ? _("Show all") : _("Errors only")} onPress={() => { this.toggleErrorsOnly(); }}/>
</View>
</View>
</View>
);
}

View File

@@ -30,6 +30,7 @@ shared.saveNoteButton_press = async function(comp) {
options.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note);
}
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title);
if (hasAutoTitle) {
note.title = Note.defaultTitle(note);
@@ -63,11 +64,33 @@ shared.saveNoteButton_press = async function(comp) {
note: note,
};
if (isNew) newState.newAndNoTitleChangeNoteId = note.id;
if (isNew && hasAutoTitle) newState.newAndNoTitleChangeNoteId = note.id;
comp.setState(newState);
if (isNew) Note.updateGeolocation(note.id);
if (isNew) {
Note.updateGeolocation(note.id).then((geoNote) => {
const stateNote = comp.state.note;
if (!stateNote || !geoNote) return;
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
// Geo-location for this note has been saved to the database however the properties
// are is not in the state so set them now.
const geoInfo = {
longitude: geoNote.longitude,
latitude: geoNote.latitude,
altitude: geoNote.altitude,
}
const modNote = Object.assign({}, stateNote, geoInfo);
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
comp.refreshNoteMetadata();
});
}
comp.refreshNoteMetadata();
if (isNew) {

View File

@@ -36,12 +36,17 @@ shared.synchronize_press = async function(comp) {
const action = comp.props.syncStarted ? 'cancel' : 'start';
if (!reg.syncTarget().isAuthenticated()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: 'OneDriveLogin',
});
return 'auth';
if (!reg.syncTarget().isAuthenticated()) {
if (reg.syncTarget().authRouteName()) {
comp.props.dispatch({
type: 'NAV_GO',
routeName: reg.syncTarget().authRouteName(),
});
return 'auth';
}
reg.logger().info('Not authentified with sync target - please check your credential.');
return 'error';
}
let sync = null;

View File

@@ -1,5 +1,5 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
@@ -44,8 +44,6 @@ class FileApiDriverLocal {
path: stat.path,
created_time: stat.birthtime.getTime(),
updated_time: stat.mtime.getTime(),
created_time_orig: stat.birthtime,
updated_time_orig: stat.mtime,
isDir: stat.isDirectory(),
};
}
@@ -68,41 +66,14 @@ class FileApiDriverLocal {
}
async delta(path, options) {
const itemIds = await options.allItemIdsHandler();
const getStatFn = async (path) => {
const stats = await this.fsDriver().readDirStats(path);
return this.metadataFromStats_(stats);
};
try {
const stats = await this.fsDriver().readDirStats(path);
let output = this.metadataFromStats_(stats);
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
let found = false;
for (let j = 0; j < output.length; j++) {
const item = output[j];
if (BaseItem.pathToId(item.path) == itemId) {
found = true;
break;
}
}
if (!found) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
return {
hasMore: false,
context: null,
items: output,
};
const output = await basicDelta(path, getStatFn, options);
return output;
} catch(error) {
throw this.fsErrorToJsError_(error, path);
}
@@ -253,6 +224,11 @@ class FileApiDriverLocal {
throw new Error('Not supported');
}
async clearRoot(baseDir) {
await this.fsDriver().remove(baseDir);
await this.fsDriver().mkdir(baseDir);
}
}
module.exports = { FileApiDriverLocal };

View File

@@ -1,5 +1,6 @@
const { time } = require('lib/time-utils.js');
const fs = require('fs-extra');
const { basicDelta } = require('lib/file-api');
class FileApiDriverMemory {
@@ -144,51 +145,25 @@ class FileApiDriverMemory {
}
async delta(path, options = null) {
let limit = 3;
let output = {
hasMore: false,
context: {},
items: [],
const getStatFn = async (path) => {
let output = this.items_.slice();
for (let i = 0; i < output.length; i++) {
const item = Object.assign({}, output[i]);
item.path = item.path.substr(path.length + 1);
output[i] = item;
}
return output;
};
let context = options ? options.context : null;
let fromTime = 0;
if (context) fromTime = context.fromTime;
let sortedItems = this.items_.slice().concat(this.deletedItems_);
sortedItems.sort((a, b) => {
if (a.updated_time < b.updated_time) return -1;
if (a.updated_time > b.updated_time) return +1;
return 0;
});
let hasMore = false;
let items = [];
let maxTime = 0;
for (let i = 0; i < sortedItems.length; i++) {
let item = sortedItems[i];
if (item.updated_time >= fromTime) {
item = Object.assign({}, item);
item.path = item.path.substr(path.length + 1);
items.push(item);
if (item.updated_time > maxTime) maxTime = item.updated_time;
}
if (items.length >= limit) {
hasMore = true;
break;
}
}
output.items = items;
output.hasMore = hasMore;
output.context = { fromTime: maxTime };
const output = await basicDelta(path, getStatFn, options);
return output;
}
clearRoot() {
this.items_ = [];
return Promise.resolve();
}
}
module.exports = { FileApiDriverMemory };

View File

@@ -189,6 +189,10 @@ class FileApiDriverOneDrive {
return this.pathCache_[path];
}
clearRoot() {
throw new Error('Not implemented');
}
async delta(path, options = null) {
let output = {
hasMore: false,

View File

@@ -0,0 +1,357 @@
const BaseItem = require('lib/models/BaseItem.js');
const { time } = require('lib/time-utils.js');
const { basicDelta } = require('lib/file-api');
const { rtrimSlashes, ltrimSlashes } = require('lib/path-utils.js');
const Entities = require('html-entities').AllHtmlEntities;
const html_entity_decode = (new Entities()).decode;
const { shim } = require('lib/shim');
const { basename } = require('lib/path-utils');
const JoplinError = require('lib/JoplinError');
class FileApiDriverWebDav {
constructor(api) {
this.api_ = api;
}
api() {
return this.api_;
}
async stat(path) {
try {
const result = await this.api().execPropFind(path, 0, [
'd:getlastmodified',
'd:resourcetype',
'd:getcontentlength', // Remove this once PUT call issue is sorted out
]);
const resource = this.api().objectFromJson(result, ['d:multistatus', 'd:response', 0]);
return this.statFromResource_(resource, path);
} catch (error) {
if (error.code === 404) return null;
throw error;
}
}
statFromResource_(resource, path) {
const isCollection = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:resourcetype', 0, 'd:collection', 0]);
const lastModifiedString = this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getlastmodified', 0]);
const sizeDONOTUSE = Number(this.api().stringFromJson(resource, ['d:propstat', 0, 'd:prop', 0, 'd:getcontentlength', 0]));
if (isNaN(sizeDONOTUSE)) throw new Error('Cannot get content size: ' + JSON.stringify(resource));
if (!lastModifiedString) throw new Error('Could not get lastModified date: ' + JSON.stringify(resource));
const lastModifiedDate = new Date(lastModifiedString);
if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + lastModifiedString);
return {
path: path,
created_time: lastModifiedDate.getTime(),
updated_time: lastModifiedDate.getTime(),
isDir: isCollection === '',
sizeDONOTUSE: sizeDONOTUSE, // This property is used only for the WebDAV PUT hack (see below) so mark it as such so that it can be removed with the hack later on.
};
}
async setTimestamp(path, timestampMs) {
throw new Error('Not implemented'); // Not needed anymore
}
async delta(path, options) {
const getDirStats = async (path) => {
const result = await this.list(path);
return result.items;
};
return await basicDelta(path, getDirStats, options);
}
// A file href, as found in the result of a PROPFIND, can be either an absolute URL or a
// relative URL (an absolute URL minus the protocol and domain), while the sync algorithm
// works with paths relative to the base URL.
hrefToRelativePath_(href, baseUrl, relativeBaseUrl) {
let output = '';
if (href.indexOf(baseUrl) === 0) {
output = href.substr(baseUrl.length);
} else if (href.indexOf(relativeBaseUrl) === 0) {
output = href.substr(relativeBaseUrl.length);
} else {
throw new Error('href ' + href + ' not in baseUrl ' + baseUrl + ' nor relativeBaseUrl ' + relativeBaseUrl);
}
return rtrimSlashes(ltrimSlashes(output));
}
statsFromResources_(resources) {
const relativeBaseUrl = this.api().relativeBaseUrl();
const baseUrl = this.api().baseUrl();
let output = [];
for (let i = 0; i < resources.length; i++) {
const resource = resources[i];
const href = this.api().stringFromJson(resource, ['d:href', 0]);
const path = this.hrefToRelativePath_(href, baseUrl, relativeBaseUrl);
// if (href.indexOf(relativeBaseUrl) !== 0) throw new Error('Path "' + href + '" not inside base URL: ' + relativeBaseUrl);
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
if (path === '') continue; // The list of resources includes the root dir too, which we don't want
const stat = this.statFromResource_(resources[i], path);
output.push(stat);
}
return output;
}
async list(path, options) {
// const relativeBaseUrl = this.api().relativeBaseUrl();
// function parsePropFindXml(xmlString) {
// return new Promise(async (resolve, reject) => {
// const saxOptions = {};
// const saxParser = require('sax').parser(false, { position: false });
// let stats = [];
// let currentStat = null;
// let currentText = '';
// // When this is on, the tags from the bloated XML string are replaced by shorter ones,
// // which makes parsing about 25% faster. However it's a bit of a hack so keep it as
// // an option so that it can be disabled if it causes problems.
// const optimizeXml = true;
// const tagResponse = optimizeXml ? 'd:r' : 'd:response';
// const tagGetLastModified = optimizeXml ? 'd:glm' : 'd:getlastmodified';
// const tagPropStat = optimizeXml ? 'd:ps' : 'd:propstat';
// const replaceUrls = optimizeXml;
// saxParser.onerror = function (error) {
// reject(new Error(e.toString()));
// };
// saxParser.ontext = function (t) {
// currentText += t;
// };
// saxParser.onopentag = function (node) {
// const tagName = node.name.toLowerCase();
// currentText = '';
// if (tagName === tagResponse) {
// currentStat = { isDir: false };
// }
// };
// saxParser.onclosetag = function(tagName) {
// tagName = tagName.toLowerCase();
// if (tagName === tagResponse) {
// if (currentStat.path) { // The list of resources includes the root dir too, which we don't want
// if (!currentStat.updated_time) throw new Error('Resource does not have a getlastmodified prop');
// stats.push(currentStat);
// }
// currentStat = null;
// }
// if (tagName === 'd:href') {
// const href = currentText;
// if (replaceUrls) {
// currentStat.path = rtrimSlashes(ltrimSlashes(href));
// } else {
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// currentStat.path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// }
// }
// if (tagName === tagGetLastModified) {
// const lastModifiedDate = new Date(currentText);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + currentText);
// currentStat.updated_time = lastModifiedDate.getTime();
// currentStat.created_time = currentStat.updated_time;
// }
// if (tagName === 'd:collection') {
// currentStat.isDir = true;
// }
// currentText = '';
// }
// saxParser.onend = function () {
// resolve(stats);
// };
// if (optimizeXml) {
// xmlString = xmlString.replace(/<d:status>HTTP\/1\.1 200 OK<\/d:status>/ig, '');
// xmlString = xmlString.replace(/<d:resourcetype\/>/ig, '');
// xmlString = xmlString.replace(/d:getlastmodified/ig, tagGetLastModified);
// xmlString = xmlString.replace(/d:response/ig, tagResponse);
// xmlString = xmlString.replace(/d:propstat/ig, tagPropStat);
// if (replaceUrls) xmlString = xmlString.replace(new RegExp(relativeBaseUrl, 'gi'), '');
// }
// let idx = 0;
// let size = 1024 * 100;
// while (true) {
// sub = xmlString.substr(idx, size);
// if (!sub.length) break;
// saxParser.write(sub);
// idx += size;
// //await time.msleep(500);
// }
// saxParser.close();
// //saxParser.write(xmlString).close();
// });
// }
// For performance reasons, the response of the PROPFIND call is manually parsed with a regex below
// instead of being processed by xml2json like the other WebDAV responses. This is over 2 times faster
// and it means the mobile app does not freeze during sync.
// async function parsePropFindXml2(xmlString) {
// const regex = /<d:response>[\S\s]*?<d:href>([\S\s]*?)<\/d:href>[\S\s]*?<d:getlastmodified>(.*?)<\/d:getlastmodified>/g;
// let output = [];
// let match = null;
// while (match = regex.exec(xmlString)) {
// const href = html_entity_decode(match[1]);
// if (href.indexOf(relativeBaseUrl) < 0) throw new Error('Path not inside base URL: ' + relativeBaseUrl); // Normally not possible
// const path = rtrimSlashes(ltrimSlashes(href.substr(relativeBaseUrl.length)));
// if (!path) continue; // The list of resources includes the root dir too, which we don't want
// const lastModifiedDate = new Date(match[2]);
// if (isNaN(lastModifiedDate.getTime())) throw new Error('Invalid date: ' + match[2]);
// output.push({
// path: path,
// updated_time: lastModifiedDate.getTime(),
// created_time: lastModifiedDate.getTime(),
// isDir: !BaseItem.isSystemPath(path),
// });
// }
// return output;
// }
// const resultXml = await this.api().execPropFind(path, 1, [
// 'd:getlastmodified',
// //'d:resourcetype', // Include this to use parsePropFindXml()
// ], { responseFormat: 'text' });
// const stats = await parsePropFindXml2(resultXml);
// return {
// items: stats,
// hasMore: false,
// context: null,
// };
const result = await this.api().execPropFind(path, 1, [
'd:getlastmodified',
'd:resourcetype',
]);
const resources = this.api().arrayFromJson(result, ['d:multistatus', 'd:response']);
const stats = this.statsFromResources_(resources)
return {
items: stats,
hasMore: false,
context: null,
};
}
async get(path, options) {
if (!options) options = {};
if (!options.responseFormat) options.responseFormat = 'text';
try {
const response = await this.api().exec('GET', path, null, null, options);
// This is awful but instead of a 404 Not Found, Microsoft IIS returns an HTTP code 200
// with a response body "The specified file doesn't exist." for non-existing files,
// so we need to check for this.
if (response === "The specified file doesn't exist.") throw new JoplinError(response, 404);
return response;
} catch (error) {
if (error.code !== 404) throw error;
}
}
async mkdir(path) {
try {
await this.api().exec('MKCOL', path);
} catch (error) {
if (error.code === 405) return; // 405 means that the collection already exists (Method Not Allowed)
// 409 should only be returned if a parent path does not exists (eg. when trying to create a/b/c when a/b does not exist)
// however non-compliant servers (eg. Microsoft IIS) also return this code when the directory already exists. So here, if
// we get this code, verify that indeed the directory already exists and exit if it does.
if (error.code === 409) {
const stat = await this.stat(path);
if (stat) return;
}
throw error;
}
}
async put(path, content, options = null) {
// In theory, if a client doesn't complete an upload, the file will not appear in the Nextcloud app. Likewise if
// the server interrupts the upload midway, the client should receive some kind of error and try uploading the
// file again next time. At the very least the file should not appear half-uploaded on the server. In practice
// however it seems some files might end up half uploaded on the server (at least on ocloud.de) so, for now,
// instead of doing a simple PUT, we do it to a temp file on Nextcloud, then check the file size and, if it
// matches, move it its actual place (hoping the server won't mess up and only copy half of the file).
// This is innefficient so once the bug is better understood it should hopefully be possible to go back to
// using a single PUT call.
let contentSize = 0;
if (content) contentSize = content.length;
if (options && options.path) {
const stat = await shim.fsDriver().stat(options.path);
contentSize = stat.size;
}
const tempPath = this.fileApi_.tempDirName() + '/' + basename(path) + '_' + Date.now();
await this.api().exec('PUT', tempPath, content, null, options);
const stat = await this.stat(tempPath);
if (stat.sizeDONOTUSE != contentSize) {
// await this.delete(tempPath);
throw new Error('WebDAV PUT - Size check failed for ' + tempPath + ' Expected: ' + contentSize + '. Found: ' + stat.sizeDONOTUSE);
}
await this.move(tempPath, path);
}
async delete(path) {
try {
await this.api().exec('DELETE', path);
} catch (error) {
if (error.code !== 404) throw error;
}
}
async move(oldPath, newPath) {
await this.api().exec('MOVE', oldPath, null, {
'Destination': this.api().baseUrl() + '/' + newPath,
'Overwrite': 'T',
});
}
format() {
throw new Error('Not supported');
}
async clearRoot() {
await this.delete('');
await this.mkdir('');
}
}
module.exports = { FileApiDriverWebDav };

View File

@@ -1,5 +1,9 @@
const { isHidden } = require('lib/path-utils.js');
const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim');
const BaseItem = require('lib/models/BaseItem.js');
const JoplinError = require('lib/JoplinError');
const ArrayUtils = require('lib/ArrayUtils');
class FileApi {
@@ -8,6 +12,21 @@ class FileApi {
this.driver_ = driver;
this.logger_ = new Logger();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
}
tempDirName() {
if (this.tempDirName_ === null) throw Error('Temp dir not set!');
return this.tempDirName_;
}
setTempDirName(v) {
this.tempDirName_ = v;
}
fsDriver() {
return shim.fsDriver();
}
driver() {
@@ -32,9 +51,10 @@ class FileApi {
}
fullPath_(path) {
let output = this.baseDir_;
if (path != '') output += '/' + path;
return output;
let output = [];
if (this.baseDir_) output.push(this.baseDir_);
if (path) output.push(path);
return output.join('/');
}
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
@@ -57,6 +77,7 @@ class FileApi {
});
}
// Deprectated
setTimestamp(path, timestampMs) {
this.logger().debug('setTimestamp ' + this.fullPath_(path));
return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
@@ -83,8 +104,13 @@ class FileApi {
return this.driver_.get(this.fullPath_(path), options);
}
put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path));
async put(path, content, options = null) {
this.logger().debug('put ' + this.fullPath_(path), options);
if (options && options.source === 'file') {
if (!await this.fsDriver().exists(options.path)) throw new JoplinError('File not found: ' + options.path, 'fileNotFound');
}
return this.driver_.put(this.fullPath_(path), content, options);
}
@@ -93,15 +119,21 @@ class FileApi {
return this.driver_.delete(this.fullPath_(path));
}
// Deprectated
move(oldPath, newPath) {
this.logger().debug('move ' + this.fullPath_(oldPath) + ' => ' + this.fullPath_(newPath));
return this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath));
}
// Deprectated
format() {
return this.driver_.format();
}
clearRoot() {
return this.driver_.clearRoot(this.baseDir_);
}
delta(path, options = null) {
this.logger().debug('delta ' + this.fullPath_(path));
return this.driver_.delta(this.fullPath_(path), options);
@@ -109,4 +141,120 @@ class FileApi {
}
module.exports = { FileApi };
function basicDeltaContextFromOptions_(options) {
let output = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
statIdsCache: null,
deletedItemsProcessed: false,
};
if (!options || !options.context) return output;
const d = new Date(options.context.timestamp);
output.timestamp = isNaN(d.getTime()) ? 0 : options.context.timestamp;
output.filesAtTimestamp = Array.isArray(options.context.filesAtTimestamp) ? options.context.filesAtTimestamp.slice() : [];
output.statsCache = options.context && options.context.statsCache ? options.context.statsCache : null;
output.statIdsCache = options.context && options.context.statIdsCache ? options.context.statIdsCache : null;
output.deletedItemsProcessed = options.context && ('deletedItemsProcessed' in options.context) ? options.context.deletedItemsProcessed : false;
return output;
}
// This is the basic delta algorithm, which can be used in case the cloud service does not have
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
// the file system do not.
async function basicDelta(path, getDirStatFn, options) {
const outputLimit = 1000;
const itemIds = await options.allItemIdsHandler();
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
const context = basicDeltaContextFromOptions_(options);
let newContext = {
timestamp: context.timestamp,
filesAtTimestamp: context.filesAtTimestamp.slice(),
statsCache: context.statsCache,
statIdsCache: context.statIdsCache,
deletedItemsProcessed: context.deletedItemsProcessed,
};
// Stats are cached until all items have been processed (until hasMore is false)
if (newContext.statsCache === null) {
newContext.statsCache = await getDirStatFn(path);
newContext.statsCache.sort(function(a, b) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache.map((item) => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
let output = [];
// Find out which files have been changed since the last time. Note that we keep
// both the timestamp of the most recent change, *and* the items that exactly match
// this timestamp. This to handle cases where an item is modified while this delta
// function is running. For example:
// t0: Item 1 is changed
// t0: Sync items - run delta function
// t0: While delta() is running, modify Item 2
// Since item 2 was modified within the same millisecond, it would be skipped in the
// next sync if we relied exclusively on a timestamp.
for (let i = 0; i < newContext.statsCache.length; i++) {
const stat = newContext.statsCache[i];
if (stat.isDir) continue;
if (stat.updated_time < context.timestamp) continue;
// Special case for items that exactly match the timestamp
if (stat.updated_time === context.timestamp) {
if (context.filesAtTimestamp.indexOf(stat.path) >= 0) continue;
}
if (stat.updated_time > newContext.timestamp) {
newContext.timestamp = stat.updated_time;
newContext.filesAtTimestamp = [];
}
newContext.filesAtTimestamp.push(stat.path);
output.push(stat);
if (output.length >= outputLimit) break;
}
if (!newContext.deletedItemsProcessed) {
// Find out which items have been deleted on the sync target by comparing the items
// we have to the items on the target.
// Note that when deleted items are processed it might result in the output having
// more items than outputLimit. This is acceptable since delete operations are cheap.
let deletedItems = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
output = output.concat(deletedItems);
}
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) newContext.statsCache = null;
return {
hasMore: hasMore,
context: newContext,
items: output,
};
}
module.exports = { FileApi, basicDelta };

View File

@@ -3,21 +3,50 @@ const { time } = require('lib/time-utils.js');
class FsDriverNode {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += '. Path: ' + path;
let output = new Error(msg);
if (error.code) output.code = error.code;
return output;
}
appendFileSync(path, string) {
return fs.appendFileSync(path, string);
}
appendFile(path, string, encoding = 'base64') {
return fs.appendFile(path, string, { encoding: encoding });
async appendFile(path, string, encoding = 'base64') {
try {
return await fs.appendFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeBinaryFile(path, content) {
let buffer = new Buffer(content);
return fs.writeFile(path, buffer);
async writeBinaryFile(path, content) {
try {
let buffer = new Buffer(content);
return await fs.writeFile(path, buffer);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
writeFile(path, string, encoding = 'base64') {
return fs.writeFile(path, string, { encoding: encoding });
async writeFile(path, string, encoding = 'base64') {
try {
return await fs.writeFile(path, string, { encoding: encoding });
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
// same as rm -rf
async remove(path) {
try {
return await fs.remove(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async move(source, dest) {
@@ -35,7 +64,7 @@ class FsDriverNode {
await time.sleep(1);
continue;
}
throw error;
throw this.fsErrorToJsError_(error);
}
}
@@ -77,12 +106,20 @@ class FsDriverNode {
return output;
}
open(path, mode) {
return fs.open(path, mode);
async open(path, mode) {
try {
return await fs.open(path, mode);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
close(handle) {
return fs.close(handle);
async close(handle) {
try {
return await fs.close(handle);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
readFile(path, encoding = 'utf8') {

View File

@@ -14,6 +14,11 @@ class FsDriverRN {
return RNFS.writeFile(path, string, encoding);
}
// same as rm -rf
async remove(path) {
throw new Error('Not implemented');
}
writeBinaryFile(path, content) {
throw new Error('Not implemented');
}

View File

@@ -297,4 +297,4 @@ function _(s, ...args) {
return sprintf(result, ...args);
}
module.exports = { _, supportedLocales, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode };
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@@ -25,6 +25,10 @@ class Logger {
return this.level_;
}
targets() {
return this.targets_;
}
clearTargets() {
this.targets_.clear();
}
@@ -81,11 +85,15 @@ class Logger {
}
// Only for database at the moment
async lastEntries(limit = 100) {
async lastEntries(limit = 100, options = null) {
if (options === null) options = {};
if (!options.levels) options.levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
if (!options.levels.length) return [];
for (let i = 0; i < this.targets_.length; i++) {
const target = this.targets_[i];
if (target.type == 'database') {
let sql = 'SELECT * FROM logs ORDER BY timestamp DESC';
let sql = 'SELECT * FROM logs WHERE level IN (' + options.levels.join(',') + ') ORDER BY timestamp DESC';
if (limit !== null) sql += ' LIMIT ' + limit
return await target.database.selectAll(sql);
}

View File

@@ -7,6 +7,7 @@ const { mime } = require('lib/mime-utils.js');
const { filename } = require('lib/path-utils.js');
const { FsDriverDummy } = require('lib/fs-driver-dummy.js');
const { markdownUtils } = require('lib/markdown-utils.js');
const JoplinError = require('lib/JoplinError');
class Resource extends BaseItem {
@@ -35,7 +36,7 @@ class Resource extends BaseItem {
static async serialize(item, type = null, shownKeys = null) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
fieldNames.push('type_');
//fieldNames = ArrayUtils.removeElement(fieldNames, 'encryption_blob_encrypted');
return super.serialize(item, 'resource', fieldNames);
}
@@ -92,7 +93,13 @@ class Resource extends BaseItem {
const encryptedPath = this.fullPath(resource, true);
if (resource.encryption_blob_encrypted) return { path: encryptedPath, resource: resource };
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
try {
await this.encryptionService().encryptFile(plainTextPath, encryptedPath);
} catch (error) {
if (error.code === 'ENOENT') throw new JoplinError('File not found:' + error.toString(), 'fileNotFound');
throw error;
}
const resourceCopy = Object.assign({}, resource);
resourceCopy.encryption_blob_encrypted = 1;

View File

@@ -60,9 +60,24 @@ class Setting extends BaseModel {
// })},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Show uncompleted todos on top of the lists') },
'trackLocation': { value: true, type: Setting.TYPE_BOOL, public: true, label: () => _('Save geo-location with notes') },
'newTodoFocus': { value: 'title', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new to-do:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'newNoteFocus': { value: 'body', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('When creating a new note:'), options: () => {
return {
'title': _('Focus title'),
'body': _('Focus body'),
};
}},
'showTrayIcon': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon') },
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false },
'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Set application zoom percentage'), minimum: "50", maximum: "500", step: "10"},
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
@@ -75,12 +90,27 @@ class Setting extends BaseModel {
};
}},
'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] },
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') },
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. If synchronising with the file system, set `sync.2.path` to specify the target directory.'), options: () => {
'sync.target': { value: SyncTargetRegistry.nameToId('onedrive'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: () => _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).'), options: () => {
return SyncTargetRegistry.idAndLabelPlainObject();
}},
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem') }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
'sync.2.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => {
try {
return settings['sync.target'] == SyncTargetRegistry.nameToId('filesystem')
} catch (error) {
return false;
}
}, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: () => _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.') },
'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud WebDAV URL') },
'sync.5.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud username') },
'sync.5.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nexcloud password'), secure: true },
'sync.6.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV URL') },
'sync.6.username': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV username') },
'sync.6.password': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('webdav') }, public: true, label: () => _('WebDAV password'), secure: true },
'sync.3.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.4.auth': { value: '', type: Setting.TYPE_STRING, public: false },
'sync.1.context': { value: '', type: Setting.TYPE_STRING, public: false },

View File

@@ -45,4 +45,12 @@ function toSystemSlashes(path, os) {
return path.replace(/\\/g, "/");
}
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes };
function rtrimSlashes(path) {
return path.replace(/\/+$/, '');
}
function ltrimSlashes(path) {
return path.replace(/^\/+/, '');
}
module.exports = { basename, dirname, filename, isHidden, fileExtension, safeFileExtension, toSystemSlashes, rtrimSlashes, ltrimSlashes };

View File

@@ -64,27 +64,38 @@ class DecryptionWorker {
let excludedIds = [];
try {
const notLoadedMasterKeyDisptaches = [];
while (true) {
const result = await BaseItem.itemsThatNeedDecryption(excludedIds);
const items = result.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
// Temp hack
if (['edf44b7a0e4f8cbf248e206cd8dfa800', '2ccb3c9af0b1adac2ec6b66a5961fbb1'].indexOf(item.id) >= 0) {
excludedIds.push(item.id);
continue;
}
const ItemClass = BaseItem.itemClass(item);
this.logger().debug('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
this.logger().info('DecryptionWorker: decrypting: ' + item.id + ' (' + ItemClass.tableName() + ')');
try {
await ItemClass.decrypt(item);
} catch (error) {
if (error.code === 'masterKeyNotLoaded' && options.materKeyNotLoadedHandler === 'dispatch') {
excludedIds.push(item.id);
this.dispatch({
type: 'MASTERKEY_ADD_NOT_LOADED',
id: error.masterKeyId,
});
if (notLoadedMasterKeyDisptaches.indexOf(error.masterKeyId) < 0) {
this.dispatch({
type: 'MASTERKEY_ADD_NOT_LOADED',
id: error.masterKeyId,
});
notLoadedMasterKeyDisptaches.push(error.masterKeyId);
}
continue;
}
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')');
throw error;
this.logger().warn('DecryptionWorker: error for: ' + item.id + ' (' + ItemClass.tableName() + ')', error);
}
}

View File

@@ -378,8 +378,8 @@ class EncryptionService {
read: async (size) => {
return this.fsDriver().readFileChunk(reader.handle, size, encoding);
},
close: () => {
this.fsDriver().close(reader.handle);
close: async () => {
await this.fsDriver().close(reader.handle);
},
};
return reader;
@@ -412,9 +412,9 @@ class EncryptionService {
let source = await this.fileReader_(srcPath, 'base64');
let destination = await this.fileWriter_(destPath, 'ascii');
const cleanUp = () => {
if (source) source.close();
if (destination) destination.close();
const cleanUp = async () => {
if (source) await source.close();
if (destination) await destination.close();
source = null;
destination = null;
}
@@ -428,16 +428,16 @@ class EncryptionService {
throw error;
}
cleanUp();
await cleanUp();
}
async decryptFile(srcPath, destPath) {
let source = await this.fileReader_(srcPath, 'ascii');
let destination = await this.fileWriter_(destPath, 'base64');
const cleanUp = () => {
if (source) source.close();
if (destination) destination.close();
const cleanUp = async () => {
if (source) await source.close();
if (destination) await destination.close();
source = null;
destination = null;
}
@@ -451,7 +451,7 @@ class EncryptionService {
throw error;
}
cleanUp();
await cleanUp();
}
decodeHeaderVersion_(hexaByte) {
@@ -507,7 +507,8 @@ class EncryptionService {
}
isValidHeaderIdentifier(id, ignoreTooLongLength = false) {
if (!ignoreTooLongLength && !id || id.length !== 5) return false;
if (!id) return false;
if (!ignoreTooLongLength && id.length !== 5) return false;
return /JED\d\d/.test(id);
}
@@ -515,7 +516,7 @@ class EncryptionService {
if (!item) throw new Error('No item');
const ItemClass = BaseItem.itemClass(item);
if (!ItemClass.encryptionSupported()) return false;
return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text);
return item.encryption_applied && this.isValidHeaderIdentifier(item.encryption_cipher_text, true);
}
async fileIsEncrypted(path) {

View File

@@ -4,14 +4,20 @@ const { GeolocationNode } = require('lib/geolocation-node.js');
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { time } = require('lib/time-utils.js');
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
function shimInit() {
shim.fs = fs;
shim.fsDriver = () => { throw new Error('Not implemented') }
shim.FileApiDriverLocal = FileApiDriverLocal;
shim.Geolocation = GeolocationNode;
shim.FormData = require('form-data');
shim.sjclModule = require('lib/vendor/sjcl.js');
shim.fsDriver = () => {
if (!shim.fsDriver_) shim.fsDriver_ = new FsDriverNode();
return shim.fsDriver_;
}
shim.randomBytes = async (count) => {
const buffer = require('crypto').randomBytes(count);
return Array.from(buffer);

View File

@@ -3,6 +3,8 @@ const { GeolocationReact } = require('lib/geolocation-react.js');
const { PoorManIntervals } = require('lib/poor-man-intervals.js');
const RNFetchBlob = require('react-native-fetch-blob').default;
const { generateSecureRandom } = require('react-native-securerandom');
const FsDriverRN = require('lib/fs-driver-rn.js').FsDriverRN;
const urlValidator = require('valid-url');
function shimInit() {
shim.Geolocation = GeolocationReact;
@@ -10,6 +12,11 @@ function shimInit() {
shim.clearInterval = PoorManIntervals.clearInterval;
shim.sjclModule = require('lib/vendor/sjcl-rn.js');
shim.fsDriver = () => {
if (!shim.fsDriver_) shim.fsDriver_ = new FsDriverRN();
return shim.fsDriver_;
}
shim.randomBytes = async (count) => {
const randomBytes = await generateSecureRandom(count);
let temp = [];
@@ -21,8 +28,15 @@ function shimInit() {
}
shim.fetch = async function(url, options = null) {
// The native fetch() throws an uncatable error that crashes the app if calling it with an
// invalid URL such as '//.resource' or "http://ocloud. de" so detect if the URL is valid beforehand
// and throw a catchable error.
// Bug: https://github.com/facebook/react-native/issues/7436
const validatedUrl = urlValidator.isUri(url);
if (!validatedUrl) throw new Error('Not a valid URL: ' + url);
return shim.fetchWithRetry(() => {
return shim.nativeFetch_(url, options)
return fetch(validatedUrl, options);
}, options);
}
@@ -31,12 +45,20 @@ function shimInit() {
let headers = options.headers ? options.headers : {};
let method = options.method ? options.method : 'GET';
const overwrite = 'overwrite' in options ? options.overwrite : true;
let dirs = RNFetchBlob.fs.dirs;
let localFilePath = options.path;
if (localFilePath.indexOf('/') !== 0) localFilePath = dirs.DocumentDir + '/' + localFilePath;
if (!overwrite) {
if (await shim.fsDriver().exists(localFilePath)) {
return { ok: true };
}
}
delete options.path;
delete options.overwrite;
const doFetchBlob = () => {
return RNFetchBlob.config({

View File

@@ -104,10 +104,9 @@ shim.fetchWithRetry = async function(fetchFn, options = null) {
}
}
shim.nativeFetch_ = typeof fetch !== 'undefined' ? fetch : null;
shim.fetch = () => { throw new Error('Not implemented'); }
shim.FormData = typeof FormData !== 'undefined' ? FormData : null;
shim.fs = null;
shim.fsDriver = () => { throw new Error('Not implemented') }
shim.FileApiDriverLocal = null;
shim.readLocalFileBase64 = (path) => { throw new Error('Not implemented'); }
shim.uploadBlob = () => { throw new Error('Not implemented'); }

View File

@@ -10,7 +10,7 @@ const { time } = require('lib/time-utils.js');
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
const { shim } = require('lib/shim.js');
const moment = require('moment');
const JoplinError = require('lib/JoplinError');
class Synchronizer {
@@ -18,7 +18,7 @@ class Synchronizer {
this.state_ = 'idle';
this.db_ = db;
this.api_ = api;
//this.syncDirName_ = '.sync';
this.syncDirName_ = '.sync';
this.resourceDirName_ = '.resource';
this.logger_ = new Logger();
this.appType_ = appType;
@@ -27,7 +27,7 @@ class Synchronizer {
// Debug flags are used to test certain hard-to-test conditions
// such as cancelling in the middle of a loop.
this.debugFlags_ = [];
this.testingHooks_ = [];
this.onProgress_ = function(s) {};
this.progressReport_ = {};
@@ -195,7 +195,8 @@ class Synchronizer {
this.logSyncOperation('starting', null, null, 'Starting synchronisation to target ' + syncTargetId + '... [' + synchronizationId + ']');
try {
//await this.api().mkdir(this.syncDirName_);
await this.api().mkdir(this.syncDirName_);
this.api().setTempDirName(this.syncDirName_);
await this.api().mkdir(this.resourceDirName_);
let donePaths = [];
@@ -279,7 +280,7 @@ class Synchronizer {
const localResourceContentPath = result.path;
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
} catch (error) {
if (error && error.code === 'rejectedByTarget') {
if (error && ['rejectedByTarget', 'fileNotFound'].indexOf(error.code) >= 0) {
await handleCannotSyncItem(syncTargetId, local, error.message);
action = null;
} else {
@@ -289,25 +290,9 @@ class Synchronizer {
}
if (action == 'createRemote' || action == 'updateRemote') {
// Make the operation atomic by doing the work on a copy of the file
// and then copying it back to the original location.
// let tempPath = this.syncDirName_ + '/' + path + '_' + time.unixMs();
//
// Atomic operation is disabled for now because it's not possible
// to do an atomic move with OneDrive (see file-api-driver-onedrive.js)
// await this.api().put(tempPath, content);
// await this.api().setTimestamp(tempPath, local.updated_time);
// await this.api().move(tempPath, path);
let canSync = true;
try {
if (this.debugFlags_.indexOf('rejectedByTarget') >= 0) {
const error = new Error('Testing rejectedByTarget');
error.code = 'rejectedByTarget';
throw error;
}
if (this.testingHooks_.indexOf('rejectedByTarget') >= 0) throw new JoplinError('Testing rejectedByTarget', 'rejectedByTarget');
const content = await ItemClass.serializeForSync(local);
await this.api().put(path, content);
} catch (error) {
@@ -334,7 +319,12 @@ class Synchronizer {
// change is uniquely identified. Leaving it like this for now.
if (canSync) {
await this.api().setTimestamp(path, local.updated_time);
// 2018-01-21: Setting timestamp is not needed because the delta() logic doesn't rely
// on it (instead it uses a more reliable `context` object) and the itemsThatNeedSync loop
// above also doesn't use it because it fetches the whole remote object and read the
// more reliable 'updated_time' property. Basically remote.updated_time is deprecated.
// await this.api().setTimestamp(path, local.updated_time);
await ItemClass.saveSyncTime(syncTargetId, local, local.updated_time);
}
@@ -449,7 +439,7 @@ class Synchronizer {
this.logSyncOperation('fetchingTotal', null, null, 'Fetching delta items from sync target', remotes.length);
for (let i = 0; i < remotes.length; i++) {
if (this.cancelling() || this.debugFlags_.indexOf('cancelDeltaLoop2') >= 0) {
if (this.cancelling() || this.testingHooks_.indexOf('cancelDeltaLoop2') >= 0) {
hasCancelled = true;
break;
}
@@ -460,7 +450,7 @@ class Synchronizer {
if (!BaseItem.isSystemPath(remote.path)) continue; // The delta API might return things like the .sync, .resource or the root folder
const loadContent = async () => {
content = await this.api().get(path);
let content = await this.api().get(path);
if (!content) return null;
return await BaseItem.unserialize(content);
}
@@ -500,7 +490,7 @@ class Synchronizer {
if (action == 'createLocal' || action == 'updateLocal') {
if (content === null) {
this.logger().warn('Remote has been deleted between now and the list() call? In that case it will be handled during the next sync: ' + path);
this.logger().warn('Remote has been deleted between now and the delta() call? In that case it will be handled during the next sync: ' + path);
continue;
}
content = ItemClass.filter(content);

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

Some files were not shown because too many files have changed in this diff Show More