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

Compare commits

..

99 Commits

Author SHA1 Message Date
Laurent Cozic
65065a62d8 CLI v1.0.107 2018-05-11 13:50:19 +01:00
Laurent Cozic
482e9340bc Android release v1.0.125 2018-05-10 21:31:58 +01:00
Laurent Cozic
69d490996e Mobile: Remove uneeded GCM and C2DM dependencies from Android to make it acceptable for F-Droid 2018-05-10 21:25:06 +01:00
Laurent Cozic
3494937e34 Mobile: Resolves #503: Share note with other apps 2018-05-10 20:39:41 +01:00
Laurent Cozic
41ba1043be All: Fixed incorrect timeout for sync-after-save (was using ms instead of sec). Removed needless caching of note IDs in database. 2018-05-10 19:50:44 +01:00
Laurent Cozic
cc57de60c0 Update website 2018-05-10 15:48:16 +01:00
Laurent Cozic
60a2b9e5c6 Electron release v1.0.91 2018-05-10 15:24:46 +01:00
Laurent Cozic
8e1fb666a5 Electron: Fixes #510: Removed reference to missing file 2018-05-10 15:24:38 +01:00
Laurent Cozic
f4ad777bbf Update website 2018-05-10 14:32:33 +01:00
Laurent Cozic
2eacf6146a Android release v1.0.124 2018-05-10 12:24:36 +01:00
Laurent Cozic
fe2ba34cb4 Electron release v1.0.90 2018-05-10 12:22:33 +01:00
Laurent Cozic
84daa0db61 Update readme 2018-05-10 12:22:14 +01:00
Laurent Cozic
b9118a90be All: Resolves #443: Various optimisations to make dealing with large notes easier and make Katex re-rendering faster 2018-05-10 12:02:39 +01:00
Laurent Cozic
ef2ffd4e52 Electron: Resolves #200, Resolves #416: Allow attaching images by pasting them in. Allow attaching files by drag and dropping them. Insert attachement at cursor position. 2018-05-10 10:45:44 +01:00
Laurent Cozic
5e3063abe0 Updated translations 2018-05-09 21:05:52 +01:00
Laurent Cozic
f460b2497a Merge pull request #506 from fmrtn/master
Updated Spanish translation
2018-05-09 21:04:20 +01:00
Laurent Cozic
c080d7054f Merge branch 'master' of github.com:laurent22/joplin 2018-05-09 21:00:33 +01:00
Laurent Cozic
61dd4cefbc All: Resolves #345: Option to hide completed todos 2018-05-09 21:00:05 +01:00
Laurent Cozic
63d99b2d70 Mobile: Fixes #497: Make sure Dropbox text input is visible when keyboard is visible on iPhone SE 2018-05-09 19:11:48 +01:00
Laurent Cozic
55332d7671 Electron: Fixes #481: Shortcuts were not working when text editor had focus 2018-05-09 18:41:32 +01:00
Laurent Cozic
16635defcd Mobile: Fixes #455: Use active folder when creating new note from Welcome screen 2018-05-09 18:12:00 +01:00
Laurent Cozic
595cf3fcad Mobile: Fixes #433: Don't scroll note back to top when changing checkbox state 2018-05-09 18:04:48 +01:00
Fernando
c9b9f82130 Updated Spanish translation 2018-05-09 18:48:32 +02:00
Laurent Cozic
f5bca733d7 Fixed translator email encoding issue 2018-05-09 17:06:02 +01:00
Laurent Cozic
494e235e18 Electron: Resolves #500: Fixed XSS security vulnerability 2018-05-09 16:59:33 +01:00
Laurent Cozic
85219a6004 Android release v1.0.123 2018-05-09 16:43:33 +01:00
Laurent Cozic
e4a7851e57 Update debugging.md 2018-05-09 16:33:16 +01:00
Laurent Cozic
b7529b40b5 Updated tests 2018-05-09 16:14:27 +01:00
Laurent Cozic
74827e5324 Electron: Fixed tag display 2018-05-09 15:31:42 +01:00
Laurent Cozic
2e16cc5433 ios-v10.0.21 2018-05-09 14:15:04 +01:00
Laurent Cozic
7f41bc5703 Update website 2018-05-09 14:10:13 +01:00
Laurent Cozic
a2380fb752 Android release v1.0.122 2018-05-09 13:18:39 +01:00
Laurent Cozic
f6a902809d Electron release v1.0.89 2018-05-09 13:17:08 +01:00
Laurent Cozic
33a853397d Electron release v1.0.88 2018-05-09 13:16:55 +01:00
Laurent Cozic
4f02481899 Electron release v1.0.87 2018-05-09 13:14:42 +01:00
Laurent Cozic
b18076565f Update translations 2018-05-09 13:14:17 +01:00
Laurent Cozic
853ddc5840 Update website 2018-05-09 13:11:03 +01:00
Laurent Cozic
7930ab66c6 Merge branch 'master' into subnotebooks 2018-05-09 13:10:20 +01:00
Laurent Cozic
c7716c0d59 All: Resolves #122: Sub-notebook support in desktop, mobile and cli app 2018-05-09 13:08:00 +01:00
Laurent Cozic
49cbb254d0 CLI: Fixed link handling 2018-05-09 12:50:50 +01:00
Laurent Cozic
cf9246796d CLI: Added support for sub-notebooks 2018-05-09 12:39:27 +01:00
Laurent Cozic
e1dee546dc Mobile: Added support for sub-notebooks 2018-05-09 12:39:17 +01:00
Laurent Cozic
da6fdad2de All: Handle saving collapsed states of sub-notebook 2018-05-09 10:49:31 +01:00
Laurent Cozic
567596643c Electron: Handle drag and dropping notebooks to change the parent 2018-05-09 09:53:47 +01:00
Laurent Cozic
cb617e1b14 All: Fixes #61: Handle path that ends with slash for file system sync. 2018-05-08 11:29:25 +01:00
Laurent Cozic
facf8afa8b Update translations 2018-05-08 11:12:36 +01:00
Laurent Cozic
f0dd61a711 Merge pull request #495 from fmrtn/master
Updated Spanish translation
2018-05-08 11:12:01 +01:00
Laurent Cozic
e958211a13 Merge pull request #496 from zuphilip/patch-1
Update address pronouns "du" in German translation
2018-05-08 11:11:45 +01:00
Philipp Zumstein
0ed170b5bc Update address pronouns "du" in German translation 2018-05-07 07:12:01 +02:00
Fernando
473d3453a2 Updated Spanish translation 2018-05-06 20:29:35 +02:00
Laurent Cozic
fa9d7b0408 Electron: Started UI and backend for sub-notebook support 2018-05-06 12:11:59 +01:00
Laurent Cozic
d4a28f48c9 Update website 2018-05-06 11:17:34 +01:00
Laurent Cozic
ead6fff861 Merge branch 'master' of github.com:laurent22/joplin 2018-05-06 11:16:52 +01:00
Laurent Cozic
c7d06b35cd Merge pull request #494 from stweil/typo
Fix some typos
2018-05-06 11:16:43 +01:00
Laurent Cozic
fa939e5c76 Merge branch 'master' of github.com:laurent22/joplin 2018-05-06 11:16:19 +01:00
Laurent Cozic
1bf2601f4f Merge pull request #492 from zuphilip/patch-1
Update de_DE.po
2018-05-06 11:15:59 +01:00
Stefan Weil
feb0c02c9a ReactNativeClient: Fix some typos (found by codespell)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:25:37 +02:00
Stefan Weil
40a34a7c05 Fix typos in documentation (found by codespell)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:23:48 +02:00
Stefan Weil
c62dcd96b0 CliClient: Fix some typos (found by codespell)
Remove also a "translation" which was none from locales/hr_HR.po.

Signed-off-by: Stefan Weil <sw@weilnetz.de>
2018-05-05 16:22:14 +02:00
Philipp Zumstein
1364d6786d Update de_DE.po 2018-05-05 11:08:47 +02:00
Laurent Cozic
a6a351e68d Electron: Export/Import links to notes 2018-05-03 13:11:45 +01:00
Laurent Cozic
1db38a9699 Merge pull request #484 from fmrtn/master
Spanish translation updated
2018-05-03 12:08:57 +01:00
Fernando
c57db1834f Spanish translation updated 2018-05-03 13:07:36 +02:00
Laurent Cozic
3aeb49b469 Merge branch 'master' of github.com:laurent22/joplin 2018-05-03 11:31:17 +01:00
Laurent Cozic
80b467eead All: For now, disable attaching resources larger than 10MB due to #371 2018-05-03 11:31:07 +01:00
Laurent Cozic
61572f287a Update README.md
Added note about large resources
2018-05-03 11:06:12 +01:00
Laurent Cozic
0e545baf10 Update issue_template.md 2018-05-02 21:39:11 +01:00
Laurent Cozic
e65e647359 Update issue_template.md 2018-05-02 21:38:49 +01:00
Laurent Cozic
238268884e Update debugging.md 2018-05-02 20:16:08 +01:00
Laurent Cozic
4c210d0956 Electron: Fixes #480: Ignore invalid flag automatically passed by macOS 2018-05-02 15:51:33 +01:00
Laurent Cozic
5f32c6466a Update website 2018-05-02 15:33:18 +01:00
Laurent Cozic
71bd39a8a3 Update website 2018-05-02 15:31:17 +01:00
Laurent Cozic
ffb660f0f4 Move tool file 2018-05-02 15:28:13 +01:00
Laurent Cozic
dde23632c1 Move tool file 2018-05-02 15:27:12 +01:00
Laurent Cozic
9d26f13db0 Android release v1.0.120 2018-05-02 15:18:13 +01:00
Laurent Cozic
2a4c9c4427 Electron release v1.0.86 2018-05-02 15:16:02 +01:00
Laurent Cozic
3bfde26b74 Merge branch 'master' of github.com:laurent22/joplin 2018-05-02 15:13:28 +01:00
Laurent Cozic
a419bc7253 All: Resolves #134: Allow linking to a note from another note 2018-05-02 15:13:20 +01:00
Laurent Cozic
89e0dad88b Update README.md
32-bit is now supported
2018-05-02 13:16:04 +01:00
Laurent Cozic
ff1ee1249b Mobile: Resolves #61: Enable File System sync on mobile as the driver seems to be working now 2018-05-02 10:27:37 +01:00
Laurent Cozic
ba9cfd8041 Electron: Increased timeout for sync-after-save to 30 seconds 2018-05-02 08:34:54 +01:00
Laurent Cozic
80a51e02a4 Update readme downloads 2018-05-01 22:33:01 +01:00
Laurent Cozic
a2e2a9a2f5 Electron release v1.0.85 2018-05-01 21:16:24 +01:00
Laurent Cozic
49e4c37cac Electron: Check that the filename contains 'Setup' when auto-updating 2018-05-01 21:13:41 +01:00
Laurent Cozic
11d323d8b7 Electron: Fixes #479: Currently loaded note was cleared when creating new note 2018-05-01 21:13:17 +01:00
Laurent Cozic
784ba45f1f Electron release v1.0.84 2018-05-01 19:39:06 +01:00
Laurent Cozic
e534414874 All: Fixes #434: Handle Katex block mode 2018-05-01 19:34:42 +01:00
Laurent Cozic
01f4faf8f1 Mobile: Fixes #426: Fix missing menu icon on Android 4 2018-05-01 19:30:41 +01:00
Laurent Cozic
b33d30ca47 Merge branch 'master' of github.com:laurent22/joplin 2018-05-01 19:05:33 +01:00
Laurent Cozic
1ba3fae101 All: Resolves #470: Make it clear that spaces in URLs are invalid. 2018-05-01 19:05:14 +01:00
Laurent Cozic
9550347e04 Merge pull request #448 from solariz/master
Fix for Issue #430
2018-05-01 18:54:43 +01:00
Laurent Cozic
398946d39a Mobile: Fixes #393: Fixed moving new notes before they are saved 2018-05-01 18:53:45 +01:00
Laurent Cozic
05faf55e8d All: Fixes #363: Fixed indentation and rendering of lists 2018-05-01 16:45:17 +01:00
Laurent Cozic
4cf5525e20 Electron: Fixes #355: Set undo state properly when loading new note 2018-05-01 10:48:15 +01:00
Laurent Cozic
62e91c44d7 Electron: Fixes #346: Make sure links have an address when exporting to PDF 2018-05-01 10:14:48 +01:00
Laurent Cozic
e4ec4ae92b Mobile: Fix action button when note is being edited. 2018-05-01 10:09:36 +01:00
Laurent Cozic
c1f5dfd9cc Keep Blob tests to revisit in a few weeks 2018-04-30 21:21:17 +01:00
Laurent Cozic
0c0efeac1f Android release v1.0.119 2018-04-30 18:34:55 +01:00
Marco G
577bef5704 Fix for Issue #430 2018-04-21 10:25:13 +02:00
123 changed files with 4176 additions and 2105 deletions

View File

@@ -1,5 +1,6 @@
const { Logger } = require('lib/logger.js'); const { Logger } = require('lib/logger.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
@@ -9,6 +10,8 @@ const { reducer, defaultState } = require('lib/reducer.js');
const { splitCommandString } = require('lib/string-utils.js'); const { splitCommandString } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const chalk = require('chalk'); const chalk = require('chalk');
const tk = require('terminal-kit'); const tk = require('terminal-kit');
@@ -638,12 +641,27 @@ class AppGui {
return true; return true;
} }
if (link.type === 'resource') { if (link.type === 'item') {
const resourceId = link.id; const itemId = link.id;
let resource = await Resource.load(resourceId); let item = await BaseItem.loadItemById(itemId);
if (!resource) throw new Error('No resource with ID ' + resourceId); // Should be nearly impossible if (!item) throw new Error('No item with ID ' + itemId); // Should be nearly impossible
if (resource.mime) response.setHeader('Content-Type', resource.mime);
response.write(await Resource.content(resource)); if (item.type_ === BaseModel.TYPE_RESOURCE) {
if (item.mime) response.setHeader('Content-Type', item.mime);
response.write(await Resource.content(item));
} else if (item.type_ === BaseModel.TYPE_NOTE) {
const html = [`
<!DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head><meta charset="UTF-8"/></head><body>
`];
html.push('<pre>' + htmlentities(item.title) + '\n\n' + htmlentities(item.body) + '</pre>');
html.push('</body></html>');
response.write(html.join(''));
} else {
throw new Error('Unsupported item type: ' + item.type_);
}
return true; return true;
} }
@@ -659,7 +677,7 @@ class AppGui {
if (resourceIdRegex.test(url)) { if (resourceIdRegex.test(url)) {
noteLinks[index] = { noteLinks[index] = {
type: 'resource', type: 'item',
id: url.substr(2), id: url.substr(2),
}; };
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) { } else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {

View File

@@ -36,7 +36,7 @@ async function handleAutocompletionPromise(line) {
if (next[0] === '-') { if (next[0] === '-') {
for (let i = 0; i<metadata.options.length; i++) { for (let i = 0; i<metadata.options.length; i++) {
const options = metadata.options[i][0].split(' '); const options = metadata.options[i][0].split(' ');
//if there are multiple options then they will be seperated by comma and //if there are multiple options then they will be separated by comma and
//space. The comma should be removed //space. The comma should be removed
if (options[0][options[0].length - 1] === ',') { if (options[0][options[0].length - 1] === ',') {
options[0] = options[0].slice(0, -1); options[0] = options[0].slice(0, -1);

View File

@@ -72,7 +72,7 @@ class Command extends BaseCommand {
this.stdout(''); this.stdout('');
this.stdout(commandNames.join(', ')); this.stdout(commandNames.join(', '));
this.stdout(''); this.stdout('');
this.stdout(_('In any command, a note or notebook can be refered to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.')); this.stdout(_('In any command, a note or notebook can be referred to by title or ID, or using the shortcuts `$n` or `$b` for, respectively, the currently selected note or notebook. `$c` can be used to refer to the currently selected item.'));
this.stdout(''); this.stdout('');
this.stdout(_('To move from one pane to another, press Tab or Shift+Tab.')); this.stdout(_('To move from one pane to another, press Tab or Shift+Tab.'));
this.stdout(_('Use the arrows and page up/down to scroll the lists and text areas (including this console).')); this.stdout(_('Use the arrows and page up/down to scroll the lists and text areas (including this console).'));

View File

@@ -29,7 +29,7 @@ class Command extends BaseCommand {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern); const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern)); if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook? All notes within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' }); const ok = force ? true : await this.prompt(_('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.'), { booleanAnswerDefault: 'n' });
if (!ok) return; if (!ok) return;
await Folder.delete(folder.id); await Folder.delete(folder.id);

View File

@@ -18,19 +18,20 @@ class FolderListWidget extends ListWidget {
this.notesParentType_ = 'Folder'; this.notesParentType_ = 'Folder';
this.updateIndexFromSelectedFolderId_ = false; this.updateIndexFromSelectedFolderId_ = false;
this.updateItems_ = false; this.updateItems_ = false;
this.trimItemTitle = false;
this.itemRenderer = (item) => { this.itemRenderer = (item) => {
let output = []; let output = [];
if (item === '-') { if (item === '-') {
output.push('-'.repeat(this.innerWidth)); output.push('-'.repeat(this.innerWidth));
} else if (item.type_ === Folder.modelType()) { } else if (item.type_ === Folder.modelType()) {
output.push(Folder.displayTitle(item)); output.push(' '.repeat(this.folderDepth(this.folders, item.id)) + Folder.displayTitle(item));
} else if (item.type_ === Tag.modelType()) { } else if (item.type_ === Tag.modelType()) {
output.push('[' + Folder.displayTitle(item) + ']'); output.push('[' + Folder.displayTitle(item) + ']');
} else if (item.type_ === BaseModel.TYPE_SEARCH) { } else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:')); output.push(_('Search:'));
output.push(item.title); output.push(item.title);
} }
// if (item && item.id) output.push(item.id.substr(0, 5)); // if (item && item.id) output.push(item.id.substr(0, 5));
@@ -38,6 +39,17 @@ class FolderListWidget extends ListWidget {
}; };
} }
folderDepth(folders, folderId) {
let output = 0;
while (true) {
const folder = BaseModel.byId(folders, folderId);
if (!folder.parent_id) return output;
output++;
folderId = folder.parent_id;
}
throw new Error('unreachable');
}
get selectedFolderId() { get selectedFolderId() {
return this.selectedFolderId_; return this.selectedFolderId_;
} }

View File

@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Dostupné příkazy:" msgstr "Dostupné příkazy:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -364,7 +364,10 @@ msgstr "Smaže vybraný zápisník."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Smaže zápisník bez potvrzení." msgstr "Smaže zápisník bez potvrzení."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "Smazat zápisník? Budou smazány i všechny poznámky v něm obsažené." msgstr "Smazat zápisník? Budou smazány i všechny poznámky v něm obsažené."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -826,6 +829,10 @@ msgstr "Přidat či odebrat tagy"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Přepnout mezi poznámkou a to-do" msgstr "Přepnout mezi poznámkou a to-do"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Smazat" msgstr "Smazat"
@@ -1035,6 +1042,10 @@ msgstr "Nelze editovat zašifrovanou položku"
msgid "Conflicts" msgid "Conflicts"
msgstr "Konflikty" msgstr "Konflikty"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Poznámku nelze přesunout do zápisníku \"%s\""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Zápisník s tímto názvem již existuje: \"%s\"" msgstr "Zápisník s tímto názvem již existuje: \"%s\""
@@ -1088,6 +1099,10 @@ msgstr "Tmavý"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Nedokončené to-do listy nahoře" msgstr "Nedokončené to-do listy nahoře"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Nedokončené to-do listy nahoře"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Řadit poznámky podle" msgstr "Řadit poznámky podle"
@@ -1366,6 +1381,14 @@ msgstr "Uložit změny"
msgid "Discard changes" msgid "Discard changes"
msgstr "Zahodit změny" msgstr "Zahodit změny"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Nepodporovaný formát obrázku: %s" msgstr "Nepodporovaný formát obrázku: %s"

View File

@@ -2,7 +2,7 @@
# Copyright (C) YEAR Laurent Cozic # Copyright (C) YEAR Laurent Cozic
# This file is distributed under the same license as the Joplin-CLI package. # This file is distributed under the same license as the Joplin-CLI package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n" "Project-Id-Version: Joplin-CLI 1.0.0\n"
@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Mulige kommandoer er:" msgstr "Mulige kommandoer er:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -367,7 +367,10 @@ msgstr "Sletter aktuelle notesbog."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Sletter notesbogen uden at bede om bekræftelse." msgstr "Sletter notesbogen uden at bede om bekræftelse."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "Slet notesbog? Alle noter i notesbogen bliver også slettet." msgstr "Slet notesbog? Alle noter i notesbogen bliver også slettet."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -835,6 +838,10 @@ msgstr "Tilføj eller slet mærker"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Skift mellem note- og opgave type" msgstr "Skift mellem note- og opgave type"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Slet" msgstr "Slet"
@@ -1044,6 +1051,10 @@ msgstr "Krypteret emner kan ikke rettes"
msgid "Conflicts" msgid "Conflicts"
msgstr "Konflikter" msgstr "Konflikter"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kan ikke flytte note til \"%s\" notesbog"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "En notesbog bruger allerede dette navn: \"%s\"" msgstr "En notesbog bruger allerede dette navn: \"%s\""
@@ -1097,6 +1108,10 @@ msgstr "Mørkt"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Ufærdige opgaver øverst" msgstr "Ufærdige opgaver øverst"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Ufærdige opgaver øverst"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Sorter noter efter" msgstr "Sorter noter efter"
@@ -1375,6 +1390,14 @@ msgstr "Gem ændringer"
msgid "Discard changes" msgid "Discard changes"
msgstr "Fortryd ændringer" msgstr "Fortryd ændringer"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Ulovlig billedtype: %s" msgstr "Ulovlig billedtype: %s"

View File

@@ -7,13 +7,13 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n" "Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"Last-Translator: Tobias Grasse <mail@tobias-grasse.net>\n" "Last-Translator: Philipp Zumstein <zuphilip@gmail.com>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.6\n" "X-Generator: Poedit 2.0.7\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "To delete a tag, untag the associated notes." msgid "To delete a tag, untag the associated notes."
@@ -238,7 +238,7 @@ msgid "The possible commands are:"
msgstr "Mögliche Befehle lauten:" msgstr "Mögliche Befehle lauten:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -259,9 +259,8 @@ msgstr ""
"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu " "Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu "
"scrollen (inklusive diesem Terminal)." "scrollen (inklusive diesem Terminal)."
#, fuzzy
msgid "To maximise/minimise the console, press \"tc\"." msgid "To maximise/minimise the console, press \"tc\"."
msgstr "Um das Terminal zu maximieren/minimieren, drücke \"TC\"." msgstr "Um das Terminal zu maximieren/minimieren, drücke \"tc\"."
msgid "To enter command line mode, press \":\"" msgid "To enter command line mode, press \":\""
msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\"" msgstr "Um den Kommandozeilen Modus aufzurufen, drücke \":\""
@@ -380,7 +379,10 @@ msgstr "Löscht das ausgewählte Notizbuch."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen." msgstr "Löscht das Notizbuch, ohne nach einer Bestätigung zu fragen."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht." "Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht."
@@ -433,12 +435,14 @@ msgstr ""
msgid "" msgid ""
"To allow Joplin to synchronise with Dropbox, please follow the steps below:" "To allow Joplin to synchronise with Dropbox, please follow the steps below:"
msgstr "" msgstr ""
"Um Joplin die Synchronisation mit Dropbox zu ermöglichen, folge bitte den "
"folgenden Schritten:"
msgid "Step 1: Open this URL in your browser to authorise the application:" msgid "Step 1: Open this URL in your browser to authorise the application:"
msgstr "" msgstr "Schritt 1: URL im Browser öffnen um die Anwendung zu autorisieren:"
msgid "Step 2: Enter the code provided by Dropbox:" msgid "Step 2: Enter the code provided by Dropbox:"
msgstr "" msgstr "Schritt 2: Den von Dropbox bereitgestellten Code eingeben:"
#, javascript-format #, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials." msgid "Not authentified with %s. Please provide any missing credentials."
@@ -531,9 +535,8 @@ msgstr "Standard: %s"
msgid "Possible keys/values:" msgid "Possible keys/values:"
msgstr "Mögliche Werte:" msgstr "Mögliche Werte:"
#, fuzzy
msgid "Type `joplin help` for usage information." msgid "Type `joplin help` for usage information."
msgstr "Zeigt die Nutzungsstatistik an." msgstr "Gib `joplin help` ein um die Nutzungsstatistik anzuzeigen."
msgid "Fatal error:" msgid "Fatal error:"
msgstr "Schwerwiegender Fehler:" msgstr "Schwerwiegender Fehler:"
@@ -541,7 +544,7 @@ msgstr "Schwerwiegender Fehler:"
msgid "" msgid ""
"The application has been authorised - you may now close this browser tab." "The application has been authorised - you may now close this browser tab."
msgstr "" msgstr ""
"Das Programm wurde autorisiert - Du kannst diesen Browsertab nun schließen." "Das Programm wurde autorisiert - du kannst diesen Browsertab nun schließen."
msgid "The application has been successfully authorised." msgid "The application has been successfully authorised."
msgstr "Das Programm wurde erfolgreich autorisiert." msgstr "Das Programm wurde erfolgreich autorisiert."
@@ -647,7 +650,7 @@ msgid "View"
msgstr "Ansicht" msgstr "Ansicht"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr "Seitenleiste ein/aus"
msgid "Toggle editor layout" msgid "Toggle editor layout"
msgstr "Editor Layout umschalten" msgstr "Editor Layout umschalten"
@@ -719,7 +722,7 @@ msgid "Save"
msgstr "Speichern" msgstr "Speichern"
msgid "Submit" msgid "Submit"
msgstr "" msgstr "Absenden"
msgid "" msgid ""
"Disabling encryption means *all* your notes and attachments are going to be " "Disabling encryption means *all* your notes and attachments are going to be "
@@ -795,6 +798,8 @@ msgid ""
"For more information about End-To-End Encryption (E2EE) and advices on how " "For more information about End-To-End Encryption (E2EE) and advices on how "
"to enable it please check the documentation:" "to enable it please check the documentation:"
msgstr "" msgstr ""
"Weitere Informationen zur Ende-zu-Ende-Verschlüsselung (E2EE) und Hinweise "
"zur Aktivierung findest du in der Dokumentation (auf Englisch):"
msgid "Status" msgid "Status"
msgstr "Status" msgstr "Status"
@@ -836,9 +841,8 @@ msgstr "Alarm erstellen:"
msgid "Layout" msgid "Layout"
msgstr "Layout" msgstr "Layout"
#, fuzzy
msgid "Search..." msgid "Search..."
msgstr "Suchen" msgstr "Suchen..."
msgid "Some items cannot be synchronised." msgid "Some items cannot be synchronised."
msgstr "Manche Objekte können nicht synchronisiert werden." msgstr "Manche Objekte können nicht synchronisiert werden."
@@ -858,6 +862,10 @@ msgstr "Markierungen hinzufügen oder entfernen"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Zwischen Notiz und To-Do Typ wechseln" msgstr "Zwischen Notiz und To-Do Typ wechseln"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@@ -872,8 +880,8 @@ msgstr ""
msgid "" msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"." "There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr "" msgstr ""
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den " "Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf "
"(+) Knopf drückst." "\"Neues Notizbuch\" drückst."
msgid "Open..." msgid "Open..."
msgstr "Öffne..." msgstr "Öffne..."
@@ -922,7 +930,7 @@ msgid "OneDrive Login"
msgstr "OneDrive Login" msgstr "OneDrive Login"
msgid "Dropbox Login" msgid "Dropbox Login"
msgstr "" msgstr "Dropbox Anmeldung"
msgid "Options" msgid "Options"
msgstr "Optionen" msgstr "Optionen"
@@ -961,7 +969,7 @@ msgid "Unknown flag: %s"
msgstr "Unbekanntes Argument: %s" msgstr "Unbekanntes Argument: %s"
msgid "Dropbox" msgid "Dropbox"
msgstr "" msgstr "Dropbox"
msgid "File system" msgid "File system"
msgstr "Dateisystem" msgstr "Dateisystem"
@@ -1040,9 +1048,9 @@ msgstr "Remote Objekte gelöscht: %d."
msgid "Fetched items: %d/%d." msgid "Fetched items: %d/%d."
msgstr "Geladene Objekte: %d/%d." msgstr "Geladene Objekte: %d/%d."
#, fuzzy, javascript-format #, javascript-format
msgid "State: %s." msgid "State: %s."
msgstr "Status: \"%s\"." msgstr "Status: %s."
msgid "Cancelling..." msgid "Cancelling..."
msgstr "Abbrechen..." msgstr "Abbrechen..."
@@ -1074,6 +1082,10 @@ msgstr "Verschlüsselte Objekte können nicht verändert werden"
msgid "Conflicts" msgid "Conflicts"
msgstr "Konflikte" msgstr "Konflikte"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kann Notiz nicht zu Notizbuch \"%s\" verschieben"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Ein Notizbuch mit diesem Titel existiert bereits : \"%s\"" msgstr "Ein Notizbuch mit diesem Titel existiert bereits : \"%s\""
@@ -1129,6 +1141,10 @@ msgstr "Dunkel"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Zeige unvollständige To-Dos an oberster Stelle" msgstr "Zeige unvollständige To-Dos an oberster Stelle"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Zeige unvollständige To-Dos an oberster Stelle"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Sortiere Notizen nach" msgstr "Sortiere Notizen nach"
@@ -1154,7 +1170,7 @@ msgid "Show tray icon"
msgstr "Zeige Tray Icon" msgstr "Zeige Tray Icon"
msgid "Note: Does not work in all desktop environments." msgid "Note: Does not work in all desktop environments."
msgstr "" msgstr "Hinweis: Funktioniert nicht in allen Desktopumgebungen."
msgid "Global zoom percentage" msgid "Global zoom percentage"
msgstr "Zoomstufe der Benutzeroberfläche" msgstr "Zoomstufe der Benutzeroberfläche"
@@ -1284,7 +1300,7 @@ msgid ""
"(which is displayed in brackets above)." "(which is displayed in brackets above)."
msgstr "" msgstr ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum " "Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie " "Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchst du "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)." "entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)" msgid "Sync status (synced items / total items)"
@@ -1360,17 +1376,16 @@ msgid "Cancel synchronisation"
msgstr "Synchronisation abbrechen" msgstr "Synchronisation abbrechen"
msgid "New tags:" msgid "New tags:"
msgstr "" msgstr "Neue Markierungen:"
msgid "Type new tags or select from list" msgid "Type new tags or select from list"
msgstr "" msgstr "Neue Markierungen eingeben oder aus der Liste auswählen"
msgid "Joplin website" msgid "Joplin website"
msgstr "Website von Joplin" msgstr "Website von Joplin"
#, fuzzy
msgid "Login with Dropbox" msgid "Login with Dropbox"
msgstr "Mit OneDrive anmelden" msgstr "Mit Dropbox anmelden"
#, javascript-format #, javascript-format
msgid "Master Key %s" msgid "Master Key %s"
@@ -1411,6 +1426,14 @@ msgstr "Änderungen speichern"
msgid "Discard changes" msgid "Discard changes"
msgstr "Änderungen verwerfen" msgstr "Änderungen verwerfen"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Nicht unterstütztes Fotoformat: %s" msgstr "Nicht unterstütztes Fotoformat: %s"

View File

@@ -210,7 +210,7 @@ msgid "The possible commands are:"
msgstr "" msgstr ""
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -332,7 +332,9 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "" msgstr ""
msgid "Delete notebook? All notes within this notebook will also be deleted." msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -753,6 +755,9 @@ msgstr ""
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "" msgstr ""
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@@ -954,6 +959,9 @@ msgstr ""
msgid "Conflicts" msgid "Conflicts"
msgstr "" msgstr ""
msgid "Cannot move notebook to this location"
msgstr ""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "" msgstr ""
@@ -1005,6 +1013,9 @@ msgstr ""
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "" msgstr ""
msgid "Show completed to-dos"
msgstr ""
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1271,6 +1282,14 @@ msgstr ""
msgid "Discard changes" msgid "Discard changes"
msgstr "" msgstr ""
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "" msgstr ""

View File

@@ -230,7 +230,7 @@ msgid "The possible commands are:"
msgstr "Los posibles comandos son:" msgstr "Los posibles comandos son:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -250,7 +250,6 @@ msgstr ""
"Para desplazar en las listas y areas de texto (incluyendo la consola) " "Para desplazar en las listas y areas de texto (incluyendo la consola) "
"utilice las flechas y re pág/av pág." "utilice las flechas y re pág/av pág."
#, fuzzy
msgid "To maximise/minimise the console, press \"tc\"." msgid "To maximise/minimise the console, press \"tc\"."
msgstr "Para maximizar/minimizar la consola, presione \"tc\"." msgstr "Para maximizar/minimizar la consola, presione \"tc\"."
@@ -369,10 +368,12 @@ msgstr "Elimina la libreta dada."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Elimina una libreta sin pedir confirmación." msgstr "Elimina una libreta sin pedir confirmación."
msgid "Delete notebook? All notes within this notebook will also be deleted." msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"¿Desea eliminar la libreta? Todas las notas dentro de esta libreta también " "¿Desea eliminar la libreta? Todas las notas y sublibretas dentro de esta "
"serán eliminadas." "libreta también serán eliminadas."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
msgstr "Elimina las notas que coinciden con <note-pattern>." msgstr "Elimina las notas que coinciden con <note-pattern>."
@@ -634,7 +635,7 @@ msgid "View"
msgstr "Ver" msgstr "Ver"
msgid "Toggle sidebar" msgid "Toggle sidebar"
msgstr "" msgstr "Cambia la barra lateral"
msgid "Toggle editor layout" msgid "Toggle editor layout"
msgstr "Cambia el diseño del editor" msgstr "Cambia el diseño del editor"
@@ -845,6 +846,9 @@ msgstr "Añadir o borrar etiquetas"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Cambiar entre nota y lista de tareas" msgstr "Cambiar entre nota y lista de tareas"
msgid "Copy Markdown link"
msgstr "Copiar el enlace de Markdown"
msgid "Delete" msgid "Delete"
msgstr "Eliminar" msgstr "Eliminar"
@@ -1056,6 +1060,9 @@ msgstr "Los elementos cifrados no pueden ser modificados"
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflictos" msgstr "Conflictos"
msgid "Cannot move notebook to this location"
msgstr "No se puede mover la libreta a este lugar"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Ya existe una libreta con este nombre: «%s»" msgstr "Ya existe una libreta con este nombre: «%s»"
@@ -1110,6 +1117,10 @@ msgstr "Oscuro"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Mostrar tareas incompletas al inicio de las listas" msgstr "Mostrar tareas incompletas al inicio de las listas"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Mostrar tareas incompletas al inicio de las listas"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Ordenar notas por" msgstr "Ordenar notas por"
@@ -1135,7 +1146,7 @@ msgid "Show tray icon"
msgstr "Mostrar icono en la bandeja" msgstr "Mostrar icono en la bandeja"
msgid "Note: Does not work in all desktop environments." msgid "Note: Does not work in all desktop environments."
msgstr "" msgstr "Nota: No funciona en todos los entornos de escritorio."
msgid "Global zoom percentage" msgid "Global zoom percentage"
msgstr "Establecer el porcentaje de aumento de la aplicación" msgstr "Establecer el porcentaje de aumento de la aplicación"
@@ -1388,6 +1399,15 @@ msgstr "Guardar cambios"
msgid "Discard changes" msgid "Discard changes"
msgstr "Descartar cambios" msgstr "Descartar cambios"
#, javascript-format
msgid "No item with ID %s"
msgstr "No hay elementos con el ID %s"
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
"La aplicación móvil de Joplin no soporta actualmente este tipo de enlace: %s"
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Tipo de imagen no soportado: %s" msgstr "Tipo de imagen no soportado: %s"

View File

@@ -227,7 +227,7 @@ msgid "The possible commands are:"
msgstr "Litezkeen komandoak hauek dira:" msgstr "Litezkeen komandoak hauek dira:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -368,7 +368,10 @@ msgstr "Ezabatu emandako koadernoak."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Ezabatu koadernoak berrespenik gabe." msgstr "Ezabatu koadernoak berrespenik gabe."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "Koadernoa ezabatu? Dituen ohar guztiak ere ezabatuko dira." msgstr "Koadernoa ezabatu? Dituen ohar guztiak ere ezabatuko dira."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -845,6 +848,9 @@ msgstr "Gehitu edo ezabatu etiketak"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Aldatu oharra eta zeregin eren artean." msgstr "Aldatu oharra eta zeregin eren artean."
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "Ezabatu" msgstr "Ezabatu"
@@ -1060,6 +1066,10 @@ msgstr "Zifratutako itemak ezin aldatu daitezke"
msgid "Conflicts" msgid "Conflicts"
msgstr "Gatazkak" msgstr "Gatazkak"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Ezin eraman daiteke oharra \"%s\" koadernora"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Dagoeneko bada koaderno bat izen horrekin: \"%s\"" msgstr "Dagoeneko bada koaderno bat izen horrekin: \"%s\""
@@ -1115,6 +1125,10 @@ msgstr "Iluna"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Bete gabeko zereginak erakutsi zerrendaren goiko partean" msgstr "Bete gabeko zereginak erakutsi zerrendaren goiko partean"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Bete gabeko zereginak erakutsi zerrendaren goiko partean"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1398,6 +1412,14 @@ msgstr "Gorde aldaketak"
msgid "Discard changes" msgid "Discard changes"
msgstr "Bertan behera utzi aldaketak" msgstr "Bertan behera utzi aldaketak"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Irudi formatua ez onartua: %s" msgstr "Irudi formatua ez onartua: %s"

View File

@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Les commandes possibles sont :" msgstr "Les commandes possibles sont :"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -365,10 +365,12 @@ msgstr "Supprimer le carnet."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Supprimer le carnet sans demander la confirmation." msgstr "Supprimer le carnet sans demander la confirmation."
msgid "Delete notebook? All notes within this notebook will also be deleted." msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Effacer le carnet ? Toutes les notes dans ce carnet seront également " "Effacer le carnet ? Toutes les notes et sous-carnets dans ce carnet seront "
"effacées." "également effacés."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
msgstr "Supprimer les notes correspondants à <note-pattern>." msgstr "Supprimer les notes correspondants à <note-pattern>."
@@ -848,6 +850,9 @@ msgstr "Gérer les étiquettes"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Alterner entre note et tâche" msgstr "Alterner entre note et tâche"
msgid "Copy Markdown link"
msgstr "Copier lien Markdown"
msgid "Delete" msgid "Delete"
msgstr "Supprimer" msgstr "Supprimer"
@@ -1063,6 +1068,9 @@ msgstr "Les objets cryptés ne peuvent être modifiés"
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflits" msgstr "Conflits"
msgid "Cannot move notebook to this location"
msgstr "Impossible de déplacer le carnet vers le carnet \"%s\""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Un carnet avec ce titre existe déjà : \"%s\"" msgstr "Un carnet avec ce titre existe déjà : \"%s\""
@@ -1116,6 +1124,9 @@ msgstr "Sombre"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Tâches non-terminées en haut" msgstr "Tâches non-terminées en haut"
msgid "Show completed to-dos"
msgstr "Afficher les tâches complétées"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Trier les notes par" msgstr "Trier les notes par"
@@ -1396,6 +1407,15 @@ msgstr "Enregistrer les changements"
msgid "Discard changes" msgid "Discard changes"
msgstr "Ignorer les changements" msgstr "Ignorer les changements"
#, javascript-format
msgid "No item with ID %s"
msgstr "Aucun objet avec identifiant %s"
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
"L'application mobile Joplin ne gère pas pour l'instant ce type de lien : %s"
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Type d'image non géré : %s" msgstr "Type d'image non géré : %s"

View File

@@ -226,7 +226,7 @@ msgid "The possible commands are:"
msgstr "As ordes posíbeis son:" msgstr "As ordes posíbeis son:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -364,7 +364,10 @@ msgstr "Eliminar o carderno indicado."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Elimina o caderno indicado sen solicitar confirmación." msgstr "Elimina o caderno indicado sen solicitar confirmación."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Desexa eliminar o caderno? Tamén se eliminarán todas as notas deste caderno." "Desexa eliminar o caderno? Tamén se eliminarán todas as notas deste caderno."
@@ -832,6 +835,10 @@ msgstr "Engadir ou eliminar etiquetas"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Cambiar entre notas e tarefas" msgstr "Cambiar entre notas e tarefas"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Eliminar" msgstr "Eliminar"
@@ -1043,6 +1050,10 @@ msgstr "Non é posíbel modificar elementos cifrados"
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflitos" msgstr "Conflitos"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Non é posíbel mover a nota ao caderno «%s»"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Xa existe un caderno con ese título: «%s»" msgstr "Xa existe un caderno con ese título: «%s»"
@@ -1096,6 +1107,10 @@ msgstr "Escuro"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Tarefas sen completar arriba" msgstr "Tarefas sen completar arriba"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Tarefas sen completar arriba"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Ordenar notas por" msgstr "Ordenar notas por"
@@ -1374,6 +1389,14 @@ msgstr "Gardar cambios"
msgid "Discard changes" msgid "Discard changes"
msgstr "Desbotar os cambios" msgstr "Desbotar os cambios"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Tipo de imaxe incompatíbel: %s" msgstr "Tipo de imaxe incompatíbel: %s"

View File

@@ -229,15 +229,11 @@ msgstr ""
msgid "The possible commands are:" msgid "The possible commands are:"
msgstr "Moguće naredbe su:" msgstr "Moguće naredbe su:"
#, fuzzy
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
"In any command, a note or notebook can be refered to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgid "To move from one pane to another, press Tab or Shift+Tab." msgid "To move from one pane to another, press Tab or Shift+Tab."
msgstr "Za prijelaz iz jednog okna u drugo, pritisni Tab ili Shift+Tab." msgstr "Za prijelaz iz jednog okna u drugo, pritisni Tab ili Shift+Tab."
@@ -372,7 +368,10 @@ msgstr "Briše datu bilježnicu."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Briše bilježnicu bez traženja potvrde." msgstr "Briše bilježnicu bez traženja potvrde."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Obrisati bilježnicu? Sve bilješke u toj bilježnici će također biti obrisane." "Obrisati bilježnicu? Sve bilješke u toj bilježnici će također biti obrisane."
@@ -837,6 +836,9 @@ msgstr "Dodaj ili makni oznake"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Zamijeni bilješku i zadatak" msgstr "Zamijeni bilješku i zadatak"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "Obriši" msgstr "Obriši"
@@ -1048,6 +1050,10 @@ msgstr "Neke stavke se ne mogu sinkronizirati."
msgid "Conflicts" msgid "Conflicts"
msgstr "Sukobi" msgstr "Sukobi"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Ne mogu premjestiti bilješku u bilježnicu %s"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Bilježnica s ovim naslovom već postoji: \"%s\"" msgstr "Bilježnica s ovim naslovom već postoji: \"%s\""
@@ -1102,6 +1108,10 @@ msgstr "Tamna"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Prikaži nezavršene zadatke na vrhu liste" msgstr "Prikaži nezavršene zadatke na vrhu liste"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Prikaži nezavršene zadatke na vrhu liste"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1379,6 +1389,14 @@ msgstr "Spremi promjene"
msgid "Discard changes" msgid "Discard changes"
msgstr "Odbaci promjene" msgstr "Odbaci promjene"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Nepodržana vrsta slike: %s" msgstr "Nepodržana vrsta slike: %s"

View File

@@ -226,7 +226,7 @@ msgid "The possible commands are:"
msgstr "I possibili comandi sono:" msgstr "I possibili comandi sono:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -364,7 +364,9 @@ msgstr "Elimina il seguente blocco note."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Elimina il blocco note senza richiedere una conferma." msgstr "Elimina il blocco note senza richiedere una conferma."
msgid "Delete notebook? All notes within this notebook will also be deleted." msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -819,6 +821,9 @@ msgstr "Aggiungi o rimuovi etichetta"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Passa da un tipo di nota a un elenco di attività" msgstr "Passa da un tipo di nota a un elenco di attività"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "Elimina" msgstr "Elimina"
@@ -1034,6 +1039,10 @@ msgstr "Alcuni elementi non possono essere sincronizzati."
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflitti" msgstr "Conflitti"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Non posso spostare la nota nel blocco note \"%s\""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Esiste già un blocco note col titolo \"%s\"" msgstr "Esiste già un blocco note col titolo \"%s\""
@@ -1088,6 +1097,10 @@ msgstr "Scuro"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Mostra todo inclompleti in cima alla lista" msgstr "Mostra todo inclompleti in cima alla lista"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Mostra todo inclompleti in cima alla lista"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1365,6 +1378,14 @@ msgstr "Salva i cambiamenti"
msgid "Discard changes" msgid "Discard changes"
msgstr "Ignora modifiche" msgstr "Ignora modifiche"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Tipo di immagine non supportata: %s" msgstr "Tipo di immagine non supportata: %s"

View File

@@ -224,7 +224,7 @@ msgid "The possible commands are:"
msgstr "有効なコマンドは:" msgstr "有効なコマンドは:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -361,7 +361,10 @@ msgstr "指定されたノートブックを削除します。"
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "ノートブックを確認なしで削除します。" msgstr "ノートブックを確認なしで削除します。"
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "ノートブックを削除しますか?中にあるノートはすべて消えてしまいます。" msgstr "ノートブックを削除しますか?中にあるノートはすべて消えてしまいます。"
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -823,6 +826,9 @@ msgstr "タグの追加・削除"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "ノートとToDoを切り替え" msgstr "ノートとToDoを切り替え"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "削除" msgstr "削除"
@@ -1036,6 +1042,10 @@ msgstr "いくつかの項目は同期されませんでした。"
msgid "Conflicts" msgid "Conflicts"
msgstr "衝突" msgstr "衝突"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "ノートをノートブック \"%s\"に移動できませんでした。"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "\"%s\"という名前のノートブックはすでに存在しています。" msgstr "\"%s\"という名前のノートブックはすでに存在しています。"
@@ -1092,6 +1102,10 @@ msgstr "暗い"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "未完のToDoをリストの上部に表示" msgstr "未完のToDoをリストの上部に表示"
#, fuzzy
msgid "Show completed to-dos"
msgstr "未完のToDoをリストの上部に表示"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1369,6 +1383,14 @@ msgstr "変更を保存"
msgid "Discard changes" msgid "Discard changes"
msgstr "変更を破棄" msgstr "変更を破棄"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "サポートされていないイメージ形式: %s." msgstr "サポートされていないイメージ形式: %s."

View File

@@ -210,7 +210,7 @@ msgid "The possible commands are:"
msgstr "" msgstr ""
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -332,7 +332,9 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "" msgstr ""
msgid "Delete notebook? All notes within this notebook will also be deleted." msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -753,6 +755,9 @@ msgstr ""
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "" msgstr ""
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "" msgstr ""
@@ -954,6 +959,9 @@ msgstr ""
msgid "Conflicts" msgid "Conflicts"
msgstr "" msgstr ""
msgid "Cannot move notebook to this location"
msgstr ""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "" msgstr ""
@@ -1005,6 +1013,9 @@ msgstr ""
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "" msgstr ""
msgid "Show completed to-dos"
msgstr ""
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1271,6 +1282,14 @@ msgstr ""
msgid "Discard changes" msgid "Discard changes"
msgstr "" msgstr ""
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "" msgstr ""

View File

@@ -230,7 +230,7 @@ msgid "The possible commands are:"
msgstr "Mogelijke commando's zijn:" msgstr "Mogelijke commando's zijn:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -369,7 +369,10 @@ msgstr "Verwijdert het opgegeven notitieboek."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Verwijdert het notitieboek zonder te vragen om bevestiging." msgstr "Verwijdert het notitieboek zonder te vragen om bevestiging."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Notitieboek verwijderen? Alle notities in dit notitieboek zullen ook " "Notitieboek verwijderen? Alle notities in dit notitieboek zullen ook "
"verwijderd worden." "verwijderd worden."
@@ -847,6 +850,9 @@ msgstr "Voeg tag toe of verwijder tag"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Wissel tussen notitie en to-do type" msgstr "Wissel tussen notitie en to-do type"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "Verwijderen" msgstr "Verwijderen"
@@ -1062,6 +1068,10 @@ msgstr "Versleutelde items kunnen niet aangepast worden"
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflicten" msgstr "Conflicten"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kan notitie niet naar notitieboek \"%s\" verplaatsen."
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Er bestaat al een notitieboek met \"%s\" als titel" msgstr "Er bestaat al een notitieboek met \"%s\" als titel"
@@ -1119,6 +1129,10 @@ msgstr "Donker"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Toon onvoltooide to-do's aan de top van de lijsten" msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Toon onvoltooide to-do's aan de top van de lijsten"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "" msgstr ""
@@ -1400,6 +1414,14 @@ msgstr "Sla wijzigingen op"
msgid "Discard changes" msgid "Discard changes"
msgstr "Verwijder wijzigingen" msgstr "Verwijder wijzigingen"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Afbeeldingstype %s wordt niet ondersteund" msgstr "Afbeeldingstype %s wordt niet ondersteund"

View File

@@ -227,7 +227,7 @@ msgid "The possible commands are:"
msgstr "Os comandos possíveis são:" msgstr "Os comandos possíveis são:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -363,7 +363,10 @@ msgstr "Exclui o caderno informado."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Exclui o caderno sem pedir confirmação." msgstr "Exclui o caderno sem pedir confirmação."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "" msgstr ""
"Excluir o caderno? Todas as notas deste caderno notebook também serão " "Excluir o caderno? Todas as notas deste caderno notebook também serão "
"excluídas." "excluídas."
@@ -842,6 +845,10 @@ msgstr "Adicionar ou remover tags"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Alternar entre os tipos Nota e Tarefa" msgstr "Alternar entre os tipos Nota e Tarefa"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Excluir" msgstr "Excluir"
@@ -1055,6 +1062,10 @@ msgstr "Itens encriptados não podem ser modificados"
msgid "Conflicts" msgid "Conflicts"
msgstr "Conflitos" msgstr "Conflitos"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Não é possível mover a nota para o caderno \"%s\""
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Já existe caderno com este título: \"%s\"" msgstr "Já existe caderno com este título: \"%s\""
@@ -1109,6 +1120,10 @@ msgstr "Dark"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Mostrar tarefas incompletas no topo" msgstr "Mostrar tarefas incompletas no topo"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Mostrar tarefas incompletas no topo"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Ordenar notas por" msgstr "Ordenar notas por"
@@ -1387,6 +1402,14 @@ msgstr "Gravar alterações"
msgid "Discard changes" msgid "Discard changes"
msgstr "Descartar alterações" msgstr "Descartar alterações"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Tipo de imagem não suportada: %s" msgstr "Tipo de imagem não suportada: %s"

View File

@@ -230,7 +230,7 @@ msgid "The possible commands are:"
msgstr "Доступные команды:" msgstr "Доступные команды:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -369,7 +369,10 @@ msgstr "Удаляет заданный блокнот."
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "Удаляет блокнот без запроса подтверждения." msgstr "Удаляет блокнот без запроса подтверждения."
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "Удалить блокнот? Все заметки в этом блокноте также будут удалены." msgstr "Удалить блокнот? Все заметки в этом блокноте также будут удалены."
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -842,6 +845,10 @@ msgstr "Добавить или удалить теги"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "Переключить тип между заметкой и задачей" msgstr "Переключить тип между заметкой и задачей"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete" msgid "Delete"
msgstr "Удалить" msgstr "Удалить"
@@ -1053,6 +1060,10 @@ msgstr "Зашифрованные элементы не могут быть и
msgid "Conflicts" msgid "Conflicts"
msgstr "Конфликты" msgstr "Конфликты"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Не удалось переместить заметку в блокнот «%s»"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "Блокнот с таким названием уже существует: «%s»" msgstr "Блокнот с таким названием уже существует: «%s»"
@@ -1106,6 +1117,10 @@ msgstr "Тёмная"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "Незавершённые задачи сверху" msgstr "Незавершённые задачи сверху"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Незавершённые задачи сверху"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "Сортировать заметки по" msgstr "Сортировать заметки по"
@@ -1385,6 +1400,14 @@ msgstr "Сохранить изменения"
msgid "Discard changes" msgid "Discard changes"
msgstr "Отменить изменения" msgstr "Отменить изменения"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "Неподдерживаемый формат изображения: %s" msgstr "Неподдерживаемый формат изображения: %s"

View File

@@ -220,7 +220,7 @@ msgid "The possible commands are:"
msgstr "可用命令为:" msgstr "可用命令为:"
msgid "" msgid ""
"In any command, a note or notebook can be refered to by title or ID, or " "In any command, a note or notebook can be referred to by title or ID, or "
"using the shortcuts `$n` or `$b` for, respectively, the currently selected " "using the shortcuts `$n` or `$b` for, respectively, the currently selected "
"note or notebook. `$c` can be used to refer to the currently selected item." "note or notebook. `$c` can be used to refer to the currently selected item."
msgstr "" msgstr ""
@@ -350,7 +350,10 @@ msgstr "删除给定笔记本。"
msgid "Deletes the notebook without asking for confirmation." msgid "Deletes the notebook without asking for confirmation."
msgstr "删除笔记本(不要求确认)。" msgstr "删除笔记本(不要求确认)。"
msgid "Delete notebook? All notes within this notebook will also be deleted." #, fuzzy
msgid ""
"Delete notebook? All notes and sub-notebooks within this notebook will also "
"be deleted."
msgstr "是否删除笔记本?此笔记本内所有笔记也将被删除。" msgstr "是否删除笔记本?此笔记本内所有笔记也将被删除。"
msgid "Deletes the notes matching <note-pattern>." msgid "Deletes the notes matching <note-pattern>."
@@ -802,6 +805,9 @@ msgstr "添加或删除标签"
msgid "Switch between note and to-do type" msgid "Switch between note and to-do type"
msgstr "在笔记和待办事项类型之间切换" msgstr "在笔记和待办事项类型之间切换"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete" msgid "Delete"
msgstr "删除" msgstr "删除"
@@ -1008,6 +1014,10 @@ msgstr "无法修改加密项目。"
msgid "Conflicts" msgid "Conflicts"
msgstr "冲突文件" msgstr "冲突文件"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "无法移动笔记至\"%s\"笔记本"
#, javascript-format #, javascript-format
msgid "A notebook with this title already exists: \"%s\"" msgid "A notebook with this title already exists: \"%s\""
msgstr "以此标题命名的笔记本已存在:\"%s\"" msgstr "以此标题命名的笔记本已存在:\"%s\""
@@ -1059,6 +1069,10 @@ msgstr "深色"
msgid "Uncompleted to-dos on top" msgid "Uncompleted to-dos on top"
msgstr "未完成的待办事项在顶端" msgstr "未完成的待办事项在顶端"
#, fuzzy
msgid "Show completed to-dos"
msgstr "未完成的待办事项在顶端"
msgid "Sort notes by" msgid "Sort notes by"
msgstr "排序笔记" msgstr "排序笔记"
@@ -1332,6 +1346,14 @@ msgstr "保存更改"
msgid "Discard changes" msgid "Discard changes"
msgstr "放弃更改" msgstr "放弃更改"
#, javascript-format
msgid "No item with ID %s"
msgstr ""
#, javascript-format
msgid "The Joplin mobile app does not currently support this type of link: %s"
msgstr ""
#, javascript-format #, javascript-format
msgid "Unsupported image type: %s" msgid "Unsupported image type: %s"
msgstr "不支持的图片格式:%s" msgstr "不支持的图片格式:%s"

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
], ],
"owner": "Laurent Cozic" "owner": "Laurent Cozic"
}, },
"version": "1.0.106", "version": "1.0.107",
"bin": { "bin": {
"joplin": "./main.js" "joplin": "./main.js"
}, },
@@ -58,9 +58,10 @@
"strip-ansi": "^4.0.0", "strip-ansi": "^4.0.0",
"tar": "^4.4.0", "tar": "^4.4.0",
"tcp-port-used": "^0.1.2", "tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.25", "tkwidgets": "^0.5.26",
"url-parse": "^1.2.0", "url-parse": "^1.2.0",
"uuid": "^3.0.1", "uuid": "^3.0.1",
"valid-url": "^1.0.9",
"word-wrap": "^1.2.3", "word-wrap": "^1.2.3",
"xml2js": "^0.4.19", "xml2js": "^0.4.19",
"yargs-parser": "^7.0.0" "yargs-parser": "^7.0.0"

View File

@@ -9,7 +9,7 @@ rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
mkdir -p "$BUILD_DIR/data" mkdir -p "$BUILD_DIR/data"
if [[ $TEST_FILE == "" ]]; then if [[ $TEST_FILE == "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js tests-build/services_InteropService.js) (cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js tests-build/models_Note.js tests-build/models_Folder.js tests-build/services_InteropService.js)
else else
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js) (cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi fi

View File

@@ -44,4 +44,13 @@ describe('ArrayUtils', function() {
done(); done();
}); });
it('should compare arrays', async (done) => {
expect(ArrayUtils.contentEquals([], [])).toBe(true);
expect(ArrayUtils.contentEquals(['a'], ['a'])).toBe(true);
expect(ArrayUtils.contentEquals(['b', 'a'], ['a', 'b'])).toBe(true);
expect(ArrayUtils.contentEquals(['b'], ['a', 'b'])).toBe(false);
done();
});
}); });

View File

@@ -0,0 +1,55 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
async function allItems() {
let folders = await Folder.all();
let notes = await Note.all();
return folders.concat(notes);
}
describe('models_Folder', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should tell if a notebook can be nested under another one', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder1" });
let f2 = await Folder.save({ title: "folder2", parent_id: f1.id });
let f3 = await Folder.save({ title: "folder3", parent_id: f2.id });
let f4 = await Folder.save({ title: "folder4" });
expect(await Folder.canNestUnder(f1.id, f2.id)).toBe(false);
expect(await Folder.canNestUnder(f2.id, f2.id)).toBe(false);
expect(await Folder.canNestUnder(f3.id, f1.id)).toBe(true);
expect(await Folder.canNestUnder(f4.id, f1.id)).toBe(true);
expect(await Folder.canNestUnder(f2.id, f3.id)).toBe(false);
expect(await Folder.canNestUnder(f3.id, f2.id)).toBe(true);
expect(await Folder.canNestUnder(f1.id, '')).toBe(true);
expect(await Folder.canNestUnder(f2.id, '')).toBe(true);
}));
it('should recursively delete notes and sub-notebooks', asyncTest(async () => {
let f1 = await Folder.save({ title: "folder1" });
let f2 = await Folder.save({ title: "folder2", parent_id: f1.id });
let n1 = await Note.save({ title: 'note1', parent_id: f2.id });
await Folder.delete(f1.id);
const all = await allItems();
expect(all.length).toBe(0);
}));
});

View File

@@ -0,0 +1,39 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('models_Note', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should find resource and note IDs', asyncTest(async () => {
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id });
let items = await Note.linkedItems(note2.body);
expect(items.length).toBe(1);
expect(items[0].id).toBe(note1.id);
await shim.attachFileToNote(note2, __dirname + '/../tests/support/photo.jpg');
note2 = await Note.load(note2.id);
items = await Note.linkedItems(note2.body);
expect(items.length).toBe(2);
expect(items[0].type_).toBe(BaseModel.TYPE_NOTE);
expect(items[1].type_).toBe(BaseModel.TYPE_RESOURCE);
}));
});

View File

@@ -180,7 +180,7 @@ describe('services_InteropService', function() {
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg'); await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
note1 = await Note.load(note1.id); note1 = await Note.load(note1.id);
let resourceIds = Note.linkedResourceIds(note1.body); let resourceIds = await Note.linkedResourceIds(note1.body);
let resource1 = await Resource.load(resourceIds[0]); let resource1 = await Resource.load(resourceIds[0]);
await service.export({ path: filePath }); await service.export({ path: filePath });
@@ -193,7 +193,7 @@ describe('services_InteropService', function() {
let note2 = (await Note.all())[0]; let note2 = (await Note.all())[0];
expect(note2.body).not.toBe(note1.body); expect(note2.body).not.toBe(note1.body);
resourceIds = Note.linkedResourceIds(note2.body); resourceIds = await Note.linkedResourceIds(note2.body);
expect(resourceIds.length).toBe(1); expect(resourceIds.length).toBe(1);
let resource2 = await Resource.load(resourceIds[0]); let resource2 = await Resource.load(resourceIds[0]);
expect(resource2.id).not.toBe(resource1.id); expect(resource2.id).not.toBe(resource1.id);
@@ -249,4 +249,28 @@ describe('services_InteropService', function() {
expect(folder2.title).toBe('folder1'); expect(folder2.title).toBe('folder1');
})); }));
it('should export and import links to notes', asyncTest(async () => {
const service = new InteropService();
const filePath = exportDir() + '/test.jex';
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
let note2 = await Note.save({ title: 'ma deuxième note', body: 'Lien vers première note : ' + Note.markdownTag(note1), parent_id: folder1.id });
await service.export({ path: filePath, sourceFolderIds: [folder1.id] });
await Note.delete(note1.id);
await Note.delete(note2.id);
await Folder.delete(folder1.id);
await service.import({ path: filePath });
expect(await Note.count()).toBe(2);
expect(await Folder.count()).toBe(1);
let note1_2 = await Note.loadByTitle('ma note');
let note2_2 = await Note.loadByTitle('ma deuxième note');
expect(note2_2.body.indexOf(note1_2.id) >= 0).toBe(true);
}));
}); });

View File

@@ -164,7 +164,7 @@ class Application extends BaseApplication {
} }
if (["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) { if (["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(5, { syncSteps: ["update_remote", "delete_remote"] }); if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(30 * 1000, { syncSteps: ["update_remote", "delete_remote"] });
} }
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) { if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
@@ -437,6 +437,14 @@ class Application extends BaseApplication {
click: () => { click: () => {
Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop')); Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
}, },
}, {
label: Setting.settingMetadata('showCompletedTodos').label(),
type: 'checkbox',
checked: Setting.value('showCompletedTodos'),
screens: ['Main'],
click: () => {
Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
},
}], }],
}, { }, {
label: _('Tools'), label: _('Tools'),
@@ -615,6 +623,11 @@ class Application extends BaseApplication {
id: Setting.value('activeFolderId'), id: Setting.value('activeFolderId'),
}); });
this.store().dispatch({
type: 'FOLDER_SET_COLLAPSED_ALL',
ids: Setting.value('collapsedFolderIds'),
});
// Note: Auto-update currently doesn't work in Linux: it downloads the update // Note: Auto-update currently doesn't work in Linux: it downloads the update
// but then doesn't install it on exit. // but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) { if (shim.isWindows() || shim.isMac()) {

View File

@@ -43,7 +43,7 @@ async function fetchLatestRelease() {
for (let i = 0; i < json.assets.length; i++) { for (let i = 0; i < json.assets.length; i++) {
const asset = json.assets[i]; const asset = json.assets[i];
let found = false; let found = false;
if (platform === 'win32' && asset.name.indexOf('.exe') >= 0) { if (platform === 'win32' && asset.name.indexOf('.exe') >= 0 && asset.name.indexOf('Setup') >= 0) {
found = true; found = true;
} else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) { } else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) {
found = true; found = true;

View File

@@ -96,6 +96,16 @@ class NoteListComponent extends React.Component {
} }
}})); }}));
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
const { clipboard } = require('electron');
const links = [];
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
links.push(Note.markdownTag(note));
}
clipboard.writeText(links.join(' '));
}}));
const exportMenu = new Menu(); const exportMenu = new Menu();
const ioService = new InteropService(); const ioService = new InteropService();

View File

@@ -1,5 +1,6 @@
const React = require('react'); const React = require('react');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
const Search = require('lib/models/Search.js'); const Search = require('lib/models/Search.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
@@ -19,6 +20,10 @@ const MenuItem = bridge().MenuItem;
const { shim } = require('lib/shim.js'); const { shim } = require('lib/shim.js');
const eventManager = require('../eventManager'); const eventManager = require('../eventManager');
const fs = require('fs-extra'); const fs = require('fs-extra');
const {clipboard} = require('electron')
const md5 = require('md5');
const mimeUtils = require('lib/mime-utils.js').mime;
const ArrayUtils = require('lib/ArrayUtils');
require('brace/mode/markdown'); require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html // https://ace.c9.io/build/kitchen-sink.html
@@ -46,6 +51,7 @@ class NoteTextComponent extends React.Component {
// changed by the user, this variable contains that note ID. Used // changed by the user, this variable contains that note ID. Used
// to automatically set the title. // to automatically set the title.
newAndNoTitleChangeNoteId: null, newAndNoTitleChangeNoteId: null,
bodyHtml: '',
}; };
this.lastLoadedNoteId_ = null; this.lastLoadedNoteId_ = null;
@@ -54,6 +60,8 @@ class NoteTextComponent extends React.Component {
this.ignoreNextEditorScroll_ = false; this.ignoreNextEditorScroll_ = false;
this.scheduleSaveTimeout_ = null; this.scheduleSaveTimeout_ = null;
this.restoreScrollTop_ = null; this.restoreScrollTop_ = null;
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
// Complicated but reliable method to get editor content height // Complicated but reliable method to get editor content height
// https://github.com/ajaxorg/ace/issues/2046 // https://github.com/ajaxorg/ace/issues/2046
@@ -71,12 +79,67 @@ class NoteTextComponent extends React.Component {
this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); } this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
this.onEditorPaste_ = async (event) => {
const formats = clipboard.availableFormats();
for (let i = 0; i < formats.length; i++) {
const format = formats[i].toLowerCase();
const formatType = format.split('/')[0]
if (formatType === 'image') {
event.preventDefault();
const image = clipboard.readImage();
const fileExt = mimeUtils.toFileExtension(format);
const filePath = Setting.value('tempDir') + '/' + md5(Date.now()) + '.' + fileExt;
await shim.writeImageToFile(image, format, filePath);
await this.commandAttachFile([filePath]);
await shim.fsDriver().remove(filePath);
}
}
}
this.onDrop_ = async (event) => {
const files = event.dataTransfer.files;
if (!files || !files.length) return;
const filesToAttach = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.path) continue;
filesToAttach.push(file.path);
}
await this.commandAttachFile(filesToAttach);
}
}
cursorPosition() {
if (!this.editor_ || !this.editor_.editor || !this.state.note || !this.state.note.body) return 0;
const cursorPos = this.editor_.editor.getCursorPosition();
const noteLines = this.state.note.body.split('\n');
let pos = 0;
for (let i = 0; i < noteLines.length; i++) {
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
if (i === cursorPos.row) {
pos += cursorPos.column;
break;
} else {
pos += noteLines[i].length;
}
}
return pos;
} }
mdToHtml() { mdToHtml() {
if (this.mdToHtml_) return this.mdToHtml_; if (this.mdToHtml_) return this.mdToHtml_;
this.mdToHtml_ = new MdToHtml({ this.mdToHtml_ = new MdToHtml({
supportsResourceLinks: true,
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/', resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
}); });
return this.mdToHtml_; return this.mdToHtml_;
@@ -210,10 +273,22 @@ class NoteTextComponent extends React.Component {
} }
if (this.editor_) { if (this.editor_) {
const session = this.editor_.editor.getSession(); // Calling setValue here does two things:
const undoManager = session.getUndoManager(); // 1. It sets the initial value as recorded by the undo manager. If we were to set it instead to "" and wait for the render
undoManager.reset(); // phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
session.setUndoManager(undoManager); // would clear it.
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
// Note: calling undoManager.reset() doesn't work
try {
this.editor_.editor.getSession().setValue(note ? note.body : '');
} catch (error) {
if (error.message === "Cannot read property 'match' of undefined") {
// The internals of Ace Editor throws an exception when creating a new note,
// but that can be ignored.
} else {
console.error(error);
}
}
this.editor_.editor.clearSelection(); this.editor_.editor.clearSelection();
this.editor_.editor.moveCursorTo(0,0); this.editor_.editor.moveCursorTo(0,0);
} }
@@ -231,7 +306,12 @@ class NoteTextComponent extends React.Component {
newState.newAndNoTitleChangeNoteId = null; newState.newAndNoTitleChangeNoteId = null;
} }
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
this.setState(newState); this.setState(newState);
this.updateHtml(newState.note ? newState.note.body : '');
} }
async componentWillReceiveProps(nextProps) { async componentWillReceiveProps(nextProps) {
@@ -322,11 +402,29 @@ class NoteTextComponent extends React.Component {
menu.popup(bridge().window()); menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) { } else if (msg.indexOf('joplin://') === 0) {
const resourceId = msg.substr('joplin://'.length); const itemId = msg.substr('joplin://'.length);
Resource.load(resourceId).then((resource) => { const item = await BaseItem.loadItemById(itemId);
const filePath = Resource.fullPath(resource);
if (!item) throw new Error('No item with ID ' + itemId);
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const filePath = Resource.fullPath(item);
bridge().openItem(filePath); bridge().openItem(filePath);
}); } else if (item.type_ === BaseModel.TYPE_NOTE) {
this.props.dispatch({
type: "FOLDER_SELECT",
id: item.parent_id,
});
setTimeout(() => {
this.props.dispatch({
type: 'NOTE_SELECT',
id: item.id,
});
}, 10);
} else {
throw new Error('Unsupported item type: ' + item.type_);
}
} else { } else {
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg)); bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
} }
@@ -391,6 +489,7 @@ class NoteTextComponent extends React.Component {
if (this.editor_) { if (this.editor_) {
this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_); this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_);
document.querySelector('#note-editor').removeEventListener('paste', this.onEditorPaste_, true);
} }
this.editor_ = element; this.editor_ = element;
@@ -398,7 +497,14 @@ class NoteTextComponent extends React.Component {
if (this.editor_) { if (this.editor_) {
this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_); this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_);
const cancelledKeys = ['Ctrl+F', 'Ctrl+T', 'Ctrl+P', 'Ctrl+Q', 'Ctrl+L', 'Ctrl+,']; const cancelledKeys = [];
const letters = ['F', 'T', 'P', 'Q', 'L', ','];
for (let i = 0; i < letters.length; i++) {
const l = letters[i];
cancelledKeys.push('Ctrl+' + l);
cancelledKeys.push('Command+' + l);
}
for (let i = 0; i < cancelledKeys.length; i++) { for (let i = 0; i < cancelledKeys.length; i++) {
const k = cancelledKeys[i]; const k = cancelledKeys[i];
this.editor_.editor.commands.bindKey(k, () => { this.editor_.editor.commands.bindKey(k, () => {
@@ -409,6 +515,8 @@ class NoteTextComponent extends React.Component {
throw new Error('HACK: Overriding Ace Editor shortcut: ' + k); throw new Error('HACK: Overriding Ace Editor shortcut: ' + k);
}); });
} }
document.querySelector('#note-editor').addEventListener('paste', this.onEditorPaste_, true);
} }
} }
@@ -443,9 +551,52 @@ class NoteTextComponent extends React.Component {
aceEditor_change(body) { aceEditor_change(body) {
shared.noteComponent_change(this, 'body', body); shared.noteComponent_change(this, 'body', body);
this.scheduleHtmlUpdate();
this.scheduleSave(); this.scheduleSave();
} }
scheduleHtmlUpdate(timeout = 500) {
if (this.scheduleHtmlUpdateIID_) {
clearTimeout(this.scheduleHtmlUpdateIID_);
this.scheduleHtmlUpdateIID_ = null;
}
if (timeout) {
this.scheduleHtmlUpdateIID_ = setTimeout(() => {
this.updateHtml();
}, timeout);
} else {
this.updateHtml();
}
}
updateHtml(body = null) {
const mdOptions = {
onResourceLoaded: () => {
this.updateHtml();
this.forceUpdate();
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
const theme = themeStyle(this.props.theme);
let bodyToRender = body;
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
let bodyHtml = '';
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
// Fixes https://github.com/laurent22/joplin/issues/217
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
}
bodyHtml = this.mdToHtml().render(bodyToRender, theme, mdOptions);
this.setState({ bodyHtml: bodyHtml });
}
async doCommand(command) { async doCommand(command) {
if (!command) return; if (!command) return;
@@ -479,27 +630,34 @@ class NoteTextComponent extends React.Component {
} }
} }
async commandAttachFile() { async commandAttachFile(filePaths = null) {
const filePaths = bridge().showOpenDialog({ if (!filePaths) {
properties: ['openFile', 'createDirectory', 'multiSelections'], filePaths = bridge().showOpenDialog({
}); properties: ['openFile', 'createDirectory', 'multiSelections'],
if (!filePaths || !filePaths.length) return; });
if (!filePaths || !filePaths.length) return;
}
await this.saveIfNeeded(true); await this.saveIfNeeded(true);
let note = await Note.load(this.state.note.id); let note = await Note.load(this.state.note.id);
const position = this.cursorPosition();
for (let i = 0; i < filePaths.length; i++) { for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i]; const filePath = filePaths[i];
try { try {
reg.logger().info('Attaching ' + filePath); reg.logger().info('Attaching ' + filePath);
note = await shim.attachFileToNote(note, filePath); note = await shim.attachFileToNote(note, filePath, position);
reg.logger().info('File was attached.'); reg.logger().info('File was attached.');
this.setState({ this.setState({
note: Object.assign({}, note), note: Object.assign({}, note),
lastSavedNote: Object.assign({}, note), lastSavedNote: Object.assign({}, note),
}); });
this.updateHtml(note.body);
} catch (error) { } catch (error) {
reg.logger().error(error); reg.logger().error(error);
bridge().showErrorMessageBox(error.message);
} }
} }
} }
@@ -644,25 +802,21 @@ class NoteTextComponent extends React.Component {
} }
if (this.state.webviewReady) { if (this.state.webviewReady) {
const mdOptions = { let html = this.state.bodyHtml;
onResourceLoaded: () => {
this.forceUpdate();
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
let bodyToRender = body; const htmlHasChanged = this.lastSetHtml_ !== html;
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) { if (htmlHasChanged) {
// Fixes https://github.com/laurent22/joplin/issues/217 this.webview_.send('setHtml', html);
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*'; this.lastSetHtml_ = html;
} }
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
this.webview_.send('setHtml', html);
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
const keywords = search ? Search.keywords(search.query_pattern) : []; const keywords = search ? Search.keywords(search.query_pattern) : [];
this.webview_.send('setMarkers', keywords);
if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords)) {
this.lastSetMarkers_ = [];
this.webview_.send('setMarkers', keywords);
}
} }
const toolbarItems = []; const toolbarItems = [];
@@ -708,7 +862,7 @@ class NoteTextComponent extends React.Component {
const titleBarDate = <span style={Object.assign({}, theme.textStyle, {color: theme.colorFaded})}>{time.formatMsToLocal(note.user_updated_time)}</span> const titleBarDate = <span style={Object.assign({}, theme.textStyle, {color: theme.colorFaded})}>{time.formatMsToLocal(note.user_updated_time)}</span>
const viewer = <webview const viewer = <webview
style={viewerStyle} style={viewerStyle}
nodeintegration="1" nodeintegration="1"
src="gui/note-viewer/index.html" src="gui/note-viewer/index.html"
@@ -763,7 +917,7 @@ class NoteTextComponent extends React.Component {
/> />
return ( return (
<div style={rootStyle}> <div style={rootStyle} onDrop={this.onDrop_}>
<div style={titleBarStyle}> <div style={titleBarStyle}>
{ titleEditor } { titleEditor }
{ titleBarDate } { titleBarDate }

View File

@@ -14,6 +14,57 @@ const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require("../InteropServiceHelper.js"); const InteropServiceHelper = require("../InteropServiceHelper.js");
class SideBarComponent extends React.Component { class SideBarComponent extends React.Component {
constructor() {
super();
this.onFolderDragStart_ = (event) => {
const folderId = event.currentTarget.getAttribute('folderid');
if (!folderId) return;
event.dataTransfer.setDragImage(new Image(), 1, 1);
event.dataTransfer.clearData();
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
};
this.onFolderDragOver_ = (event) => {
if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") >= 0) event.preventDefault();
if (event.dataTransfer.types.indexOf("text/x-jop-folder-ids") >= 0) event.preventDefault();
};
this.onFolderDrop_ = async (event) => {
const folderId = event.currentTarget.getAttribute('folderid');
const dt = event.dataTransfer;
if (!dt) return;
if (dt.types.indexOf("text/x-jop-note-ids") >= 0) {
event.preventDefault();
const noteIds = JSON.parse(dt.getData("text/x-jop-note-ids"));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folderId);
}
} else if (dt.types.indexOf("text/x-jop-folder-ids") >= 0) {
event.preventDefault();
const folderIds = JSON.parse(dt.getData("text/x-jop-folder-ids"));
for (let i = 0; i < folderIds.length; i++) {
await Folder.moveToFolder(folderIds[i], folderId);
}
}
};
this.onFolderToggleClick_ = async (event) => {
const folderId = event.currentTarget.getAttribute('folderid');
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: folderId,
});
};
}
style() { style() {
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
@@ -23,23 +74,39 @@ class SideBarComponent extends React.Component {
root: { root: {
backgroundColor: theme.backgroundColor2, backgroundColor: theme.backgroundColor2,
}, },
listItem: { listItemContainer: {
boxSizing: "border-box",
height: itemHeight, height: itemHeight,
// paddingLeft: 14,
display: "flex",
alignItems: "stretch",
},
listItem: {
fontFamily: theme.fontFamily, fontFamily: theme.fontFamily,
fontSize: theme.fontSize, fontSize: theme.fontSize,
textDecoration: "none", textDecoration: "none",
boxSizing: "border-box",
color: theme.color2, color: theme.color2,
paddingLeft: 14,
display: "flex",
alignItems: "center",
cursor: "default", cursor: "default",
opacity: 0.8, opacity: 0.8,
whiteSpace: "nowrap", whiteSpace: "nowrap",
display: "flex",
flex: 1,
alignItems: 'center',
}, },
listItemSelected: { listItemSelected: {
backgroundColor: theme.selectedColor2, backgroundColor: theme.selectedColor2,
}, },
listItemExpandIcon: {
color: theme.color2,
cursor: "default",
opacity: 0.8,
// fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
textDecoration: "none",
paddingRight: 5,
display: "flex",
alignItems: 'center',
},
conflictFolder: { conflictFolder: {
color: theme.colorError2, color: theme.colorError2,
fontWeight: "bold", fontWeight: "bold",
@@ -89,6 +156,10 @@ class SideBarComponent extends React.Component {
}, },
}; };
style.tagItem = Object.assign({}, style.listItem);
style.tagItem.paddingLeft = 23;
style.tagItem.height = itemHeight;
return style; return style;
} }
@@ -101,7 +172,7 @@ class SideBarComponent extends React.Component {
let deleteMessage = ""; let deleteMessage = "";
if (itemType === BaseModel.TYPE_FOLDER) { if (itemType === BaseModel.TYPE_FOLDER) {
deleteMessage = _("Delete notebook? All notes within this notebook will also be deleted."); deleteMessage = _("Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.");
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
deleteMessage = _("Remove this tag from all the notes?"); deleteMessage = _("Remove this tag from all the notes?");
} else if (itemType === BaseModel.TYPE_SEARCH) { } else if (itemType === BaseModel.TYPE_SEARCH) {
@@ -150,6 +221,19 @@ class SideBarComponent extends React.Component {
}) })
); );
// menu.append(
// new MenuItem({
// label: _("Move"),
// click: async () => {
// this.props.dispatch({
// type: "WINDOW_COMMAND",
// name: "renameFolder",
// id: itemId,
// });
// },
// })
// );
menu.append(new MenuItem({ type: "separator" })); menu.append(new MenuItem({ type: "separator" }));
const InteropService = require("lib/services/InteropService.js"); const InteropService = require("lib/services/InteropService.js");
@@ -194,53 +278,51 @@ class SideBarComponent extends React.Component {
await shared.synchronize_press(this); await shared.synchronize_press(this);
} }
folderItem(folder, selected) { folderItem(folder, selected, hasChildren, depth) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().listItem);
if (selected) style = Object.assign(style, this.style().listItemSelected);
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder); if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
const onDragOver = (event, folder) => {
if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") >= 0) event.preventDefault();
};
const onDrop = async (event, folder) => {
if (event.dataTransfer.types.indexOf("text/x-jop-note-ids") < 0) return;
event.preventDefault();
const noteIds = JSON.parse(event.dataTransfer.getData("text/x-jop-note-ids"));
for (let i = 0; i < noteIds.length; i++) {
await Note.moveToFolder(noteIds[i], folder.id);
}
};
const itemTitle = Folder.displayTitle(folder); const itemTitle = Folder.displayTitle(folder);
let containerStyle = Object.assign({}, this.style().listItemContainer);
// containerStyle.paddingLeft = containerStyle.paddingLeft + depth * 10;
if (selected) containerStyle = Object.assign(containerStyle, this.style().listItemSelected);
let expandLinkStyle = Object.assign({}, this.style().listItemExpandIcon);
let expandIconStyle = {
visibility: hasChildren ? 'visible' : 'hidden',
paddingLeft: 8 + depth * 10,
}
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'fa-plus-square' : 'fa-minus-square';
const expandIcon = <i style={expandIconStyle} className={"fa " + iconName}></i>
const expandLink = hasChildren ? <a style={expandLinkStyle} href="#" folderid={folder.id} onClick={this.onFolderToggleClick_}>{expandIcon}</a> : <span style={expandLinkStyle}>{expandIcon}</span>
return ( return (
<a <div className="list-item-container" style={containerStyle} key={folder.id} onDragStart={this.onFolderDragStart_} onDragOver={this.onFolderDragOver_} onDrop={this.onFolderDrop_} draggable={true} folderid={folder.id}>
className="list-item" { expandLink }
onDragOver={event => { <a
onDragOver(event, folder); className="list-item"
}} href="#"
onDrop={event => { data-id={folder.id}
onDrop(event, folder); data-type={BaseModel.TYPE_FOLDER}
}} onContextMenu={event => this.itemContextMenu(event)}
href="#" style={style}
data-id={folder.id} folderid={folder.id}
data-type={BaseModel.TYPE_FOLDER} onClick={() => {
onContextMenu={event => this.itemContextMenu(event)} this.folderItem_click(folder);
key={folder.id} }}
style={style} onDoubleClick={this.onFolderToggleClick_}
onClick={() => { >
this.folderItem_click(folder); {itemTitle}
}} </a>
> </div>
{itemTitle}
</a>
); );
} }
tagItem(tag, selected) { tagItem(tag, selected) {
let style = Object.assign({}, this.style().listItem); let style = Object.assign({}, this.style().tagItem);
if (selected) style = Object.assign(style, this.style().listItemSelected); if (selected) style = Object.assign(style, this.style().listItemSelected);
return ( return (
<a <a
@@ -285,11 +367,11 @@ class SideBarComponent extends React.Component {
return <div style={{ height: 2, backgroundColor: "blue" }} key={key} />; return <div style={{ height: 2, backgroundColor: "blue" }} key={key} />;
} }
makeHeader(key, label, iconName) { makeHeader(key, label, iconName, extraProps = {}) {
const style = this.style().header; const style = this.style().header;
const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />; const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />;
return ( return (
<div style={style} key={key}> <div style={style} key={key} {...extraProps}>
{icon} {icon}
{label} {label}
</div> </div>
@@ -326,7 +408,10 @@ class SideBarComponent extends React.Component {
let items = []; let items = [];
items.push(this.makeHeader("folderHeader", _("Notebooks"), "fa-folder-o")); items.push(this.makeHeader("folderHeader", _("Notebooks"), "fa-folder-o", {
onDrop: this.onFolderDrop_,
folderid: '',
}));
if (this.props.folders.length) { if (this.props.folders.length) {
const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this)); const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this));
@@ -345,18 +430,6 @@ class SideBarComponent extends React.Component {
); );
} }
// if (this.props.searches.length) {
// items.push(this.makeHeader("searchHeader", _("Searches"), "fa-search"));
// const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this));
// items.push(
// <div className="searches" key="search_items">
// {searchItems}
// </div>
// );
// }
let lines = Synchronizer.reportToLines(this.props.syncReport); let lines = Synchronizer.reportToLines(this.props.syncReport);
const syncReportText = []; const syncReportText = [];
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@@ -396,6 +469,7 @@ const mapStateToProps = state => {
notesParentType: state.notesParentType, notesParentType: state.notesParentType,
locale: state.settings.locale, locale: state.settings.locale,
theme: state.settings.theme, theme: state.settings.theme,
collapsedFolderIds: state.collapsedFolderIds,
}; };
}; };

View File

@@ -124,17 +124,17 @@
loadAndApplyHljs(); loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding // Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox'); // const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) { // for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i]; // const cb = checkboxes[i];
const ul = cb.parentElement.parentElement; // const ul = cb.parentElement.parentElement;
if (!ul) { // if (!ul) {
console.warn('Unexpected layout for checkbox'); // console.warn('Unexpected layout for checkbox');
continue; // continue;
} // }
ul.style.listStyleType = 'none'; // ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0; // ul.style.paddingLeft = 0;
} // }
let previousContentHeight = contentElement.scrollHeight; let previousContentHeight = contentElement.scrollHeight;
let startTime = Date.now(); let startTime = Date.now();

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

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 it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "Joplin", "name": "Joplin",
"version": "1.0.83", "version": "1.0.91",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@@ -92,6 +92,7 @@
"markdown-it": "^8.4.0", "markdown-it": "^8.4.0",
"markdown-it-katex": "^2.0.3", "markdown-it-katex": "^2.0.3",
"md5": "^2.2.1", "md5": "^2.2.1",
"mermaid": "^8.0.0-rc.8",
"mime": "^2.0.3", "mime": "^2.0.3",
"moment": "^2.19.1", "moment": "^2.19.1",
"node-fetch": "^1.7.3", "node-fetch": "^1.7.3",
@@ -113,6 +114,7 @@
"tcp-port-used": "^0.1.2", "tcp-port-used": "^0.1.2",
"url-parse": "^1.2.0", "url-parse": "^1.2.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"valid-url": "^1.0.9",
"xml2js": "^0.4.19" "xml2js": "^0.4.19"
} }
} }

View File

@@ -34,17 +34,15 @@ table td, table th {
background-color: rgba(0,160,255,0.1) !important; background-color: rgba(0,160,255,0.1) !important;
} }
.side-bar .list-item:hover, /*.side-bar .list-item:hover,
.side-bar .synchronize-button:hover { .side-bar .synchronize-button:hover {
/*background-color: #453E53;*/
background-color: #01427B; background-color: #01427B;
} }
.side-bar .list-item:active, .side-bar .list-item:active,
.side-bar .synchronize-button:active { .side-bar .synchronize-button:active {
/*background-color: #564B6C;*/
background-color: #0465BB; background-color: #0465BB;
} }*/
.editor-toolbar .button:not(.disabled):hover, .editor-toolbar .button:not(.disabled):hover,
.header .button:not(.disabled):hover { .header .button:not(.disabled):hover {

View File

@@ -20,15 +20,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
Operating System | Download | Alternative Operating System | Download | Alternative
-----------------|--------|------------------- -----------------|--------|-------------------
Windows (64-bit only) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-Setup-1.0.83.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> | Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> |
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-1.0.83.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> | macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> |
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-1.0.83-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application). Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application).
## Mobile applications ## Mobile applications
Operating System | Download | Alt. Download 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://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.118/joplin-v1.0.118.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://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.125/joplin-v1.0.125.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplin.cozic.net/images/BadgeIOS.png'/></a> | - iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplin.cozic.net/images/BadgeIOS.png'/></a> | -
## Terminal application ## Terminal application
@@ -37,7 +37,7 @@ Operating system | Method
-----------------|---------------- -----------------|----------------
macOS | `brew install joplin` macOS | `brew install joplin`
Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work. Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
Arch Linux | An Arch Linux pakage is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild). Arch Linux | An Arch Linux package is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild).
To start it, type `joplin`. To start it, type `joplin`.
@@ -183,7 +183,11 @@ For a more technical description, mostly relevant for development or to review t
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application. Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
Resources that are not attached to any note will be automatically deleted after a day or two. On the **desktop application**, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.
Resources that are not attached to any note will be automatically deleted after a day or two (see [rationale](https://github.com/laurent22/joplin/issues/154#issuecomment-356582366)).
**Important:** Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.
# Notifications # Notifications
@@ -199,10 +203,28 @@ On mobile, the alarms will be displayed using the built-in notification system.
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues). If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
# Sub-notebooks
Sub-notebooks allow organising multiple notebooks into a tree of notebooks. For example it can be used to regroup all the notebooks related to work, to family or to a particular project under a parent notebook.
![](https://joplin.cozic.net/images/SubNotebooks.png)
- On the **desktop application**, to create a subnotebook, drag and drop it onto another notebook. To move it back to the root, drag and drop it on the "Notebooks" header. Currently only the desktop app can be used to organise the notebooks.
- The **mobile application** supports displaying and collapsing/expanding the tree of notebooks, however it does not currently support moving the subnotebooks to different notebooks.
- The **terminal app** supports displaying the tree of subnotebooks but it does not support collapsing/expanding them or moving the subnotebooks around.
# Markdown # Markdown
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular: Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
## Links to other notes
You can create a link to a note by specifying its ID in the URL. For example:
[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the **desktop app**, right click on a note an select "Copy Markdown link". In the **mobile app**, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.
## Math notation ## Math notation
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow: Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:
@@ -233,6 +255,10 @@ Checkboxes can be added like so:
The checkboxes can then be ticked in the mobile and desktop applications. The checkboxes can then be ticked in the mobile and desktop applications.
## HTML support
Only the `<br>` tag is supported - it can be used to force a new line, which is convenient to insert new lines inside table cells. For security reasons, other HTML tags are not supported.
# Donations # Donations
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard. Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
@@ -242,6 +268,7 @@ Please see the [donation page](https://joplin.cozic.net/donate/) for information
# Community # Community
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account. - For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
- Also see here for information about [the latest releases and general news](https://discourse.joplin.cozic.net/c/news).
- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues). - For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).
- The latest news are often posted [on this Twitter account](https://twitter.com/laurent2233). - The latest news are often posted [on this Twitter account](https://twitter.com/laurent2233).
@@ -267,25 +294,26 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED --> <!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done &nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|--- ---|---|---|---|---
![](https://joplin.cozic.net/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 76% ![](https://joplin.cozic.net/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 75%
![](https://joplin.cozic.net/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić <trbuhom@net.hr> | 62% ![](https://joplin.cozic.net/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 61%
![](https://joplin.cozic.net/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt <lukas@aiya.cz> | 96% ![](https://joplin.cozic.net/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 95%
![](https://joplin.cozic.net/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér <mjjzf@syntaktisk. | 98% ![](https://joplin.cozic.net/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Tobias Grasse <mail@tobias-grasse.net> | 95% ![](https://joplin.cozic.net/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Philipp Zumstein (zuphilip@gmail.com) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100% ![](https://joplin.cozic.net/images/flags/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín <f@mrtn.es> | 99% ![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín (f@mrtn.es) | 99%
![](https://joplin.cozic.net/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100% ![](https://joplin.cozic.net/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans <marcoslansgarza@gmail.com> | 97% ![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 95%
![](https://joplin.cozic.net/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 63% ![](https://joplin.cozic.net/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 63%
![](https://joplin.cozic.net/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 77% ![](https://joplin.cozic.net/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 75%
![](https://joplin.cozic.net/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos <rnbastos@gmail.com> | 99% ![](https://joplin.cozic.net/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 95% ![](https://joplin.cozic.net/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 94%
![](https://joplin.cozic.net/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 92% ![](https://joplin.cozic.net/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 90%
![](https://joplin.cozic.net/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 62% ![](https://joplin.cozic.net/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 61%
<!-- LOCALE-TABLE-AUTO-GENERATED --> <!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs # Known bugs
- Resources larger than 10 MB are not currently supported on mobile. They will crash the application so it is recommended not to attach such resources at the moment. The issue is being looked at.
- Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console. - Non-alphabetical characters such as Chinese or Arabic might create glitches in the terminal on Windows. This is a limitation of the current Windows console.
- It is only possible to upload files of up to 4MB to OneDrive due to a limitation of [the API](https://docs.microsoft.com/en-gb/onedrive/developer/rest-api/api/driveitem_put_content) being currently used. There is currently no plan to support OneDrive "large file" API. - It is only possible to upload files of up to 4MB to OneDrive due to a limitation of [the API](https://docs.microsoft.com/en-gb/onedrive/developer/rest-api/api/driveitem_put_content) being currently used. There is currently no plan to support OneDrive "large file" API.

View File

@@ -89,9 +89,9 @@ android {
defaultConfig { defaultConfig {
applicationId "net.cozic.joplin" applicationId "net.cozic.joplin"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 22 targetSdkVersion 26
versionCode 2097296 versionCode 2097303
versionName "1.0.118" versionName "1.0.125"
ndk { ndk {
abiFilters "armeabi-v7a", "x86" abiFilters "armeabi-v7a", "x86"
} }

View File

@@ -26,7 +26,7 @@
<uses-sdk <uses-sdk
android:minSdkVersion="16" android:minSdkVersion="16"
android:targetSdkVersion="22" /> android:targetSdkVersion="26" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -38,15 +38,6 @@
<!-- ==================================== --> <!-- ==================================== -->
<!-- START react-native-push-notification --> <!-- START react-native-push-notification -->
<!-- ==================================== --> <!-- ==================================== -->
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="${applicationId}" />
</intent-filter>
</receiver>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" /> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver"> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
@@ -55,13 +46,6 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/> <service android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationRegistrationService"/>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<!-- ================================== --> <!-- ================================== -->
<!-- END react-native-push-notification --> <!-- END react-native-push-notification -->
<!-- ================================== --> <!-- ================================== -->

View File

@@ -306,6 +306,34 @@
remoteGlobalIDString = 139D7E881E25C6D100323FB7; remoteGlobalIDString = 139D7E881E25C6D100323FB7;
remoteInfo = "double-conversion"; remoteInfo = "double-conversion";
}; };
4D7F8DA020A32BA0008B757D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = EBF21BDC1FC498900052F4D5;
remoteInfo = jsinspector;
};
4D7F8DA220A32BA0008B757D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = EBF21BFA1FC4989A0052F4D5;
remoteInfo = "jsinspector-tvOS";
};
4D7F8DA420A32BA0008B757D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 9936F3131F5F2E4B0010BF04;
remoteInfo = privatedata;
};
4D7F8DA620A32BA0008B757D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04;
remoteInfo = "privatedata-tvOS";
};
4DA7F80C1FC1DA9C00353191 /* PBXContainerItemProxy */ = { 4DA7F80C1FC1DA9C00353191 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */; containerPortal = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */;
@@ -540,10 +568,14 @@
4D2AFF8D1FDA002000599716 /* libcxxreact.a */, 4D2AFF8D1FDA002000599716 /* libcxxreact.a */,
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */, 3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */,
4D2AFF8F1FDA002000599716 /* libjschelpers.a */, 4D2AFF8F1FDA002000599716 /* libjschelpers.a */,
4D7F8DA120A32BA0008B757D /* libjsinspector.a */,
4D7F8DA320A32BA0008B757D /* libjsinspector-tvOS.a */,
4D3A19271FBDDA9400457703 /* libthird-party.a */, 4D3A19271FBDDA9400457703 /* libthird-party.a */,
4D2AFF911FDA002000599716 /* libthird-party.a */, 4D2AFF911FDA002000599716 /* libthird-party.a */,
4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */, 4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */,
4D2AFF931FDA002000599716 /* libdouble-conversion.a */, 4D2AFF931FDA002000599716 /* libdouble-conversion.a */,
4D7F8DA520A32BA0008B757D /* libprivatedata.a */,
4D7F8DA720A32BA0008B757D /* libprivatedata-tvOS.a */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1150,6 +1182,34 @@
remoteRef = 4D3A192A1FBDDA9400457703 /* PBXContainerItemProxy */; remoteRef = 4D3A192A1FBDDA9400457703 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR; sourceTree = BUILT_PRODUCTS_DIR;
}; };
4D7F8DA120A32BA0008B757D /* libjsinspector.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libjsinspector.a;
remoteRef = 4D7F8DA020A32BA0008B757D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D7F8DA320A32BA0008B757D /* libjsinspector-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libjsinspector-tvOS.a";
remoteRef = 4D7F8DA220A32BA0008B757D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D7F8DA520A32BA0008B757D /* libprivatedata.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libprivatedata.a;
remoteRef = 4D7F8DA420A32BA0008B757D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D7F8DA720A32BA0008B757D /* libprivatedata-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libprivatedata-tvOS.a";
remoteRef = 4D7F8DA620A32BA0008B757D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4DA7F80D1FC1DA9C00353191 /* libRNImagePicker.a */ = { 4DA7F80D1FC1DA9C00353191 /* libRNImagePicker.a */ = {
isa = PBXReferenceProxy; isa = PBXReferenceProxy;
fileType = archive.ar; fileType = archive.ar;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>10.0.20</string> <string>10.0.21</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>20</string> <string>21</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>

View File

@@ -45,4 +45,17 @@ ArrayUtils.findByKey = function(array, key, value) {
return null; return null;
} }
ArrayUtils.contentEquals = function(array1, array2) {
if (array1 === array2) return true;
if (!array1.length && !array2.length) return true;
if (array1.length !== array2.length) return false;
for (let i = 0; i < array1.length; i++) {
const a1 = array1[i];
if (array2.indexOf(a1) < 0) return false;
}
return true;
}
module.exports = ArrayUtils; module.exports = ArrayUtils;

View File

@@ -19,6 +19,7 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { fileExtension } = require('lib/path-utils.js'); const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js'); const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const reduxSharedMiddleware = require('lib/components/shared/reduxSharedMiddleware');
const os = require('os'); const os = require('os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const JoplinError = require('lib/JoplinError'); const JoplinError = require('lib/JoplinError');
@@ -143,6 +144,14 @@ class BaseApplication {
continue; continue;
} }
if (arg.indexOf('-psn') === 0) {
// Some weird flag passed by macOS - can be ignored.
// https://github.com/laurent22/joplin/issues/480
// https://stackoverflow.com/questions/10242115
argv.splice(0, 1);
continue;
}
if (arg.length && arg[0] == '-') { if (arg.length && arg[0] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else { } else {
@@ -190,6 +199,7 @@ class BaseApplication {
let options = { let options = {
order: stateUtils.notesOrder(state.settings), order: stateUtils.notesOrder(state.settings),
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'), uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
showCompletedTodos: Setting.value('showCompletedTodos'),
caseInsensitive: true, caseInsensitive: true,
}; };
@@ -262,6 +272,8 @@ class BaseApplication {
const newState = store.getState(); const newState = store.getState();
let refreshNotes = false; let refreshNotes = false;
reduxSharedMiddleware(store, next, action);
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) { if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
Setting.setValue('activeFolderId', newState.selectedFolderId); Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null; this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
@@ -272,6 +284,10 @@ class BaseApplication {
refreshNotes = true; refreshNotes = true;
} }
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showCompletedTodos') || action.type == 'SETTING_UPDATE_ALL')) {
refreshNotes = true;
}
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) { if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
refreshNotes = true; refreshNotes = true;
} }

View File

@@ -44,6 +44,15 @@ class BaseModel {
return null; return null;
} }
// Prefer the use of this function to compare IDs as it handles the case where
// one ID is null and the other is "", in which case they are actually considered to be the same.
static idsEqual(id1, id2) {
if (!id1 && !id2) return true;
if (!id1 && !!id2) return false;
if (!!id1 && !id2) return false;
return id1 === id2;
}
static modelTypeToName(type) { static modelTypeToName(type) {
for (let i = 0; i < BaseModel.typeEnum_.length; i++) { for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i]; const e = BaseModel.typeEnum_[i];

View File

@@ -14,7 +14,6 @@ class MdToHtml {
constructor(options = null) { constructor(options = null) {
if (!options) options = {}; if (!options) options = {};
this.supportsResourceLinks_ = !!options.supportsResourceLinks;
this.loadedResources_ = {}; this.loadedResources_ = {};
this.cachedContent_ = null; this.cachedContent_ = null;
this.cachedContentKey_ = null; this.cachedContentKey_ = null;
@@ -132,43 +131,38 @@ class MdToHtml {
const isResourceUrl = Resource.isResourceUrl(href); const isResourceUrl = Resource.isResourceUrl(href);
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href; const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
if (isResourceUrl && !this.supportsResourceLinks_) { let resourceIdAttr = "";
// In mobile, links to local resources, such as PDF, etc. currently aren't supported. let icon = "";
// Ideally they should be opened in the user's browser. let hrefAttr = '#';
return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']'; if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else { } else {
let resourceIdAttr = ""; // If the link is a plain URL (as opposed to a resource link), set the href to the actual
let icon = ""; // link. This allows the link to be exported too when exporting to PDF.
if (isResourceUrl) { hrefAttr = href;
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
}
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + icon;
return output;
} }
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
return output;
} }
renderCloseLink_(attrs, options) { renderCloseLink_(attrs, options) {
const href = this.getAttr_(attrs, 'href'); return '</a>';
const isResourceUrl = Resource.isResourceUrl(href);
if (isResourceUrl && !this.supportsResourceLinks_) {
return ')</span>';
} else {
return '</a>';
}
} }
rendererPlugin_(language) { rendererPlugin_(language) {
if (!language) return null; if (!language) return null;
const handlers = {}; if (!this.rendererPlugins_) {
handlers['katex'] = new MdToHtml_Katex(); this.rendererPlugins_ = {};
return language in handlers ? handlers[language] : null; this.rendererPlugins_['katex'] = new MdToHtml_Katex();
}
return language in this.rendererPlugins_ ? this.rendererPlugins_[language] : null;
} }
parseInlineCodeLanguage_(content) { parseInlineCodeLanguage_(content) {
@@ -220,12 +214,7 @@ class MdToHtml {
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage); if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
if (previousToken && previousToken.tag === 'li' && tag === 'p') { if (isInlineCode) {
// 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; openTag = null;
} else if (tag && t.type.indexOf('html_inline') >= 0) { } else if (tag && t.type.indexOf('html_inline') >= 0) {
openTag = null; openTag = null;
@@ -403,7 +392,7 @@ class MdToHtml {
const md = new MarkdownIt({ const md = new MarkdownIt({
breaks: true, breaks: true,
linkify: true, linkify: true,
html: true, html: false, // For security, HTML tags are not supported - https://github.com/laurent22/joplin/issues/500
}); });
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated // This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
@@ -420,8 +409,8 @@ class MdToHtml {
if (HORRIBLE_HACK) { if (HORRIBLE_HACK) {
let counter = -1; let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
body = body.replace(/- \[(X| )\]/, function(v, p1) { body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK'; let s = p1 == ' ' ? 'NOTICK' : 'TICK';
counter++; counter++;
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm'; return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
@@ -449,6 +438,9 @@ class MdToHtml {
} }
} }
// Support <br> tag to allow newlines inside table cells
renderedBody = renderedBody.replace(/&lt;br&gt;/gi, '<br>');
// https://necolas.github.io/normalize.css/ // https://necolas.github.io/normalize.css/
const normalizeCss = ` const normalizeCss = `
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
@@ -490,6 +482,9 @@ class MdToHtml {
ul { ul {
padding-left: 1.3em; padding-left: 1.3em;
} }
li p {
margin-bottom: 0;
}
.resource-icon { .resource-icon {
display: inline-block; display: inline-block;
position: relative; position: relative;
@@ -577,10 +572,10 @@ class MdToHtml {
toggleTickAt(body, index) { toggleTickAt(body, index) {
let counter = -1; let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) { while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
counter++; counter++;
body = body.replace(/- \[(X| )\]/, function(v, p1) { body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK'; let s = p1 == ' ' ? 'NOTICK' : 'TICK';
if (index == counter) { if (index == counter) {
s = s == 'NOTICK' ? 'TICK' : 'NOTICK'; s = s == 'NOTICK' ? 'TICK' : 'NOTICK';

View File

@@ -5,13 +5,28 @@ const Setting = require('lib/models/Setting');
class MdToHtml_Katex { class MdToHtml_Katex {
constructor() {
this.cache_ = {};
this.assetsLoaded_ = false;
}
name() { name() {
return 'katex'; return 'katex';
} }
processContent(renderedTokens, content, tagType) { processContent(renderedTokens, content, tagType) {
try { try {
let renderered = katex.renderToString(content); const cacheKey = tagType + '_' + content;
let renderered = null;
if (this.cache_[cacheKey]) {
renderered = this.cache_[cacheKey];
} else {
renderered = katex.renderToString(content, {
displayMode: tagType === 'block',
});
this.cache_[cacheKey] = renderered;
}
if (tagType === 'block') renderered = '<p>' + renderered + '</p>'; if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
@@ -27,6 +42,8 @@ class MdToHtml_Katex {
} }
async loadAssets() { async loadAssets() {
if (this.assetsLoaded_) return;
// 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 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 // In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
@@ -41,6 +58,8 @@ class MdToHtml_Katex {
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_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' }); 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' });
} }
this.assetsLoaded_ = true;
} }
} }

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { Platform, WebView, View, Linking } = require('react-native'); const { Platform, WebView, View } = require('react-native');
const { globalStyle } = require('lib/components/global-style.js'); const { globalStyle } = require('lib/components/global-style.js');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
@@ -19,7 +19,7 @@ class NoteBodyViewer extends Component {
} }
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: false }); this.mdToHtml_ = new MdToHtml();
this.isMounted_ = true; this.isMounted_ = true;
} }
@@ -39,6 +39,19 @@ class NoteBodyViewer extends Component {
}, 100); }, 100);
} }
shouldComponentUpdate(nextProps, nextState) {
// To address https://github.com/laurent22/joplin/issues/433
// If a checkbox in a note is ticked, the body changes, which normally would trigger a re-render
// of this component, which has the unfortunate side effect of making the view scroll back to the top.
// This re-rendering however is uncessary since the component is already visually updated via JS.
// So here, if the note has not changed, we prevent the component from updating.
// This fixes the above issue. A drawback of this is if the note is updated via sync, this change
// will not be displayed immediately.
const currentNoteId = this.props && this.props.note ? this.props.note.id : null;
const nextNoteId = nextProps && nextProps.note ? nextProps.note.id : null;
return currentNoteId !== nextNoteId || nextState.webViewLoaded !== this.state.webViewLoaded;
}
render() { render() {
const note = this.props.note; const note = this.props.note;
const style = this.props.style; const style = this.props.style;
@@ -109,13 +122,13 @@ class NoteBodyViewer extends Component {
let msg = event.nativeEvent.data; let msg = event.nativeEvent.data;
if (msg.indexOf('checkboxclick:') === 0) { if (msg.indexOf('checkboxclick:') === 0) {
const newBody = this.mdToHtml_.handleCheckboxClick(msg, note.body); const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.props.note.body);
if (onCheckboxChange) onCheckboxChange(newBody); if (onCheckboxChange) onCheckboxChange(newBody);
} else if (msg.indexOf('bodyscroll:') === 0) { } else if (msg.indexOf('bodyscroll:') === 0) {
//msg = msg.split(':'); //msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]); //this.bodyScrollTop_ = Number(msg[1]);
} else { } else {
Linking.openURL(msg); this.props.onJoplinLinkClick(msg);
} }
}} }}
/> />

View File

@@ -99,7 +99,8 @@ class ScreenHeaderComponent extends Component {
height: 18, height: 18,
}, },
contextMenuTrigger: { contextMenuTrigger: {
fontSize: 25, fontSize: 30,
paddingLeft: 10,
paddingRight: theme.marginRight, paddingRight: theme.marginRight,
color: theme.raisedColor, color: theme.raisedColor,
fontWeight: 'bold', fontWeight: 'bold',
@@ -445,7 +446,7 @@ class ScreenHeaderComponent extends Component {
const menuComp = !showContextMenuButton ? null : ( const menuComp = !showContextMenuButton ? null : (
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}> <Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}> <MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
<Text style={this.styles().contextMenuTrigger}> &#8942;</Text> <Icon name='md-more' style={this.styles().contextMenuTrigger} />
</MenuTrigger> </MenuTrigger>
<MenuOptions> <MenuOptions>
<ScrollView style={{ maxHeight: windowHeight }}> <ScrollView style={{ maxHeight: windowHeight }}>

View File

@@ -1,5 +1,5 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet } = require('react-native'); const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { ScreenHeader } = require('lib/components/screen-header.js'); const { ScreenHeader } = require('lib/components/screen-header.js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
@@ -53,7 +53,7 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
<View style={this.styles().screen}> <View style={this.styles().screen}>
<ScreenHeader title={_('Login with Dropbox')}/> <ScreenHeader title={_('Login with Dropbox')}/>
<View style={this.styles().container}> <ScrollView style={this.styles().container}>
<Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text> <Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text>
<Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text> <Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
<View> <View>
@@ -65,7 +65,10 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
<TextInput selectionColor={theme.textSelectionColor} value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput}/> <TextInput selectionColor={theme.textSelectionColor} value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput}/>
<Button disabled={this.state.checkingAuthToken} title={_("Submit")} onPress={this.shared_.submit_click}></Button> <Button disabled={this.state.checkingAuthToken} title={_("Submit")} onPress={this.shared_.submit_click}></Button>
</View>
{/* Add this extra padding to make sure the view is scrollable when the keyboard is visible on small screens (iPhone SE) */}
<View style={{ height: 200 }}></View>
</ScrollView>
<DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/> <DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
</View> </View>

View File

@@ -1,9 +1,10 @@
const React = require('react'); const Component = React.Component; const React = require('react'); const Component = React.Component;
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native'); const { Platform, Clipboard, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, Share } = require('react-native');
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
const RNFS = require('react-native-fs'); const RNFS = require('react-native-fs');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const Setting = require('lib/models/Setting.js'); const Setting = require('lib/models/Setting.js');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
@@ -22,8 +23,8 @@ const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js'); const { reg } = require('lib/registry.js');
const { shim } = require('lib/shim.js'); const { shim } = require('lib/shim.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js'); const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { dialogs } = require('lib/dialogs.js');
const { globalStyle, themeStyle } = require('lib/components/global-style.js'); const { globalStyle, themeStyle } = require('lib/components/global-style.js');
const { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default; const DialogBox = require('react-native-dialogbox').default;
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js'); const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
const RNFetchBlob = require('react-native-fetch-blob').default; const RNFetchBlob = require('react-native-fetch-blob').default;
@@ -107,6 +108,39 @@ class NoteScreenComponent extends BaseScreenComponent {
this.noteTagDialog_closeRequested = () => { this.noteTagDialog_closeRequested = () => {
this.setState({ noteTagDialogShown: false }); this.setState({ noteTagDialogShown: false });
} }
this.onJoplinLinkClick_ = async (msg) => {
try {
if (msg.indexOf('joplin://') === 0) {
const itemId = msg.substr('joplin://'.length);
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error(_('No item with ID %s', itemId));
if (item.type_ === BaseModel.TYPE_NOTE) {
// Easier to just go back, then go to the note since
// the Note screen doesn't handle reloading a different note
this.props.dispatch({
type: 'NAV_BACK',
});
setTimeout(() => {
this.props.dispatch({
type: 'NAV_GO',
routeName: 'Note',
noteId: item.id,
});
}, 5);
} else {
throw new Error(_('The Joplin mobile app does not currently support this type of link: %s', BaseModel.modelTypeToName(item.type_)));
}
} else {
Linking.openURL(msg);
}
} catch (error) {
dialogs.error(this, error.message);
}
}
} }
styles() { styles() {
@@ -186,8 +220,8 @@ class NoteScreenComponent extends BaseScreenComponent {
shared.noteComponent_change(this, 'body', text); shared.noteComponent_change(this, 'body', text);
} }
async saveNoteButton_press() { async saveNoteButton_press(folderId = null) {
await shared.saveNoteButton_press(this); await shared.saveNoteButton_press(this, folderId);
Keyboard.dismiss(); Keyboard.dismiss();
} }
@@ -330,10 +364,16 @@ class NoteScreenComponent extends BaseScreenComponent {
return; return;
} else { } else {
await RNFetchBlob.fs.cp(localFilePath, targetPath); await RNFetchBlob.fs.cp(localFilePath, targetPath);
const stat = await shim.fsDriver().stat(targetPath);
if (stat.size >= 10000000) {
await shim.fsDriver().remove(targetPath);
throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
}
} }
} }
} catch (error) { } catch (error) {
reg.logger().warn('Could not attach file:', error); reg.logger().warn('Could not attach file:', error);
await dialogs.error(this, error.message);
return; return;
} }
@@ -369,6 +409,13 @@ class NoteScreenComponent extends BaseScreenComponent {
this.setState({ noteTagDialogShown: true }); this.setState({ noteTagDialogShown: true });
} }
async share_onPress() {
await Share.share({
message: this.state.note.title + '\n\n' + this.state.note.body,
title: this.state.note.title,
});
}
setAlarm_onPress() { setAlarm_onPress() {
this.setState({ alarmDialogShown: true }); this.setState({ alarmDialogShown: true });
} }
@@ -402,6 +449,11 @@ class NoteScreenComponent extends BaseScreenComponent {
} }
} }
copyMarkdownLink_onPress() {
const note = this.state.note;
Clipboard.setString(Note.markdownTag(note));
}
menuOptions() { menuOptions() {
const note = this.state.note; const note = this.state.note;
const isTodo = note && !!note.is_todo; const isTodo = note && !!note.is_todo;
@@ -423,8 +475,10 @@ class NoteScreenComponent extends BaseScreenComponent {
output.push({ title: _('Set alarm'), onPress: () => { this.setState({ alarmDialogShown: true }) }});; output.push({ title: _('Set alarm'), onPress: () => { this.setState({ alarmDialogShown: true }) }});;
} }
output.push({ title: _('Share'), onPress: () => { this.share_onPress(); } });
if (isSaved) output.push({ title: _('Tags'), onPress: () => { this.tags_onPress(); } }); if (isSaved) output.push({ title: _('Tags'), onPress: () => { this.tags_onPress(); } });
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } }); output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
output.push({ isDivider: true }); output.push({ isDivider: true });
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } }); if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } }); output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
@@ -466,7 +520,13 @@ class NoteScreenComponent extends BaseScreenComponent {
this.saveOneProperty('body', newBody); this.saveOneProperty('body', newBody);
}; };
bodyComponent = <NoteBodyViewer style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/> bodyComponent = <NoteBodyViewer
onJoplinLinkClick={this.onJoplinLinkClick_}
style={this.styles().noteBodyViewer}
webViewStyle={theme}
note={note}
onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}
/>
} else { } else {
const focusBody = !isNew && !!note.title; const focusBody = !isNew && !!note.title;
@@ -497,7 +557,7 @@ class NoteScreenComponent extends BaseScreenComponent {
}, },
}); });
if (this.state.mode == 'edit') return <ActionButton style={{display:'none'}}/>; if (this.state.mode == 'edit') return null;//<ActionButton style={{display:'none'}}/>;
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} /> return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />
} }
@@ -560,7 +620,12 @@ class NoteScreenComponent extends BaseScreenComponent {
enabled: true, enabled: true,
selectedFolderId: folder ? folder.id : null, selectedFolderId: folder ? folder.id : null,
onValueChange: async (itemValue, itemIndex) => { onValueChange: async (itemValue, itemIndex) => {
if (note.id) await Note.moveToFolder(note.id, itemValue); if (!note.id) {
await this.saveNoteButton_press(itemValue);
} else {
await Note.moveToFolder(note.id, itemValue);
}
note.parent_id = itemValue; note.parent_id = itemValue;
const folder = await Folder.load(note.parent_id); const folder = await Folder.load(note.parent_id);

View File

@@ -53,6 +53,11 @@ class NotesScreenComponent extends BaseScreenComponent {
id: { name: 'uncompletedTodosOnTop', value: !Setting.value('uncompletedTodosOnTop') }, id: { name: 'uncompletedTodosOnTop', value: !Setting.value('uncompletedTodosOnTop') },
}); });
buttons.push({
text: makeCheckboxText(Setting.value('showCompletedTodos'), 'tick', '[ ' + Setting.settingMetadata('showCompletedTodos').label() + ' ]'),
id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') },
});
const r = await dialogs.pop(this, Setting.settingMetadata('notes.sortOrder.field').label(), buttons); const r = await dialogs.pop(this, Setting.settingMetadata('notes.sortOrder.field').label(), buttons);
if (!r) return; if (!r) return;
@@ -79,6 +84,7 @@ class NotesScreenComponent extends BaseScreenComponent {
let options = { let options = {
order: props.notesOrder, order: props.notesOrder,
uncompletedTodosOnTop: props.uncompletedTodosOnTop, uncompletedTodosOnTop: props.uncompletedTodosOnTop,
showCompletedTodos: props.showCompletedTodos,
caseInsensitive: true, caseInsensitive: true,
}; };
@@ -107,7 +113,7 @@ class NotesScreenComponent extends BaseScreenComponent {
} }
deleteFolder_onPress(folderId) { deleteFolder_onPress(folderId) {
dialogs.confirm(this, _('Delete notebook? All notes within this notebook will also be deleted.')).then((ok) => { dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then((ok) => {
if (!ok) return; if (!ok) return;
Folder.delete(folderId).then(() => { Folder.delete(folderId).then(() => {
@@ -219,6 +225,7 @@ const NotesScreen = connect(
notes: state.notes, notes: state.notes,
notesSource: state.notesSource, notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop, uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
theme: state.settings.theme, theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled, noteSelectionEnabled: state.noteSelectionEnabled,
notesOrder: stateUtils.notesOrder(state.settings), notesOrder: stateUtils.notesOrder(state.settings),

View File

@@ -2,6 +2,7 @@ const { reg } = require('lib/registry.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const BaseModel = require('lib/BaseModel.js'); const BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const shared = {}; const shared = {};
@@ -10,15 +11,19 @@ shared.noteExists = async function(noteId) {
return !!existingNote; return !!existingNote;
} }
shared.saveNoteButton_press = async function(comp) { shared.saveNoteButton_press = async function(comp, folderId = null) {
let note = Object.assign({}, comp.state.note); let note = Object.assign({}, comp.state.note);
// Note has been deleted while user was modifying it. In that case, we // Note has been deleted while user was modifying it. In that case, we
// just save a new note by clearing the note ID. // just save a new note by clearing the note ID.
if (note.id && !(await shared.noteExists(note.id))) delete note.id; if (note.id && !(await shared.noteExists(note.id))) delete note.id;
if (!note.parent_id) { if (folderId) {
let folder = await Folder.defaultFolder(); note.parent_id = folderId;
} else if (!note.parent_id) {
const activeFolderId = Setting.value('activeFolderId');
let folder = await Folder.load(activeFolderId);
if (!folder) folder = await Folder.defaultFolder();
if (!folder) return; if (!folder) return;
note.parent_id = folder.id; note.parent_id = folder.id;
} }

View File

@@ -0,0 +1,11 @@
const Setting = require('lib/models/Setting');
const reduxSharedMiddleware = function(store, next, action) {
const newState = store.getState();
if (action.type == 'FOLDER_SET_COLLAPSED' || action.type == 'FOLDER_TOGGLE') {
Setting.setValue('collapsedFolderIds', newState.collapsedFolderIds);
}
}
module.exports = reduxSharedMiddleware;

View File

@@ -1,14 +1,48 @@
const ArrayUtils = require('lib/ArrayUtils');
const Folder = require('lib/models/Folder');
const BaseModel = require('lib/BaseModel');
let shared = {}; let shared = {};
shared.renderFolders = function(props, renderItem) { function folderHasChildren_(folders, folderId) {
let items = []; for (let i = 0; i < folders.length; i++) {
for (let i = 0; i < props.folders.length; i++) { let folder = folders[i];
let folder = props.folders[i]; if (folder.parent_id === folderId) return true;
items.push(renderItem(folder, props.selectedFolderId == folder.id && props.notesParentType == 'Folder')); }
return false;
}
function folderIsVisible(folders, folderId, collapsedFolderIds) {
if (!collapsedFolderIds || !collapsedFolderIds.length) return true;
while (true) {
let folder = BaseModel.byId(folders, folderId);
if (!folder) throw new Error('No folder with id ' + folder.id);
if (!folder.parent_id) return true;
if (collapsedFolderIds.indexOf(folder.parent_id) >= 0) return false;
folderId = folder.parent_id;
}
return true;
}
function renderFoldersRecursive_(props, renderItem, items, parentId, depth) {
const folders = props.folders;
for (let i = 0; i < folders.length; i++) {
let folder = folders[i];
if (!Folder.idsEqual(folder.parent_id, parentId)) continue;
if (!folderIsVisible(props.folders, folder.id, props.collapsedFolderIds)) continue;
const hasChildren = folderHasChildren_(folders, folder.id);
items.push(renderItem(folder, props.selectedFolderId == folder.id && props.notesParentType == 'Folder', hasChildren, depth));
if (hasChildren) items = renderFoldersRecursive_(props, renderItem, items, folder.id, depth + 1);
} }
return items; return items;
} }
shared.renderFolders = function(props, renderItem) {
return renderFoldersRecursive_(props, renderItem, [], '', 0);
}
shared.renderTags = function(props, renderItem) { shared.renderTags = function(props, renderItem) {
let tags = props.tags.slice(); let tags = props.tags.slice();
tags.sort((a, b) => { return a.title < b.title ? -1 : +1; }); tags.sort((a, b) => { return a.title < b.title ? -1 : +1; });

View File

@@ -64,11 +64,13 @@ class SideMenuContentComponent extends Component {
}; };
styles.folderButton = Object.assign({}, styles.button); styles.folderButton = Object.assign({}, styles.button);
styles.folderButton.paddingLeft = 0;
styles.folderButtonText = Object.assign({}, styles.buttonText); styles.folderButtonText = Object.assign({}, styles.buttonText);
styles.folderButtonSelected = Object.assign({}, styles.folderButton); styles.folderButtonSelected = Object.assign({}, styles.folderButton);
styles.folderButtonSelected.backgroundColor = theme.selectedColor; styles.folderButtonSelected.backgroundColor = theme.selectedColor;
styles.folderIcon = Object.assign({}, theme.icon); styles.folderIcon = Object.assign({}, theme.icon);
styles.folderIcon.color = '#0072d5'; styles.folderIcon.color = theme.colorFaded;//'#0072d5';
styles.folderIcon.paddingTop = 3;
styles.tagButton = Object.assign({}, styles.button); styles.tagButton = Object.assign({}, styles.button);
styles.tagButtonSelected = Object.assign({}, styles.tagButton); styles.tagButtonSelected = Object.assign({}, styles.tagButton);
@@ -94,6 +96,13 @@ class SideMenuContentComponent extends Component {
}); });
} }
folder_togglePress(folder) {
this.props.dispatch({
type: 'FOLDER_TOGGLE',
id: folder.id,
});
}
tag_press(tag) { tag_press(tag) {
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
@@ -109,17 +118,41 @@ class SideMenuContentComponent extends Component {
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' }); if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
} }
folderItem(folder, selected) { folderItem(folder, selected, hasChildren, depth) {
const iconComp = selected ? <Icon name='md-folder-open' style={this.styles().folderIcon} /> : <Icon name='md-folder' style={this.styles().folderIcon} />; const theme = themeStyle(this.props.theme);
const folderButtonStyle = selected ? this.styles().folderButtonSelected : this.styles().folderButton;
const folderButtonStyle = {
flex: 1,
flexDirection: 'row',
height: 36,
alignItems: 'center',
paddingLeft: theme.marginLeft,
paddingRight: theme.marginRight,
};
if (selected) folderButtonStyle.backgroundColor = theme.selectedColor;
folderButtonStyle.paddingLeft = depth * 10;
const iconWrapperStyle = { paddingLeft: 10, paddingRight: 10 };
if (selected) iconWrapperStyle.backgroundColor = theme.selectedColor;
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'md-arrow-dropdown' : 'md-arrow-dropup';
const iconComp = <Icon name={iconName} style={this.styles().folderIcon} />
const iconWrapper = !hasChildren ? null : (
<TouchableOpacity style={iconWrapperStyle} folderid={folder.id} onPress={() => { if (hasChildren) this.folder_togglePress(folder) }}>
{ iconComp }
</TouchableOpacity>
);
return ( return (
<TouchableOpacity key={folder.id} onPress={() => { this.folder_press(folder) }}> <View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
<View style={folderButtonStyle}> <TouchableOpacity style={{ flex: 1 }} onPress={() => { this.folder_press(folder) }}>
{ iconComp } <View style={folderButtonStyle}>
<Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text> <Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
{ iconWrapper }
</View>
); );
} }
@@ -204,9 +237,6 @@ class SideMenuContentComponent extends Component {
return ( return (
<View style={style}> <View style={style}>
<View style={{flex:1, opacity: this.props.opacity}}> <View style={{flex:1, opacity: this.props.opacity}}>
<View style={{flexDirection:'row'}}>
<Image style={{flex:1, height: 100}} source={require('../images/SideMenuHeader.png')} />
</View>
<ScrollView scrollsToTop={false} style={this.styles().menu}> <ScrollView scrollsToTop={false} style={this.styles().menu}>
{ items } { items }
</ScrollView> </ScrollView>
@@ -229,6 +259,7 @@ const SideMenuContent = connect(
locale: state.settings.locale, locale: state.settings.locale,
theme: state.settings.theme, theme: state.settings.theme,
opacity: state.sideMenuOpenPercent, opacity: state.sideMenuOpenPercent,
collapsedFolderIds: state.collapsedFolderIds,
}; };
} }
)(SideMenuContentComponent) )(SideMenuContentComponent)

View File

@@ -38,7 +38,7 @@ class FileApiDriverWebDav {
} }
statFromResource_(resource, path) { statFromResource_(resource, path) {
// WebDAV implementations are always slighly different from one server to another but, at the minimum, // WebDAV implementations are always slightly different from one server to another but, at the minimum,
// a resource should have a propstat key - if not it's probably an error. // a resource should have a propstat key - if not it's probably an error.
const propStat = this.api().arrayFromJson(resource, ['d:propstat']); const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource)); if (!Array.isArray(propStat)) throw new Error('Invalid WebDAV resource format: ' + JSON.stringify(resource));

View File

@@ -311,6 +311,7 @@ async function basicDelta(path, getDirStatFn, options) {
// Clear temporary info from context. It's especially important to remove deletedItemsProcessed // Clear temporary info from context. It's especially important to remove deletedItemsProcessed
// so that they are processed again on the next sync. // so that they are processed again on the next sync.
newContext.statsCache = null; newContext.statsCache = null;
newContext.statIdsCache = null;
delete newContext.deletedItemsProcessed; delete newContext.deletedItemsProcessed;
} }

View File

@@ -202,14 +202,14 @@ class JoplinDatabase extends Database {
// default value and thus might cause problems. In that case, the default value // default value and thus might cause problems. In that case, the default value
// must be set in the synchronizer too. // must be set in the synchronizer too.
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion); let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
if (currentVersionIndex < 0) throw new Error('Unknown profile version. Most likely this is an old version of Joplin, while the profile was created by a newer version. Please upgrade Joplin at https://joplin.cozic.net and try again.');
// currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer // currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer
// version of the database, so that migration is not run in this case. // version of the database, so that migration is not run in this case.
if (currentVersionIndex < 0) throw new Error('Unknown profile version. Most likely this is an old version of Joplin, while the profile was created by a newer version. Please upgrade Joplin at https://joplin.cozic.net and try again.');
if (currentVersionIndex == existingDatabaseVersions.length - 1) return false; if (currentVersionIndex == existingDatabaseVersions.length - 1) return false;
while (currentVersionIndex < existingDatabaseVersions.length - 1) { while (currentVersionIndex < existingDatabaseVersions.length - 1) {
@@ -344,6 +344,10 @@ class JoplinDatabase extends Database {
upgradeVersion10(); upgradeVersion10();
} }
if (targetVersion == 12) {
queries.push('ALTER TABLE folders ADD COLUMN parent_id TEXT NOT NULL DEFAULT ""');
}
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] }); queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
await this.transactionExecBatch(queries); await this.transactionExecBatch(queries);

View File

@@ -1,9 +1,9 @@
const layoutUtils = {}; const layoutUtils = {};
layoutUtils.size = function(prefered, min, max) { layoutUtils.size = function(preferred, min, max) {
if (prefered < min) return min; if (preferred < min) return min;
if (typeof max !== 'undefined' && prefered > max) return max; if (typeof max !== 'undefined' && preferred > max) return max;
return prefered; return preferred;
} }
module.exports = layoutUtils; module.exports = layoutUtils;

View File

@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js'); const { sprintf } = require('sprintf-js');
const { _ } = require('lib/locale.js'); const { _ } = require('lib/locale.js');
const moment = require('moment'); const moment = require('moment');
const { markdownUtils } = require('lib/markdown-utils.js');
class BaseItem extends BaseModel { class BaseItem extends BaseModel {
@@ -649,6 +650,15 @@ class BaseItem extends BaseModel {
return super.save(o, options); return super.save(o, options);
} }
static markdownTag(item) {
const output = [];
output.push('[');
output.push(markdownUtils.escapeLinkText(item.title));
output.push(']');
output.push('(:/' + item.id + ')');
return output.join('');
}
} }
BaseItem.encryptionService_ = null; BaseItem.encryptionService_ = null;

View File

@@ -18,7 +18,7 @@ class Folder extends BaseItem {
static async serialize(folder) { static async serialize(folder) {
let fieldNames = this.fieldNames(); let fieldNames = this.fieldNames();
fieldNames.push('type_'); fieldNames.push('type_');
lodash.pull(fieldNames, 'parent_id'); //lodash.pull(fieldNames, 'parent_id');
return super.serialize(folder, 'folder', fieldNames); return super.serialize(folder, 'folder', fieldNames);
} }
@@ -57,6 +57,11 @@ class Folder extends BaseItem {
}); });
} }
static async subFolderIds(parentId) {
const rows = await this.db().selectAll('SELECT id FROM folders WHERE parent_id = ?', [parentId]);
return rows.map(r => r.id);
}
static async noteCount(parentId) { static async noteCount(parentId) {
let r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]); let r = await this.db().selectOne('SELECT count(*) as total FROM notes WHERE is_conflict = 0 AND parent_id = ?', [parentId]);
return r ? r.total : 0; return r ? r.total : 0;
@@ -79,6 +84,11 @@ class Folder extends BaseItem {
for (let i = 0; i < noteIds.length; i++) { for (let i = 0; i < noteIds.length; i++) {
await Note.delete(noteIds[i]); await Note.delete(noteIds[i]);
} }
let subFolderIds = await Folder.subFolderIds(folderId);
for (let i = 0; i < subFolderIds.length; i++) {
await Folder.delete(subFolderIds[i]);
}
} }
await super.delete(folderId, options); await super.delete(folderId, options);
@@ -101,6 +111,7 @@ class Folder extends BaseItem {
return { return {
type_: this.TYPE_FOLDER, type_: this.TYPE_FOLDER,
id: this.conflictFolderId(), id: this.conflictFolderId(),
parent_id: '',
title: this.conflictFolderTitle(), title: this.conflictFolderTitle(),
updated_time: time.unixMs(), updated_time: time.unixMs(),
user_updated_time: time.unixMs(), user_updated_time: time.unixMs(),
@@ -125,6 +136,39 @@ class Folder extends BaseItem {
return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1'); return this.modelSelectOne('SELECT * FROM folders ORDER BY created_time DESC LIMIT 1');
} }
static async canNestUnder(folderId, targetFolderId) {
if (folderId === targetFolderId) return false;
const conflictFolderId = Folder.conflictFolderId();
if (folderId == conflictFolderId || targetFolderId == conflictFolderId) return false;
if (!targetFolderId) return true;
while (true) {
let folder = await Folder.load(targetFolderId);
if (!folder.parent_id) break;
if (folder.parent_id === folderId) return false;
targetFolderId = folder.parent_id;
}
return true;
}
static async moveToFolder(folderId, targetFolderId) {
if (!(await this.canNestUnder(folderId, targetFolderId))) throw new Error(_('Cannot move notebook to this location'));
// When moving a note to a different folder, the user timestamp is not updated.
// However updated_time is updated so that the note can be synced later on.
const modifiedFolder = {
id: folderId,
parent_id: targetFolderId,
updated_time: time.unixMs(),
};
return Folder.save(modifiedFolder, { autoTimestamp: false });
}
// These "duplicateCheck" and "reservedTitleCheck" should only be done when a user is // These "duplicateCheck" and "reservedTitleCheck" should only be done when a user is
// manually creating a folder. They shouldn't be done for example when the folders // manually creating a folder. They shouldn't be done for example when the folders
// are being synced to avoid any strange side-effects. Technically it's possible to // are being synced to avoid any strange side-effects. Technically it's possible to

View File

@@ -107,7 +107,7 @@ class Note extends BaseItem {
return BaseModel.TYPE_NOTE; return BaseModel.TYPE_NOTE;
} }
static linkedResourceIds(body) { static linkedItemIds(body) {
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b) // For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
if (!body || body.length <= 32) return []; if (!body || body.length <= 32) return [];
const matches = body.match(/\(:\/.{32}\)/g); const matches = body.match(/\(:\/.{32}\)/g);
@@ -115,6 +115,35 @@ class Note extends BaseItem {
return matches.map((m) => m.substr(3, 32)); return matches.map((m) => m.substr(3, 32));
} }
static async linkedItems(body) {
const itemIds = this.linkedItemIds(body);
const output = [];
for (let i = 0; i < itemIds.length; i++) {
const item = await BaseItem.loadItemById(itemIds[i]);
if (!item) continue;
output.push(item);
}
return output;
}
static async linkedItemIdsByType(type, body) {
const items = await this.linkedItems(body);
const output = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type_ === type) output.push(item.id);
}
return output;
}
static async linkedResourceIds(body) {
return await this.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, body);
}
static new(parentId = '') { static new(parentId = '') {
let output = super.new(); let output = super.new();
output.parent_id = parentId; output.parent_id = parentId;
@@ -208,6 +237,7 @@ class Note extends BaseItem {
if (!options.conditionsParams) options.conditionsParams = []; if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields(); if (!options.fields) options.fields = this.previewFields();
if (!options.uncompletedTodosOnTop) options.uncompletedTodosOnTop = false; if (!options.uncompletedTodosOnTop) options.uncompletedTodosOnTop = false;
if (!('showCompletedTodos' in options)) options.showCompletedTodos = true;
if (parentId == BaseItem.getClass('Folder').conflictFolderId()) { if (parentId == BaseItem.getClass('Folder').conflictFolderId()) {
options.conditions.push('is_conflict = 1'); options.conditions.push('is_conflict = 1');
@@ -236,6 +266,10 @@ class Note extends BaseItem {
} }
} }
if (!options.showCompletedTodos) {
options.conditions.push('todo_completed <= 0');
}
if (options.uncompletedTodosOnTop && hasTodos) { if (options.uncompletedTodosOnTop && hasTodos) {
let cond = options.conditions.slice(); let cond = options.conditions.slice();
cond.push('is_todo = 1'); cond.push('is_todo = 1');

View File

@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js'); const { sprintf } = require('sprintf-js');
const ObjectUtils = require('lib/ObjectUtils'); const ObjectUtils = require('lib/ObjectUtils');
const { toTitleCase } = require('lib/string-utils.js'); const { toTitleCase } = require('lib/string-utils.js');
const { rtrimSlashes } = require('lib/path-utils.js');
const { _, supportedLocalesToLanguages, defaultLocale } = require('lib/locale.js'); const { _, supportedLocalesToLanguages, defaultLocale } = require('lib/locale.js');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
@@ -60,6 +61,7 @@ class Setting extends BaseModel {
return output; return output;
}}, }},
'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') }, 'uncompletedTodosOnTop': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['cli'], label: () => _('Uncompleted to-dos on top') },
'showCompletedTodos': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['cli'], label: () => _('Show completed to-dos') },
'notes.sortOrder.field': { value: 'user_updated_time', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['cli'], label: () => _('Sort notes by'), options: () => { 'notes.sortOrder.field': { value: 'user_updated_time', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['cli'], label: () => _('Sort notes by'), options: () => {
const Note = require('lib/models/Note'); const Note = require('lib/models/Note');
const noteSortFields = ['user_updated_time', 'user_created_time', 'title']; const noteSortFields = ['user_updated_time', 'user_created_time', 'title'];
@@ -91,6 +93,8 @@ class Setting extends BaseModel {
'showTrayIcon': { value: platform !== 'linux', type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon'), description: () => { 'showTrayIcon': { value: platform !== 'linux', type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Show tray icon'), description: () => {
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : null; return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : null;
}}, }},
'collapsedFolderIds': { value: [], type: Setting.TYPE_ARRAY, public: false },
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false }, 'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false }, 'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
@@ -122,6 +126,8 @@ class Setting extends BaseModel {
} catch (error) { } catch (error) {
return false; return false;
} }
}, filter: (value) => {
return value ? rtrimSlashes(value) : '';
}, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: (appType) => { return appType !== 'cli' ? null : _('The path to synchronise with when file system synchronisation is enabled. See `sync.target`.'); } }, }, public: true, label: () => _('Directory to synchronise with (absolute path)'), description: (appType) => { return appType !== 'cli' ? null : _('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: () => _('Nextcloud WebDAV URL') }, 'sync.5.path': { value: '', type: Setting.TYPE_STRING, show: (settings) => { return settings['sync.target'] == SyncTargetRegistry.nameToId('nextcloud') }, public: true, label: () => _('Nextcloud WebDAV URL') },
@@ -204,6 +210,7 @@ class Setting extends BaseModel {
if (!this.keyExists(c.key)) continue; if (!this.keyExists(c.key)) continue;
c.value = this.formatValue(c.key, c.value); c.value = this.formatValue(c.key, c.value);
c.value = this.filterValue(c.key, c.value);
this.cache_.push(c); this.cache_.push(c);
} }
@@ -237,6 +244,7 @@ class Setting extends BaseModel {
if (!this.cache_) throw new Error('Settings have not been initialized!'); if (!this.cache_) throw new Error('Settings have not been initialized!');
value = this.formatValue(key, value); value = this.formatValue(key, value);
value = this.filterValue(key, value);
for (let i = 0; i < this.cache_.length; i++) { for (let i = 0; i < this.cache_.length; i++) {
let c = this.cache_[i]; let c = this.cache_[i];
@@ -307,6 +315,11 @@ class Setting extends BaseModel {
throw new Error('Unhandled value type: ' + md.type); throw new Error('Unhandled value type: ' + md.type);
} }
static filterValue(key, value) {
const md = this.settingMetadata(key);
return md.filter ? md.filter(value) : value;
}
static formatValue(key, value) { static formatValue(key, value) {
const md = this.settingMetadata(key); const md = this.settingMetadata(key);

View File

@@ -46,7 +46,7 @@ function toSystemSlashes(path, os) {
} }
function rtrimSlashes(path) { function rtrimSlashes(path) {
return path.replace(/\/+$/, ''); return path.replace(/[\/\\]+$/, '');
} }
function ltrimSlashes(path) { function ltrimSlashes(path) {

View File

@@ -26,6 +26,7 @@ const defaultState = {
appState: 'starting', appState: 'starting',
hasDisabledSyncItems: false, hasDisabledSyncItems: false,
newNote: null, newNote: null,
collapsedFolderIds: [],
}; };
const stateUtils = {}; const stateUtils = {};
@@ -51,6 +52,23 @@ function stateHasEncryptedItems(state) {
return false; return false;
} }
function folderSetCollapsed(state, action) {
const collapsedFolderIds = state.collapsedFolderIds.slice();
const idx = collapsedFolderIds.indexOf(action.id);
if (action.collapsed) {
if (idx >= 0) return state;
collapsedFolderIds.push(action.id);
} else {
if (idx < 0) return state;
collapsedFolderIds.splice(idx, 1);
}
newState = Object.assign({}, state);
newState.collapsedFolderIds = collapsedFolderIds;
return newState;
}
// When deleting a note, tag or folder // When deleting a note, tag or folder
function handleItemDelete(state, action) { function handleItemDelete(state, action) {
let newState = Object.assign({}, state); let newState = Object.assign({}, state);
@@ -339,6 +357,26 @@ const reducer = (state = defaultState, action) => {
newState.folders = action.items; newState.folders = action.items;
break; break;
case 'FOLDER_SET_COLLAPSED':
newState = folderSetCollapsed(state, action);
break;
case 'FOLDER_TOGGLE':
if (state.collapsedFolderIds.indexOf(action.id) >= 0) {
newState = folderSetCollapsed(state, Object.assign({ collapsed: false }, action));
} else {
newState = folderSetCollapsed(state, Object.assign({ collapsed: true }, action));
}
break;
case 'FOLDER_SET_COLLAPSED_ALL':
newState = Object.assign({}, state);
newState.collapsedFolderIds = action.ids.slice();
break;
case 'TAG_UPDATE_ALL': case 'TAG_UPDATE_ALL':
newState = Object.assign({}, state); newState = Object.assign({}, state);

View File

@@ -59,10 +59,10 @@ reg.scheduleSync = async (delay = null, syncOptions = null) => {
reg.logger().info('Scheduling sync operation...'); reg.logger().info('Scheduling sync operation...');
if (Setting.value("env") === "dev" && delay !== 0) { // if (Setting.value("env") === "dev" && delay !== 0) {
reg.logger().info("Schedule sync DISABLED!!!"); // reg.logger().info("Schedule sync DISABLED!!!");
return; // return;
} // }
const timeoutCallback = async () => { const timeoutCallback = async () => {
reg.scheduleSyncId_ = null; reg.scheduleSyncId_ = null;

View File

@@ -189,7 +189,7 @@ class InteropService {
await queueExportItem(BaseModel.TYPE_NOTE, note); await queueExportItem(BaseModel.TYPE_NOTE, note);
exportedNoteIds.push(noteId); exportedNoteIds.push(noteId);
const rids = Note.linkedResourceIds(note.body); const rids = await Note.linkedResourceIds(note.body);
resourceIds = resourceIds.concat(rids); resourceIds = resourceIds.concat(rids);
} }
} }

View File

@@ -18,22 +18,19 @@ const { uuid } = require('lib/uuid.js');
class InteropService_Importer_Raw extends InteropService_Importer_Base { class InteropService_Importer_Raw extends InteropService_Importer_Base {
async exec(result) { async exec(result) {
const noteIdMap = {}; const itemIdMap = {};
const folderIdMap = {};
const resourceIdMap = {};
const tagIdMap = {};
const createdResources = {}; const createdResources = {};
const noteTagsToCreate = []; const noteTagsToCreate = [];
const destinationFolderId = this.options_.destinationFolderId; const destinationFolderId = this.options_.destinationFolderId;
const replaceResourceNoteIds = (noteBody) => { const replaceLinkedItemIds = async (noteBody) => {
let output = noteBody; let output = noteBody;
const resourceIds = Note.linkedResourceIds(noteBody); const itemIds = Note.linkedItemIds(noteBody);
for (let i = 0; i < resourceIds.length; i++) { for (let i = 0; i < itemIds.length; i++) {
const id = resourceIds[i]; const id = itemIds[i];
if (!resourceIdMap[id]) resourceIdMap[id] = uuid.create(); if (!itemIdMap[id]) itemIdMap[id] = uuid.create();
output = output.replace(new RegExp(id, 'gi'), resourceIdMap[id]); output = output.replace(new RegExp(id, 'gi'), itemIdMap[id]);
} }
return output; return output;
@@ -77,41 +74,40 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
// - If a destination folder was specified, move the note to it. // - If a destination folder was specified, move the note to it.
// - Otherwise, if the associated folder exists, use this. // - Otherwise, if the associated folder exists, use this.
// - If it doesn't exist, use the default folder. This is the case for example when importing JEX archives that contain only one or more notes, but no folder. // - If it doesn't exist, use the default folder. This is the case for example when importing JEX archives that contain only one or more notes, but no folder.
if (!folderIdMap[item.parent_id]) { if (!itemIdMap[item.parent_id]) {
if (destinationFolderId) { if (destinationFolderId) {
folderIdMap[item.parent_id] = destinationFolderId; itemIdMap[item.parent_id] = destinationFolderId;
} else if (!folderExists(stats, item.parent_id)) { } else if (!folderExists(stats, item.parent_id)) {
const parentFolder = await defaultFolder(); const parentFolder = await defaultFolder();
folderIdMap[item.parent_id] = parentFolder.id; itemIdMap[item.parent_id] = parentFolder.id;
} else { } else {
folderIdMap[item.parent_id] = uuid.create(); itemIdMap[item.parent_id] = uuid.create();
} }
} }
const noteId = uuid.create(); if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
noteIdMap[item.id] = noteId; item.id = itemIdMap[item.id]; //noteId;
item.id = noteId; item.parent_id = itemIdMap[item.parent_id];
item.parent_id = folderIdMap[item.parent_id]; item.body = await replaceLinkedItemIds(item.body);
item.body = replaceResourceNoteIds(item.body);
} else if (itemType === BaseModel.TYPE_FOLDER) { } else if (itemType === BaseModel.TYPE_FOLDER) {
if (destinationFolderId) continue; if (destinationFolderId) continue;
if (!folderIdMap[item.id]) folderIdMap[item.id] = uuid.create(); if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = folderIdMap[item.id]; item.id = itemIdMap[item.id];
item.title = await Folder.findUniqueFolderTitle(item.title); item.title = await Folder.findUniqueFolderTitle(item.title);
} else if (itemType === BaseModel.TYPE_RESOURCE) { } else if (itemType === BaseModel.TYPE_RESOURCE) {
if (!resourceIdMap[item.id]) resourceIdMap[item.id] = uuid.create(); if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = resourceIdMap[item.id]; item.id = itemIdMap[item.id];
createdResources[item.id] = item; createdResources[item.id] = item;
} else if (itemType === BaseModel.TYPE_TAG) { } else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.loadByTitle(item.title); const tag = await Tag.loadByTitle(item.title);
if (tag) { if (tag) {
tagIdMap[item.id] = tag.id; itemIdMap[item.id] = tag.id;
continue; continue;
} }
const tagId = uuid.create(); const tagId = uuid.create();
tagIdMap[item.id] = tagId; itemIdMap[item.id] = tagId;
item.id = tagId; item.id = tagId;
} else if (itemType === BaseModel.TYPE_NOTE_TAG) { } else if (itemType === BaseModel.TYPE_NOTE_TAG) {
noteTagsToCreate.push(item); noteTagsToCreate.push(item);
@@ -123,8 +119,8 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
for (let i = 0; i < noteTagsToCreate.length; i++) { for (let i = 0; i < noteTagsToCreate.length; i++) {
const noteTag = noteTagsToCreate[i]; const noteTag = noteTagsToCreate[i];
const newNoteId = noteIdMap[noteTag.note_id]; const newNoteId = itemIdMap[noteTag.note_id];
const newTagId = tagIdMap[noteTag.tag_id]; const newTagId = itemIdMap[noteTag.tag_id];
if (!newNoteId) { if (!newNoteId) {
result.warnings.push(sprintf('Non-existent note %s referenced in tag %s', noteTag.note_id, noteTag.tag_id)); result.warnings.push(sprintf('Non-existent note %s referenced in tag %s', noteTag.note_id, noteTag.tag_id));
@@ -149,7 +145,7 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
for (let i = 0; i < resourceStats.length; i++) { for (let i = 0; i < resourceStats.length; i++) {
const resourceFilePath = this.sourcePath_ + '/resources/' + resourceStats[i].path; const resourceFilePath = this.sourcePath_ + '/resources/' + resourceStats[i].path;
const oldId = Resource.pathToId(resourceFilePath); const oldId = Resource.pathToId(resourceFilePath);
const newId = resourceIdMap[oldId]; const newId = itemIdMap[oldId];
if (!newId) { if (!newId) {
result.warnings.push(sprintf('Resource file is not referenced in any note and so was not imported: %s', oldId)); result.warnings.push(sprintf('Resource file is not referenced in any note and so was not imported: %s', oldId));
continue; continue;

View File

@@ -44,7 +44,7 @@ class ResourceService extends BaseService {
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) { if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
const note = noteById(change.item_id); const note = noteById(change.item_id);
const resourceIds = Note.linkedResourceIds(note.body); const resourceIds = await Note.linkedResourceIds(note.body);
await NoteResource.setAssociatedResources(note.id, resourceIds); await NoteResource.setAssociatedResources(note.id, resourceIds);
} else if (change.type === ItemChange.TYPE_DELETE) { } else if (change.type === ItemChange.TYPE_DELETE) {
await NoteResource.remove(change.item_id); await NoteResource.remove(change.item_id);

View File

@@ -5,6 +5,7 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const { time } = require('lib/time-utils.js'); const { time } = require('lib/time-utils.js');
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js'); const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const { FsDriverNode } = require('lib/fs-driver-node.js'); const { FsDriverNode } = require('lib/fs-driver-node.js');
const urlValidator = require('valid-url');
function shimInit() { function shimInit() {
shim.fsDriver = () => { throw new Error('Not implemented') } shim.fsDriver = () => { throw new Error('Not implemented') }
@@ -34,6 +35,23 @@ function shimInit() {
return locale; return locale;
} }
// For Electron only
shim.writeImageToFile = async function(nativeImage, mime, targetPath) {
let buffer = null;
mime = mime.toLowerCase();
if (mime === 'image/png') {
buffer = nativeImage.toPNG();
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
buffer = nativeImage.toJPEG(90);
}
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
}
const resizeImage_ = async function(filePath, targetPath, mime) { const resizeImage_ = async function(filePath, targetPath, mime) {
if (shim.isElectron()) { // For Electron if (shim.isElectron()) { // For Electron
const nativeImage = require('electron').nativeImage; const nativeImage = require('electron').nativeImage;
@@ -57,17 +75,7 @@ function shimInit() {
image = image.resize(options); image = image.resize(options);
let buffer = null; await shim.writeImageToFile(image, mime, targetPath);
if (mime === 'image/png') {
buffer = image.toPNG();
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
buffer = image.toJPEG(90);
}
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
} else { // For the CLI tool } else { // For the CLI tool
const sharp = require('sharp'); const sharp = require('sharp');
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
@@ -88,7 +96,7 @@ function shimInit() {
} }
} }
shim.attachFileToNote = async function(note, filePath) { shim.attachFileToNote = async function(note, filePath, position = null) {
const Resource = require('lib/models/Resource.js'); const Resource = require('lib/models/Resource.js');
const { uuid } = require('lib/uuid.js'); const { uuid } = require('lib/uuid.js');
const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js'); const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js');
@@ -110,14 +118,23 @@ function shimInit() {
if (resource.mime == 'image/jpeg' || resource.mime == 'image/jpg' || resource.mime == 'image/png') { if (resource.mime == 'image/jpeg' || resource.mime == 'image/jpg' || resource.mime == 'image/png') {
const result = await resizeImage_(filePath, targetPath, resource.mime); const result = await resizeImage_(filePath, targetPath, resource.mime);
} else { } else {
const stat = await shim.fsDriver().stat(filePath);
if (stat.size >= 10000000) throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
await fs.copy(filePath, targetPath, { overwrite: true }); await fs.copy(filePath, targetPath, { overwrite: true });
} }
await Resource.save(resource, { isNew: true }); await Resource.save(resource, { isNew: true });
const newBody = []; const newBody = [];
if (note.body) newBody.push(note.body);
if (position === null) {
position = note.body ? note.body.length : 0;
}
if (note.body && position) newBody.push(note.body.substr(0, position));
newBody.push(Resource.markdownTag(resource)); newBody.push(Resource.markdownTag(resource));
newBody.push(note.body.substr(position));
const newNote = Object.assign({}, note, { const newNote = Object.assign({}, note, {
body: newBody.join('\n\n'), body: newBody.join('\n\n'),
@@ -133,6 +150,9 @@ function shimInit() {
} }
shim.fetch = async function(url, options = null) { shim.fetch = async function(url, options = null) {
const validatedUrl = urlValidator.isUri(url);
if (!validatedUrl) throw new Error('Not a valid URL: ' + url);
return shim.fetchWithRetry(() => { return shim.fetchWithRetry(() => {
return nodeFetch(url, options) return nodeFetch(url, options)
}, options); }, options);

View File

@@ -263,7 +263,7 @@ class Synchronizer {
// //
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe // TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test // could expose this with a accurateTimestamps() method that returns "true". In that case, the test
// could be done using the file timestamp and the potentially unecessary content loading could be skipped. // could be done using the file timestamp and the potentially unnecessary content loading could be skipped.
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be // OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
// a few seconds ahead of what it was set with setTimestamp() // a few seconds ahead of what it was set with setTimestamp()
remoteContent = await this.api().get(path); remoteContent = await this.api().get(path);
@@ -567,7 +567,7 @@ class Synchronizer {
// If user has cancelled, don't record the new context (2) so that synchronisation // If user has cancelled, don't record the new context (2) so that synchronisation
// can start again from the previous context (1) next time. It is ok if some items // can start again from the previous context (1) next time. It is ok if some items
// have been synced between (1) and (2) because the loop above will handle the same // have been synced between (1) and (2) because the loop above will handle the same
// items being synced twice as an update. If the local and remote items are indentical // items being synced twice as an update. If the local and remote items are identical
// the update will simply be skipped. // the update will simply be skipped.
if (!hasCancelled) { if (!hasCancelled) {
if (!listResult.hasMore) { if (!listResult.hasMore) {

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

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